Find free time

Read busy blocks from one calendar, or find meeting slots that work across several providers.

You're building a booking page and need the times when both people are free, one on Google and the other on Outlook. That takes two steps: read when each calendar is busy, then find the gaps.

Busy blocks

getFreeBusy returns when each calendar is busy, without event details:

import { getFreeBusy } from '@cal-sdk/core';

const { calendars } = await getFreeBusy({
  calendar,
  calendarIds: ['primary', 'team@example.com'],
  timeMin: new Date(),
  timeMax: new Date(Date.now() + 7 * 86_400_000),
});

calendars.primary?.busy; // [{ start: '2026-11-09T08:30:00.000Z', end: '2026-11-09T08:45:00.000Z' }, ...]

Blocks come back merged, sorted and clipped to the window, so two overlapping meetings show up as one busy stretch. calendarIds defaults to ['primary']. A calendar that can't be read, for example one that isn't shared with the user, gets an error string instead of failing the whole call.

Only Google has a free/busy endpoint, which also works for calendars the user can only see as free or busy. For every other provider, calsdk lists the events and works out the busy times itself, with a compatibility warning. Either way, events shown as free and cancelled events don't count as busy.

Slots that work for everyone

findFreeTime does both steps for you. It takes calendars from any mix of providers and returns the times when all of them are free:

import { findFreeTime } from '@cal-sdk/core';
import { google } from '@cal-sdk/google';
import { microsoft } from '@cal-sdk/microsoft';

const { slots } = await findFreeTime({
  calendars: [
    { calendar: google({ accessToken: anaToken }) },
    { calendar: microsoft({ accessToken: rajToken }), calendarIds: ['primary', roomCalendarId] },
  ],
  timeMin: '2026-11-09T00:00:00Z',
  timeMax: '2026-11-14T00:00:00Z',
  duration: 30,
  workingHours: { start: '09:00', end: '17:00', timeZone: 'Europe/Berlin' },
});

// [{ start: '2026-11-09T08:00:00.000Z', end: '2026-11-09T08:30:00.000Z' }, ...]

duration is the meeting length in minutes. workingHours keeps slots inside those hours, Monday to Friday unless you pass days (0 is Sunday); leave it out to search the whole window. slotInterval is the minutes between slot starts, 15 by default, and slots line up with the hour in the working hours' time zone, so 15 gives :00, :15, :30 and :45.

Slots overlap when slotInterval is shorter than duration: a 60 minute meeting every 15 minutes gives 9:00 to 10:00, 9:15 to 10:15 and so on. On a booking page, show them as start times.

If any calendar can't be read, findFreeTime throws FreeBusyUnavailableError rather than offering times that might clash. A slot that ignores Raj's calendar is a double booking waiting to happen. The error has the provider and calendarId, but its message leaves out the id, which is often an email address.

Book the slot

A slot is a pair of UTC instants, so create the event in the zone you want it shown in:

const [slot] = slots;

if (slot) {
  await createEvent({
    calendar,
    event: {
      title: 'Intro call',
      start: { dateTime: slot.start, timeZone: 'Europe/Berlin' },
      end: { dateTime: slot.end, timeZone: 'Europe/Berlin' },
      attendees: [{ email: 'raj@example.com' }],
    },
  });
}

On this page