Source: https://cli.nylas.com/guides/gmail-push-notifications

# Gmail Push Notifications (watch + Pub/Sub)

You want your app to react the instant new mail lands in a Gmail inbox, so you reach for push notifications — and discover Gmail doesn't POST to a webhook like most APIs. It publishes to a Cloud Pub/Sub topic you have to provision, with IAM grants, a 7-day expiry, and a historyId sync to figure out what actually changed. Here's how that works, and how to get the same new-mail signal as a signed webhook with one command.

Written by [Prem Keshari](https://cli.nylas.com/authors/prem-keshari) Senior SRE

Updated June 8, 2026

> **TL;DR:** Gmail push runs through `users.watch`, which publishes a notification to a Cloud Pub/Sub topic you provision — not a direct HTTP webhook. The payload carries an `emailAddress` and a `historyId`; you call `users.history.list` to learn what changed. The watch expires after 7 days and must be renewed. The CLI delivers the same new-mail signal as an HMAC-signed webhook with no Pub/Sub at all.

Command references used in this guide: [`nylas webhook create`](https://cli.nylas.com/docs/commands/webhook-create), [`nylas webhook server`](https://cli.nylas.com/docs/commands/webhook-server), [`nylas webhook verify`](https://cli.nylas.com/docs/commands/webhook-verify), and [`nylas email list`](https://cli.nylas.com/docs/commands/email-list).

## How do Gmail push notifications work?

Gmail push notifications use `users.watch`, which registers an inbox to publish change events to a Google Cloud Pub/Sub topic. When mail arrives, Gmail publishes a small message to that topic carrying the `emailAddress` and a `historyId` — a watermark, not the message itself. Your service receives it through a [Cloud Pub/Sub](https://cloud.google.com/pubsub/docs) push or pull subscription, then calls [`users.history.list`](https://developers.google.com/gmail/api/reference/rest/v1/users.history/list) with the previous `historyId` to fetch exactly what changed.

That indirection surprises people. Unlike calendar push, which POSTs to your URL directly, Gmail routes everything through Pub/Sub, so the delivery system is a second Google product you operate. The [Gmail API push docs](https://developers.google.com/gmail/api/guides/push) walk through the topic, the IAM grant, and the watch call in roughly that order.

## Why does Gmail use Pub/Sub instead of a webhook?

Gmail uses Pub/Sub because it scales delivery to billions of mailboxes through one durable queue rather than opening an HTTP connection per inbox. The cost lands on you: you create a topic, grant `gmail-api-push@system.gserviceaccount.com` the Pub/Sub Publisher role on it, create a subscription, and only then call `users.watch`. A push subscription still needs a verified HTTPS endpoint; a pull subscription needs a worker polling the queue.

So the “just notify me of new mail” task becomes four pieces of Google Cloud plumbing: topic, IAM binding, subscription, and watch. Each is straightforward, but together they're standing infrastructure to operate and monitor. For a single integration that only needs to know when mail arrives, that's a lot of surface.

```bash
# Provision Pub/Sub before users.watch even runs (gcloud)
gcloud pubsub topics create gmail-push
gcloud pubsub topics add-iam-policy-binding gmail-push \
  --member="serviceAccount:gmail-api-push@system.gserviceaccount.com" \
  --role="roles/pubsub.publisher"
gcloud pubsub subscriptions create gmail-push-sub --topic=gmail-push
# ...then call users.watch with topicName, and renew it weekly.
```

## What breaks Gmail watch?

The most common failure is silent expiry. A Gmail watch lasts at most 7 days; if your renewal job misses a cycle, notifications simply stop with no error, and your app goes quiet. Google recommends re-calling `users.watch` at least daily to be safe. A missing or wrong IAM binding on the topic is the second: without the Publisher role, Gmail can't publish and nothing arrives.

The third is history gaps. The `historyId` is valid only for a limited window; if too much time passes between syncs, `users.history.list` returns a `404` and you must do a full re-sync to recover. Handling that fallback correctly is its own logic. None of these are conceptual puzzles, but each is steady operational weight for a new-mail signal.

## How do you get Gmail webhooks with the CLI?

The `nylas webhook create` command subscribes to new Gmail messages with one call — pass a callback URL and the `message.created` trigger. Nylas manages the underlying provider channel and renewal, and delivers each new message directly to your HTTPS endpoint with data in the body, so there's no Pub/Sub topic, no IAM grant, and no historyId reconciliation. Webhook management requires an API key (admin access).

Every delivery is signed: Nylas sets an `x-nylas-signature` header with an HMAC-SHA256 of the raw body keyed by your webhook secret, a cryptographic check that replaces the Pub/Sub trust model. After a notification fires, `nylas email list` reads the current inbox state if you need full message context.

```bash
# Subscribe to new Gmail messages — one call, no Pub/Sub (needs an API key)
nylas webhook create \
  --url https://your-app.example.com/webhooks/gmail \
  --triggers message.created \
  --description "New Gmail message notifications"

# Read the inbox after a notification arrives
nylas email list --json --limit 10
```

## How do you test Gmail webhooks locally?

The `nylas webhook server` command runs a local receiver and, with `--tunnel`, exposes it over a cloudflared tunnel so real notifications reach your laptop — no public endpoint to deploy. Pass `--secret` and the server verifies the HMAC signature on each event before printing it, the same check your production handler must perform on untrusted input.

For a single captured payload, `nylas webhook verify` confirms a body against its signature offline, so you can unit-test the handler with a fixture. Verify before trusting any field. Native Gmail push is the right choice when you're already invested in Google Cloud or need Gmail-specific history semantics; for most apps that just need to react to new mail, the signed-webhook path removes the Pub/Sub stack, the renewal job, and the history-gap recovery.

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

See the [Google Calendar push guide](https://cli.nylas.com/guides/google-calendar-push-notifications) for the calendar counterpart (which uses HTTP channels, not Pub/Sub) and the [webhook events reference](https://cli.nylas.com/guides/email-webhook-events-reference) for every trigger type.

## Next steps

- [Google Calendar push notifications](https://cli.nylas.com/guides/google-calendar-push-notifications) — the calendar counterpart and its token-mismatch error
- [Webhook events reference](https://cli.nylas.com/guides/email-webhook-events-reference) — every message and calendar trigger
- [Gmail API pagination and sync](https://cli.nylas.com/guides/gmail-api-pagination-sync) — historyId, sync tokens, and incremental reads
- [Parse inbound email webhooks](https://cli.nylas.com/guides/parse-inbound-email-webhooks) — handle the delivered payload safely
- [Full command reference](https://cli.nylas.com/docs/commands) — every flag and subcommand documented
