Skip to main content

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

  1. An OAuth connection from Orbit to your Calendly account, with the API key Orbit uses to call Calendly stored on the tenant connection record.
  2. 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.
  3. The HMAC-verified receiver itself — contact upserts, event persistence, and the tenant webhook fan-out (calendly.invitee.created and siblings).
  4. 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-level event 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)

Field-by-field on the invitee payload:
  • 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 on invitee.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-complete hook fires; it fetches your Calendly user + organization URIs and registers a webhook subscription.
What connect-complete registers, exactly:
  1. 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.
  2. A per-tenant signing_key, minted at registration and stored on your connection record — inbound events verify against it.
  3. 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 hits POST /api/v1/integrations/webhooks/calendly?org=<orgId>, mounted ahead of Orbit’s usual auth because the HMAC is the auth. The receiver, in order:
  1. Tenant resolution. Looks up the org from ?org=<orgId> and reads the stored calendly_signing_key off that tenant’s connection record.
  2. Signature parse. Calendly-Webhook-Signature carries t=<unix-seconds>,v1=<hex> — the timestamp and the expected digest.
  3. Replay window. t must sit within 3 minutes of now (Calendly’s documented replay-protection threshold). A stale timestamp is rejected.
  4. HMAC. Computes HMAC-SHA256("<t>.<raw-body>", signing_key) and compares it to v1 with a constant-time compare.
  5. 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.”
The worked HMAC, exactly the way the receiver computes it — useful for reproducing a delivery in a sandbox:
Note the hex digest — not base64, which is the common mistake when reproducing this against the Shopify sibling receiver.

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.
That flow definition is illustrative — the production flow form is in Five flow recipes with the Calendly verbs substituted.

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, with duplicate: true — Calendly halts its retry chain and your webhook subscriptions skip a second fire.
A missing 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= returns 503 WEBHOOK_NOT_PROVISIONED. Both are expected fail-closed behavior.
  • Webhook subscription includes the Calendly verbs — Settings → Webhooks carries calendly.invitee.created and calendly.invitee.canceled at minimum.
  • Dedupe path observed — re-deliver the same event (Calendly’s resend control) and observe duplicate: true with 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.