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: 349 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
Acuity Scheduling
Acuity Scheduling API integration for appointment booking, availability management, and calendar sync
Cal.com
"Cal.com: open-source scheduling, booking API, event types, availability, webhooks, embeds, self-hosted"
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"