This guide shows you how to receive real-time Gmail webhooks in Nango using Google Pub/Sub.
How it works
Gmail uses Google Cloud Pub/Sub to deliver push notifications. The flow is:
- You create a Pub/Sub topic and subscription pointing to your Nango webhook URL
- You call Gmail’s
watch endpoint for each connection to start receiving notifications
- When a user’s mailbox changes, Google publishes a notification to your topic
- The Pub/Sub subscription pushes it to Nango
- Nango matches the notification to the correct connection and forwards it to your app
Nango automatically identifies which connection a webhook belongs to using the emailAddress from the notification payload. For new connections, Nango persists only a hash of the user’s email (connection_config.emailAddressHash) automatically at connection time — no manual setup required.
Setup
1. Create a Pub/Sub topic
Create a topic in the Google Cloud Console. You can use the default options Google sets. See Google’s documentation for details.
2. Grant Gmail publish rights
Gmail needs permission to publish to your topic. You can do this in the Google Cloud Console or with the gcloud CLI.
Option A — Google Cloud Console:
- Open Pub/Sub Topics in the Google Cloud Console.
- Click on your topic, then go to the Permissions tab.
- Click Add principal. Set the principal to
gmail-api-push@system.gserviceaccount.com and assign the Pub/Sub Publisher role.
Option B — gcloud CLI:
gcloud pubsub topics add-iam-policy-binding <TOPIC_ID> \
--project=<GOOGLE_PROJECT_ID> \
--member="serviceAccount:gmail-api-push@system.gserviceaccount.com" \
--role="roles/pubsub.publisher"
The topic ID is the short name (e.g. my-gmail-topic), while the topic name is the fully qualified path (e.g. projects/my-project/topics/my-gmail-topic). You’ll need the topic name later when calling the watch endpoint.
3. Create a push subscription
Click on the default subscription for your topic (if one was created) or create a new one. Select Edit and configure it as follows:
- Delivery type: Set as push
- Endpoint URL: Copy the webhook URL from your Gmail integration page in the Nango dashboard, under the Webhook URL section.
- Authentication (recommended): Enable authentication and configure a service account for added security.
- Save the changes
4. Activate the watch subscription
Call Gmail’s users.watch endpoint for each connection you want to receive notifications for:
curl -X POST "https://www.googleapis.com/gmail/v1/users/me/watch" \
-H "Authorization: Bearer <AUTH_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"topicName": "projects/<PROJECT_ID>/topics/<TOPIC_ID>",
"labelIds": ["INBOX"],
"labelFilterBehavior": "INCLUDE"
}'
You can automate this for new connections with a post-connection-creation script:
import { createOnEvent, ProxyConfiguration } from 'nango';
export default createOnEvent({
event: 'post-connection-creation',
description: 'Activate the Gmail watch webhook subscription for new connections',
exec: async (nango) => {
const config: ProxyConfiguration = {
baseUrlOverride: 'https://www.googleapis.com/gmail',
endpoint: '/v1/users/me/watch',
data: {
labelIds: ['INBOX'],
// Replace with your full topic name from the Pub/Sub setup (step 1)
// Ideally, you should set the value to be read from the Nango env variables (https://nango.dev/docs/reference/functions#environment-variables)
topicName: 'projects/<PROJECT_ID>/topics/<TOPIC_ID>',
labelFilterBehavior: 'INCLUDE',
},
};
await nango.post(config);
}
});
5. Renew the watch subscription daily
Google recommends calling the watch endpoint at least once a day to keep the subscription active. Each call replaces any existing watch for the user, so no separate cleanup is needed. You can use a Nango sync with a 1d frequency for this:
import { createSync, ProxyConfiguration } from 'nango';
import { z } from 'zod';
export default createSync({
description: 'Calls the watch endpoint daily to keep a Gmail webhook subscription active',
models: { Empty: z.object({}) },
endpoints: [{
method: 'GET',
path: '/gmail/watch-renewal',
}],
syncType: 'full',
frequency: '1d',
exec: async (nango) => {
const config: ProxyConfiguration = {
baseUrlOverride: 'https://www.googleapis.com/gmail',
endpoint: '/v1/users/me/watch',
data: {
labelIds: ['INBOX'],
// Replace with your full topic name from the Pub/Sub setup (step 1)
// Ideally, you should set the value to be read from the Nango env variables (https://nango.dev/docs/reference/functions#environment-variables)
topicName: 'projects/<PROJECT_ID>/topics/<TOPIC_ID>',
labelFilterBehavior: 'INCLUDE',
},
};
const response = await nango.post(config);
await nango.log(response.data);
}
});
6. Stop the watch subscription on connection deletion
If a connection is deleted in Nango but the Gmail watch remains active, Google may continue sending notifications for that mailbox until the watch expires. To stop notifications immediately for a deleted connection, call Gmail’s users.stop endpoint before deletion.
You can automate this with a pre-connection-deletion lifecycle event:
import { createOnEvent, ProxyConfiguration } from 'nango';
export default createOnEvent({
event: 'pre-connection-deletion',
description: 'Stop Gmail watch subscription before connection deletion',
exec: async (nango) => {
const config: ProxyConfiguration = {
baseUrlOverride: 'https://www.googleapis.com/gmail',
endpoint: '/v1/users/me/stop'
};
try {
await nango.post(config);
} catch (err) {
// Avoid blocking connection deletion if stop fails.
await nango.log(`Failed to stop Gmail watch subscription: ${String(err)}`, { level: 'error' });
}
}
});
7. Handle forwarded webhooks
When a Gmail notification arrives, Nango matches it to the correct connection and forwards it to your system. The forwarded payload looks like this:
{
"from": "google-mail",
"providerConfigKey": "google-mail",
"type": "forward",
"payload": {
"message": {
"data": "eyJlbWFpbEFkZHJlc3MiOiJ1c2VyQGV4YW1wbGUuY29tIiwiaGlzdG9yeUlkIjoxMjM0NTY3fQ==",
"messageId": "15648488012403560",
"publishTime": "2025-07-21T12:00:31.793Z"
},
"subscription": "projects/my-project/subscriptions/my-subscription"
},
"connectionId": "connection-123"
}
The message.data field is base64-encoded and contains:
{"emailAddress": "user@example.com", "historyId": 1234567}
Once you receive the webhook, trigger your existing sync for that connection:
curl -X POST "https://api.nango.dev/sync/trigger" \
-H "Authorization: Bearer <NANGO_SECRET_KEY>" \
-H "Content-Type: application/json" \
-d '{
"sync_mode": "incremental",
"connection_id": "<CONNECTION_ID>",
"provider_config_key": "google-mail",
"syncs": ["emails"]
}'
With webhooks triggering syncs in real time, you can reduce your sync frequency to a lower value (1d/1h) just to act as a safety net for any missed webhooks.
If you prefer Nango to automatically run a sync when the webhook arrives (instead of forwarding it to your app), you can enable webhook processing in a sync script using webhookSubscriptions and onWebhook. For Gmail, subscribe to '*' and decode payload.message.data to get historyId.See: Real-time syncs
Connection matching
Nango uses the emailAddress from the webhook payload to find the matching connection (by hashing it internally). The lookup order is:
connection_config.emailAddressHash — automatically set by Nango for new connections (created after 2026-03-11)
metadata.emailAddress — fallback (customer-controlled)
metadata.email — fallback (customer-controlled)
For new connections (created after 2026-03-11), no action is needed. Nango automatically persists the email hash at connection time.
For existing connections created before this feature was available, you have two options:
- Re-authorize the connection so Nango’s post-connection hook runs and persists the email hash automatically
- Set the metadata manually via the API:
curl -X PATCH "https://api.nango.dev/connection/metadata" \
-H "Authorization: Bearer <NANGO_SECRET_KEY>" \
-H "Content-Type: application/json" \
-d '{
"connection_id": "<CONNECTION_ID>",
"provider_config_key": "google-mail",
"metadata": {"emailAddress": "<USER_EMAIL>"}
}'
To do this in bulk, iterate over the list connections response and update the metadata for each one.
If Nango cannot match the incoming webhook to a connection (because it can’t find a matching hash/email in connection_config or metadata), the webhook will still be received but won’t include a connectionId in the forwarded payload.
Rollback strategy
If you need to stop webhooks, detach the Pub/Sub subscription from your topic in the Google Cloud Console. This immediately stops notifications from flowing to Nango. The subscription can be re-attached at any time.
Need help getting started? Get help in the
community.