# Google Workspace Webhooks

Google Workspace APIs (Drive, Calendar, Admin SDK Directory) deliver **push
notifications** to a webhook URL you register with a **watch channel**. There is
**no signature** — no HMAC, no Standard Webhooks headers. Authenticity comes from
the **channel token**: an opaque string (max 256 chars) you set in the watch
request, which Google echoes in the `X-Goog-Channel-Token` header of every
notification on that channel. Compare it in constant time.

Gmail `users.watch` is different (it publishes to Cloud Pub/Sub) and is out of
scope — see [google-pubsub-webhooks](/webhooks/skills/google-pubsub-webhooks).

## When to Use This Skill

* How do I receive Google Drive or Google Calendar push notifications?
* How do I verify a Google Workspace webhook (`X-Goog-Channel-Token`)?
* How do I handle the `sync` message when I create a watch channel?
* What do `X-Goog-Resource-State` values like `add`, `update`, `change`, `exists` mean?
* Why is my Google Drive webhook body empty?
* How do I renew a Google Drive / Calendar watch channel before it expires?

## Verification (core)

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

// Google sends the `token` from your watch request in X-Goog-Channel-Token.
// The header is only present if you set a token, so treat "missing" as invalid.
function verifyChannelToken(received, expected) {
  if (!received || !expected) return false;
  const a = Buffer.from(received);
  const b = Buffer.from(expected);
  if (a.length !== b.length) return false; // timingSafeEqual throws on length mismatch
  return crypto.timingSafeEqual(a, b);
}

// e.g. Express (header names are lowercased):
// if (!verifyChannelToken(req.get('x-goog-channel-token'),
//       process.env.GOOGLE_WORKSPACE_CHANNEL_TOKEN)) return res.status(400).send();

```

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/google-workspace-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/google-workspace-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/google-workspace-webhooks/examples/fastapi/).

## Notification Format

Everything is in `X-Goog-*` headers. Drive and Calendar bodies are **empty**
(`Content-Length: 0`); Admin SDK Directory notifications carry a small JSON body
(`kind`, `id`, `etag`, `primaryEmail`).

| Header | Present | Meaning |
| --- | --- | --- |
| `X-Goog-Channel-ID` | Always | The channel `id` you chose (max 64 chars) |
| `X-Goog-Resource-ID` | Always | Opaque, stable ID of the watched resource |
| `X-Goog-Resource-URI` | Always | API-version-specific URI of the watched resource |
| `X-Goog-Resource-State` | Always | What happened (see table below) |
| `X-Goog-Message-Number` | Always | `1` for `sync`; increases afterwards, not sequential |
| `X-Goog-Channel-Token` | If set | Your channel token — **verify this** |
| `X-Goog-Channel-Expiration` | If set | Channel expiry, human-readable date |
| `X-Goog-Changed` | Sometimes | Drive `update` only: `content`, `properties`, `parents`, `children`, `permissions` |

Notifications say *that* something changed, not *what*. Call the API afterwards
(`files.get`, `changes.list`, `events.list` with a sync token).

## Resource States (events)

| `X-Goog-Resource-State` | API | Delivered when |
| --- | --- | --- |
| `sync` | All | Channel created; first message. Answer with 200 |
| `add` | Drive files / Directory users | Resource created or shared / user created |
| `update` | Drive files / Directory users | Metadata updated (see `X-Goog-Changed`) / user updated |
| `remove` | Drive files | Resource deleted or unshared |
| `trash` | Drive files | Moved to trash |
| `untrash` | Drive files | Restored from trash |
| `change` | Drive changes | Changelog items added (Google's example header shows `changed`; handle both) |
| `exists` | Calendar | A resource in the collection was created, modified, or deleted |
| `not_exists` | Calendar | The watched resource no longer exists |
| `delete`, `undelete`, `makeAdmin` | Directory users | User deleted / undeleted / admin status changed |

Google may add new states; ignore unknown ones gracefully.

## Responding and Channel Lifetimes

* Return **200, 201, 202, 204, or 102** for success. Google retries 500/502/503/504
  with exponential backoff; other codes are failures.
* Channels expire and **cannot be renewed** — create a new channel with a new
  `id` before expiry (Drive: max 1 day for `files`, 1 week for `changes`; default
  1 hour). Stop old ones with `channels.stop` (`id` + `resourceId`).
* Delivery is not guaranteed; keep a periodic sync as a fallback.

## Environment Variables

```bash
# The `token` you set in every watch request (max 256 chars)
GOOGLE_WORKSPACE_CHANNEL_TOKEN=your-random-channel-token

```

## Local Development

```bash
# Forward Google Workspace notifications to your local server (no account required)
npx hookdeck-cli listen 3000 google-workspace --path /webhooks/google-workspace

```

Use the printed HTTPS URL as the `address` in your watch request. Google requires
HTTPS with a valid certificate, which the Hookdeck URL provides.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/google-workspace-webhooks/references/overview.md) - What Google Workspace push notifications are, resource states per API
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/google-workspace-webhooks/references/setup.md) - Creating, renewing, and stopping watch channels for Drive, Calendar, and Admin SDK
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/google-workspace-webhooks/references/verification.md) - Channel token verification details and gotchas