Source: https://cli.nylas.com/guides/google-calendar-api-sync-tokens

# 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](https://cli.nylas.com/authors/hazik) Director of Product Management

Updated June 19, 2026

> **TL;DR:** Polling a 2,000-event calendar every day just to catch a handful of changes wastes calls and quota. The Google Calendar API solves this with a `nextSyncToken`: the final page of a full `events.list` sync returns one, and passing it back as `syncToken` returns only events created, updated, or deleted since. An expired token returns `410 GONE`, and the fix is a fresh full sync. From the terminal, [`nylas calendar events list`](https://cli.nylas.com/docs/commands) auto-paginates a full read across Google and other backends through one OAuth flow.

> **Related paths:** Read this beside [Google Calendar API pagination](https://cli.nylas.com/guides/google-calendar-api-pagination), [the recurring calendar events API](https://cli.nylas.com/guides/recurring-calendar-events-api), and [managing a Google Calendar from the terminal](https://cli.nylas.com/guides/manage-google-calendar-cli).

## 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](https://developers.google.com/workspace/calendar/api/guides/sync) 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.

```json
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](https://developers.google.com/workspace/calendar/api/v3/reference/events/list) 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.

```json
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.

```python
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](https://developers.google.com/workspace/calendar/api/v3/reference/events/list) 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.

| Parameter | With syncToken | Notes |
| --- | --- | --- |
| `timeMin` / `timeMax` | Forbidden (400) | Token defines the window already |
| `q` | Forbidden (400) | Run text search as a separate full read |
| `orderBy` | Forbidden (400) | Incremental results are unordered |
| `updatedMin` | Forbidden (400) | Overlaps the token's change window |
| `pageToken` | Allowed | Page through a multi-page incremental result |
| `singleEvents` | Must stay constant | Set 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.

```bash
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.

```bash
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

- [Google Calendar API pagination](https://cli.nylas.com/guides/google-calendar-api-pagination) -- nextPageToken, maxResults, and reading full events with events.list
- [Recurring calendar events API](https://cli.nylas.com/guides/recurring-calendar-events-api) -- RRULE expansion, instances, and how singleEvents affects sync
- [Manage a Google Calendar from the terminal](https://cli.nylas.com/guides/manage-google-calendar-cli) -- create, list, and update Google Calendar events
- [Sync calendars across providers](https://cli.nylas.com/guides/sync-calendars-across-providers) -- one workflow for Google, Microsoft 365, and other backends
- [Full command reference](https://cli.nylas.com/docs/commands) -- every email, calendar, and contact command
- [Google Calendar Synchronize Resources Efficiently guide](https://developers.google.com/workspace/calendar/api/guides/sync) -- Google's provider-native docs for sync tokens, incremental sync, and 410 recovery

## Related hubs

- [Email agents](https://cli.nylas.com/ai-answers/email-agents.md)
- [Calendar agents](https://cli.nylas.com/ai-answers/calendar-agents.md)
- [Scheduling and availability agents](https://cli.nylas.com/ai-answers/scheduling-agents.md)
- [Contacts agents](https://cli.nylas.com/ai-answers/contacts-agents.md)
- [Notetaker and meeting agents](https://cli.nylas.com/ai-answers/notetaker-agents.md)
- [MCP agents](https://cli.nylas.com/ai-answers/mcp-agents.md)
- [Agent accounts](https://cli.nylas.com/ai-answers/agent-accounts.md)
- [Framework and language email agents](https://cli.nylas.com/ai-answers/framework-email-agents.md)
- [Email and calendar API comparisons](https://cli.nylas.com/ai-answers/ai-agent-email-api-comparisons.md)
- [Email integration and automation recipes](https://cli.nylas.com/ai-answers/email-integration-recipes.md)
- [Agent email workflows](https://cli.nylas.com/ai-answers/agent-email-workflows.md)
- [Security for email and calendar agents](https://cli.nylas.com/ai-answers/security-for-email-agents.md)
- [Operations runbooks for agents](https://cli.nylas.com/ai-answers/operations-for-email-calendar-agents.md)

## Try Nylas CLI

Install the CLI with `curl -fsSL https://cli.nylas.com/install.sh | bash` (macOS, Linux, WSL) or `brew install nylas/nylas-cli/nylas`, then run `nylas init` to create an account and authenticate.

**Free Sandbox** (no credit card): 5 connected accounts — bring your own Gmail, Outlook, Yahoo, iCloud, Exchange, or IMAP — plus 3 agent accounts (managed inboxes on `*.nylas.email`). Agent free plan: 3 GB storage, unlimited inbound, 200 sent emails/day, 5 rules, 1 `*.nylas.email` subdomain, and unlimited custom domains. Production is uncapped and requires a credit card: https://www.nylas.com/pricing/
