The loop
Create an endpoint and it prints a permanent ingest URL. Everything sent there is captured whole — headers, body, the exact bytes — before anything else happens. Stream those events to a port on your machine, and replay any one of them as often as you like while you fix the handler. Forwarding runs from your own machine, so it isn't a billable delivery.
What's different about Calendly
Calendly documents webhook creation only through the API — the help centre's entire create-a-webhook step is to follow the steps in the Developer Portal. Registration happens by POSTing to the webhook subscriptions endpoint on api.calendly.com with a personal access token. The field that catches people is organization: it's required on every create call, including user and group scopes, so a user-scoped subscription still needs the org URI alongside the user one. And there's nothing to press afterwards. Calendly's instruction is to trigger a new event — a booking, a cancellation, a reschedule — and it notes webhooks are not triggered by past events.
Where the URL goes
POST to https://api.calendly.com/webhook_subscriptions with url, events, organization and scope in the body — those four are required — plus user or group when you're scoping that narrowly. Authenticate with a personal access token or an OAuth token. For a personal access token: log in, go to the Integrations page, select the API & Webhooks tile, generate a token. One scope caveat: routing_form_submission.created accepts organization only. Re-registering a URL that already has a subscription comes back as a 409.
There's no sample-send control, no test event, no CLI. Calendly's own instruction is the whole story: "To test your webhook, trigger a new event (such as a booking, cancellation, or reschedule). Webhooks are not triggered by past events." So you book a slot on your own scheduling link, then cancel it — which, conveniently, exercises two of the events you probably care about.
What Calendly sends
The vocabulary is resource.action, with the resource in snake_case and the action a past-tense verb. Watch the American single-l spelling in invitee.canceled. The verbs don't combine freely either — there's contact.updated but no invitee.updated. Take the list from the enum on the create-subscription reference, not the prose table above it.
- invitee.created
- invitee.canceled
- invitee_no_show.created
- invitee_no_show.deleted
- routing_form_submission.created
- contact.created
Worth knowing first
A reschedule fires two webhooks
Reschedule isn't an event of its own. Calendly triggers invitee.canceled for the old booking, carrying rescheduled set to true, and invitee.created for the new one. Count naively and you record a cancellation plus an unrelated booking. The old_invitee and new_invitee URIs sit on the invitee model, not the event model — that's what links the pair.
Answer inside fifteen seconds
Delivery uses a 10-second connection timeout and a 15-second read timeout. Acknowledge first, then do the slow work in a background job. This bites hardest during local debugging: a breakpoint sitting in your handler is indistinguishable, from Calendly's side, from an endpoint that has stopped answering, and a connection timeout starts the retry process.
A failing endpoint gets disabled
3xx, 4xx and 5xx responses are retried with exponential back-off for 24 hours, or until 24 hours after the booking of the event passes. After that the webhook is disabled if another message was not delivered successfully, and the subscription has to be recreated. Other messages keep being sent while one is failing.
The signature carries its own timestamp
When a signing key is set, deliveries carry a Calendly-Webhook-Signature header with a t= timestamp and a v1= hex digest. You HMAC-SHA256 the timestamp, a literal dot, and the raw request body. The key is optional on personal-access-token subscriptions and generated automatically for OAuth 2.0 apps. Calendly's sample code rejects a t older than 180 seconds.
Watch the subscription's state field
The subscription resource exposes state, active or disabled, plus retry_started_at. Polling Get Webhook Subscription is the practical way to spot a subscription that's partway through being retired, rather than discovering it when bookings quietly stop arriving. Worth wiring into a check while you still remember why it matters.
Signature checking is handled for you once the secret is registered — you can also check a captured signature by hand in the signature verifier.