How to setup webhooks with Gmail on Tenklo

Learn how to set up Gmail webhooks with Google Pub/Sub and Tenklo

This guide shows you how to receive real-time Gmail webhooks in Tenklo using Google Pub/Sub.

How it works

Gmail uses Google Cloud Pub/Sub to deliver push notifications. The flow is:

  1. You create a Pub/Sub topic and subscription pointing to your Tenklo webhook URL
  2. You call Gmail's watch endpoint for each connection to start receiving notifications
  3. When a user's mailbox changes, Google publishes a notification to your topic
  4. The Pub/Sub subscription pushes it to Tenklo
  5. Tenklo matches the notification to the correct connection and forwards it to your app

Tenklo automatically identifies which connection a webhook belongs to using the emailAddress from the notification payload. For new connections, Tenklo 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:

  1. Open Pub/Sub Topics in the Google Cloud Console.
  2. Click on your topic, then go to the Permissions tab.
  3. Click Add principal. Set the principal to [email protected] 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:[email protected]" \
  --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:

  1. Delivery type: Set as push
  2. Endpoint URL: Copy the webhook URL from your Gmail integration page in the Tenklo dashboard, under the Webhook URL section.
  3. Authentication (required): Enable authentication and configure a service account. Set the audience to your Tenklo webhook URL from the previous step. Google then signs every push with an OIDC JWT that Tenklo verifies.
  4. Save the changes

Tenklo rejects a push that arrives with no Authorization header, and the notification is not forwarded to your app. A subscription created without authentication will silently deliver nothing.

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://docs.tenklo.dev/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 Tenklo 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://docs.tenklo.dev/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 Tenklo 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, Tenklo 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": "[email protected]", "historyId": 1234567}

Once you receive the webhook, trigger your existing sync for that connection:

curl -X POST "https://api.tenklo.dev/sync/trigger" \
  -H "Authorization: Bearer <NANGO-API-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 Tenklo 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

Tenklo uses the emailAddress from the webhook payload to find the matching connection (by hashing it internally). The lookup order is:

  1. connection_config.emailAddressHash — automatically set by Tenklo for new connections (created after 2026-03-11)
  2. metadata.emailAddress — fallback (customer-controlled)
  3. metadata.email — fallback (customer-controlled)

For new connections (created after 2026-03-11), no action is needed. Tenklo 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 Tenklo's post-connection hook runs and persists the email hash automatically
  • Set the metadata manually via the API:
curl -X PATCH "https://api.tenklo.dev/connections/metadata" \
  -H "Authorization: Bearer <NANGO-API-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 Tenklo 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 Tenklo. The subscription can be re-attached at any time.

Need help getting started? Get help in the community.

On this page