Connect Calendly with Zoom

Implementation Guide

Overview: Connecting Calendly and Zoom

The Calendly-to-Zoom integration automates the complete meeting scheduling lifecycle by eliminating the manual step of creating Zoom meeting links after a Calendly booking is confirmed. Without this integration, the workflow requires a meeting organiser to receive a Calendly booking confirmation, open a separate browser tab to access Zoom, manually create a new meeting for the correct date and time, copy the generated join URL, navigate back to an email client or CRM, and send the Zoom link to the invitee—a multi-step process that is both operationally inefficient and error-prone under high scheduling volume. With the integration active, this entire sequence is fully automated: the moment an invitee selects a time slot in Calendly, the integration creates a corresponding Zoom meeting with the correct start time and duration, stores the Zoom meeting ID for future reference, and makes the join URL available for downstream distribution to confirmation emails, CRM records, and calendar invitations.

The business impact of this integration is most acutely felt in organisations running high-volume scheduling workflows: sales teams conducting dozens of discovery calls per day, customer success teams managing onboarding sessions, recruiting teams scheduling back-to-back interviews, and support teams handling technical consultations. In each of these contexts, a missing or delayed Zoom link translates to direct revenue and experience impact. Automating the conferencing link creation and cleanup lifecycle removes an entire category of operational error and frees coordinators to focus on meeting preparation rather than meeting logistics.

Core Prerequisites

For Calendly, webhook functionality and the v2 REST API are only available on paid plans: Professional, Teams, or Enterprise. The free Basic tier does not support programmatic webhook subscriptions. Authentication is handled via a Personal Access Token (PAT) for single-user integrations, or via an OAuth 2.0 application for multi-user platform deployments. To generate a PAT, navigate to Calendly > Integrations & Apps > API & Webhooks and create a new token. The token must be included in all API requests as the Authorization header value in the format Bearer {personal_access_token}. Before registering a webhook subscription, retrieve your organisation URI by calling GET https://api.calendly.com/users/me and extracting the current_organization field from the response body—this URI is required as the organization parameter when creating the webhook subscription. For OAuth 2.0 application registration, visit developer.calendly.com and request the default scope, which covers all Calendly API operations for the authorised user.

For Zoom, the recommended credential type for server-side integration is a Server-to-Server OAuth App (also called an account-level app), available in the Zoom Marketplace at marketplace.zoom.us. Create a new Server-to-Server OAuth app and note the Account ID, Client ID, and Client Secret values. Activate the app after creation—it will remain in a deactivated state until explicitly activated by an account admin. The required API scopes to add to the app are: meeting:write:admin (to create and update meetings on behalf of account users), meeting:read:admin (to retrieve meeting details), and user:read:admin (to look up the host user ID by email). Server-to-Server OAuth apps authenticate by posting to https://zoom.us/oauth/token with grant_type=account_credentials and a Basic Auth header constructed from the Client ID and Client Secret. Access tokens issued through this flow have a 1-hour TTL and do not come with refresh tokens—when the token expires, request a new one using the same account_credentials grant.

Top Enterprise Use Cases

The primary use case is fully automated Zoom meeting creation triggered by Calendly booking confirmation. The moment a Calendly invitee.created webhook event fires, the integration extracts the scheduled start time, duration (derived from the event type), and invitee details from the payload, creates a Zoom meeting via the Zoom API with those parameters, and stores the Zoom meeting_id and join_url in a persistent data store keyed on the Calendly event UUID. The join_url is then available to any downstream system—CRM deal records, custom confirmation email flows, or notification webhooks—that needs to communicate it to the invitee.

The second critical use case is automated meeting cancellation and resource cleanup. When an invitee cancels a Calendly booking, an invitee.canceled event fires. The integration uses the Calendly event UUID to look up the corresponding Zoom meeting_id in the data store, calls the Zoom Delete Meeting API to remove the orphaned meeting, and marks the data store record as cancelled. Without this cleanup step, cancelled Calendly bookings accumulate as ghost meetings in the host's Zoom account, degrading the usability of the Zoom meeting list and potentially confusing attendees who retained old join links.

A third use case is reschedule handling with atomic meeting recreation. When an invitee reschedules a Calendly appointment, Calendly fires an invitee.canceled event for the original time slot immediately followed by a new invitee.created event for the rescheduled time. The integration must handle both events in sequence: on the cancel event, delete the old Zoom meeting and mark the data store record as superseded; on the create event, create a new Zoom meeting for the new time and update all downstream systems with the new join URL. This requires idempotent, event-ordered processing—a message queue such as AWS SQS or a database-backed job queue is recommended for production reliability.

Step-by-Step Implementation Guide

The integration is driven by Calendly's outbound webhook system. To register a webhook subscription that covers all scheduling events for your Calendly organisation, issue the following API call with your PAT:

curl -X POST "https://api.calendly.com/webhook_subscriptions" \
  -H "Authorization: Bearer {personal_access_token}" \
  -H "Content-Type: application/json" \
  -d "{
    \"url\": \"https://your-integration.example.com/webhooks/calendly\",
    \"events\": [\"invitee.created\", \"invitee.canceled\"],
    \"organization\": \"https://api.calendly.com/organizations/{org_uuid}\",
    \"scope\": \"organization\",
    \"signing_key\": \"your-32-character-minimum-signing-key\"
  }"

The signing_key value is used by Calendly to compute an HMAC-SHA256 signature for each delivered webhook request. This signature is included in the Calendly-Webhook-Signature header of every incoming POST request in the format t={timestamp},v1={signature}. To validate the signature on your server, extract the timestamp and signature values, compute HMAC-SHA256(signing_key, "v1:" + timestamp + "." + raw_request_body_as_string), and compare the result to the v1 value. Reject any request where the signatures do not match or where the timestamp is more than 5 minutes in the past, as this prevents replay attacks.

A Calendly invitee.created webhook payload has the following structure, which contains all the data needed to construct the Zoom meeting:

{
  "event": "invitee.created",
  "payload": {
    "event_type": {
      "uuid": "AAABBB111222",
      "kind": "One-on-One",
      "slug": "30min-discovery-call",
      "name": "30 Min Discovery Call",
      "duration": 30,
      "owner": {
        "type": "User",
        "uuid": "USERXYZ987"
      }
    },
    "event": {
      "uuid": "GHIJKL789012",
      "start_time": "2025-10-21T14:00:00.000000Z",
      "end_time": "2025-10-21T14:30:00.000000Z",
      "location": { "type": "zoom", "status": "pending" }
    },
    "invitee": {
      "uuid": "MNOPQR345678",
      "email": "[email protected]",
      "name": "Jane Smith",
      "timezone": "America/New_York"
    }
  }
}

Using the start_time, duration, and invitee data from this payload, your integration creates a Zoom meeting by authenticating first (requesting a Server-to-Server OAuth access token) and then calling the Zoom Meetings Create API:

curl -X POST "https://api.zoom.us/v2/users/{host_user_id}/meetings" \
  -H "Authorization: Bearer {zoom_access_token}" \
  -H "Content-Type: application/json" \
  -d "{
    \"topic\": \"30 Min Discovery Call with Jane Smith\",
    \"type\": 2,
    \"start_time\": \"2025-10-21T14:00:00Z\",
    \"duration\": 30,
    \"timezone\": \"UTC\",
    \"settings\": {
      \"host_video\": true,
      \"participant_video\": true,
      \"join_before_host\": false,
      \"waiting_room\": true,
      \"mute_upon_entry\": true,
      \"auto_recording\": \"none\"
    }
  }"

The Zoom API response body contains id (the numeric Zoom meeting ID), join_url (the participant join link), and start_url (the authenticated host start link). Persist the id, join_url, and start_url values in your data store with the Calendly event.uuid as the primary key. This stored record is essential for the cancellation and reschedule flows.

To obtain the {host_user_id} required in the URL path, call GET https://api.zoom.us/v2/users?status=active and find the user whose email matches the Calendly event type owner's email address. Alternatively, call GET https://api.zoom.us/v2/users/me if the integration operates under a single host account, which returns the authenticated account's Zoom user ID directly.

In Make, the scenario for this integration begins with a Webhooks > Custom Webhook trigger module. Register the Make-generated webhook URL in Calendly's webhook subscription configuration. Add a JSON > Parse JSON module to deserialise the payload body and expose nested fields as individual variables. Use a Router module with two branches: one for invitee.created events and one for invitee.canceled events. On the create branch, add an HTTP > Make a Request module to POST to https://api.zoom.us/v2/users/{userId}/meetings with the Zoom Bearer token in the Authorization header and the meeting parameters mapped from the Calendly payload. Add a Data Store > Add/Replace a Record module to persist the Zoom meeting ID keyed on the Calendly event UUID. On the cancel branch, add a Data Store > Get a Record module to retrieve the stored meeting ID, followed by an HTTP > Make a Request module that issues a DELETE to https://api.zoom.us/v2/meetings/{meetingId}.

In Zapier, use the Calendly > Invitee Created trigger and Zoom > Create Meeting action. The Zapier Calendly integration exposes Start Time, End Time, Event Duration, Invitee Name, and Invitee Email as mapped fields from the trigger, making the Zoom meeting creation step straightforward. For cancellation handling, create a separate Zap with the Calendly > Invitee Canceled trigger and a Zoom > Delete Meeting action. Use Zapier Storage (or a Code by Zapier step querying an external data store) to retrieve the Zoom meeting ID that was saved when the original booking was created.

Common Pitfalls & Troubleshooting

A 401 Unauthorized from the Zoom API with error body {"code": 124, "message": "Invalid access token."} means the Server-to-Server OAuth access token has expired. Unlike standard OAuth flows, Server-to-Server apps do not issue refresh tokens—simply request a new access token using the same account_credentials grant and retry the failed operation. Implement an access token cache with an expiry of 55 minutes (5 minutes before the actual 60-minute TTL) to minimise unnecessary token request overhead.

A 404 Not Found response when attempting to DELETE a Zoom meeting via DELETE https://api.zoom.us/v2/meetings/{meetingId} indicates the meeting no longer exists in Zoom—either it was deleted manually by the host or the meeting ID stored in your data layer is incorrect. Treat 404 responses on deletion as a successfully achieved goal state: the desired outcome (no meeting exists) is satisfied regardless of whether the API deleted it or it was already absent. Log the discrepancy for audit purposes but do not raise an error that would halt the cancellation workflow.

A 429 Too Many Requests from Zoom's API indicates you have exceeded the rate limit for the calling account. Zoom's Meetings API enforces a limit of 100 meeting creation requests per day for Basic accounts, with higher thresholds for Pro and Business plan accounts. For high-volume scheduling deployments, ensure the Zoom account used by the integration is on an appropriate plan tier. The 429 response includes a Retry-After header; implement exponential backoff in your retry logic with a minimum initial delay equal to the Retry-After value.

Calendly webhook signature validation failures are among the most frequently reported implementation issues and almost universally stem from the same root cause: the request body being parsed by a middleware layer before the raw bytes are read for HMAC computation. In Node.js with Express, calling express.json() as global middleware before your signature validation handler transforms the body from a raw Buffer into a parsed JavaScript object, and JSON.stringify of that object will not exactly reproduce the original byte sequence that Calendly signed. Solve this by using express.raw({ type: "application/json" }) for the Calendly webhook route specifically, reading the raw Buffer, computing the HMAC against that buffer, and only then parsing the JSON for business logic.