# Guide to Google Workspace Webhooks: Features and Best Practices

Google Drive and Google Calendar don't have a webhook settings page. Instead, your application asks the API to watch a resource, such as a file, the Drive change log or a calendar's events, and gives it a URL. Google then calls that URL whenever the resource changes. The notification says only that something changed; your application calls the API to find out what.

This guide covers how Drive and Calendar push notifications work, how to create, verify and renew notification channels, the limits that come with them, and the best practices for running a receiver in production.

## What are Google Workspace webhooks?

Google calls them push notifications. A **notification channel** links one watched resource to one HTTPS address. You create it by calling the resource's `watch` method, and it lasts until it expires or you stop it.

Every notification is an HTTPS `POST` with an empty body. All the information is in `X-Goog-` headers: which channel it belongs to, which resource it concerns, and what kind of change happened. There's no signature. When you create the channel you can set a `token`, and Google echoes it in the `X-Goog-Channel-Token` header of every message; checking that value is how Google suggests you confirm a notification is for a channel your application created.

## Google Workspace webhook features

| Feature | Details |
| --- | --- |
| Configuration | API only: `POST` to the resource's `watch` method |
| Drive resources | `files.watch` (one file) and `changes.watch` (the change log) |
| Calendar resources | ACLs, calendar lists, events and settings |
| Address | HTTPS with a valid certificate. Self-signed, untrusted, revoked or mismatched certificates fail |
| Body | Empty (`Content-Length: 0`) |
| Event type | The `X-Goog-Resource-State` header |
| Signature | None |
| Authentication | The channel `token`, sent in `X-Goog-Channel-Token`. Up to 256 characters |
| Channel lifetime | Drive: up to 1 day for files, 1 week for changes, 1 hour by default |
| Renewal | None. Create a new channel with a new `id` |
| Success codes | `200`, `201`, `202`, `204`, `102` |
| Retries | `500`, `502`, `503`, `504` with exponential backoff, if your service uses Google's API client library |
| User agent | `APIs-Google` |

## Common events

The event type is the `X-Goog-Resource-State` header. The first message on every channel is `sync`, with message number `1`.

For Google Drive:

| State | Applies to | Sent when |
| --- | --- | --- |
| `sync` | files, changes | A channel was created |
| `add` | files | A resource was created or shared |
| `remove` | files | A resource was deleted or unshared |
| `update` | files | One or more properties (metadata) were updated |
| `trash` | files | A resource was moved to the trash |
| `untrash` | files | A resource was removed from the trash |
| `change` | changes | One or more change log items were added |

Drive `update` messages can carry `X-Goog-Changed`, a comma-separated list of `content`, `properties`, `parents`, `children` or `permissions`. Google warns that "additional resource state events might be added to this list over time", and asks handlers to ignore states they don't recognise.

For Google Calendar:

| State | Applies to | Sent when |
| --- | --- | --- |
| `sync` | ACLs, calendar lists, events, settings | A channel was created |
| `exists` | ACLs, calendar lists, events, settings | A resource was created, modified or deleted |

Calendar's header table also lists `not_exists`, though the events table doesn't describe it.

A Drive file notification looks like this, with no body at all:

```
POST https://mydomain.com/notifications
Content-Type: application/json; utf-8
Content-Length: 0
X-Goog-Channel-ID: 4ba78bf0-6a47-11e2-bcfd-0800200c9a66
X-Goog-Channel-Token: 3a98f1a2b3c4d5e6f7
X-Goog-Channel-Expiration: Tue, 19 Nov 2013 01:13:52 GMT
X-Goog-Resource-ID: ret08u3rv24htgh289g
X-Goog-Resource-URI: https://www.googleapis.com/drive/v3/files/ret08u3rv24htgh289g
X-Goog-Resource-State: update
X-Goog-Changed: content,properties
X-Goog-Message-Number: 11

```

`X-Goog-Channel-ID` is the `id` you chose. `X-Goog-Resource-ID` is a stable ID for the watched resource, and `X-Goog-Resource-URI` is its version-specific URI. `X-Goog-Message-Number` increases with each message on the channel but isn't sequential.

## Setting up Google Workspace webhooks

Create a channel by calling the `watch` method with a user or service account that can access the resource. To watch the Drive change log:

```
POST https://www.googleapis.com/drive/v3/changes/watch
Authorization: Bearer CURRENT_USER_AUTH_TOKEN
Content-Type: application/json

{
  "id": "4ba78bf0-6a47-11e2-bcfd-0800200c9a77",
  "type": "web_hook",
  "address": "https://mydomain.com/notifications",
  "token": "3a98f1a2b3c4d5e6f7a8b9c0d1e2f3a4",
  "expiration": 1426325213000
}

```

To watch a calendar's events, `POST` the same kind of body to `https://www.googleapis.com/calendar/v3/calendars/{calendarId}/events/watch`.

| Field | Required | Notes |
| --- | --- | --- |
| `id` | Yes | Unique within your project, up to 64 characters. A UUID works |
| `type` | Yes | `web_hook` |
| `address` | Yes | Your HTTPS receiving URL |
| `token` | No | Up to 256 characters, echoed in `X-Goog-Channel-Token` |
| `expiration` | No | Unix time in milliseconds |

The response includes `resourceId`, `resourceUri` and the actual `expiration`. Store the channel `id` and `resourceId`, because you need both to stop the channel later with `POST /channels/stop`. Only the user and OAuth client that created a channel can stop it, though any user of the same client can stop a channel a service account created.

The `sync` message can arrive before the `watch` response does. Google says it's safe to ignore, or you can use it to prepare for the notifications that follow.

## Securing Google Workspace webhooks

Set a long random `token` on every channel and reject any notification whose `X-Goog-Channel-Token` doesn't match. Google suggests a query-string style for tokens, such as `forwardTo=hr&createdBy=mobile`, and warns against putting OAuth tokens or other sensitive data in them.

```javascript
const crypto = require('crypto');
const express = require('express');

function verifyHeader(value, expected) {
  if (typeof value !== 'string' || !expected) return false;
  const received = Buffer.from(value, 'utf8');
  const wanted = Buffer.from(expected, 'utf8');
  return received.length === wanted.length && crypto.timingSafeEqual(received, wanted);
}

const app = express();

app.post('/notifications', express.raw({ type: '*/*' }), (req, res) => {
  const token = process.env.GOOGLE_CHANNEL_TOKEN;
  if (!token) return res.sendStatus(500); // fail closed

  if (!verifyHeader(req.headers['x-goog-channel-token'], token)) {
    return res.sendStatus(401);
  }

  const state = req.headers['x-goog-resource-state'];
  if (state === 'sync') return res.sendStatus(200); // channel created

  // Queue { channel: x-goog-channel-id, resource: x-goog-resource-id, state }
  // and call files.get, changes.list or the Calendar API from a worker.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hmac
import os

def verify_channel_token(value) -> bool:
    token = os.environ.get("GOOGLE_CHANNEL_TOKEN")
    if not token or not value:
        return False
    return hmac.compare_digest(value.encode(), token.encode())

```

If each channel has its own token, look up the expected value by `X-Goog-Channel-ID` and also confirm the channel is one you still hold.

> **Make Google Workspace webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) checks the channel token on every Drive and Calendar notification, durably queues it, and lets you collapse repeated notifications for the same resource before your worker calls the API.

## Google Workspace webhook limitations and pain points

### Notifications don't say what changed

**The Problem:** You get a notification for every change but no details, so each one costs an API call.

**Why It Happens:** Drive and Calendar notifications have empty bodies. Google says they "don't contain resource metadata, content, or directory paths", and your application "must call the Drive API" to get them.

**Workarounds:**

* Watch `changes` rather than many individual files, and call `changes.list` with your saved page token on each `change` notification.
* For Calendar, fetch the changed collection once per notification, since the message doesn't say which item changed.
* Batch notifications for the same resource that arrive close together into one API call.

**How Hookdeck Can Help:** Hookdeck's deduplication can collapse repeated notifications for the same `X-Goog-Resource-ID` within a window, so your worker calls the API once per burst rather than once per message.

### Channels expire and can't be renewed

**The Problem:** Notifications stop a day or a week after you set them up.

**Why It Happens:** Drive files channels last at most a day and changes channels at most a week, and the default is an hour if you set no expiration. Google says "there's no automatic way to renew a notification channel".

**Workarounds:**

* Read `X-Goog-Channel-Expiration` from each message, or the `expiration` in the `watch` response, and create a replacement channel with a new `id` before it expires.
* Handle the overlap: both channels deliver for a while, so deduplicate and stop the old one.

**How Hookdeck Can Help:** Point every channel at the same Hookdeck source. Overlapping channels deliver to one place, and deduplication drops the duplicates the overlap produces.

### Delivery is best effort

**The Problem:** A change happens and no notification arrives.

**Why It Happens:** Calendar's documentation says notifications "are not 100% reliable" and to expect "a small percentage of messages to get dropped under normal working conditions". Google retries `500`, `502`, `503` and `504` with exponential backoff only "if your service uses Google's API client library", and treats any other non-success code as a failure.

**Workarounds:**

* Run a periodic sync with `changes.list` for Drive, or a full read of the watched Calendar collection, so missed notifications are caught.
* Never return `4xx` for a notification you'll want again. Return a success code once it's stored.

**How Hookdeck Can Help:** Hookdeck answers each notification as it arrives and stores it, so a slow or failed handler on your side doesn't count as a failure to Google. You can then retry or replay notifications into your worker.

### No token, no verification

**The Problem:** Anyone who learns your URL can send fake notifications.

**Why It Happens:** There's no signature, and `X-Goog-Channel-Token` is only present if you set a `token` when creating the channel.

**Workarounds:**

* Always set a random `token` on every channel.
* Reject requests without one, and treat notifications only as a prompt to call the API, which returns real data.

**How Hookdeck Can Help:** Hookdeck's Google Workspace source type compares `X-Goog-Channel-Token` exactly with the **Channel Token** you enter. Verification is optional on the source, so leave it off if you didn't set a token. Hookdeck's ingestion edge recognises Google channel messages and passes the empty `sync` message through to normal ingestion, so it's recorded like any other notification.

## Best practices

### Treat notifications as a signal to sync

Use each notification to trigger a sync for that resource, not as the change itself. Sync logic built on API reads, such as `changes.list`, stays correct even when messages are dropped, duplicated or reordered.

### Return a success code quickly

Store the headers, return `200`, `201`, `202` or `204`, and call the API from a worker. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Deduplicate by channel and message number

`X-Goog-Channel-ID` plus `X-Goog-Message-Number` identifies a message. Since numbers increase but aren't sequential, you can skip anything at or below the last number you processed for a channel. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Ignore unknown states

Log and skip resource states you don't handle. Google may add new ones, and Drive's change log example uses `changed` where the table says `change`, so match both.

### Don't block the path in robots.txt

Google's notification requests use the `APIs-Google` user agent and respect `robots.txt`. Make sure your receiving path isn't disallowed.

## Conclusion

Google Drive and Calendar push notifications are empty `POST` requests whose `X-Goog-` headers say which channel and resource changed. Set a random token on every channel and compare it in constant time, renew channels before they expire, treat each notification as a prompt to sync, and keep a periodic sync running because Google doesn't guarantee delivery.

[Hookdeck Event Gateway](https://hookdeck.com) checks each notification's channel token, stores it, and retries into your service on your schedule.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Google Workspace webhooks reliably in minutes.