# Google Calendar (https://www.cal-sdk.com/docs/providers/google)

Google is the most complete provider: it supports everything calsdk models, including splitting a recurring series with `thisAndFollowing`, Meet links, search and a real free/busy endpoint. If your users are mostly on Google Workspace or Gmail, this is the one you'll spend the least time working around.

```npm
npm install @cal-sdk/core @cal-sdk/google
```

```ts
import { listEvents } from '@cal-sdk/core';
import { google } from '@cal-sdk/google';

const calendar = google({ accessToken: () => getFreshAccessToken() });

const { events } = await listEvents({
  calendar,
  timeMin: '2026-11-01T00:00:00Z',
  timeMax: '2026-12-01T00:00:00Z',
});
```

## Credentials [#credentials]

`accessToken` is an OAuth access token with a Calendar scope, such as `https://www.googleapis.com/auth/calendar`, or `calendar.readonly` if you only read. calsdk doesn't run the OAuth flow, since your app almost certainly has an auth library already. Get tokens there and pass a function that returns a fresh one: calsdk calls it before every request, so a token that expires mid-sync isn't a problem.

## Settings [#settings]

<TypeTable
  type={{
    accessToken: { type: 'string | (() => string | PromiseLike<string>)', description: 'OAuth access token, or a function that returns a fresh one.', required: true },
    baseURL: { type: 'string', default: "'https://www.googleapis.com/calendar/v3'" },
    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.' },
  }}
/>

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

### Splitting a series [#splitting-a-series]

Google's API has no "this and following" operation, so calsdk splits the series itself for `thisAndFollowing`. It starts a new series at the chosen instance, copying attendees and the meeting link, and ends the old one just before it. A `COUNT` is split between the two, and `EXDATE` and `RDATE` lines go to the half they fall in. If ending the old series fails, calsdk deletes the new one so you're not left with both.

One case can't be done safely. A series with a `COUNT` and `EXDATE` or `RDATE` lines before the split throws `UnsupportedFunctionalityError`, because calsdk can't count the occurrences that are left. Give the series an `UNTIL` first.

### Moving a series [#moving-a-series]

A new `start` with scope `all` or `thisAndFollowing` moves the series on the wall clock of its time zone, so a 9:00 standup stays at 9:00 across daylight saving changes. Rules that pick days of the month or year can't be moved this way and throw `UnsupportedFunctionalityError`. You also can't change a series' time zone through one of its instances: update the series id instead.

Google resets single-instance changes when a series moves or splits. calsdk keeps the occurrences you deleted, as `EXDATE`s, but edited ones lose their changes, and you get a `compatibility` warning.

When reading, Google's expanded listings don't say which instances of a series were edited. To tell an `exception` from a plain `occurrence`, calsdk makes one extra request per page that has recurring instances.

### Meet links and colors [#meet-links-and-colors]

`createConference: true` adds a Google Meet link. Colors are less flexible: Google has a fixed palette of 11 event colors, not arbitrary hex values, so if you pass another color, calsdk saves the nearest one and returns a `compatibility` warning.

### Free/busy and sync [#freebusy-and-sync]

Free/busy uses Google's own endpoint, so events marked as free are ignored, and calendars the user can only see as free/busy still work. Sync uses Google sync tokens. An expired one throws `SyncCursorExpiredError`, and you start over with a full sync.

## Provider options [#provider-options]

```ts
await createEvent({
  calendar,
  event,
  providerOptions: {
    google: {
      guestsCanModify: true,
      guestsCanInviteOthers: false,
      guestsCanSeeOtherGuests: false,
      extendedProperties: { private: { orderId: '42' } },
    },
  },
});

await listEvents({
  calendar,
  timeMin,
  timeMax,
  providerOptions: { google: { eventTypes: ['default'] } },
});
```

### createEvent and updateEvent [#createevent-and-updateevent]

<TypeTable
  type={{
    guestsCanModify: { type: 'boolean', description: 'Guests can edit the event.' },
    guestsCanInviteOthers: { type: 'boolean', description: 'Guests can invite other people.' },
    guestsCanSeeOtherGuests: { type: 'boolean', description: 'Guests can see the guest list.' },
    extendedProperties: { type: '{ private?: Record<string, string>; shared?: Record<string, string> }', description: '`private` is only visible on this calendar.' },
  }}
/>

### listEvents [#listevents]

<TypeTable
  type={{
    eventTypes: { type: "('birthday' | 'default' | 'focusTime' | 'fromGmail' | 'outOfOffice' | 'workingLocation')[]", description: 'Only return these types. By default Google returns every type.' },
  }}
/>

## Metadata [#metadata]

Events carry `providerMetadata.google`:

| Field                | Type and notes |
| -------------------- | -------- |
| `iCalUID`            | `string` |
| `eventType`          | `string`, such as `'default'`, `'outOfOffice'` or `'focusTime'` |
| `extendedProperties` | `{ private?: Record<string, string>; shared?: Record<string, string> }` |
