Calendly Integration
Receive and route Calendly webhooks for bookings, cancellations, no-shows and routing form submissions.
Setup
1. Create a Source in Hookbase
curl -X POST https://api.hookbase.app/api/sources \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Calendly Production",
"slug": "calendly",
"provider": "calendly",
"signingSecret": "your-calendly-signing-key"
}'Save your webhook URL:
https://api.hookbase.app/ingest/{orgSlug}/calendly2. Create the Calendly Webhook Subscription
Calendly webhook subscriptions are created through its API.
POST https://api.calendly.com/webhook_subscriptions- Set
urlto your Hookbase ingest URL - Set
scopetoorganizationoruser - List the events you want in the
eventsarray — for exampleinvitee.created,invitee.canceled,routing_form_submission.created - Copy
signing_keyout of the response and store it on your Hookbase source assigningSecret
Creating a subscription requires a Calendly Standard, Teams or Enterprise plan and either a personal access token or an OAuth token. See Calendly's webhook documentation for the request shape.
Info
signing_key is returned once, in the create response. If you did not keep it, delete the
subscription and create a new one — a new subscription issues a new key, and the old one stops being
the value Calendly signs with.
3. Create Destinations and Routes
# Create a destination
curl -X POST https://api.hookbase.app/api/destinations \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "CRM Sync", "slug": "calendly-crm", "url": "https://api.myapp.com/webhooks/calendly"}'
# Create a route
curl -X POST https://api.hookbase.app/api/routes \
-H "Authorization: Bearer whr_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name": "Calendly to CRM", "sourceId": "src_...", "destinationId": "dst_..."}'Signature Verification
Calendly signs a timestamp and the body together, not the body alone. The
Calendly-Webhook-Signature header carries both parts as comma-separated key=value pairs:
Calendly-Webhook-Signature: t=1700000000,v1=6f3d2c1b0a9988776655443322110ffeeddccbbaa99887766554433221100ffev1 is the hex HMAC-SHA256 of the string {t}.{body} — the timestamp, a literal dot, then the raw
request body — keyed with your signing_key.
Hookbase verifies this automatically once the source has both fields set:
{
"provider": "calendly",
"signingSecret": "your-calendly-signing-key"
}Info
The timestamp is checked, not just signed. Hookbase rejects a Calendly signature whose t is more
than 180 seconds away from the receiving clock — the three-minute tolerance Calendly's own
documentation names. A correctly signed request that arrives late, or is replayed later, does not
verify.
Once events are arriving with signature_valid: true, set rejectInvalidSignatures: true on the
source to have unverified requests refused with 401 instead of stored.
Common Events
| Event | Description |
|---|---|
invitee.created | A new invitee scheduled an event |
invitee.canceled | An invitee or host canceled a scheduled event |
invitee_no_show.created | A host marked an invitee as a no-show |
invitee_no_show.deleted | A no-show marking was removed from an invitee |
routing_form_submission.created | Someone submitted a routing form, whether or not they went on to book |
Invitee Created
{
"created_at": "2024-01-15T10:30:00.000000Z",
"created_by": "https://api.calendly.com/users/AAAAAAAAAAAAAAAA",
"event": "invitee.created",
"payload": {
"cancel_url": "https://calendly.com/cancellations/BBBBBBBBBBBBBBBB",
"created_at": "2024-01-15T10:30:00.000000Z",
"email": "jane@example.com",
"event": "https://api.calendly.com/scheduled_events/CCCCCCCCCCCCCCCC",
"name": "Jane Doe",
"new_invitee": null,
"old_invitee": null,
"questions_and_answers": [
{
"question": "What would you like to discuss?",
"answer": "Onboarding walkthrough",
"position": 0
}
],
"reschedule_url": "https://calendly.com/reschedulings/BBBBBBBBBBBBBBBB",
"rescheduled": false,
"status": "active",
"text_reminder_number": null,
"timezone": "America/New_York",
"tracking": {
"utm_source": null,
"utm_campaign": null,
"salesforce_uuid": null
},
"updated_at": "2024-01-15T10:30:00.000000Z",
"uri": "https://api.calendly.com/scheduled_events/CCCCCCCCCCCCCCCC/invitees/BBBBBBBBBBBBBBBB"
}
}Invitee Canceled
{
"created_at": "2024-01-16T09:00:00.000000Z",
"created_by": "https://api.calendly.com/users/AAAAAAAAAAAAAAAA",
"event": "invitee.canceled",
"payload": {
"cancel_url": "https://calendly.com/cancellations/BBBBBBBBBBBBBBBB",
"cancellation": {
"canceled_by": "Jane Doe",
"canceler_type": "invitee",
"reason": "Schedule conflict"
},
"created_at": "2024-01-15T10:30:00.000000Z",
"email": "jane@example.com",
"event": "https://api.calendly.com/scheduled_events/CCCCCCCCCCCCCCCC",
"name": "Jane Doe",
"reschedule_url": "https://calendly.com/reschedulings/BBBBBBBBBBBBBBBB",
"status": "canceled",
"timezone": "America/New_York",
"updated_at": "2024-01-16T09:00:00.000000Z",
"uri": "https://api.calendly.com/scheduled_events/CCCCCCCCCCCCCCCC/invitees/BBBBBBBBBBBBBBBB"
}
}Transform Examples
A javascript transform receives the parsed payload and returns the body Hookbase delivers.
CRM Contact Shape
function transform(payload) {
const p = payload.payload;
return {
event: payload.event,
email: p.email,
name: p.name,
timezone: p.timezone,
status: p.status,
scheduled_event_url: p.event,
invitee_uri: p.uri,
answers: (p.questions_and_answers || []).map(qa => ({
question: qa.question,
answer: qa.answer
})),
utm_source: p.tracking ? p.tracking.utm_source : null,
occurred_at: payload.created_at
};
}Slack Alert on Cancellation
function transform(payload) {
const p = payload.payload;
const c = p.cancellation || {};
return {
text: `Booking canceled: ${p.name}`,
blocks: [
{
type: "section",
fields: [
{ type: "mrkdwn", text: `*Invitee:*\n${p.name} (${p.email})` },
{ type: "mrkdwn", text: `*Canceled by:*\n${c.canceled_by || "unknown"}` },
{ type: "mrkdwn", text: `*Reason:*\n${c.reason || "none given"}` }
]
}
]
};
}Filter Examples
Filter conditions read dotted paths out of the payload. logic must be AND or OR.
New Bookings Only
{
"name": "New Bookings",
"logic": "AND",
"conditions": [
{
"field": "event",
"operator": "equals",
"value": "invitee.created"
}
]
}Bookings or Cancellations, Ignoring Routing Forms
{
"name": "Booking Lifecycle",
"logic": "OR",
"conditions": [
{
"field": "event",
"operator": "equals",
"value": "invitee.created"
},
{
"field": "event",
"operator": "equals",
"value": "invitee.canceled"
}
]
}Only Invitees From a Campaign
{
"name": "Campaign Bookings",
"logic": "AND",
"conditions": [
{
"field": "payload.tracking.utm_campaign",
"operator": "exists",
"value": ""
}
]
}Headers
| Header | Description |
|---|---|
Calendly-Webhook-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256 of "{t}.{body}"> |
Hookbase does not forward these headers to your destination. A delivery is a fresh request:
Hookbase sets Content-Type, User-Agent: Hookbase/1.0, X-Delivery-ID and X-Event-ID, then
adds the headers and auth you configured on the destination. Nothing Calendly sent reaches your
handler as a header — read what you need out of the body, or set it on the destination yourself.
Of the incoming headers, only content-type, user-agent, x-github-event, x-gitlab-event and
stripe-signature are stored on the event, so those are the only ones visible later in the
dashboard or the API.
Troubleshooting
Signature Verification Failed
- Confirm the source's
provideriscalendly— a source left oncustomreadsX-Signature,X-Webhook-SignatureandX-Hub-Signature-256, and Calendly sends its own header - Confirm
signingSecretis the subscription'ssigning_key, not a personal access token - Check the clock: the signed timestamp must be within 180 seconds. A delivery that sat in a retry queue for longer will not verify even though it is genuine
- Confirm you are not replaying a captured request — outside the tolerance, a real signature is still refused, which is the point of the timestamp
Missing Events
- Check the subscription's
eventsarray actually lists the event you expect - Check the subscription
scope— auser-scoped subscription sees only that user's bookings - Calendly subscriptions are per-organization or per-user; a second team needs its own
Duplicate Events
Hookbase has no provider event-id extractor for Calendly, so the default auto dedup strategy falls
back to hashing the payload. Use payload.uri in your handler if you need an idempotency key that
survives a payload edit.