Guide to Calendly Webhooks: Features and Best Practices
Calendly webhooks notify your systems about scheduling activity in real time: an invitee books a meeting, cancels one, gets marked as a no-show, or submits a routing form. If you're building on Calendly's scheduling platform, webhooks are how you sync CRM records, send confirmations, and route leads without polling the API.
This guide covers how Calendly webhooks work, the signed-timestamp verification model, the event envelope and common events, the API-only subscription workflow, and the best practices for production.
What are Calendly webhooks?
Calendly sends an HTTP POST to your endpoint whenever a subscribed event occurs. You register a webhook subscription via the API, and Calendly signs each request with the Calendly-Webhook-Signature header so you can verify the payload is authentic, hasn't been tampered with, and isn't a replay. Your endpoint verifies the signature, then returns a 2xx status code to acknowledge receipt.
Calendly webhook features
| Feature | Details |
|---|---|
| Configuration | API-only: POST https://api.calendly.com/webhook_subscriptions with organization or user scope; requires a Standard plan or higher |
| Verification | Calendly-Webhook-Signature header (t=<timestamp>,v1=<signature>): HMAC-SHA256 (hex) over {timestamp}.{raw body} with the subscription's signing key |
| Replay protection | Signed timestamp in the header; reject timestamps older than ~3 minutes (180 seconds) |
| Envelope | event, created_at, created_by, and a payload whose shape varies by event type |
| Events | Invitee scheduling and cancellation, no-show marks, and routing form submissions |
| Signing keys | One per subscription: provide your own at creation or store the one Calendly returns |
| Acknowledgement | Return a 2xx status code |
| SDK | No official verification helper; verify manually in every framework |
Common events
Calendly event names are the top-level event field values. The ones most integrations start with:
| Event | Fires when |
|---|---|
invitee.created | An invitee schedules an event |
invitee.canceled | An invitee cancels a scheduled event |
invitee_no_show.created | An invitee is marked as a no-show |
invitee_no_show.deleted | A no-show mark is removed from an invitee |
routing_form_submission.created | A routing form is submitted |
Branch on the event field. The payload shape varies by event type: an invitee resource for invitee.* events, a submission for routing_form_submission.created. Every delivery shares the same envelope of event, created_at (an ISO 8601 timestamp of when the event occurred), created_by, and payload.
See Calendly webhook payloads in action. Inspect and replay sample Calendly webhook payloads in the Hookdeck Console — no account or setup required.
Setting up Calendly webhooks
Calendly webhooks are created via the API, not the dashboard. You need a Calendly account on a paid plan (webhooks require a Standard plan or higher), a personal access token (created under Integrations & apps > API & webhooks) or an OAuth access token, and your organization or user URI, which you can fetch from GET https://api.calendly.com/users/me.
Create a webhook subscription with the events you want to receive:
curl --request POST \
--url https://api.calendly.com/webhook_subscriptions \
--header "Authorization: Bearer $CALENDLY_ACCESS_TOKEN" \
--header "Content-Type: application/json" \
--data '{
"url": "https://your-app.com/webhooks/calendly",
"events": [
"invitee.created",
"invitee.canceled",
"invitee_no_show.created",
"routing_form_submission.created"
],
"organization": "https://api.calendly.com/organizations/AAAAAAAAAAAAAAAA",
"scope": "organization",
"signing_key": "your_generated_signing_key"
}'
scope can be organization or user; for user scope, also include a user URI. For signing_key, provide your own random secret (recommended, so you know it up front) or omit it and read the value Calendly returns. Either way, the response's resource.signing_key is what you store as CALENDLY_WEBHOOK_SIGNING_KEY and use for verification. Each subscription has its own signing key. To manage subscriptions later, list them with GET https://api.calendly.com/webhook_subscriptions?organization=...&scope=organization and delete with DELETE https://api.calendly.com/webhook_subscriptions/{uuid}.
The subscription url must be a publicly reachable HTTPS endpoint. For local development, the Hookdeck CLI (hookdeck listen 3000 calendly --path /webhooks/calendly) provides the HTTPS URL plus an inspector for replaying deliveries; trigger real events by scheduling, canceling, or marking a no-show on a booked event.
Securing Calendly webhooks
Calendly sends the signature as a comma-separated list of key=value pairs:
Calendly-Webhook-Signature: t=1719921600,v1=8f2d...c1a9
t is the Unix timestamp (in seconds) when Calendly generated the signature, and v1 is the hex-encoded HMAC-SHA256 signature. To verify: parse t and v1, build the signed content by concatenating {t}.{raw request body}, compute HMAC-SHA256 of that string with your subscription's signing key, compare against v1 with a timing-safe comparison, and reject requests whose timestamp falls outside your tolerance (~180 seconds) to prevent replay attacks.
const crypto = require("crypto");
function verifyCalendlySignature(rawBody, header, signingKey, toleranceSec = 180) {
if (!header) return false;
// Parse "t=...,v1=..." into { t, v1 }
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const timestamp = parts.t;
const signature = parts.v1;
if (!timestamp || !signature) return false;
// Replay protection: reject stale timestamps
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - Number(timestamp)) > toleranceSec) return false;
// Signed content = "{timestamp}.{raw body}"
const expected = crypto
.createHmac("sha256", signingKey)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
try {
return crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex")
);
} catch {
return false; // length mismatch = invalid
}
}
app.post(
"/webhooks/calendly",
express.raw({ type: "application/json" }),
(req, res) => {
const header = req.headers["calendly-webhook-signature"];
const signingKey = process.env.CALENDLY_WEBHOOK_SIGNING_KEY;
if (!verifyCalendlySignature(req.body, header, signingKey)) {
return res.sendStatus(401);
}
const { event, payload } = JSON.parse(req.body);
// Acknowledge, then process asynchronously
processQueue.add({ event, payload });
res.sendStatus(200);
}
);
Three details matter. Compute the HMAC over the exact bytes Calendly sent (express.raw() in Express, await request.text() in Next.js, await request.body() in FastAPI); parsing and re-serializing JSON changes key ordering or whitespace and breaks the signature. The signed string is {t}.{body}, not just the body. And v1 is hex-encoded, so decode and compare as hex. Most frameworks lowercase header names, so read calendly-webhook-signature.
Make Calendly webhooks production-ready. Hookdeck Event Gateway verifies signatures upstream, deduplicates, and durably queues every event.
Calendly webhook limitations and pain points
Subscriptions are API-only and plan-gated
The Problem: There is no dashboard UI for creating webhooks. You need a paid plan (Standard or higher), an access token, and your organization or user URI before you can register a single endpoint.
Why It Happens: Calendly exposes webhook subscriptions purely as an API resource, available on Standard plans and above.
Workarounds:
- Create a personal access token under Integrations & apps > API & webhooks, fetch your URI from
GET /users/me, and script subscription creation so it's repeatable across environments. - Use the list and delete endpoints to audit what's registered.
How Hookdeck Can Help: Point one subscription at Hookdeck and fan events out with Filters, routing each event type to the handler that cares about it, so you rarely need to touch the subscription API again.
Verification is manual and raw-body sensitive
The Problem: Calendly has no SDK verification helper, and the signed content is {timestamp}.{raw body}, so framework middleware that parses the JSON before you verify silently breaks every signature. Signing only the body, or comparing as base64 instead of hex, produces the same always-failing result.
Why It Happens: The scheme signs the timestamp plus the exact bytes on the wire, and there is no official library to hide those details, so every implementation is hand-rolled.
Workarounds:
- Capture the raw body before parsing (
express.raw(),await request.text(),await request.body()). - Build the signed content as
{t}.{body}, compute HMAC-SHA256, and compare the hex digest timing-safely.
How Hookdeck Can Help: Hookdeck verifies Calendly signatures at the edge, so unverified traffic never reaches your handler and your application code stays free of raw-body plumbing.
Replay protection depends on your clock
The Problem: Rejecting stale timestamps is the replay defense, but it compares Calendly's clock to yours. Clock drift or an overly tight tolerance produces verification failures that only appear intermittently, often after a delay.
Why It Happens: The timestamp is part of the signed content, so staleness checking is how you stop an attacker from resending a previously valid request; the check is only as accurate as your server's clock.
Workarounds:
- Keep your server clock in sync with NTP.
- Use a tolerance of around 180 seconds and compare the absolute difference, not just one direction.
How Hookdeck Can Help: Hookdeck verifies each delivery on arrival, then retries delivery to your handler on its own schedule, so a redelivery hours later never trips a staleness check in your code.
One signing key per subscription
The Problem: Each webhook subscription has its own signing key. Run subscriptions for multiple environments or scopes and a request verified with the wrong key fails, and that failure looks identical to a forged request.
Why It Happens: The signing key is a property of the subscription, supplied or returned at creation, not a single account-wide secret.
Workarounds:
- Provide your own
signing_keyat creation so you know each key up front. - Store keys per subscription and verify every request with the key that belongs to the subscription that sent it.
How Hookdeck Can Help: Give each subscription its own Hookdeck source with its own key, and use Issues to get alerted when deliveries start failing, so a key mismatch surfaces immediately instead of showing up later as missing events.
Best practices
Verify on the raw body, before parsing
Compute the HMAC over the exact bytes received, respond 401 on mismatch, and only then JSON.parse. Any middleware that parses first belongs after verification.
Reject stale timestamps
Enforce the ~180-second tolerance with a timing-safe comparison of the signature itself. Skipping the timestamp check leaves your endpoint open to replayed requests that carry valid signatures.
Acknowledge fast, process asynchronously
Return a 2xx as soon as verification passes and hand the event to a queue or background worker. See why to process webhooks asynchronously.
Handle events idempotently
Build handlers so that processing the same delivery twice is safe, keying work off the resource URI in the payload. See our guide to webhook idempotency.
Manage signing keys deliberately
Supply your own signing_key when creating each subscription, store one key per subscription, and match key to subscription at verification time.
Audit subscriptions regularly
List subscriptions with the API and delete ones you no longer need, so stale endpoints don't keep receiving signed scheduling data.
Conclusion
Calendly webhooks are registered through the API on a Standard plan or higher, signed with a timestamped HMAC-SHA256 in the Calendly-Webhook-Signature header, and verified against a per-subscription signing key over the raw request body. Verify timing-safely, reject stale timestamps, acknowledge with a 2xx, and process the work off the request path.
Hookdeck Event Gateway verifies Calendly signatures at the edge, deduplicates, durably queues every event, and retries your handler independently, so the raw-body, clock-tolerance, and key-management details stop being constraints your application code has to satisfy.
Get started with Hookdeck for free and handle Calendly webhooks reliably in minutes.