/v1/webhooks
Register and manage webhook subscriptions programmatically, the same real-time callbacks you can set up by hand on the Webhooks tab of Admin Center → API, but driven from your own code so an installed integration can wire up its own delivery endpoint without an admin clicking through the dashboard.
For the delivery contract itself (the request envelope, the
X-42min-Webhook-Signature header, how to verify it, the retry schedule,
and auto-pause behavior), see
API & webhooks. This page is
the management surface; that page is the receiving side.
The per-credential sandbox
This is the one rule that makes /v1/webhooks behave differently from the
rest of the API:
A credential only ever sees and mutates the webhooks it created.
Concretely:
- Webhooks created through this API by a PAT are owned by that token.
Webhooks created by an OAuth client are owned by that client (all of
the client's access tokens share one sandbox: the binding is the stable
client_id, so a token refresh doesn't orphan anything; a PAT, by contrast, is bound to that specific token, so rotating the PAT leaves its webhooks unreachable from the new token). - Webhooks created in the admin UI are invisible to
/v1/webhooks, and webhooks created over the API don't appear in another credential's listing. The admin UI still sees every webhook in the account, so the sandbox is one-directional. - Two different integrations that legitimately point at the same ingestion URL are two real subscribers and both receive every matching event, because the duplicate-URL check is scoped to your own sandbox, not the whole account.
Because of the sandbox, GET /v1/webhooks returning [] does not mean
the account has no webhooks, only that this credential created none.
Conventions specific to this resource
- Scopes:
webhooks:readfor theGETs,webhooks:writefor every mutation (create, update, delete, rotate-secret, test). See Scopes. - No
Idempotency-Key, noIf-Match. Unlike/v1/bookings, the webhook endpoints don't take an idempotency key or an ETag, and they aren't optimistically locked. Last write wins. - The signing secret is shown exactly once. It is server-generated (you
can't supply your own) and appears as
signing_secretonly in thePOST(create) androtate-secretresponses. It is never returned by anyGET. Lost it? Rotate. - Account ceiling: 42 webhooks. The cap is account-wide across every credential and the admin UI combined, not 42 per credential.
The webhook object
{
"id": "01H…",
"url": "https://hooks.example.com/42min",
"events": ["booking_created", "booking_updated"],
"status": "active",
"description": "Prod CRM sync",
"paused_reason": null,
"last_delivery_at": "2026-05-18T14:03:00.000Z",
"last_delivery_ok": true,
"created_at": "2026-05-10T09:00:00.000Z",
"updated_at": "2026-05-18T14:03:00.000Z",
"signing_secret": "42min_wh_…"
}
statusisactiveorpaused.paused_reasonis set when 42min auto-paused the hook (after 5 consecutive failed deliveries) and isnullotherwise.last_delivery_okistrue/false/null(never delivered yet).signing_secretis present only on create and rotate-secret responses.
GET /v1/webhooks
List the webhooks this credential created.
| Method | GET |
| URL | https://api.42min.us/v1/webhooks |
| Scope | webhooks:read |
| Auth | Required |
Returns a plain array (newest first). This endpoint is not paginated.
{
"data": [ { "id": "01H…", "url": "https://…", "events": ["booking_created"], "status": "active", "…": "…" } ],
"meta": { "request_id": "req_…" }
}
curl
curl -H "Authorization: Bearer $TOKEN" \
https://api.42min.us/v1/webhooks
GET /v1/webhooks/:id
Return one webhook this credential created.
| Method | GET |
| URL | https://api.42min.us/v1/webhooks/{id} |
| Scope | webhooks:read |
| Auth | Required |
Common errors
404 webhook.notFound: no such webhook in this credential's sandbox (it may exist for another credential or in the admin UI; you just can't see it).
POST /v1/webhooks
Create a webhook subscription.
| Method | POST |
| URL | https://api.42min.us/v1/webhooks |
| Scope | webhooks:write |
| Auth | Required |
Body
{
"url": "https://hooks.example.com/42min",
"events": ["booking_created", "booking_updated", "event_type_updated"],
"description": "Prod CRM sync"
}
| Field | Type | Notes |
|---|---|---|
url | string | Required. Must be https:// and resolve to a public address; http://, localhost, and private/loopback IP ranges are rejected. Max 2000 chars. Normalized (fragment and trailing slash stripped) before storage. |
events | string[] | Required, at least one. See Event names and use the underscore form here. |
description | string | Optional. Max 255 chars. |
Response
201 Created with the webhook object including signing_secret. This is
the only GET-style payload that ever carries the secret on create. Store it
now; you'll need it to verify deliveries and it is never shown again.
curl
curl -X POST https://api.42min.us/v1/webhooks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/42min",
"events": ["booking_created", "booking_updated"],
"description": "Prod CRM sync"
}'
Common errors
400:urlisn't a public HTTPS endpoint, oreventsis empty / contains an unknown event name.409 webhook.duplicateUrl: this credential already has a webhook on that URL.409: the account is at the 42-webhook ceiling.
PATCH /v1/webhooks/:id
Update a webhook. All fields optional; only those present are changed.
| Method | PATCH |
| URL | https://api.42min.us/v1/webhooks/{id} |
| Scope | webhooks:write |
| Auth | Required |
Body
{
"url": "https://hooks.example.com/42min/v2",
"events": ["booking_created", "booking_canceled"],
"description": "Prod CRM sync (v2)",
"status": "active"
}
| Field | Type | Notes |
|---|---|---|
url | string | Same rules as create. Changing it re-runs the HTTPS/public check and the per-sandbox duplicate check. |
events | string[] | Replaces the event list wholesale (at least one). |
description | string | Max 255. |
status | string | active or paused. Setting it to active resets the consecutive-failure counter and clears paused_reason. This is how you re-enable an auto-paused hook. |
Response
200 OK with the updated webhook object (no signing_secret).
curl
# Resume an auto-paused webhook
curl -X PATCH https://api.42min.us/v1/webhooks/01H… \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"status":"active"}'
Common errors
404 webhook.notFound: not in this credential's sandbox.400/409: same URL validation and duplicate-URL rules as create.
DELETE /v1/webhooks/:id
Permanently delete a webhook and its delivery history.
| Method | DELETE |
| URL | https://api.42min.us/v1/webhooks/{id} |
| Scope | webhooks:write |
| Auth | Required |
Response
204 No Content, with an empty body.
Common errors
404 webhook.notFound: not in this credential's sandbox.
POST /v1/webhooks/:id/rotate-secret
Issue a new signing secret. The old secret stops being valid immediately, so update your verifier before you call this, or accept a brief window of signature failures.
| Method | POST |
| URL | https://api.42min.us/v1/webhooks/{id}/rotate-secret |
| Scope | webhooks:write |
| Auth | Required |
Response
200 OK with the webhook object including the new signing_secret, the
second and last place the secret is ever returned.
curl
curl -X POST https://api.42min.us/v1/webhooks/01H…/rotate-secret \
-H "Authorization: Bearer $TOKEN"
POST /v1/webhooks/:id/test
Enqueue a synthetic booking.created delivery so you can confirm your
endpoint is reachable and your signature check works. The payload's
data.booking is obviously fake ("test": true, id: "test-booking-id").
| Method | POST |
| URL | https://api.42min.us/v1/webhooks/{id}/test |
| Scope | webhooks:write |
| Auth | Required |
Response
200 OK:
{ "data": { "ok": true, "delivery_id": "01H…" }, "meta": { "request_id": "req_…" } }
The delivery is queued, not synchronous, so poll
/deliveries with the returned
delivery_id to see the outcome.
GET /v1/webhooks/:id/deliveries
The 50 most recent delivery attempts for this webhook, newest first. Older attempts are pruned after 30 days.
| Method | GET |
| URL | https://api.42min.us/v1/webhooks/{id}/deliveries |
| Scope | webhooks:read |
| Auth | Required |
Response
{
"data": [
{
"id": "01H…",
"event": "booking_created",
"attempt": 1,
"status_code": 200,
"error": null,
"delivered_at": "2026-05-18T14:03:01.000Z",
"created_at": "2026-05-18T14:03:00.000Z"
}
],
"meta": { "request_id": "req_…" }
}
status_codeis the HTTP status your endpoint returned (nullif the request never completed: DNS failure, timeout, connection refused).errorcarries the failure reason when there is one;nullon success.delivered_atis set only once a 2xx is received; it staysnullfor attempts that failed or are still pending.
Event names
The events array you send to create/update uses the enum
(underscore) form. The event field in the delivered envelope and the
X-42min-Webhook-Event header use the dot form. They map one-to-one:
Send in events | Delivered as | Fires when |
|---|---|---|
booking_created | booking.created | A meeting is booked. |
booking_rescheduled | booking.rescheduled | A booking moves to a new time. |
booking_canceled | booking.canceled | A booking is canceled. |
booking_no_show | booking.no_show | A booking is marked no-show. |
booking_updated | booking.updated | PATCH /v1/bookings/:uid changed metadata, responses, or attendee_name. The payload adds changed_fields[] naming exactly which of those changed. This is the only path that mutates those fields silently; no booking.rescheduled/booking.canceled covers it. See /v1/bookings. |
event_type_updated | event_type.updated | An event type is created-via-edit, edited, or toggled on/off from the Event Types dashboard. Payload data.event_type is the same shape as GET /v1/event-types/:idOrSlug (locations, questions[], limits, and the rest). |
event_type_deleted | event_type.deleted | An event type is deleted. Payload carries only { id, slug, deleted_at }, because the row is gone. |
event_type.updated / event_type.deleted fire from dashboard changes, since
there are no public write endpoints for event types yet, but the webhooks let
an integration keep its mirror of your event-type catalog in sync.
Catch-up after a pause. If a webhook was auto-paused and you missed
booking.*events, reconcile withGET /v1/bookings?updated_since=…&sort=updated_at_ascrather than guessing: it returns every booking touched since a timestamp, in stable ascending order, ready to page through.
Where this sits
- Receiving, verifying, retries, auto-pause, the activity log → API & webhooks.
- Minting the PAT or OAuth client that calls this API → Authentication.
- The exact scope each endpoint needs → Scopes.
Last updated May 19, 2026.