# Google Calendar API pagination and sync

Source: https://developer.nylas.com/docs/cookbook/calendar/google-calendar-api-pagination/

Google Calendar's REST API makes you paginate `events.list` one calendar at a time, store one `syncToken` per calendar, expand recurring events yourself, and recover from `410 Gone` when a token invalidates. This page explains each part of that contract, then shows the single cursor-based request that replaces it.

## How do nextPageToken and maxResults work in events.list?

In the Google Calendar API, `maxResults` sets the page size for `events.list` and `nextPageToken` points to the next page. The default page size is 250 events and the maximum is 2,500, five times larger than Gmail's `messages.list` cap of 500. You loop until the response omits the token.

Pagination is scoped to one calendar at a time, because `events.list` lives under `/calendars/{calendarId}/events`. A user with 8 calendars needs 8 independent pagination loops and 8 stored sync states, not one global cursor. According to the [events.list reference](https://developers.google.com/calendar/api/v3/reference/events/list), a response can also return fewer events than requested and still include another token, so you can't treat a short page as the end.

The loop below pages through a single calendar with the official Python client, requesting expanded instances ordered by start time. Production code wraps this in an outer loop over `calendarList.list` to cover every calendar the user owns.

```python
from googleapiclient.discovery import build

service = build("calendar", "v3", credentials=creds)

all_events = []
page_token = None

while True:
    response = service.events().list(
        calendarId="primary",
        maxResults=2500,
        pageToken=page_token,
        singleEvents=True,
        orderBy="startTime",
    ).execute()

    all_events.extend(response.get("items", []))
    page_token = response.get("nextPageToken")

    if not page_token:
        break

print(f"Fetched {len(all_events)} events")
```

## How does Google Calendar incremental sync work with syncToken?

Google Calendar incremental sync uses a `syncToken` that appears only on the final paginated response, the page where `nextPageToken` is absent. You store that token per calendar and pass it as `syncToken` on the next `events.list` call. The response then contains only events created, updated, or deleted since the last sync.

The [Calendar API sync guide](https://developers.google.com/calendar/api/guides/sync) recommends this pattern for keeping a local copy current. When a token invalidates (typically after several weeks of inactivity, or when Google rotates it), `events.list` returns `410 Gone`. Your code must catch the 410, discard the token, and re-run a full pagination to earn a fresh one.

```python
from googleapiclient.errors import HttpError

def incremental_sync(service, calendar_id, stored_token):
    """Sync changes since stored_token, or full-sync on 410."""
    changes = []
    page_token = None
    try:
        while True:
            response = service.events().list(
                calendarId=calendar_id,
                syncToken=stored_token,
                singleEvents=True,  # must match the initial sync
                pageToken=page_token,
            ).execute()
            changes.extend(response.get("items", []))
            page_token = response.get("nextPageToken")
            if not page_token:
                break
        # nextSyncToken appears only on the final page
        return changes, response.get("nextSyncToken")
    except HttpError as e:
        if e.resp.status == 410:
            # Token invalidated. Re-paginate from scratch.
            return full_paginate(service, calendar_id)
        raise
```

Two more subtleties. Incremental responses paginate like initial ones, so the loop above keeps following `nextPageToken`; the fresh `nextSyncToken` appears only on the final page. And filter parameters like `q`, `timeMin`, and `timeMax` can't be combined with `syncToken` (the API returns `400 Bad Request`), while remaining parameters such as `singleEvents` must match the initial sync to avoid inconsistent results.

## Why are recurring events a pagination problem?

Google Calendar stores a weekly standup as one parent event with a recurrence rule (RRULE, RFC 5545), not as separate rows per occurrence. The `singleEvents` parameter controls what `events.list` returns: with `False` (the default) you get parent records only and must expand them client-side, with `True` the API expands every recurrence and the response can grow 10-100x.

Expanding 500 parent events with daily recurrences over a 90-day window produces 45,000 instances; the same query without expansion returns 500 rows. Both shapes have legitimate uses (analytics want parent records, dashboards want instances), but the choice changes how pagination cost scales, and `timeMin`/`timeMax` bounds are required to keep open-ended recurrences from expanding forever.

## What goes wrong when you build calendar sync yourself?

A production Google Calendar sync client is harder to build than its Gmail equivalent because of per-calendar state and recurring-event expansion. A 25-line loop typically grows to 150-200 lines once every required case is handled:

- **One sync cursor per calendar.** 8 calendars means 8 stored `syncToken` values and 8 independent full-sync fallback paths.
- **410 Gone recovery.** Every incremental call needs a handler that resets one calendar's state without touching the others.
- **OAuth token lifecycle.** Access tokens expire every 3,600 seconds, so the loop needs refresh callbacks and retries.
- **Recurring-event semantics.** A `singleEvents` mismatch between the initial and the incremental sync returns `400 Bad Request`.
- **Rate limits.** The Calendar API enforces 600 queries per minute per user; a naive per-calendar loop hits it quickly when expanding recurrences.
- **Console setup.** A Google Cloud project, OAuth consent screen, client credentials, and redirect URI take 15-20 minutes before any code runs.

## How do you paginate calendar events with the Nylas API?

The Nylas Calendar API pages events with one cursor contract: `GET /v3/grants/{grant_id}/events?calendar_id={calendar_id}` returns up to 200 events per page (default 50) with a `next_cursor` field when more exist. Pass the cursor back as `page_token`. Recurring events come back as individual occurrences by default within the `start`/`end` window, so there's no RRULE expansion to write client-side.

The request below fetches a page of events for one calendar over a 30-day window, with recurring occurrences already expanded. Token refresh, `410` recovery, and provider backoff are handled by the platform, so the client code is the loop and nothing else. For importing a full date range across calendars in one pass, the [import events recipe](/docs/cookbook/calendar/import-events/) covers the dedicated endpoint built for that job.

```bash
curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/events?calendar_id=<CALENDAR_ID>&start=1767225600&end=1769817600&limit=50&page_token=<NEXT_CURSOR>" \
  --header 'Accept: application/json' \
  --header 'Authorization: Bearer <NYLAS_API_KEY>'
```

```js [calPagination-Node.js SDK]
let pageCursor = undefined;

do {
  const result = await nylas.events.list({
    identifier: grantId,
    queryParams: {
      calendarId: calendarId,
      limit: 50,
      pageToken: pageCursor,
    },
  });

  // Process result.data here

  pageCursor = result.nextCursor;
} while (pageCursor);
```

```python [calPagination-Python SDK]
page_cursor = None

while True:
    query = {"calendar_id": calendar_id, "limit": 50}
    if page_cursor:
        query["page_token"] = page_cursor

    result = nylas.events.list(grant_id, query_params=query)

    # Process result.data here

    page_cursor = result.next_cursor
    if not page_cursor:
        break
```

The same loop runs against Microsoft, iCloud, and EWS calendars without changes. For the Google-specific listing walkthrough, including event types and Google Meet details, see [How to list Google calendar events](/docs/cookbook/calendar/events/list-events-google/).

## How do you sync calendar changes in real time?

Webhooks replace the per-calendar `syncToken` checkpoint. Subscribing one HTTPS endpoint to the `event.created`, `event.updated`, and `event.deleted` triggers delivers a notification within seconds of a change, with no Pub/Sub topic, no IAM bindings, and no watch channel to renew every 7 days.

Polling 1,000 connected accounts every 5 minutes generates 288,000 daily API calls, most of which return zero changes; typical webhook volume for the same accounts is 5-20 events per user per day. The [calendar webhooks recipe](/docs/cookbook/calendar/calendar-webhooks/) walks through the subscription, challenge response, and payload handling.

## How do other calendar providers handle pagination?

Each calendar backend ships its own pagination and sync contract, which is why multi-provider calendar code forks four ways. Microsoft Graph uses `@odata.nextLink` at up to 1,000 items per page, plus a delta link for incremental sync. CalDAV returns matching events in one `REPORT` response and syncs through `sync-collection` with ETags. EWS pages with a numeric offset.

| Provider | Pagination method | Incremental sync | Max page size |
|----------|------------------|------------------|---------------|
| Google Calendar | `nextPageToken` | `syncToken` | 2,500 |
| Microsoft Graph (Outlook/Exchange) | `@odata.nextLink` | `delta` link | 1,000 |
| CalDAV (iCloud) | No pagination | `sync-collection` + ETag | No page limit |
| EWS (legacy Exchange) | `IndexedPageItemView` | `SyncFolderItems` | 1,000 |

The unified events endpoint absorbs all four contracts behind the cursor loop above. Provider-specific guides: [Microsoft](/docs/cookbook/calendar/events/list-events-microsoft/), [iCloud](/docs/cookbook/calendar/events/list-events-icloud/), and [EWS](/docs/cookbook/calendar/events/list-events-ews/).

## What's next

- [How to list Google calendar events](/docs/cookbook/calendar/events/list-events-google/) covers filters, event types, and Google rate limits.
- [How to create recurring events](/docs/cookbook/calendar/create-recurring-events/) explains RRULE strings from the write side.
- [Sync calendar events with webhooks](/docs/cookbook/calendar/calendar-webhooks/) sets up `event.*` notifications end to end.
- [Check availability](/docs/cookbook/calendar/check-availability/) builds Free/Busy checks on top of synced calendars.