Calendly webhooks: API v2 subscriptions, events and signature checks
To test Calendly webhooks, create a webhook subscription with API v2 that points at a free webhooks.sh bin (no signup, 48 hours, 100 requests a day), then book a test meeting to see invitee.created live. Calendly webhooks need a paid plan and retry failed deliveries for 24 hours.
Calendly webhooks are created with one API v2 call, and pointing that call at a free bin and then booking a test meeting shows the exact invitee.created request. This page covers the subscription call, events, scopes and plans, signature checks and the retry rules.
Subscribe a free bin to invitee events
- Create a free bin in the browser at view.webhooks.page or run
curl -X POST https://webhooks.sh/v1/binsto get aningest_urlandview_url. - Using a personal access token or an OAuth token that has the
webhooks:writescope,POSTtohttps://api.calendly.com/webhook_subscriptionswith your bin'singest_urlasurl, theeventsarray, yourorganizationanduserURIs,scopeset touser, and asigning_key. - Book a meeting on your own scheduling page, since bookings that already happened don't trigger webhooks.
- Watch the
invitee.createdrequest arrive live in the bin viewer with itsCalendly-Webhook-Signatureheader and full JSON body.
A 201 response returns the new Calendly webhook subscription with state set to active.
# 1. Create a free bin (or open view.webhooks.page)
curl -X POST https://webhooks.sh/v1/bins
# 2. Subscribe it to booking events
curl -X POST https://api.calendly.com/webhook_subscriptions \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://<bin>.webhooks.page/calendly",
"events": ["invitee.created", "invitee.canceled"],
"organization": "https://api.calendly.com/organizations/<org>",
"user": "https://api.calendly.com/users/<user>",
"scope": "user",
"signing_key": "<random secret>"
}'
Events, scopes and plans
When you create a subscription, the scope parameter controls whose bookings and changes fire events: user ties the subscription to one user given in the user field, organization covers the whole organization, and group covers a single group given in the group field. Not every event works with every scope: the meeting recap events (meeting_recap.created, meeting_recap.updated, meeting_recap.deleted) work only with user scope, and routing_form_submission.created works only with organization scope.
Webhooks require a paid Calendly plan (Professional, Standard, Standard Plus, Teams, Teams Plus or Enterprise). If you call the Calendly webhooks API on an account without access, it answers 403 with Please upgrade your Calendly account to Standard.
| Area | Events |
|---|---|
| Bookings | invitee.created, invitee.canceled |
| No-shows | invitee_no_show.created, invitee_no_show.deleted |
| Event types | event_type.created, event_type.updated, event_type.deleted |
| Routing forms | routing_form_submission.created (organization scope) |
| Meeting recaps | meeting_recap.created, meeting_recap.updated, meeting_recap.deleted (user scope) |
| Contacts | contact.created, contact.updated, contact.deleted |
Verify Calendly-Webhook-Signature
If you pass a signing_key when creating the subscription, every delivery carries a Calendly-Webhook-Signature header formatted as t=<unix timestamp>,v1=<signature>. To verify the Calendly webhook signature, split the header on the comma, join the t timestamp, a literal dot (.) and the raw request body into <t>.<raw body>, and compute the hex HMAC-SHA256 using your signing_key. Compare your digest to v1 in constant time, and check the timestamp against the current time to prevent replay attacks; the example code in Calendly's webhook signatures documentation rejects requests older than 3 minutes (180 seconds).
Signature headers aren't redacted in the webhooks.sh viewer, so you can copy the t value, v1 signature and raw body from a captured request and check your math in the HMAC signature verifier using Custom HMAC, SHA-256, hex output, and a payload template of your t value, a dot and {body}.
import { createHmac, timingSafeEqual } from "node:crypto";
export function isFromCalendly(header, rawBody, signingKey) {
const parts = Object.fromEntries(
(header ?? "").split(",").map((kv) => kv.split("="))
);
const age = Date.now() / 1000 - Number(parts.t);
if (!parts.v1 || age > 180) return false;
const expected = createHmac("sha256", signingKey)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
return a.length === b.length && timingSafeEqual(a, b);
}
Retries, timeouts and the 24-hour disable
Calendly retries deliveries that get a 3xx, 4xx or 5xx response and disables the subscription if none succeeds within 24 hours.
A free bin replies with HTTP 200 right away, so nothing gets retried during normal testing, but if your bin expires after 48 hours (or hits its 100 requests a day cap and returns 429 daily_cap_reached) while a subscription still points at it, deliveries will fail and Calendly will disable the subscription after 24 hours. Point long-running test subscriptions at a claimed bin, or delete the subscription when you're done testing. See the webhook retries guide for more on how senders handle backoff and disabling.
| Rule | Value |
|---|---|
| Connection timeout | 10 seconds |
| Read timeout | 15 seconds |
| Retried on | 3xx, 4xx or 5xx responses |
| Retry window | 24 hours with exponential back-off |
| After 24 hours of failures | Subscription disabled, create a new one |
| Timestamp tolerance in Calendly's example | 3 minutes |
Get a sample payload without booking
If you only need the JSON shape for parsing code, send a GET request to https://api.calendly.com/sample_webhook_data with event, organization and scope query parameters and a token that has the webhooks:read scope. The sample payload comes back directly in the API response instead of being sent to your webhook url, so use a real test booking to see the full HTTP request and Calendly-Webhook-Signature header in your bin.
ORG=https://api.calendly.com/organizations/<org>
curl -G https://api.calendly.com/sample_webhook_data \
-H "Authorization: Bearer <token>" \
--data-urlencode "event=invitee.created" \
--data-urlencode "organization=$ORG" \
--data-urlencode "scope=organization"
Keep the URL or move to localhost
A free bin lasts 48 hours and accepts 100 requests a day with bodies up to 100KB, which is plenty for testing booking flows. To keep a test subscription pointed at the same webhooks.page URL, claim the bin with a free Google sign-in before it expires; that stops the 48-hour timer and keeps a rolling 30 days of request history.
When you're ready to run your handler locally or take production volume, Solo ($12 a month, on pricing) gives you a persistent named endpoint on webhooks.host with 250,000 requests per billing period and 90 days of history. It also includes the wsh CLI, so you can run wsh listen <endpoint> --to localhost:3000 to relay live Calendly deliveries straight to your local server and replay captured requests while you work on your code.
FAQ
Can I create a Calendly webhook without the API?
No. Calendly supports webhooks through its API v2, so you create a subscription by sending a POST request to https://api.calendly.com/webhook_subscriptions with a personal access token or OAuth token that has the webhooks:write scope.
Does the free Calendly plan support webhooks?
No, webhooks require a paid plan (Professional, Standard, Standard Plus, Teams, Teams Plus or Enterprise). On a plan without webhook access, the API returns 403 with the message Please upgrade your Calendly account to Standard.
How do I verify Calendly-Webhook-Signature?
Compute the hex HMAC-SHA256 of <t>.<raw body> - the t timestamp from the header, a dot, and the unparsed request body, using your subscription's signing_key, and compare the result to the header's v1 value. You should also reject timestamps older than 3 minutes (180 seconds).
Why didn't my existing bookings trigger a webhook?
Calendly webhooks only fire for events that happen after the subscription is created. To test your endpoint, book or cancel a new meeting on your scheduling page after POST https://api.calendly.com/webhook_subscriptions returns active.
How long does Calendly retry a failed webhook?
Calendly retries failed deliveries (any 3xx, 4xx or 5xx response) with exponential back-off for up to 24 hours, or until 24 hours after the booking. If no delivery succeeds after 24 hours, the subscription's state changes to disabled and you have to recreate it.
What is the difference between user and organization scope?
A user scope subscription only fires for one user's bookings (passed in the user field), while organization scope fires for everyone in the organization. Some events are restricted to one scope: routing_form_submission.created only works with organization scope, and the meeting_recap.* events only work with user scope.
Last updated
Try it in 10 seconds.
Create a free bin at view.webhooks.page — no signup, no install. Point your sender at the URL and watch requests land live. Bins expire after 48 hours.