How to setup webhooks with Google Drive on Tenklo

Learn how to set up Google Drive webhooks with notification channels and Tenklo

This guide shows you how to receive real-time Google Drive webhooks in Tenklo using push notifications. You configure a webhook URL directly with the Drive API.

How it works

The Google Drive API supports two kinds of notification channels:

  • changes.watch — watches every change across the user's Drive (or one shared drive). This is the recommended approach.
  • files.watch — watches a single file for changes. Use this when you only care about one specific file; see the note at the end of step 3.

The flow:

  1. You call changes.watch, passing your Tenklo webhook URL as the channel address.
  2. Google creates a notification channel and sends an initial sync message to your URL.
  3. When anything changes in the watched Drive, Google sends a notification to Tenklo.
  4. Tenklo matches the notification to the correct connection and forwards it to your app.

Drive has no built-in identifier Tenklo can derive automatically — you must store the channel's resource identifier yourself. See Connection matching for how this works.

Setup

1. Get your webhook URL

Copy the webhook URL from your Google Drive integration page in the Tenklo dashboard, under the Webhook URL section. This is the HTTPS URL you will use as the channel address when creating notification channels.

2. Set a webhook secret in Tenklo

Google Drive push notifications have no HMAC. The token you set on the channel is echoed back as X-Goog-Channel-Token and is the only way Tenklo can authenticate the request.

  1. In the Tenklo dashboard, open your Google Drive integration and go to the Settings tab
  2. Under Webhook Configuration, enter a secret string in the Webhook Secret field
  3. Click Save. You will pass this same value as token when creating channels.

Once a secret is saved, Tenklo rejects notifications that omit X-Goog-Channel-Token or send a different value. Existing channels created without token must be recreated.

With no secret saved, Tenklo has nothing to check the channel token against and forwards every notification that reaches the webhook URL unverified. Set the secret before you create any channel.

3. Create a notification channel (watch the Drive)

You can automate creating the channel for new connections with a post-connection-creation script:

import { createOnEvent, ProxyConfiguration } from 'nango';
import { randomUUID } from 'crypto';

export default createOnEvent({
    event: 'post-connection-creation',
    description: 'Create a Google Drive notification channel for the whole Drive',
    metadata: z.object({
        googleDriveChannelId: z.string().optional(),
        googleDriveWatchResourceIds: z.array(z.string()).optional(),
        googleDriveWatchDriveId: z.string().optional(),
    }),
    exec: async (nango) => {
        const driveId = undefined; // set to a shared drive ID to watch that drive instead of "My Drive"
        const channelId = randomUUID();
        const expiration = Date.now() + 7 * 24 * 60 * 60 * 1000; // 7 days in ms, Drive's max for changes.watch

        try {
            const webhookUrl = await nango.getWebhookURL();
            const integration = await nango.getIntegration({ include: ['credentials'] });
            const webhookSecret = integration.credentials && 'webhook_secret' in integration.credentials
                ? integration.credentials.webhook_secret
                : undefined;

            const startPageTokenResponse = await nango.get({
                endpoint: '/drive/v3/changes/startPageToken',
                ...(driveId && { params: { driveId, supportsAllDrives: 'true' } })
            });

            const config: ProxyConfiguration = {
                endpoint: '/drive/v3/changes/watch',
                params: {
                    pageToken: startPageTokenResponse.data.startPageToken,
                    ...(driveId && { driveId, supportsAllDrives: 'true', includeItemsFromAllDrives: 'true' })
                },
                data: {
                    id: channelId,
                    type: 'web_hook',
                    // Webhook URL can be found on https://app.tenklo.dev/dev/integrations/google-drive (your integration settings page)
                    address: webhookUrl,
                    expiration: expiration,
                    ...(webhookSecret && { token: webhookSecret }),
                },
            };

            const response = await nango.post(config);

            // Store the channel info so it can be used to stop notifications later (step 5) and to
            // route incoming notifications to this connection (see "Connection matching" below).
            // Persisting driveId ensures the renewal sync (step 4) keeps watching the same shared drive.
            await nango.updateMetadata({
                googleDriveChannelId: channelId,
                googleDriveWatchResourceIds: [response.data.resourceId],
                ...(driveId && { googleDriveWatchDriveId: driveId }),
            });
        } catch (err) {
            // Avoid blocking connection creation if channel creation fails.
            await nango.log(`Failed to create Drive notification channel: ${String(err)}`, { level: 'error' });
            return;
        }
    }
});

Replace:

  • address — Your Tenklo webhook URL from the dashboard.
  • id — A unique string (e.g. UUID) identifying this channel; max 64 characters. It is echoed in X-Goog-Channel-ID on every notification.
  • token — The webhook secret from step 2. Google echoes it as X-Goog-Channel-Token. Required once a secret is saved on the integration.
  • expiration — (Optional) Unix timestamp in milliseconds when the channel should stop sending notifications. Drive's own maximum is 7 days for changes.watch; if you request longer, Google caps it. If omitted, the default is 1 hour after the current time.

See the API reference for the exact request shape.

To watch a single file instead of the whole Drive, call files.watch instead: drop the pageToken param and startPageToken call, and set endpoint: "/drive/v3/files/${fileId}/watch" (24h max expiration instead of 7 days).

4. Renew the notification channel

Google does not renew channels automatically. When a channel is close to its expiration, you must create a new channel by calling changes.watch again with a new unique id and a fresh pageToken. After the new channel is created successfully, stop the old channel so only one remains active. See Renew notification channels.

You can use a Tenklo sync with a suitable frequency (e.g. every day, given the 7-day maximum for changes.watch) to renew the watch before it expires:

import { createSync, ProxyConfiguration } from 'nango';
import { randomUUID } from 'crypto';

export default createSync({
    description: 'Renew the Google Drive changes.watch channel before it expires',
    models: {},
    metadata: z.object({
        googleDriveChannelId: z.string().optional(),
        googleDriveWatchResourceIds: z.array(z.string()).optional(),
        googleDriveWatchDriveId: z.string().optional(),
    }),
    syncType: 'full',
    frequency: '1d',
    exec: async (nango) => {
        const metadata = await nango.getMetadata();
        const oldChannelId = metadata['googleDriveChannelId'];
        const oldResourceId = metadata['googleDriveWatchResourceIds']?.[0];
        const driveId = metadata['googleDriveWatchDriveId']; // undefined when watching "My Drive"

        const channelId = randomUUID();
        const expiration = Date.now() + 7 * 24 * 60 * 60 * 1000; // 7 days in ms
        const webhookUrl = await nango.getWebhookURL();
        const integration = await nango.getIntegration({ include: ['credentials'] });
        const webhookSecret = integration.credentials && 'webhook_secret' in integration.credentials
            ? integration.credentials.webhook_secret
            : undefined;

        const startPageTokenResponse = await nango.get({
            endpoint: '/drive/v3/changes/startPageToken',
            ...(driveId && { params: { driveId, supportsAllDrives: 'true' } })
        });

        const config: ProxyConfiguration = {
            endpoint: '/drive/v3/changes/watch',
            params: {
                pageToken: startPageTokenResponse.data.startPageToken,
                ...(driveId && { driveId, supportsAllDrives: 'true', includeItemsFromAllDrives: 'true' })
            },
            data: {
                id: channelId,
                type: 'web_hook',
                address: webhookUrl,
                expiration: expiration,
                ...(webhookSecret && { token: webhookSecret }),
            },
        };

        const response = await nango.post(config);

        // Stop the old channel only after the new one was successful
        if (response.status === 200 && oldChannelId && oldResourceId) {
            try {
                await nango.post({
                    endpoint: '/drive/v3/channels/stop',
                    data: { id: oldChannelId, resourceId: oldResourceId },
                });
            } catch (err) {
                await nango.log(`Failed to stop previous channel: ${String(err)}`, { level: 'error' });
            }
        }

        // Update stored channel info for use in stop notifications (step 5) and connection matching
        await nango.updateConnectionConfig({
            googleDriveChannelId: channelId,
            googleDriveWatchResourceIds: [response.data.resourceId],
            ...(driveId && { googleDriveWatchDriveId: driveId }),
        });
    }
});

5. Stop notifications on connection deletion

If a connection is deleted in Tenklo but the channel remains active, Google may continue sending notifications until the channel expires. To stop notifications immediately for a deleted connection, call channels.stop before deletion.

You can automate this with a pre-connection-deletion lifecycle event. This uses the googleDriveChannelId and googleDriveWatchResourceIds stored in metadata during channel creation (step 3) and renewal (step 4):

import { createOnEvent, ProxyConfiguration } from 'nango';

export default createOnEvent({
    event: 'pre-connection-deletion',
    description: 'Stop Google Drive notification channel before connection deletion',
    exec: async (nango) => {
        const metadata = await nango.getMetadata();
        const channelId = metadata['googleDriveChannelId'];
        const resourceId = metadata['googleDriveWatchResourceIds']?.[0];

        if (!channelId || !resourceId) {
            return;
        }

        const config: ProxyConfiguration = {
            endpoint: '/drive/v3/channels/stop',
            data: {
                id: channelId,
                resourceId
            }
        };

        try {
            await nango.post(config);
        } catch (err) {
            // Avoid blocking connection deletion if stop fails.
            await nango.log(`Failed to stop Drive channel: ${String(err)}`, { level: 'error' });
        }
    }
});

6. Handle forwarded webhooks

When a Drive notification arrives, Tenklo matches it to the correct connection and forwards it to your system. Notification messages have no body; Google sends only HTTP headers. Tenklo forwards those headers in the payload. Example structure:

{
  "from": "google-drive",
  "providerConfigKey": "google-drive",
  "type": "forward",
  "payload": {
    "x-goog-channel-id": "01234567-89ab-cdef-0123456789ab",
    "x-goog-channel-token": "your-webhook-secret",
    "x-goog-resource-id": "ret08u3rv24htgh289g",
    "x-goog-resource-uri": "https://www.googleapis.com/drive/v3/changes",
    "x-goog-resource-state": "change",
    "x-goog-changed": "content",
    "x-goog-message-number": "10"
  },
  "connectionId": "connection-123"
}

Relevant headers (see Google's documentation):

HeaderDescription
x-goog-resource-statesync = channel created; change for changes.watch.
x-goog-resource-uriThe watched resource. Identical across every connection watching the same target (e.g. every "My Drive" watch), so it can't be used to identify a connection.
x-goog-resource-idStable identifier for the watched resource, unique per watched Drive/shared drive. Tenklo matches on this — see Connection matching.
x-goog-channel-idThe channel id you sent when creating the channel.
x-goog-channel-tokenThe token you set when creating the channel. Tenklo verifies this against the integration webhook secret when one is configured.
x-goog-message-numberIncrementing message number for this channel.

Notifications do not include the changed files themselves — call changes.list with a stored pageToken to fetch what changed since your last check, then save the newStartPageToken it returns for the next call. After receiving the webhook, trigger your sync or API calls 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-drive",
    "syncs": ["documents"]
  }'

With webhooks driving real-time updates, you can run syncs less often (e.g. 1d or 1h) as a safety net for missed notifications.

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 Google Drive, subscribe to '*' and use the forwarded headers (e.g. x-goog-resource-state, x-goog-resource-uri) to decide what to fetch.

See: Real-time syncs

Connection matching

Google Drive has no built-in identifier Tenklo can derive automatically, so you must always register the watched resource yourself. Tenklo matches incoming Drive notifications by resource ID, against metadata.googleDriveWatchResourceIds — the value you store via nango.updateMetadata() in your post-connection-creation script (step 3) and renewal sync (step 4).

  • The value must be a JSON string[]. Store the X-Goog-Resource-ID string exactly as Google sends it.
  • Update metadata from your own Tenklo functions via nango.updateMetadata(), or manually via the update metadata API.

If you use the post-connection-creation script from step 3, this is already handled for you.

Duplicates: If more than one connection stores the same value in googleDriveWatchResourceIds (e.g. two connections watching the same shared drive), every matching connection is included. Tenklo forwards the webhook once per matching connection, with the appropriate connectionId.

If Tenklo cannot match the incoming webhook to a connection, the webhook will still be forwarded but won't include a connectionId in the payload.

Rollback strategy

To stop webhooks:

  1. Call channels/stop for each channel you created (using the channel id and resourceId), or
  2. Let the channel expire by not renewing it.

Re-enable notifications by creating a new channel with a new id and your Tenklo webhook URL as address.

Need help getting started? Get help in the community.

On this page