Skip to main content
Technology & EngineeringScheduling Services275 lines

Cal.com

"Cal.com: open-source scheduling, booking API, event types, availability, webhooks, embeds, self-hosted"

Quick Summary18 lines
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 lines
Paste into your CLAUDE.md or agent config

Cal.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

  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.

Anti-Patterns

  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.

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

Get CLI access →