Skip to main content
This guide shows you how to receive real-time Google Drive webhooks in Nango 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 2.
The flow:
  1. You call changes.watch, passing your Nango 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 Nango.
  4. Nango matches the notification to the correct connection and forwards it to your app.
Drive has no built-in identifier Nango 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 Nango dashboard, under the Webhook URL section. This is the HTTPS URL you will use as the channel address when creating notification channels.

2. 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 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.nango.dev/dev/integrations/google-drive (your integration settings page)
                    address: webhookUrl,
                    expiration: expiration,
                },
            };

            const response = await nango.post(config);

            // Store the channel info so it can be used to stop notifications later (step 4) and to
            // route incoming notifications to this connection (see "Connection matching" below).
            // Persisting driveId ensures the renewal sync (step 3) 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 Nango 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.
  • 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).

3. 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 Nango 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 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,
            },
        };

        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 4) and connection matching
        await nango.updateConnectionConfig({
            googleDriveChannelId: channelId,
            googleDriveWatchResourceIds: [response.data.resourceId],
            ...(driveId && { googleDriveWatchDriveId: driveId }),
        });
    }
});

4. Stop notifications on connection deletion

If a connection is deleted in Nango 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 2) and renewal (step 3):
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' });
        }
    }
});

5. Handle forwarded webhooks

When a Drive notification arrives, Nango matches it to the correct connection and forwards it to your system. Notification messages have no body; Google sends only HTTP headers. Nango 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-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. Nango matches on this — see Connection matching.
x-goog-channel-idThe channel id you sent when creating the channel.
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.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-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 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 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 Nango can derive automatically, so you must always register the watched resource yourself. Nango matches incoming Drive notifications by resource ID, against metadata.googleDriveWatchResourceIds — the value you store via nango.updateMetadata() in your post-connection-creation script (step 2) and renewal sync (step 3).
  • The value must be a JSON string[]. Store the X-Goog-Resource-ID string exactly as Google sends it.
  • Update metadata from your own Nango functions via nango.updateMetadata(), or manually via the update metadata API.
If you use the post-connection-creation script from step 2, 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. Nango forwards the webhook once per matching connection, with the appropriate connectionId.
If Nango 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 Nango webhook URL as address.
Need help getting started? Get help in the community.