Zoom
Read, schedule, move and delete Zoom meetings, including recurring ones, as calendar events.
Use this provider when the meeting itself is the thing you're scheduling: a sales call, a client check-in, a weekly office hour. Zoom's scheduled meetings are the events, and every one has its Zoom link in conference. Recurring meetings work, within the patterns Zoom supports, but Zoom isn't a calendar, so there are no all-day events and listing is slower than you'd expect.
npm install @cal-sdk/core @cal-sdk/zoomimport { createEvent } from '@cal-sdk/core';
import { zoom } from '@cal-sdk/zoom';
const calendar = zoom({ accessToken: () => getFreshAccessToken() });
const { event } = await createEvent({
calendar,
event: {
title: 'Planning',
start: { dateTime: '2026-11-03T10:00:00', timeZone: 'Europe/Berlin' },
end: { dateTime: '2026-11-03T10:45:00', timeZone: 'Europe/Berlin' },
recurrence: ['RRULE:FREQ=WEEKLY;COUNT=6'],
},
});
event.conference?.joinUrl; // 'https://zoom.us/j/...'Credentials
accessToken is a Zoom OAuth access token with the scopes meeting:read and meeting:write. With granular scopes: meeting:read:meeting, meeting:read:list_meetings, meeting:write:meeting, meeting:update:meeting, meeting:delete:meeting and user:read:user.
For your own account, a Server-to-Server OAuth app is the simplest route. Its tokens last an hour, so pass a function that mints and caches them. calsdk asks it for a token before each request:
let cached: { token: string; expires: number } | undefined;
const calendar = zoom({
accessToken: async () => {
if (cached && cached.expires > Date.now()) return cached.token;
const response = await fetch(
`https://zoom.us/oauth/token?grant_type=account_credentials&account_id=${ZOOM_ACCOUNT_ID}`,
{
method: 'POST',
headers: { Authorization: `Basic ${btoa(`${ZOOM_CLIENT_ID}:${ZOOM_CLIENT_SECRET}`)}` },
},
);
const { access_token, expires_in } = await response.json();
cached = { token: access_token, expires: Date.now() + (expires_in - 60) * 1000 };
return access_token;
},
});Settings
- accessTokenstring | (() => string | PromiseLike<string>)
- OAuth access token, or a function that returns a fresh one.
- headers?Record<string, string | undefined>
- Headers sent with every request.
- fetch?typeof fetch
- A custom fetch, for example to log or record requests.
Not supported
All-day events, thisAndFollowing, ifMatch and search throw UnsupportedFunctionalityError. location, visibility, reminders and color are ignored with a warning, since a Zoom meeting takes place at its link, has no visibility or color, and gets the reminders the account is set up with. Free/busy is computed from meetings. See the comparison for every provider.
How it behaves
Recurring meetings
Zoom repeats meetings daily (every 1 to 99 days), weekly on set weekdays (every 1 to 50 weeks), or monthly on a day of the month or an nth weekday (every 1 to 10 months), for up to 60 meetings or until a date. Other rules throw UnsupportedFunctionalityError rather than saving a different schedule, as do rules whose days leave out the first meeting. Zoom's weeks start on Monday, so a rule repeating every few weeks on several days with another WKST throws too. Zoom keeps no list of excluded dates: delete occurrences one by one instead of passing EXDATE lines.
Zoom schedules no meetings for a series that starts years ahead, so calsdk would have nothing to show. Creating one, or moving a meeting or series that far, throws UnsupportedFunctionalityError, and calsdk deletes what Zoom created or undoes the change.
You can only move or resize one occurrence on its own. Other changes need scope: 'all', which changes the whole series, and moving an occurrence with scope: 'all' moves the series by as much. Moving a series makes Zoom schedule every meeting of it again, so occurrences you deleted, moved or resized on their own come back as they were, with ids for their new starts. The update returns a warning when that happens, so you can tell your user.
Calendars, ids and times
'primary' holds the meetings the user hosts, and its time zone is the user's Zoom time zone. An event id is the meeting id. An occurrence of a series is <meeting id>_<original start in UTC>, such as 84512345678_20261103T090000Z. Times are shown in the time zone the meeting was scheduled in, and meetings last whole minutes, up to 24 hours.
Invitees
Attendees are Zoom's meeting invitees, by email address only. getEvent returns them with the host as organizer. Listing and sync leave attendees and organizer out, as Zoom doesn't list them. Zoom doesn't email invitees about changes made through its API, so sendUpdates values other than 'none' return a warning.
Listing and sync
Listing reads every scheduled meeting and filters them, since Zoom can't list meetings by time. Each series takes a request of its own, made one at a time to stay under Zoom's rate limits, so an account with many recurring meetings lists slowly. Zoom also cuts agendas in listings to 250 characters and stops listing meetings some time after they end.
Zoom can't list meetings by when they changed either, so each sync reads every meeting and compares it with the last sync. All changes come on one page. Meetings Zoom stops listing after they end aren't reported as deleted.
Metadata
Zoom has no provider options. Events carry providerMetadata.zoom:
| Field | Type and notes |
|---|---|
uuid | string, which Zoom uses for past meetings and recordings |
occurrenceId | string, Zoom's occurrence_id, on occurrences of a series |