/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 — 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 — 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 — 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 — 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 — 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 — 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 — 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 — all of
it).
|
event_type_deleted |
event_type.deleted |
An event type is deleted. Payload carries only
{ id, slug, deleted_at } — the row is gone.
|
event_type.updated /
event_type.deleted fire from
dashboard changes — 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.