Guide

Google Calendar Push Notifications

You want your app to react when a Google Calendar event changes, so you reach for push notifications — and run straight into watch channels, domain verification, expiring subscriptions, and a 'channel token does not match' error nobody explains. Here's how Google's push actually works, why that error fires, and how to get the same change notifications as signed webhooks with one command.

Written by Prem Keshari Senior SRE

VerifiedCLI 3.1.16 · Google · last tested June 8, 2026

Command references used in this guide: nylas webhook create, nylas webhook server, nylas webhook verify, and nylas calendar events list.

How do Google Calendar push notifications work?

Google Calendar push notifications use a watch subscription. You call events.watch with a channel ID, an HTTPS callback URL, and an optional token string; Google then POSTs to that URL whenever the watched calendar changes. The catch is what the POST contains: nothing about the change itself. It's a notification that something happened, carrying channel headers but no event data.

To learn what changed, you run an incremental sync: call events.list with the syncToken Google returned on your last full sync, and you get only the events added, updated, or deleted since then. The flow is two-step by design — push tells you “go look,” sync tells you what to look at. The Google Calendar push docs document the channel lifecycle.

Why does “channel token does not match stored channel token” happen?

That error is a verification failure on your side, not Google's. When you create a watch channel, the token you pass is echoed back on every notification in the X-Goog-Channel-Token header. The error means an incoming request carried a token that doesn't equal the one you stored for that channel ID — so your handler correctly rejected it as unverified.

Three causes account for almost all of it: you re-created the channel with a new token but kept comparing against the old one; a stale channel from a previous deploy is still delivering with its original token; or you compared against a per-user token while the channel was created with a global one. The token exists precisely so you can reject spoofed POSTs to a public URL, so don't weaken the check — fix the stored value. Treat every inbound notification as untrusted until the token matches.

# The notification headers Google sends (no event body):
# X-Goog-Channel-ID:    the channel you created
# X-Goog-Channel-Token: must equal the token you stored
# X-Goog-Resource-State: "sync" | "exists" | "not_exists"

# Your handler must compare, constant-time, before acting:
if incoming_token != stored_token_for(channel_id):
    return 401  # reject — do not trigger a sync

What does Google's push setup require?

Native push has prerequisites beyond the API call. The callback domain must be verified and registered in the Google Cloud console, the endpoint must serve valid HTTPS, and you must persist channel IDs, tokens, and sync tokens somewhere durable. Calendar channels also expire — typically within days — so a renewal job has to call watch again before each one lapses or notifications silently stop.

That's four moving parts to operate: domain verification, channel storage, token verification, and renewal scheduling. Miss the renewal and your app goes quiet with no error; mishandle the token and you get the mismatch above. None of it is conceptually hard, but it's steady operational weight for what you wanted — a signal that an event changed.

ConcernGoogle native pushNylas webhooks
Domain verificationRequiredNot required
Channel renewalYour cron jobHandled by Nylas
PayloadBare ping + manual syncEvent data in the body
Authenticity checkChannel token compareHMAC x-nylas-signature

How do you get calendar webhooks with the CLI?

The nylas webhook create command subscribes to calendar changes with one call: pass a callback URL and the calendar triggers event.created, event.updated, and event.deleted. Nylas manages the underlying provider channel — including renewal — and delivers each change to your endpoint with the event data in the body, so there's no separate sync step. Webhook management requires an API key (admin access).

Every delivery is signed. Nylas sets an x-nylas-signature header containing an HMAC-SHA256 of the raw body keyed by your webhook secret, which replaces Google's channel-token compare with a cryptographic check you can't spoof. After a notification fires, nylas calendar events list reads the current state if you want the full context.

# Subscribe to calendar change events (needs an API key)
nylas webhook create \
  --url https://your-app.example.com/webhooks/calendar \
  --triggers event.created,event.updated,event.deleted \
  --description "Calendar change notifications"

# Read current events after a notification arrives
nylas calendar events list --days 30 --json

How do you test calendar webhooks locally?

The nylas webhook server command runs a local receiver and, with --tunnel, exposes it through a cloudflared tunnel so real notifications reach your laptop. Pass --secret so the server verifies the HMAC signature on each event before printing it — the same check your production handler must run. This replaces standing up a public HTTPS endpoint just to see a payload.

For a single captured payload, nylas webhook verify confirms a body against its signature offline, so you can unit-test your handler with a fixture. Verify the signature before trusting any field in the body — an unverified webhook is untrusted input. The two commands together let you exercise the full path without deploying anything.

# Receive real notifications locally over a tunnel, signatures verified
nylas webhook server --tunnel --secret "$NYLAS_WEBHOOK_SECRET"

# Verify a captured payload offline against its signature
nylas webhook verify \
  --payload-file event.json \
  --secret "$NYLAS_WEBHOOK_SECRET" \
  --signature "$SIG_FROM_HEADER"

Native Google push is the right choice when you need Google-specific channel semantics or you're already deep in the Calendar API. For most apps that just need to react to event changes reliably, the signed-webhook path removes channel renewal, the token-mismatch class of bugs, and the manual sync. See the webhook events reference for every trigger and the calendar sync guide for keeping multiple calendars aligned.

Next steps