Skip to main content
Technology & EngineeringScheduling Services349 lines

Calendly

"Calendly API: scheduling links, event types, invitees, webhooks, organization management, OAuth, REST API"

Quick Summary18 lines
Calendly provides a polished scheduling experience through a REST API built around the concept of event types, scheduled events, and invitees. The API uses OAuth 2.0 for authentication and follows a resource-oriented design where every scheduling concept is a URI-addressable resource. Integrate Calendly when you want battle-tested scheduling with minimal setup -- the platform handles conflict detection, timezone conversion, buffer times, and calendar sync so your code does not have to.

## Key Points

1. **Store resource URIs, not just UUIDs** -- Calendly resources are identified by full URIs; store the complete URI to avoid rebuilding paths and risking version mismatches.
2. **Paginate all list endpoints** -- collection responses include `pagination.next_page_token`; always follow pagination to avoid missing records.
3. **Use webhook signing keys** -- always verify the `Calendly-Webhook-Signature` header with HMAC-SHA256 to reject forged payloads.
4. **Refresh tokens proactively** -- access tokens expire in 2 hours; schedule a refresh before expiry rather than waiting for a 401 and retrying.
5. **Scope webhooks narrowly** -- subscribe to only the events you handle; over-subscribing leads to ignored payloads and wasted compute.
6. **Respect rate limits** -- the API enforces per-token rate limits; implement exponential backoff on 429 responses and cache results where possible.
1. **Scraping scheduling pages instead of using the API** -- the HTML structure changes without notice; always use the REST API for programmatic access.
2. **Relying on event type names as identifiers** -- names can be changed by the user; use the URI as the stable identifier.
3. **Ignoring the `status` field on events** -- fetched events may already be canceled; always filter by status before acting on them.
4. **Creating one webhook per user in a multi-tenant app** -- use organization-scoped webhooks to receive events for all members under one subscription.
5. **Storing access tokens in frontend code** -- Calendly tokens grant account-level access; keep them server-side and proxy requests.
6. **Not handling webhook retries** -- Calendly retries failed deliveries; make your handler idempotent by deduplicating on the event URI.
skilldb get scheduling-services-skills/CalendlyFull skill: 349 lines
Paste into your CLAUDE.md or agent config

Calendly API Integration

Core Philosophy

Calendly provides a polished scheduling experience through a REST API built around the concept of event types, scheduled events, and invitees. The API uses OAuth 2.0 for authentication and follows a resource-oriented design where every scheduling concept is a URI-addressable resource. Integrate Calendly when you want battle-tested scheduling with minimal setup -- the platform handles conflict detection, timezone conversion, buffer times, and calendar sync so your code does not have to.

Setup

OAuth 2.0 Authentication

import axios from "axios";

interface CalendlyTokens {
  access_token: string;
  refresh_token: string;
  token_type: string;
  expires_in: number;
  created_at: number;
}

const CALENDLY_AUTH_BASE = "https://auth.calendly.com";
const CALENDLY_API_BASE = "https://api.calendly.com";

function getAuthorizationUrl(clientId: string, redirectUri: string): string {
  const params = new URLSearchParams({
    client_id: clientId,
    redirect_uri: redirectUri,
    response_type: "code",
  });
  return `${CALENDLY_AUTH_BASE}/oauth/authorize?${params}`;
}

async function exchangeCode(code: string): Promise<CalendlyTokens> {
  const response = await axios.post(`${CALENDLY_AUTH_BASE}/oauth/token`, {
    grant_type: "authorization_code",
    code,
    client_id: process.env.CALENDLY_CLIENT_ID,
    client_secret: process.env.CALENDLY_CLIENT_SECRET,
    redirect_uri: process.env.CALENDLY_REDIRECT_URI,
  });
  return response.data;
}

async function refreshAccessToken(refreshToken: string): Promise<CalendlyTokens> {
  const response = await axios.post(`${CALENDLY_AUTH_BASE}/oauth/token`, {
    grant_type: "refresh_token",
    refresh_token: refreshToken,
    client_id: process.env.CALENDLY_CLIENT_ID,
    client_secret: process.env.CALENDLY_CLIENT_SECRET,
  });
  return response.data;
}

API Client Setup

class CalendlyClient {
  private accessToken: string;

  constructor(accessToken: string) {
    this.accessToken = accessToken;
  }

  private async request<T>(method: string, path: string, data?: unknown): Promise<T> {
    const response = await axios({
      method,
      url: `${CALENDLY_API_BASE}${path}`,
      headers: {
        Authorization: `Bearer ${this.accessToken}`,
        "Content-Type": "application/json",
      },
      data,
    });
    return response.data;
  }

  async get<T>(path: string): Promise<T> {
    return this.request<T>("GET", path);
  }

  async post<T>(path: string, data: unknown): Promise<T> {
    return this.request<T>("POST", path, data);
  }

  async delete(path: string): Promise<void> {
    await this.request("DELETE", path);
  }
}

const client = new CalendlyClient(process.env.CALENDLY_ACCESS_TOKEN!);

Key Techniques

Fetching the Current User and Organization

interface CalendlyUser {
  uri: string;
  name: string;
  email: string;
  scheduling_url: string;
  timezone: string;
  current_organization: string;
}

async function getCurrentUser(client: CalendlyClient): Promise<CalendlyUser> {
  const data = await client.get<{ resource: CalendlyUser }>("/users/me");
  return data.resource;
}

// Organization members (admin scope required)
interface OrgMembership {
  uri: string;
  role: "owner" | "admin" | "user";
  user: { uri: string; name: string; email: string };
}

async function listOrgMembers(
  client: CalendlyClient,
  orgUri: string
): Promise<OrgMembership[]> {
  const params = new URLSearchParams({ organization: orgUri });
  const data = await client.get<{ collection: OrgMembership[] }>(
    `/organization_memberships?${params}`
  );
  return data.collection;
}

Working with Event Types

interface CalendlyEventType {
  uri: string;
  name: string;
  slug: string;
  active: boolean;
  duration: number;
  scheduling_url: string;
  kind: "solo" | "group";
  type: "StandardEventType" | "AdhocEventType";
}

async function listEventTypes(
  client: CalendlyClient,
  userUri: string
): Promise<CalendlyEventType[]> {
  const params = new URLSearchParams({
    user: userUri,
    active: "true",
  });
  const data = await client.get<{ collection: CalendlyEventType[] }>(
    `/event_types?${params}`
  );
  return data.collection;
}

Listing Scheduled Events and Invitees

interface ScheduledEvent {
  uri: string;
  name: string;
  status: "active" | "canceled";
  start_time: string;
  end_time: string;
  event_type: string;
  location: { type: string; location?: string; join_url?: string };
  created_at: string;
}

async function listScheduledEvents(
  client: CalendlyClient,
  userUri: string,
  minStartTime: string,
  maxStartTime: string
): Promise<ScheduledEvent[]> {
  const params = new URLSearchParams({
    user: userUri,
    min_start_time: minStartTime,
    max_start_time: maxStartTime,
    status: "active",
    sort: "start_time:asc",
  });
  const data = await client.get<{ collection: ScheduledEvent[] }>(
    `/scheduled_events?${params}`
  );
  return data.collection;
}

interface Invitee {
  uri: string;
  name: string;
  email: string;
  status: "active" | "canceled";
  timezone: string;
  questions_and_answers: Array<{ question: string; answer: string }>;
  created_at: string;
}

async function listInvitees(
  client: CalendlyClient,
  eventUri: string
): Promise<Invitee[]> {
  // Extract UUID from the event URI
  const eventUuid = eventUri.split("/").pop();
  const data = await client.get<{ collection: Invitee[] }>(
    `/scheduled_events/${eventUuid}/invitees`
  );
  return data.collection;
}

Webhook Subscriptions

interface WebhookSubscription {
  uri: string;
  callback_url: string;
  events: string[];
  scope: "user" | "organization";
  state: "active" | "disabled";
}

async function createWebhookSubscription(
  client: CalendlyClient,
  orgUri: string,
  userUri: string,
  callbackUrl: string
): Promise<WebhookSubscription> {
  const data = await client.post<{ resource: WebhookSubscription }>(
    "/webhook_subscriptions",
    {
      url: callbackUrl,
      events: [
        "invitee.created",
        "invitee.canceled",
        "routing_form_submission.created",
      ],
      organization: orgUri,
      user: userUri,
      scope: "user",
      signing_key: process.env.CALENDLY_WEBHOOK_SECRET,
    }
  );
  return data.resource;
}

Handling Webhook Events

import express from "express";
import crypto from "crypto";

const app = express();

function verifyCalendlySignature(
  payload: string,
  signature: string,
  signingKey: string,
  tolerance: number = 300
): boolean {
  const [t, v1] = signature.split(",").reduce(
    (acc, part) => {
      const [key, value] = part.split("=");
      if (key === "t") acc[0] = value;
      if (key === "v1") acc[1] = value;
      return acc;
    },
    ["", ""]
  );

  const timestampAge = Math.floor(Date.now() / 1000) - parseInt(t, 10);
  if (timestampAge > tolerance) return false;

  const signedPayload = `${t}.${payload}`;
  const expected = crypto
    .createHmac("sha256", signingKey)
    .update(signedPayload)
    .digest("hex");

  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

app.post("/webhooks/calendly", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["calendly-webhook-signature"] as string;
  if (!verifyCalendlySignature(req.body.toString(), signature, process.env.CALENDLY_WEBHOOK_SECRET!)) {
    return res.status(401).send("Invalid signature");
  }

  const event = JSON.parse(req.body.toString());

  switch (event.event) {
    case "invitee.created":
      console.log(`New booking by ${event.payload.name} (${event.payload.email})`);
      break;
    case "invitee.canceled":
      console.log(`Cancellation by ${event.payload.email}`);
      break;
  }

  res.status(200).json({ ok: true });
});

Cancellation and No-Show

async function cancelEvent(client: CalendlyClient, eventUuid: string, reason?: string): Promise<void> {
  await client.post(`/scheduled_events/${eventUuid}/cancellation`, {
    reason: reason ?? "Canceled via API",
  });
}

async function markNoShow(client: CalendlyClient, inviteeUri: string): Promise<void> {
  await client.post("/invitee_no_shows", {
    invitee: inviteeUri,
  });
}

Best Practices

  1. Store resource URIs, not just UUIDs -- Calendly resources are identified by full URIs; store the complete URI to avoid rebuilding paths and risking version mismatches.
  2. Paginate all list endpoints -- collection responses include pagination.next_page_token; always follow pagination to avoid missing records.
  3. Use webhook signing keys -- always verify the Calendly-Webhook-Signature header with HMAC-SHA256 to reject forged payloads.
  4. Refresh tokens proactively -- access tokens expire in 2 hours; schedule a refresh before expiry rather than waiting for a 401 and retrying.
  5. Scope webhooks narrowly -- subscribe to only the events you handle; over-subscribing leads to ignored payloads and wasted compute.
  6. Respect rate limits -- the API enforces per-token rate limits; implement exponential backoff on 429 responses and cache results where possible.

Anti-Patterns

  1. Scraping scheduling pages instead of using the API -- the HTML structure changes without notice; always use the REST API for programmatic access.
  2. Relying on event type names as identifiers -- names can be changed by the user; use the URI as the stable identifier.
  3. Ignoring the status field on events -- fetched events may already be canceled; always filter by status before acting on them.
  4. Creating one webhook per user in a multi-tenant app -- use organization-scoped webhooks to receive events for all members under one subscription.
  5. Storing access tokens in frontend code -- Calendly tokens grant account-level access; keep them server-side and proxy requests.
  6. Not handling webhook retries -- Calendly retries failed deliveries; make your handler idempotent by deduplicating on the event URI.

Install this skill directly: skilldb add scheduling-services-skills

Get CLI access →