Cal.com
"Cal.com: open-source scheduling, booking API, event types, availability, webhooks, embeds, self-hosted"
Cal.com is an open-source scheduling infrastructure that gives developers full control over booking flows. Unlike closed-source alternatives, Cal.com can be self-hosted, white-labeled, and deeply customized. The API-first design means every UI action has an equivalent API call, enabling headless scheduling experiences. Build on top of Cal.com when you need scheduling that lives inside your product rather than redirecting users to a third-party page. ## Key Points 1. **Use API versioning headers** -- always send `cal-api-version` to pin your integration to a known schema and avoid breaking changes on upgrade. 2. **Validate webhook signatures** -- never trust incoming webhook payloads without HMAC verification; attackers can forge booking events. 3. **Set availability schedules per event type** -- avoid a single global schedule; different meeting types often have different time windows. 4. **Use metadata fields** -- attach your internal IDs (customer ID, deal ID) to bookings via metadata so downstream systems can correlate without extra lookups. 5. **Cache availability results** -- slot queries can be expensive on self-hosted instances; cache results for 30-60 seconds to reduce database load. 6. **Prefer embeds over redirects** -- keeping users in your app with the embed React component reduces drop-off compared to external booking links. 1. **Polling for booking changes** -- use webhooks instead of repeatedly calling the bookings list endpoint; polling wastes resources and introduces latency. 2. **Hardcoding event type IDs** -- IDs change across environments; look up event types by slug at startup or use configuration files. 3. **Ignoring time zones** -- always pass the attendee's time zone explicitly; relying on server-default time zones causes off-by-hours booking errors. 4. **Skipping idempotency on booking creation** -- network retries can create duplicate bookings; use the `idempotencyKey` field to prevent this. 5. **Storing API keys in client-side code** -- Cal.com API keys grant full account access; always proxy requests through your backend. 6. **Running self-hosted without a reverse proxy** -- exposing the Next.js server directly skips TLS termination, rate limiting, and header security that nginx or Caddy provide.
skilldb get scheduling-services-skills/Cal.comFull skill: 275 linesCal.com Scheduling Platform
Core Philosophy
Cal.com is an open-source scheduling infrastructure that gives developers full control over booking flows. Unlike closed-source alternatives, Cal.com can be self-hosted, white-labeled, and deeply customized. The API-first design means every UI action has an equivalent API call, enabling headless scheduling experiences. Build on top of Cal.com when you need scheduling that lives inside your product rather than redirecting users to a third-party page.
Setup
Installation and API Configuration
// Install the Cal.com SDK
// npm install @calcom/sdk
import { CalSdk } from "@calcom/sdk";
const cal = new CalSdk({
apiKey: process.env.CAL_API_KEY,
baseUrl: process.env.CAL_BASE_URL || "https://api.cal.com/v2",
});
// Verify connectivity
async function verifyConnection(): Promise<void> {
const me = await cal.me.get();
console.log(`Connected as: ${me.name} (${me.email})`);
}
Self-Hosted Setup with Docker
// docker-compose.yml configuration for self-hosted Cal.com
// Use environment variables to configure your instance
interface CalEnvConfig {
DATABASE_URL: string;
NEXTAUTH_SECRET: string;
CALENDSO_ENCRYPTION_KEY: string;
NEXT_PUBLIC_WEBAPP_URL: string;
NEXT_PUBLIC_API_V2_URL: string;
}
function buildCalEnv(domain: string): CalEnvConfig {
return {
DATABASE_URL: `postgresql://cal:${process.env.DB_PASSWORD}@db:5432/calcom`,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET!,
CALENDSO_ENCRYPTION_KEY: process.env.ENCRYPTION_KEY!,
NEXT_PUBLIC_WEBAPP_URL: `https://${domain}`,
NEXT_PUBLIC_API_V2_URL: `https://${domain}/api/v2`,
};
}
Key Techniques
Managing Event Types
interface CreateEventTypePayload {
title: string;
slug: string;
lengthInMinutes: number;
description?: string;
locations?: Array<{ type: string; address?: string; link?: string }>;
bookingFields?: Array<{
name: string;
type: "text" | "email" | "phone" | "select";
required: boolean;
options?: string[];
}>;
}
async function createEventType(payload: CreateEventTypePayload) {
const response = await fetch("https://api.cal.com/v2/event-types", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CAL_API_KEY}`,
"Content-Type": "application/json",
"cal-api-version": "2024-06-14",
},
body: JSON.stringify({
title: payload.title,
slug: payload.slug,
lengthInMinutes: payload.lengthInMinutes,
description: payload.description,
locations: payload.locations ?? [{ type: "integrations:google:meet" }],
bookingFields: payload.bookingFields ?? [],
}),
});
return response.json();
}
// Create a 30-minute consultation event
await createEventType({
title: "Technical Consultation",
slug: "tech-consult",
lengthInMinutes: 30,
locations: [
{ type: "integrations:google:meet" },
{ type: "integrations:zoom" },
],
bookingFields: [
{ name: "company", type: "text", required: true },
{ name: "topic", type: "select", required: true, options: ["Bug", "Feature", "Architecture"] },
],
});
Querying Availability
interface AvailabilitySlot {
start: string;
end: string;
}
async function getAvailability(
username: string,
eventTypeId: number,
startDate: string,
endDate: string
): Promise<AvailabilitySlot[]> {
const params = new URLSearchParams({
username,
eventTypeId: eventTypeId.toString(),
startTime: startDate,
endTime: endDate,
});
const response = await fetch(
`https://api.cal.com/v2/slots/available?${params}`,
{
headers: {
"cal-api-version": "2024-06-14",
},
}
);
const data = await response.json();
return data.data.slots;
}
// Find open slots for the next 7 days
const now = new Date();
const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
const slots = await getAvailability(
"jane-doe",
12345,
now.toISOString(),
nextWeek.toISOString()
);
Creating Bookings Programmatically
interface BookingRequest {
eventTypeId: number;
start: string;
attendee: { name: string; email: string; timeZone: string };
metadata?: Record<string, string>;
}
async function createBooking(req: BookingRequest) {
const response = await fetch("https://api.cal.com/v2/bookings", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CAL_API_KEY}`,
"Content-Type": "application/json",
"cal-api-version": "2024-06-14",
},
body: JSON.stringify({
eventTypeId: req.eventTypeId,
start: req.start,
attendee: req.attendee,
metadata: req.metadata ?? {},
}),
});
return response.json();
}
Webhook Integration
import express from "express";
import crypto from "crypto";
const app = express();
function verifyCalWebhook(payload: string, signature: string, secret: string): boolean {
const hmac = crypto.createHmac("sha256", secret);
hmac.update(payload);
const digest = hmac.digest("hex");
return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(signature));
}
app.post("/webhooks/cal", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-cal-signature-256"] as string;
if (!verifyCalWebhook(req.body.toString(), signature, process.env.CAL_WEBHOOK_SECRET!)) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body.toString());
switch (event.triggerEvent) {
case "BOOKING_CREATED":
console.log(`New booking: ${event.payload.title} at ${event.payload.startTime}`);
break;
case "BOOKING_RESCHEDULED":
console.log(`Rescheduled: ${event.payload.uid}`);
break;
case "BOOKING_CANCELLED":
console.log(`Cancelled: ${event.payload.uid}`);
break;
}
res.status(200).json({ received: true });
});
Embedding Cal.com in Your App
// React embed component using Cal.com's embed snippet
// npm install @calcom/embed-react
import Cal, { getCalApi } from "@calcom/embed-react";
import { useEffect } from "react";
function SchedulingEmbed({ eventSlug, userName }: { eventSlug: string; userName: string }) {
useEffect(() => {
(async () => {
const cal = await getCalApi();
cal("ui", {
theme: "light",
styles: { branding: { brandColor: "#4f46e5" } },
hideEventTypeDetails: false,
});
})();
}, []);
return (
<Cal
calLink={`${userName}/${eventSlug}`}
config={{ layout: "month_view" }}
style={{ width: "100%", height: "100%", overflow: "scroll" }}
/>
);
}
Best Practices
- Use API versioning headers -- always send
cal-api-versionto pin your integration to a known schema and avoid breaking changes on upgrade. - Validate webhook signatures -- never trust incoming webhook payloads without HMAC verification; attackers can forge booking events.
- Set availability schedules per event type -- avoid a single global schedule; different meeting types often have different time windows.
- Use metadata fields -- attach your internal IDs (customer ID, deal ID) to bookings via metadata so downstream systems can correlate without extra lookups.
- Cache availability results -- slot queries can be expensive on self-hosted instances; cache results for 30-60 seconds to reduce database load.
- Prefer embeds over redirects -- keeping users in your app with the embed React component reduces drop-off compared to external booking links.
Anti-Patterns
- Polling for booking changes -- use webhooks instead of repeatedly calling the bookings list endpoint; polling wastes resources and introduces latency.
- Hardcoding event type IDs -- IDs change across environments; look up event types by slug at startup or use configuration files.
- Ignoring time zones -- always pass the attendee's time zone explicitly; relying on server-default time zones causes off-by-hours booking errors.
- Skipping idempotency on booking creation -- network retries can create duplicate bookings; use the
idempotencyKeyfield to prevent this. - Storing API keys in client-side code -- Cal.com API keys grant full account access; always proxy requests through your backend.
- Running self-hosted without a reverse proxy -- exposing the Next.js server directly skips TLS termination, rate limiting, and header security that nginx or Caddy provide.
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
Calendly
"Calendly API: scheduling links, event types, invitees, webhooks, organization management, OAuth, REST API"
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"