Skip to main content
Technology & EngineeringScheduling Services400 lines

Cronofy

"Cronofy: calendar API, availability, scheduling, real-time sync, conferencing, UI elements, enterprise calendar integration"

Quick Summary17 lines
Cronofy provides enterprise-grade calendar connectivity -- it synchronizes with Google Calendar, Outlook, Exchange, iCloud, and others through a single normalized API. The platform excels at availability queries across multiple participants and calendar providers, making it ideal for scheduling workflows where participants use different calendar systems. Cronofy separates the concerns of calendar sync, availability computation, and scheduling into distinct API surfaces, letting you compose exactly the workflow your product needs.

## Key Points

1. **Use the `sub` identifier for availability queries** -- Cronofy's `sub` is the stable cross-profile identifier; it works even when a user connects multiple calendar providers.
2. **Set `tzid` explicitly on all time parameters** -- never rely on server defaults; pass `Etc/UTC` and convert in your application layer for consistency.
3. **Use upsert semantics for events** -- the POST events endpoint creates or updates based on `event_id`; use deterministic IDs derived from your domain objects to avoid duplicates.
4. **Scope notification channels to specific calendars** -- unscoped channels fire on every change across all connected calendars, which can be noisy in enterprise deployments.
5. **Rotate element tokens per session** -- element tokens are short-lived by design; generate a fresh one each time a user loads your scheduling UI.
6. **Handle profile disconnections** -- users can revoke calendar access at any time; listen for `profile_disconnected` notifications and surface re-auth prompts in your UI.
1. **Querying availability without buffer times** -- back-to-back meetings frustrate users; always include `buffer.before` and `buffer.after` in availability requests.
2. **Ignoring the `transparency` field** -- events marked "transparent" (e.g., all-day reminders) should not block availability; filter them out or let Cronofy handle it via the availability API.
3. **Storing calendar data long-term without re-sync** -- external calendar changes are not reflected in your local copy unless you process push notifications or re-query periodically.
4. **Using personal access tokens in production** -- personal tokens are for development only; use OAuth tokens scoped to each user's connected calendar profiles.
6. **Embedding element tokens in public pages** -- element tokens grant access to the user's calendar data; only serve them to authenticated users in your application.
skilldb get scheduling-services-skills/CronofyFull skill: 400 lines
Paste into your CLAUDE.md or agent config

Cronofy Calendar Integration API

Core Philosophy

Cronofy provides enterprise-grade calendar connectivity -- it synchronizes with Google Calendar, Outlook, Exchange, iCloud, and others through a single normalized API. The platform excels at availability queries across multiple participants and calendar providers, making it ideal for scheduling workflows where participants use different calendar systems. Cronofy separates the concerns of calendar sync, availability computation, and scheduling into distinct API surfaces, letting you compose exactly the workflow your product needs.

Setup

Authentication and Client Initialization

// npm install cronofy

interface CronofyConfig {
  clientId: string;
  clientSecret: string;
  dataCenter: "us" | "eu" | "au";
}

class CronofyClient {
  private baseUrl: string;
  private accessToken: string;

  constructor(accessToken: string, dataCenter: "us" | "eu" | "au" = "us") {
    this.accessToken = accessToken;
    const dcPrefix = dataCenter === "us" ? "" : `${dataCenter}.`;
    this.baseUrl = `https://api.${dcPrefix}cronofy.com`;
  }

  async request<T>(method: string, path: string, body?: unknown): Promise<T> {
    const response = await fetch(`${this.baseUrl}${path}`, {
      method,
      headers: {
        Authorization: `Bearer ${this.accessToken}`,
        "Content-Type": "application/json",
      },
      body: body ? JSON.stringify(body) : undefined,
    });

    if (!response.ok) {
      const error = await response.text();
      throw new Error(`Cronofy API error ${response.status}: ${error}`);
    }

    if (response.status === 204) return undefined as T;
    return response.json();
  }
}

OAuth 2.0 Flow

import express from "express";

const CRONOFY_AUTH_URL = "https://app.cronofy.com/oauth/authorize";
const CRONOFY_TOKEN_URL = "https://api.cronofy.com/oauth/token";

function getAuthUrl(clientId: string, redirectUri: string, scope: string[]): string {
  const params = new URLSearchParams({
    client_id: clientId,
    redirect_uri: redirectUri,
    response_type: "code",
    scope: scope.join(" "),
  });
  return `${CRONOFY_AUTH_URL}?${params}`;
}

interface CronofyTokenResponse {
  access_token: string;
  refresh_token: string;
  expires_in: number;
  token_type: string;
  account_id: string;
  sub: string;
  linking_profile: {
    provider_name: string;
    profile_id: string;
    profile_name: string;
  };
}

async function exchangeAuthCode(code: string): Promise<CronofyTokenResponse> {
  const response = await fetch(CRONOFY_TOKEN_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      client_id: process.env.CRONOFY_CLIENT_ID,
      client_secret: process.env.CRONOFY_CLIENT_SECRET,
      grant_type: "authorization_code",
      code,
      redirect_uri: process.env.CRONOFY_REDIRECT_URI,
    }),
  });
  return response.json();
}

async function refreshToken(refreshToken: string): Promise<CronofyTokenResponse> {
  const response = await fetch(CRONOFY_TOKEN_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      client_id: process.env.CRONOFY_CLIENT_ID,
      client_secret: process.env.CRONOFY_CLIENT_SECRET,
      grant_type: "refresh_token",
      refresh_token: refreshToken,
    }),
  });
  return response.json();
}

Key Techniques

Reading and Writing Calendar Events

interface CronofyEvent {
  calendar_id: string;
  event_uid: string;
  summary: string;
  description?: string;
  start: string;   // ISO 8601
  end: string;
  location?: { description: string };
  attendees?: Array<{ email: string; display_name?: string; status: string }>;
  transparency: "opaque" | "transparent";
}

async function listEvents(
  client: CronofyClient,
  from: string,
  to: string,
  calendarIds?: string[]
): Promise<CronofyEvent[]> {
  const params: Record<string, string> = {
    from,
    to,
    tzid: "Etc/UTC",
  };
  if (calendarIds) {
    params.calendar_ids = JSON.stringify(calendarIds);
  }

  const query = new URLSearchParams(params).toString();
  const data = await client.request<{ events: CronofyEvent[] }>("GET", `/v1/events?${query}`);
  return data.events;
}

async function upsertEvent(
  client: CronofyClient,
  calendarId: string,
  eventId: string,
  event: {
    summary: string;
    description?: string;
    start: string;
    end: string;
    location?: string;
    attendees?: Array<{ email: string; display_name?: string }>;
  }
) {
  await client.request("POST", `/v1/calendars/${calendarId}/events`, {
    event_id: eventId,
    summary: event.summary,
    description: event.description,
    start: { time: event.start, tzid: "Etc/UTC" },
    end: { time: event.end, tzid: "Etc/UTC" },
    location: event.location ? { description: event.location } : undefined,
    attendees: event.attendees
      ? { invite: event.attendees }
      : undefined,
  });
}

async function deleteEvent(client: CronofyClient, calendarId: string, eventId: string) {
  await client.request("DELETE", `/v1/calendars/${calendarId}/events`, {
    event_id: eventId,
  });
}

Availability Queries

interface AvailabilityPeriod {
  start: string;
  end: string;
  participants: Array<{ sub: string }>;
}

interface AvailabilityQuery {
  participants: Array<{
    members: Array<{ sub: string; calendar_ids?: string[] }>;
    required: "all" | number;
  }>;
  requiredDuration: { minutes: number };
  availablePeriods: Array<{ start: string; end: string }>;
  responseFormat?: "overlapping_slots" | "periods";
  buffer?: { before: { minutes: number }; after: { minutes: number } };
}

async function queryAvailability(
  client: CronofyClient,
  query: AvailabilityQuery
): Promise<AvailabilityPeriod[]> {
  const data = await client.request<{ available_periods: AvailabilityPeriod[] }>(
    "POST",
    "/v1/availability",
    {
      participants: query.participants,
      required_duration: query.requiredDuration,
      available_periods: query.availablePeriods,
      response_format: query.responseFormat ?? "periods",
      buffer: query.buffer,
    }
  );
  return data.available_periods;
}

// Find 30-minute slots where all three participants are free next week
const nextMonday = "2026-03-23T09:00:00Z";
const nextFriday = "2026-03-27T17:00:00Z";

const slots = await queryAvailability(client, {
  participants: [
    {
      members: [
        { sub: "acc_001" },
        { sub: "acc_002" },
        { sub: "acc_003" },
      ],
      required: "all",
    },
  ],
  requiredDuration: { minutes: 30 },
  availablePeriods: [{ start: nextMonday, end: nextFriday }],
  buffer: { before: { minutes: 10 }, after: { minutes: 5 } },
});

Real-Time Scheduling with UI Elements

// Cronofy provides embeddable UI components via their Elements library
// Generate an element token server-side, then embed client-side

async function createElementToken(
  client: CronofyClient,
  sub: string,
  permissions: string[],
  origin: string
): Promise<string> {
  const data = await client.request<{ element_token: { token: string } }>(
    "POST",
    "/v1/element_tokens",
    {
      version: "1",
      permissions,
      subs: [sub],
      origin,
    }
  );
  return data.element_token.token;
}

// Server endpoint to generate a token for the scheduling UI
const app = express();

app.get("/api/cronofy-token", async (req, res) => {
  const token = await createElementToken(
    client,
    req.session.cronofySub,
    ["availability", "account_management"],
    process.env.APP_URL!
  );
  res.json({ token });
});

Conferencing Integration

// Create a conferencing profile connection
async function getConferencingProfiles(client: CronofyClient) {
  const data = await client.request<{
    conferencing_profiles: Array<{
      provider_name: string;
      profile_id: string;
      profile_name: string;
    }>;
  }>("GET", "/v1/conferencing_profiles");
  return data.conferencing_profiles;
}

// Add conferencing to an event during creation
async function createEventWithConferencing(
  client: CronofyClient,
  calendarId: string,
  eventId: string,
  summary: string,
  start: string,
  end: string,
  conferencingProfileId: string
) {
  await client.request("POST", `/v1/calendars/${calendarId}/events`, {
    event_id: eventId,
    summary,
    start: { time: start, tzid: "Etc/UTC" },
    end: { time: end, tzid: "Etc/UTC" },
    conferencing: {
      profile_id: conferencingProfileId,
    },
  });
}

Push Notifications (Webhooks)

interface CronofyChannel {
  channel_id: string;
  callback_url: string;
  filters: { calendar_ids?: string[]; only_managed?: boolean };
}

async function createNotificationChannel(
  client: CronofyClient,
  callbackUrl: string,
  calendarIds?: string[]
): Promise<CronofyChannel> {
  const data = await client.request<{ channel: CronofyChannel }>(
    "POST",
    "/v1/channels",
    {
      callback_url: callbackUrl,
      filters: calendarIds ? { calendar_ids: calendarIds } : {},
    }
  );
  return data.channel;
}

// Handle incoming push notifications
app.post("/webhooks/cronofy", express.json(), (req, res) => {
  const notification = req.body;

  switch (notification.type) {
    case "change":
      // A calendar changed -- fetch updated events
      console.log(`Calendar changed for account: ${notification.account_id}`);
      syncEventsForAccount(notification.account_id);
      break;
    case "profile_disconnected":
      console.log(`Profile disconnected: ${notification.profile_id}`);
      handleDisconnect(notification.account_id);
      break;
    case "verification":
      // Initial verification ping
      break;
  }

  res.status(200).send();
});

async function syncEventsForAccount(accountId: string): Promise<void> {
  // Re-fetch events for this account and update local cache
  const now = new Date();
  const twoWeeksOut = new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000);
  const events = await listEvents(
    client,
    now.toISOString(),
    twoWeeksOut.toISOString()
  );
  console.log(`Synced ${events.length} events for account ${accountId}`);
}

Best Practices

  1. Use the sub identifier for availability queries -- Cronofy's sub is the stable cross-profile identifier; it works even when a user connects multiple calendar providers.
  2. Set tzid explicitly on all time parameters -- never rely on server defaults; pass Etc/UTC and convert in your application layer for consistency.
  3. Use upsert semantics for events -- the POST events endpoint creates or updates based on event_id; use deterministic IDs derived from your domain objects to avoid duplicates.
  4. Scope notification channels to specific calendars -- unscoped channels fire on every change across all connected calendars, which can be noisy in enterprise deployments.
  5. Rotate element tokens per session -- element tokens are short-lived by design; generate a fresh one each time a user loads your scheduling UI.
  6. Handle profile disconnections -- users can revoke calendar access at any time; listen for profile_disconnected notifications and surface re-auth prompts in your UI.

Anti-Patterns

  1. Querying availability without buffer times -- back-to-back meetings frustrate users; always include buffer.before and buffer.after in availability requests.
  2. Ignoring the transparency field -- events marked "transparent" (e.g., all-day reminders) should not block availability; filter them out or let Cronofy handle it via the availability API.
  3. Storing calendar data long-term without re-sync -- external calendar changes are not reflected in your local copy unless you process push notifications or re-query periodically.
  4. Using personal access tokens in production -- personal tokens are for development only; use OAuth tokens scoped to each user's connected calendar profiles.
  5. Building custom availability logic -- Cronofy's availability endpoint handles multi-participant, cross-provider scheduling with buffers and required-participant semantics; reimplementing this logic is error-prone.
  6. Embedding element tokens in public pages -- element tokens grant access to the user's calendar data; only serve them to authenticated users in your application.

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

Get CLI access →