# Zoom (https://www.cal-sdk.com/docs/providers/zoom)

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
npm install @cal-sdk/core @cal-sdk/zoom
```

```ts
import { 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 [#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:

```ts
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 [#settings]

<TypeTable
  type={{
    accessToken: { type: 'string | (() => string | PromiseLike<string>)', description: 'OAuth access token, or a function that returns a fresh one.', required: true },
    headers: { type: 'Record<string, string | undefined>', description: 'Headers sent with every request.' },
    fetch: { type: 'typeof fetch', description: 'A custom fetch, for example to log or record requests.' },
  }}
/>

## Not supported [#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](/docs/providers) for every provider.

## How it behaves [#how-it-behaves]

### Recurring meetings [#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 [#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 [#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-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 [#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 |
