Help API

/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:read for the GETs, webhooks:write for every mutation (create, update, delete, rotate-secret, test). See Scopes.
  • No Idempotency-Key, no If-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_secret only in the POST (create) and rotate-secret responses. It is never returned by any GET. 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_…"
}
  • status is active or paused. paused_reason is set when 42min auto-paused the hook (after 5 consecutive failed deliveries) and is null otherwise.
  • last_delivery_ok is true / false / null (never delivered yet).
  • signing_secret is present only on create and rotate-secret responses.

GET /v1/webhooks

List the webhooks this credential created.

MethodGET
URLhttps://api.42min.us/v1/webhooks
Scopewebhooks:read
AuthRequired

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.

MethodGET
URLhttps://api.42min.us/v1/webhooks/{id}
Scopewebhooks:read
AuthRequired

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.

MethodPOST
URLhttps://api.42min.us/v1/webhooks
Scopewebhooks:write
AuthRequired

Body

{
  "url": "https://hooks.example.com/42min",
  "events": ["booking_created", "booking_updated", "event_type_updated"],
  "description": "Prod CRM sync"
}
FieldTypeNotes
urlstringRequired. 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.
eventsstring[]Required, at least one. See Event names and use the underscore form here.
descriptionstringOptional. 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: url isn't a public HTTPS endpoint, or events is 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.

MethodPATCH
URLhttps://api.42min.us/v1/webhooks/{id}
Scopewebhooks:write
AuthRequired

Body

{
  "url": "https://hooks.example.com/42min/v2",
  "events": ["booking_created", "booking_canceled"],
  "description": "Prod CRM sync (v2)",
  "status": "active"
}
FieldTypeNotes
urlstringSame rules as create. Changing it re-runs the HTTPS/public check and the per-sandbox duplicate check.
eventsstring[]Replaces the event list wholesale (at least one).
descriptionstringMax 255.
statusstringactive 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.

MethodDELETE
URLhttps://api.42min.us/v1/webhooks/{id}
Scopewebhooks:write
AuthRequired

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.

MethodPOST
URLhttps://api.42min.us/v1/webhooks/{id}/rotate-secret
Scopewebhooks:write
AuthRequired

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").

MethodPOST
URLhttps://api.42min.us/v1/webhooks/{id}/test
Scopewebhooks:write
AuthRequired

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.

MethodGET
URLhttps://api.42min.us/v1/webhooks/{id}/deliveries
Scopewebhooks:read
AuthRequired

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_code is the HTTP status your endpoint returned (null if the request never completed: DNS failure, timeout, connection refused).
  • error carries the failure reason when there is one; null on success.
  • delivered_at is set only once a 2xx is received; it stays null for 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 eventsDelivered asFires when
booking_createdbooking.createdA meeting is booked.
booking_rescheduledbooking.rescheduledA booking moves to a new time.
booking_canceledbooking.canceledA booking is canceled.
booking_no_showbooking.no_showA booking is marked no-show.
booking_updatedbooking.updatedPATCH /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_updatedevent_type.updatedAn 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_deletedevent_type.deletedAn 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 with GET /v1/bookings?updated_since=…&sort=updated_at_asc rather 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.