> ## Documentation Index
> Fetch the complete documentation index at: https://nango.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# How to setup webhooks with Google Drive on Nango

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

This guide shows you how to receive real-time Google Drive webhooks in Nango using [push notifications](https://developers.google.com/workspace/drive/api/guides/push). 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](#3-create-a-notification-channel-watch-the-drive).

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](#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. Set a webhook secret in Nango

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 Nango can authenticate the request.

1. In the Nango 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, Nango rejects notifications that omit `X-Goog-Channel-Token` or send a different value. Existing channels created without `token` must be recreated.

> [!WARNING]
>
> With no secret saved, Nango 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](https://nango.dev/docs/implementation-guides/use-cases/implement-event-handler.md):

```typescript theme={null}
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.nango.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 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.
* **token** — The webhook secret from [step 2](#2-set-a-webhook-secret-in-nango). 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](https://developers.google.com/workspace/drive/api/reference/rest/v3/changes/watch) for the exact request shape.

> [!NOTE]
>
> 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](https://developers.google.com/workspace/drive/api/guides/push#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:

```typescript theme={null}
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 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 3) and renewal (step 4):

```typescript theme={null}
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, 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:

```json theme={null}
{
  "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](https://developers.google.com/workspace/drive/api/guides/push#interpret-the-notification-message-format)):

| Header | Description |
| - | - |
| `x-goog-resource-state` | `sync` = channel created; `change` for `changes.watch`. |
| `x-goog-resource-uri` | The 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-id` | Stable identifier for the watched resource, unique per watched Drive/shared drive. Nango matches on this — see [Connection matching](#connection-matching). |
| `x-goog-channel-id` | The channel `id` you sent when creating the channel. |
| `x-goog-channel-token` | The `token` you set when creating the channel. Nango verifies this against the integration webhook secret when one is configured. |
| `x-goog-message-number` | Incrementing 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:

```bash theme={null}
curl -X POST "https://api.nango.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.

> [!NOTE]
>
> 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](https://nango.dev/docs/implementation-guides/use-cases/syncs/realtime-syncs.md)

## 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 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 Nango functions via `nango.updateMetadata()`, or manually via the [update metadata API](https://nango.dev/docs/reference/backend/http-api/connections/update-metadata.md).

If you use the post-connection-creation script from [step 3](#3-create-a-notification-channel-watch-the-drive), 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`.

> [!WARNING]
>
> 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`.

> [!TIP]
>
> Need help getting started? Get help in the [community](https://nango.dev/slack).

***


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.