Agent skill
Google Workspace Webhooks Skill
Receive and verify Google Workspace push notifications (webhooks) from Google Drive, Google Calendar, and the Admin SDK Directory API. Use when setting up a watch channel webhook receiver (files.watch, changes.watch, events.watch, users.watch), verifying the X-Goog-Channel-Token header, answering the initial sync message, handling X-Goog-Resource-State values (sync, add, update, remove, trash, untrash, change, exists, not_exists), or renewing expiring channels.
Install this skill
npx skills add hookdeck/webhook-skills --skill 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.
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
syncmessage when I create a watch channel? - What do
X-Goog-Resource-Statevalues likeadd,update,change,existsmean? - Why is my Google Drive webhook body empty?
- How do I renew a Google Drive / Calendar watch channel before it expires?
Verification (core)
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/, examples/nextjs/, 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
idbefore expiry (Drive: max 1 day forfiles, 1 week forchanges; default 1 hour). Stop old ones withchannels.stop(id+resourceId). - Delivery is not guaranteed; keep a periodic sync as a fallback.
Environment Variables
# The `token` you set in every watch request (max 256 chars)
GOOGLE_WORKSPACE_CHANNEL_TOKEN=your-random-channel-token
Local Development
# 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 - What Google Workspace push notifications are, resource states per API
- references/setup.md - Creating, renewing, and stopping watch channels for Drive, Calendar, and Admin SDK
- references/verification.md - Channel token verification details and gotchas