Calendly
Calendly API: scheduling links, event types, invitees, webhooks, organization management, OAuth, REST API
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: 348 linesCalendly 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
- 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.
- Paginate all list endpoints -- collection responses include
pagination.next_page_token; always follow pagination to avoid missing records. - Use webhook signing keys -- always verify the
Calendly-Webhook-Signatureheader with HMAC-SHA256 to reject forged payloads. - Refresh tokens proactively -- access tokens expire in 2 hours; schedule a refresh before expiry rather than waiting for a 401 and retrying.
- Scope webhooks narrowly -- subscribe to only the events you handle; over-subscribing leads to ignored payloads and wasted compute.
- Respect rate limits -- the API enforces per-token rate limits; implement exponential backoff on 429 responses and cache results where possible.
Anti-Patterns
- Scraping scheduling pages instead of using the API -- the HTML structure changes without notice; always use the REST API for programmatic access.
- Relying on event type names as identifiers -- names can be changed by the user; use the URI as the stable identifier.
- Ignoring the
statusfield on events -- fetched events may already be canceled; always filter by status before acting on them. - Creating one webhook per user in a multi-tenant app -- use organization-scoped webhooks to receive events for all members under one subscription.
- Storing access tokens in frontend code -- Calendly tokens grant account-level access; keep them server-side and proxy requests.
- 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
Related Skills
Cronofy
Cronofy: calendar API, availability, scheduling, real-time sync, conferencing, UI elements, enterprise calendar integration
Doodle
Doodle API integration for group scheduling polls, 1:1 booking pages, and meeting coordination
Microsoft Bookings
Microsoft Bookings integration via Microsoft Graph API for enterprise appointment scheduling and calendar management
Nylas
Nylas: unified calendar/email/contacts API, scheduling, calendar CRUD, email send/read, OAuth providers, Node SDK
Savvycal
SavvyCal API integration for personalized scheduling links, availability overlays, and calendar coordination
Acuity Scheduling
Acuity Scheduling API integration for appointment booking, availability management, and calendar sync