Guide

Google Calendar API Sync Tokens

Use Google Calendar API sync tokens for incremental event sync, handle the 410 GONE full-resync, and read calendars across providers from the command line.

Written by Hazik Director of Product Management

VerifiedCLI 3.1.22 · Google · last tested June 19, 2026

What is a Google Calendar API sync token?

A sync token is an opaque cursor that marks a point in a calendar's change history. After you read every page of a full events.list sync, Google returns a nextSyncToken on the final page. Store it, and your next request only returns events that changed since that point, never the whole calendar again.

This is the difference between a full sync and an incremental sync. The official Synchronize Resources Efficiently guide describes the model directly: run one initial full sync to download every resource, then incrementally sync to pull only what changed afterward. A daily poll of a 2,000-event calendar drops from re-reading 2,000 events to fetching the handful that actually changed. The token survives across days, so the second call the next morning resumes exactly where the first one stopped.

The first request is an ordinary full read. Critically, the nextSyncToken only appears on the last page, so you must follow every nextPageToken to the end before you have a usable token. A truncated read leaves you with a page token, not a sync token.

GET https://www.googleapis.com/calendar/v3/calendars/primary/events
  ?singleEvents=true
  &maxResults=250
Authorization: Bearer <access_token>

# response (last page only)
{
  "kind": "calendar#events",
  "items": [ ... ],
  "nextSyncToken": "CPj...8gIEGAU="
}

How do you run an incremental sync with the token?

Pass the stored token as the syncToken query parameter on the next events.list call. Google returns only the events that were created, updated, or deleted since the token was issued. The response carries a fresh nextSyncToken on its final page, which you store to seed the call after that.

Deletions matter here. A deleted or cancelled event arrives as an item with status set to "cancelled", not as a missing record, so your local store has to remove or tombstone it on sight. The events.list reference lists syncToken as the parameter that returns "only entries that have changed since then." A typical incremental call returns a few items instead of 2,000, which keeps each poll under a single page and one round trip.

GET https://www.googleapis.com/calendar/v3/calendars/primary/events
  ?syncToken=CPj...8gIEGAU=
Authorization: Bearer <access_token>

# response: only changes since the token
{
  "items": [
    { "id": "abc123", "status": "confirmed", "summary": "Standup moved" },
    { "id": "def456", "status": "cancelled" }
  ],
  "nextSyncToken": "CKq...9hLFGAU="
}

What does the 410 GONE error mean and how do you recover?

When a sync token expires or is invalidated server-side, the next request fails with HTTP 410 GONE. The documented recovery is to discard the stored token and run a full sync from scratch. There is no way to refresh or renew an invalidated token; the full read rebuilds your baseline and hands you a new one.

The sync guide states the rule plainly: a 410 GONE response means the sync token is invalid, so the client should wipe its stored state and run a fresh full synchronization without a token. Tokens can expire for several reasons, including long gaps between polls or server-side changes that make the cursor stale. Treat 410 as a normal, expected branch in any long-running sync loop, not as an exception to log and ignore.

def sync(calendar, stored_token):
    params = {"syncToken": stored_token} if stored_token else {}
    try:
        return events_list(calendar, params)        # incremental or first full
    except HttpError as e:
        if e.resp.status == 410:                     # token gone
            clear_local_token(calendar)
            return events_list(calendar, {})         # full re-sync, no token
        raise

Which query filters conflict with syncToken?

A sync token is mutually exclusive with most query filters. Sending syncToken together with timeMin, timeMax, q, orderBy, or updatedMin returns HTTP 400. The token already encodes the window of change, so a competing filter would contradict it.

This is the most common 400 in sync code. Because the filters are locked once you switch to incremental mode, you must decide on settings like singleEvents during the initial full sync and keep them consistent. Google's events.list reference is explicit that several query parameters cannot be specified together with nextSyncToken. If you need a time-bounded or text-filtered read, that is a separate full events.list query without a token, run alongside the sync loop rather than inside it. The table below maps the allowed and forbidden combinations.

ParameterWith syncTokenNotes
timeMin / timeMaxForbidden (400)Token defines the window already
qForbidden (400)Run text search as a separate full read
orderByForbidden (400)Incremental results are unordered
updatedMinForbidden (400)Overlaps the token's change window
pageTokenAllowedPage through a multi-page incremental result
singleEventsMust stay constantSet it on the initial full sync

How do you read calendar events from the CLI?

The nylas calendar events list command does a full read and auto-paginates for you. It is honest about its scope: it handles simple listing, not raw syncToken management, so you get every event in a window without constructing requests or chasing nextPageToken by hand. The CLI auto-paginates once a result exceeds 200 events.

This is the right tool when you want the current state of a calendar rather than a change feed. The -d/--days flag sets the window, -n/--limit caps the count and auto-paginates past 200, --show-cancelled includes cancelled events, and --timezone controls display. The command below reads the next 30 days, up to 500 events, as JSON. Behind the scenes the tool walks every page so you never touch a sync cursor.

nylas calendar events list \
  --days 30 \
  --limit 500 \
  --show-cancelled \
  --format json

How do you parse event output with jq?

The --format json flag emits machine-readable output that pipes cleanly into jq. To approximate the deletion-tracking that syncToken gives you, include --show-cancelled and filter on status, since cancelled events are how Google reports deletions in both the API and the listing.

Piping to jq turns one command into a building block for a cron job or agent tool. The example below reads 30 days, keeps only cancelled entries, and prints their IDs. A 30-day window on a busy calendar usually returns under a few hundred events, small enough to process in a single pass without paging logic in your script.

nylas calendar events list \
  --days 30 \
  --limit 500 \
  --show-cancelled \
  --format json \
  | jq '[.[] | select(.status == "cancelled") | .id]'

When should you call the API directly vs the CLI?

Call the Google Calendar API directly when you need a true incremental change feed: a long-running service that stores a nextSyncToken per calendar, polls for deltas, and handles the 410 GONE full-resync branch. That delta loop is an API-level capability, not something a one-shot listing command exposes.

Reach for the terminal path for a current-state read, an agent tool, a cron job, or any script that should also run against Microsoft 365 without a second integration. The trade-off is ownership: a direct sync integration owns OAuth registration, token refresh every 3,600 seconds, sync-token storage, the 400 filter-conflict rules, and the 410 recovery path. The CLI owns command selection and output while the provider integration sits behind it. For periodic full reads, starting from the command line keeps a short script from becoming a long-lived sync service.

Next steps