Calendly integration
Orbit’s Calendly integration turns bookings into messaging. When an invitee books, cancels, or no-shows, Calendly sends the event to a webhook receiver Orbit registers on your account — and Orbit upserts the invitee as a contact, persists the event for audit, and fans a tenant webhook out to your booking flows: SMS confirmations, reschedule nudges, no-show recovery. This page is the operator walkthrough for the whole loop. The composite runbook that covers Calendly alongside Zendesk, Jira, DocuSign, and the connectors lives at Connect Zendesk, Jira, Calendly, DocuSign, Slack, Zapier, and n8n — this page is the deeper, single-provider version the booking-flow operator actually works from.The connect step is owner/admin only — it’s the only step that touches provider credentials. After OAuth, the subscription registration, signing-key storage, and idempotent event handling are automatic.
What you’ll set up
- An OAuth connection from Orbit to your Calendly account, with the API key Orbit uses to call Calendly stored on the tenant connection record.
- A Calendly webhook subscription Orbit registers at connect-complete, pointing at
POST /api/v1/integrations/webhooks/calendly?org=<orgId>with a per-tenant signing key. - The HMAC-verified receiver itself — contact upserts, event persistence, and the tenant webhook fan-out (
calendly.invitee.createdand siblings). - A worked mapping:
invitee.created→ SMS confirmation,invitee.canceled→ SMS apology with a reschedule link.
1. What Calendly sends to Orbit
Calendly’s v2 webhook envelope carries a top-levelevent discriminator plus a payload object. Orbit recognises four event types:
The receiver’s discriminator is strict — an event type outside this catalog is persisted for audit but never dispatched to your webhook subscriptions, so the wire can only carry the four catalog verbs.
Payload shape — invitee.created (the one you build on)
payload.uri— the invitee URI, Orbit’s stable id for the booking. Used for dedupe and as the contact’s Calendly linkage.payload.email— join key #1 onto an Orbit contact.payload.text_reminder_number— join key #2 (the phone the booker filled on Calendly’s SMS-reminder field). E.164.payload.first_name/payload.last_name/payload.name— contact display fields.payload.event— the scheduled-event URI (the meeting itself); Orbit stores it on the contact’s Calendly attributes.payload.cancellation.reason/.canceler_type— present oninvitee.canceled; stamped onto the contact’s attributes for no-show recovery gating.payload.questions_and_answers— routing-form / booking-form answers; carried in the audit row.
2. Connect — OAuth and the webhook subscription
Step order. In Orbit, go to Settings → Integrations, find Calendly, and click Connect. Calendly’s consent screen opens; authorize the scopes. On the OAuth callback, Orbit’s connect-complete step runs automatically — the operator never registers the subscription by hand. Screen annotations, as text:- Settings → Integrations, Calendly card. Button reads Connect. After OAuth completes the card flips to Connected.
- Calendly consent screen (provider-side). Lists the requested scopes (read user + organization, register webhooks). Confirm with your Calendly account.
- Redirect back to Orbit. The
integrations/calendly/connect-completehook fires; it fetches your Calendly user + organization URIs and registers a webhook subscription.
- The webhook URL —
POST https://api.orbit.devotel.io/api/v1/integrations/webhooks/calendly?org=<orgId>. The?org=<orgId>query string routes inbound deliveries to your tenant. Calendly preserves it on every delivery. - A per-tenant
signing_key, minted at registration and stored on your connection record — inbound events verify against it. - The event set — the four verbs in the table above.
After clicking Connect and authorizing, open Settings → Integrations again — Calendly shows Connected. Then book a real test meeting on an event type your Calendly account owns with a test email; the invitee appears as an Orbit contact (provider: calendly) and a
calendly.invitee.created tenant webhook fires on your configured outbound endpoint.3. Signature verification — the HMAC receiver
Every inbound Calendly event hitsPOST /api/v1/integrations/webhooks/calendly?org=<orgId>, mounted ahead of Orbit’s usual auth because the HMAC is the auth. The receiver, in order:
- Tenant resolution. Looks up the org from
?org=<orgId>and reads the storedcalendly_signing_keyoff that tenant’s connection record. - Signature parse.
Calendly-Webhook-Signaturecarriest=<unix-seconds>,v1=<hex>— the timestamp and the expected digest. - Replay window.
tmust sit within 3 minutes of now (Calendly’s documented replay-protection threshold). A stale timestamp is rejected. - HMAC. Computes
HMAC-SHA256("<t>.<raw-body>", signing_key)and compares it tov1with a constant-time compare. - Fail closed. Missing signing key →
503 WEBHOOK_NOT_PROVISIONED(the subscription was never registered, e.g. connect-complete never ran). Bad signature, stale timestamp, or malformed header →401 INVALID_SIGNATURE. The two failure classes are deliberately different statuses so your monitoring can distinguish “we never wired the subscription” from “sender sent a bad sig.”
4. Map events to Orbit flows — the booking-confirmation worked example
Two patterns that ship out of the tenant webhook fan-out. Both trigger off the contact row the invitee upsert created, so they address the person who actually booked rather than a one-off recipient. Pattern A:invitee.created → SMS booking confirmation. Trigger a flow on the calendly.invitee.created event. The fan-out payload carries invitee_email, the scheduled-event URI, and the upserted contact_id. Read the meeting start time + location off the contact’s Calendly attributes, then send the confirmation SMS to the phone the booker gave on Calendly’s text_reminder_number field. End with a CDR log point so the confirmation send joins the message history. This is the bookings→SMS confirmation loop operators actually wire up — run it through the standard flow recipes shape with the Calendly event as the trigger.
Pattern B: invitee.canceled → SMS apology + reschedule link. Trigger a second flow on calendly.invitee.canceled. The message body includes a reschedule link — typically a CTA back to your Calendly booking page. Because the cancellation payload’s canceler_type sits on the contact’s Calendly attributes, gate the apology: a canceler-type of host (your side canceled) should get a different copy than an invitee-initiated cancel. End with the survey/callback alternative if the cancellation reason says the meeting was no longer needed.
5. Idempotency and dedupe
Calendly retries events up to 24 hours after a 5xx, and re-sends after network blips. The receiver turns those retries into no-ops at three layers:- Event uniqueness. Event rows persisted on
(event_type, event_uri)with on-conflict-do-nothing. Retried deliveries of the same invitee URI insert no duplicate row. - Dedupe on dispatch. Only a fresh (not duplicate) event fans out to your tenant webhooks, so retry bursts don’t re-trigger the SMS confirmation.
- Response
duplicate: true. On a retry the receiver still returns 200, withduplicate: true— Calendly halts its retry chain and your webhook subscriptions skip a second fire.
payload.uri (vanishingly rare) is accepted with a synthetic non-dedupable id, so the row always lands for audit — the alternative would be dropping an event that was genuinely new.
6. Troubleshooting
7. Production checklist
Run this before flipping traffic to a live Calendly account:- Connected as owner/admin — the Calendly card on Settings → Integrations reads Connected.
- Test event verified — a real test booking on a Calendly event type you own produced a contact (provider: calendly) and the tenant webhook fired.
- Signature path confirmed — a forged POST with a wrong HMAC returns
401 INVALID_SIGNATURE; an un-provisioned tenant’s?org=returns503 WEBHOOK_NOT_PROVISIONED. Both are expected fail-closed behavior. - Webhook subscription includes the Calendly verbs — Settings → Webhooks carries
calendly.invitee.createdandcalendly.invitee.canceledat minimum. - Dedupe path observed — re-deliver the same event (Calendly’s resend control) and observe
duplicate: truewith no second SMS. - Cancellation flow gated on canceler-type — a host-canceled booking doesn’t send the same copy as an invitee-cancel.
- Phone capture on the booking form — Calendly’s SMS-reminder field is the join key #2; if your booking form doesn’t capture it, contact linkage falls back to email-only.
Related reading
- Integrations API reference — endpoint map for connect, status, disconnect, and the Calendly webhook receiver.
- Connect Zendesk, Jira, Calendly, DocuSign, Slack, Zapier, and n8n — the composite runbook that covers Calendly alongside the other six.
- Five flow recipes — the flow-definition shapes the booking-confirmation pattern plugs into.
- Webhook event catalog — the full tenant webhook verb catalog,
calendly.*included. - Inbound webhook debugging — general route for tracing inbound receivers.