Incremental sync
Run a full event sync and fetch incremental changes and deletions with nextSyncToken.
Calendarpit implements Google Calendar's full and incremental synchronization flow for events.list. The flow has two steps:
- Run an initial full list to get a
nextSyncToken. - Pass that token as
syncTokenon later requests. You get only the events created, updated, or deleted since that checkpoint.
See Google's sync guide and events.list reference for protocol details.
Full synchronization
Every full events.list call (not events.instances) issues a sync token, whether or not you pass showDeleted=true.
Pagination
- Follow every
nextPageTokenuntil the final page. - Send the same query parameters with each
pageToken. Changing a parameter returns HTTP410(invalidPageToken). - Calendarpit returns
nextSyncTokenonly on the last page, never alongsidenextPageToken. SavenextSyncTokenafter you process all pages. - Page rows and the sync snapshot are frozen together on the first page. Event writes during pagination appear in the next incremental sync.
Unbounded recurring series
With singleEvents=true, always set timeMax on the full sync.
An unbounded recurring series is an RRULE without COUNT or UNTIL. When the calendar contains one, these requests fail:
singleEvents=true: A token issued withouttimeMaxreturns HTTP400(unsupportedFeature). Run a new full sync withtimeMax.singleEvents=false: A fullevents.listwithtimeMinand notimeMaxis also rejected (see Events, recurrence and time). Incremental syncs withsingleEvents=falseare not affected.
A deleted unbounded series still counts. Its tombstone stays in the calendar until a reset or fixture load.
// api is the Google SDK client configured with Calendarpit's rootUrl.
const query = {
calendarId: 'primary',
singleEvents: true,
showDeleted: true,
timeMin: '2026-10-01T00:00:00Z',
timeMax: '2026-11-01T00:00:00Z',
};
let pageToken: string | undefined;
let syncToken: string | undefined;
do {
const { data } = await api.events.list({ ...query, pageToken });
for (const event of data.items ?? []) {
if (event.status === 'cancelled') await removeLocalEvent(event.id!);
else await upsertLocalEvent(event);
}
pageToken = data.nextPageToken ?? undefined;
if (!pageToken) syncToken = data.nextSyncToken ?? undefined;
} while (pageToken);Incremental synchronization
After a watch notification or on a poll timer, call events.list with syncToken. You can paginate incremental results with maxResults and pageToken.
const previousToken = syncToken;
do {
const { data } = await api.events.list({
calendarId: 'primary',
singleEvents: true,
showDeleted: true,
syncToken: previousToken,
pageToken,
});
for (const event of data.items ?? []) {
if (event.status === 'cancelled') await removeLocalEvent(event.id!);
else await upsertLocalEvent(event);
}
pageToken = data.nextPageToken ?? undefined;
if (!pageToken) syncToken = data.nextSyncToken ?? undefined;
} while (pageToken);Query rules
Requests that break these rules fail with HTTP 400 (invalidParameter):
- Forbidden parameters: Do not combine
syncTokenwithtimeMin,timeMax,orderBy,updatedMin,privateExtendedProperty,sharedExtendedProperty, oriCalUID.events.instancesdoes not supportsyncToken. - Deleted events: Do not pass
showDeleted=false. Incremental sync always includes deletions. PassshowDeleted=trueor omitshowDeleted. - Matching expansion options:
singleEventsand the effectivetimeZonemust match the initial sync. The effectivetimeZoneis thetimeZoneparameter, or the calendar's time zone when you omit it. - Time zone changes: Changing a calendar's time zone in the admin UI or management API invalidates tokens issued without an explicit
timeZoneparameter (HTTP400).
Delta contents and behavior
-
Retries: Sync tokens are not consumed on use. If a request fails, you can retry with the same
syncToken. -
New token on every sync: Each incremental sync returns a new
nextSyncTokenon its last page. -
Empty deltas: When nothing changed,
itemsis[]. A newnextSyncTokenis still returned. -
Tombstones: Deleted events are kept as tombstones and returned with
status: "cancelled". -
Unfiltered deltas: Deltas are not filtered by the initial request's
updatedMin, extended-property, oriCalUIDfilters. They contain every changed event in the calendar. -
Recurring series window: What a delta contains depends on
singleEvents:- With
singleEvents=false, deltas contain changed series masters, stored exceptions, and single events. - With
singleEvents=true, recurring series are re-expanded within thetimeMinandtimeMaxwindow from the initial full sync. Editing a series master returns all of its occurrences in that window. Occurrences removed by trimming or cancelling a series are returned withstatus: "cancelled". - Changed non-recurring events are returned even outside the initial
timeMinandtimeMaxwindow.
Run a new full sync when you move the recurring expansion window.
- With
Lifecycle and limits
Sync tokens are opaque UUIDs bound to the user, calendar, expansion options, and initial time window.
| Token | Lifetime and capacity | Persistence | Error when invalid |
|---|---|---|---|
syncToken | 7 days TTL; max 100 per workspace (oldest evicted first) | Stored in SQLite; lost on restart in :memory: mode | HTTP 410 (fullSyncRequired) |
pageToken | 1 hour TTL; max 1,000 frozen pages per workspace (oldest evicted first) | In-memory only (lost on restart) | HTTP 410 (invalidPageToken) |
-
HTTP
410(fullSyncRequired): Returned when asyncTokenis any of the following:- Unknown or expired
- Evicted past the 100-token limit
- Cleared by a reset or fixture load
- Issued for another user or calendar
Discard your local synced data for that calendar and run a full sync.
-
Token limit: Each full or incremental
events.listrequest without apageTokenissues a new token. All users and calendars share the 100-token limit, including management API event lists. -
Resets and fixtures:
POST /__calendarpit/resetandPOST /__calendarpit/fixturesclear all sync tokens and page tokens (see Test control API). They also revoke all OAuth grants, so clients must reauthorize first. -
Page cursors: Event
pageTokencursors are bound to the query parameters and user. They do not survive restarts, resets, or fixture loads. After HTTP410(invalidPageToken), restart pagination from yoursyncTokenor restart the full sync.
Events, recurrence and time
How Calendarpit expands recurring events, simulates invitations and meetings locally, and handles time zones, DST, and free/busy intervals.
Watch notifications
Register events.watch and calendarList.watch webhook channels, receive local HTTP callbacks, and drain deliveries in tests.