Cronofy
"Cronofy: calendar API, availability, scheduling, real-time sync, conferencing, UI elements, enterprise calendar integration"
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 linesCronofy 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
- Use the
subidentifier for availability queries -- Cronofy'ssubis the stable cross-profile identifier; it works even when a user connects multiple calendar providers. - Set
tzidexplicitly on all time parameters -- never rely on server defaults; passEtc/UTCand convert in your application layer for consistency. - 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. - Scope notification channels to specific calendars -- unscoped channels fire on every change across all connected calendars, which can be noisy in enterprise deployments.
- Rotate element tokens per session -- element tokens are short-lived by design; generate a fresh one each time a user loads your scheduling UI.
- Handle profile disconnections -- users can revoke calendar access at any time; listen for
profile_disconnectednotifications and surface re-auth prompts in your UI.
Anti-Patterns
- Querying availability without buffer times -- back-to-back meetings frustrate users; always include
buffer.beforeandbuffer.afterin availability requests. - Ignoring the
transparencyfield -- events marked "transparent" (e.g., all-day reminders) should not block availability; filter them out or let Cronofy handle it via the availability API. - 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.
- Using personal access tokens in production -- personal tokens are for development only; use OAuth tokens scoped to each user's connected calendar profiles.
- 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.
- 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
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"
Calendly
"Calendly API: scheduling links, event types, invitees, webhooks, organization management, OAuth, REST API"
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"