Skip to content
TryWebhook
New URL

Scheduling · Free · No signup

Test and debug Calendly webhooks

Point a Calendly webhook subscription at a throwaway URL, book yourself a meeting, and read the delivery as it arrived — including the timestamped signature whose construction is Stripe's under a different header name.

No signup · No email · Ready in about a second

At a glance

Calendly webhooks in brief

Category
Scheduling
Where to configure
Calendly API — POST /webhook_subscriptions. There is no dashboard screen for this.
Signature header
Calendly-Webhook-Signature
Secret
Signing key (established when the subscription is created)
Retries
A few attempts with backoff. There is no delivery log in the web interface, so you inspect subscriptions and their state through the API.
Response deadline
Not published as a fixed number. Acknowledge quickly and do the work afterwards.
Treated as success
Any 2xx.

Cross-checked against Calendly's own webhook documentation. Providers change these; if something here is stale, tell us.

The payload

What Calendly actually sends

Shape-accurate, with obviously fake identifiers. These are the same samples the inspector's Send test webhook panel fires, so you can read one here and then send the identical bytes to your own URL.

POST/w/{your-bin-id}927 B

Resources are API URLs, not ids — you fetch to get the rest.

1 headers
user-agent
Calendly-Webhooks/1.0
Body · application/json · 1 line
{"created_at":"2026-06-15T10:04:00.000000Z","created_by":"https://api.calendly.com/users/FAKEUSER","event":"invitee.created","payload":{"uri":"https://api.calendly.com/scheduled_events/FAKEEVENT/invitees/FAKEINVITEE","email":"buyer@example.com","name":"Ada Lovelace","status":"active","timezone":"Europe/London","event":"https://api.calendly.com/scheduled_events/FAKEEVENT","cancel_url":"https://calendly.com/cancellations/FAKE","reschedule_url":"https://calendly.com/reschedulings/FAKE","questions_and_answers":[{"position":0,"question":"What would you like to discuss?","answer":"Webhook debugging"}],"tracking":{"utm_source":"newsletter","utm_campaign":null,"salesforce_uuid":null},"scheduled_event":{"name":"30 Minute Meeting","status":"active","start_time":"2026-06-20T15:00:00.000000Z","end_time":"2026-06-20T15:30:00.000000Z","location":{"type":"google_conference","join_url":"https://meet.google.com/fake-fake-fake"}}}}

No sample carries a signature header. A valid one can only be produced with your own signing secret, and a fabricated one would make the Signature tab report a mismatch that isn't real. Send a genuine delivery when you want to test verification.

Verification

Verifying a Calendly signature

Signed payload

The header carries two fields, `t=1750000000,v1=<hex>`. The `v1` value is a hex HMAC-SHA256 of the string `{t}.{raw body}`, keyed with the subscription's signing key. Identical in construction to Stripe's scheme.

The construction here is exactly Stripe's, but the Stripe tab reads only the `stripe-signature` header, and the Custom HMAC tab can take a timestamp from a separate header rather than from inside the signature header — so neither can be pointed at this one. Verify it in your own code; it is five lines, and the Stripe section of the signature guide describes the same algorithm step for step.

Events

Events worth subscribing to first

Not the full catalogue — the handful that carry most integrations. Subscribing to everything is the fastest way to a handler that times out.

EventFires when
invitee.createdSomeone booked a meeting. Also fires as the second half of a reschedule, with `rescheduled: true` and an `old_invitee` reference.
invitee.canceledA booking was cancelled, by either side. `cancellation` carries who did it and any reason they gave.
invitee_no_show.createdThe host marked an invitee as a no-show. Useful for follow-up automation and for billing policies.
invitee_no_show.deletedA no-show mark was removed. Rare, and easy to forget, which leaves the invitee flagged forever in your database.
routing_form_submission.createdSomeone completed a routing form, whether or not they went on to book. The only way to see traffic that bounced before scheduling.

Creating a subscription

There is no screen for this, so the first delivery costs an API call. Create a URL above, then:

curl -X POST https://api.calendly.com/webhook_subscriptions \
  -H "Authorization: Bearer $CALENDLY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://trywebhook.com/w/your-bin-id",
    "events": ["invitee.created", "invitee.canceled"],
    "organization": "https://api.calendly.com/organizations/YOUR_ORG",
    "user": "https://api.calendly.com/users/YOUR_USER",
    "scope": "user",
    "signing_key": "a-long-random-string-you-generate"
  }'

Get the organization and user URIs from GET /users/me. Scope user covers your own bookings; organization covers everyone’s and needs an admin token.

Keep the signing key. Calendly will not show it to you again, and the only way to change it is to delete the subscription and make another one.

Then book yourself a meeting. A single-use link on a throwaway event type is the cheapest way, and the delivery lands here within a second.

Reading the payload

{
  "created_at": "2026-06-15T10:04:00.000000Z",
  "event": "invitee.created",
  "payload": {
    "uri": "https://api.calendly.com/scheduled_events/FAKE/invitees/FAKE",
    "email": "buyer@example.com",
    "name": "Ada Lovelace",
    "status": "active",
    "timezone": "America/New_York",
    "rescheduled": false,
    "questions_and_answers": [{ "question": "What do you want to discuss?", "answer": "Webhooks" }],
    "scheduled_event": {
      "start_time": "2026-06-20T15:00:00.000000Z",
      "location": { "type": "google_conference", "join_url": "https://meet.google.com/fake-fake-fake" }
    }
  }
}

Switch on the top-level event. Everything else is under payload, and the two halves of the booking are separated: the invitee’s own details sit at the top, and the meeting they booked is nested under scheduled_event.

Times are UTC with microsecond precision. payload.timezone is the invitee’s timezone, and it is the one to render in — sending a confirmation that says 15:00 UTC to someone in New York is technically correct and practically useless.

location needs a branch on type. google_conference and zoom carry a join_url; physical carries an address; outbound_call carries a phone number. There is no single field that always holds “where the meeting is”.

tracking is where UTM parameters and your own utm_content end up — the join back to whatever campaign produced the booking.

Verifying Calendly-Webhook-Signature

Calendly-Webhook-Signature: t=1750000000,v1=3f9d2a7c...

Split on ,, then on =. Build the signed string as the timestamp, a literal dot, and the raw body:

const { t, v1 } = parseSignatureHeader(header);
const signed = `${t}.${rawBody}`;
const digest = hmacSha256Hex(signingKey, signed);
const ok = timingSafeEqual(digest, v1);

Then reject anything older than a few minutes, because the signature itself never expires:

if (Math.abs(Date.now() / 1000 - Number(t)) > 60 * 3) return reject();

If that looks familiar, it is: this is Stripe’s scheme with a different header name. Which is convenient — any Stripe verification code works after one change — and also a trap, because a copied verifier that still reads stripe-signature will find nothing there. Make sure the missing-header case rejects rather than skipping the check.

Reschedules, and the cancellation that is not one

The most common Calendly integration bug is treating invitee.canceled as a lost booking. Rescheduling emits both events:

invitee.canceled   payload.uri = .../invitees/OLD    (the original slot)
invitee.created    payload.rescheduled = true, payload.old_invitee = .../invitees/OLD

So the sequence for a moved meeting is indistinguishable from a cancellation followed by an unrelated new booking — unless you read rescheduled and old_invitee.

if (event === 'invitee.created' && payload.rescheduled) {
  await moveBooking(payload.old_invitee, payload.uri);
} else if (event === 'invitee.created') {
  await createBooking(payload.uri);
}

Handle the cancellation idempotently on the other side, since the two events can arrive in either order.

Failure modes

Where Calendly integrations usually break

There is no dashboard for this

Calendly webhook subscriptions exist only through the API — `POST /webhook_subscriptions` with a personal access token. You will not find a Webhooks screen in the web app, and the feature requires a paid plan, which is a surprising amount of friction before the first delivery.

The signing key is a one-time reveal

The key is established when the subscription is created and Calendly will not show it to you again. Store it next to the subscription's URI, because rotating means deleting the subscription and creating a new one.

A reschedule is two events

Rescheduling produces an `invitee.canceled` for the old booking and an `invitee.created` for the new one, with `rescheduled: true` and `old_invitee` linking them. Code that treats a cancel as a lost customer will email a farewell to someone who simply moved the meeting.

It is Stripe's signature under another name

`t=…,v1=…` over `{t}.{body}`, hex HMAC-SHA256 — the same construction as Stripe, down to the comma and the dot. Any Stripe verifier works if you change which header it reads, which also means a copied verifier that still reads `stripe-signature` finds nothing and fails open if you are not careful.

Answers are positional, and forms change

`questions_and_answers` is an array in form order. Reading `[1].answer` breaks the day someone adds a question above it. Match on the `question` text, and handle the answer being absent.

Answers

Calendly webhook questions

How do I create a Calendly webhook?+

Through the API. POST to `/webhook_subscriptions` with a personal access token, the URL, the event list, a scope of `user` or `organization`, and the matching URI. There is no UI for it, and organization scope needs an admin token.

Can TryWebhook verify the Calendly signature?+

Not automatically. The algorithm is identical to Stripe's, but the Stripe verifier here reads the `stripe-signature` header and the custom verifier cannot pull a timestamp out of a signature header. The five lines you need are in the section below.

How do I test without booking a real meeting?+

Create a throwaway event type, set it to a short duration, and book yourself using a single-use link. It produces genuine deliveries with genuine signatures. Cancelling that booking then gives you an `invitee.canceled` to inspect as well.

Why did I get a cancellation for a meeting that still exists?+

Because it was rescheduled. Calendly cancels the old booking and creates a new one, and the pair is linked by `rescheduled: true` and `old_invitee` on the creation. Check those fields before treating a cancellation as final.

Where is the meeting link in the payload?+

Under `payload.scheduled_event.location`. Its shape depends on the type — `join_url` for a Google Meet or Zoom conference, a physical address for an in-person meeting, or a phone number. Always branch on `location.type` before reading anything else.

Other providers

Testing something else?

One click

Point Calendly at a URL and watch the payload land.

Calendly API — POST /webhook_subscriptions. There is no dashboard screen for this. — paste the URL, trigger an event, and read exactly what arrived.

No signup · No email · Ready in about a second