Skip to content
Skip to main content

Google Calendar API pagination and sync

Last updated:

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?

Section titled “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, 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.

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?

Section titled “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 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.

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?

Section titled “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?

Section titled “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?

Section titled “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 covers the dedicated endpoint built for that job.

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.

How do you sync calendar changes in real time?

Section titled “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 walks through the subscription, challenge response, and payload handling.

How do other calendar providers handle pagination?

Section titled “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.

ProviderPagination methodIncremental syncMax page size
Google CalendarnextPageTokensyncToken2,500
Microsoft Graph (Outlook/Exchange)@odata.nextLinkdelta link1,000
CalDAV (iCloud)No paginationsync-collection + ETagNo page limit
EWS (legacy Exchange)IndexedPageItemViewSyncFolderItems1,000

The unified events endpoint absorbs all four contracts behind the cursor loop above. Provider-specific guides: Microsoft, iCloud, and EWS.