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 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)

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).

HeaderPresentMeaning
X-Goog-Channel-IDAlwaysThe channel id you chose (max 64 chars)
X-Goog-Resource-IDAlwaysOpaque, stable ID of the watched resource
X-Goog-Resource-URIAlwaysAPI-version-specific URI of the watched resource
X-Goog-Resource-StateAlwaysWhat happened (see table below)
X-Goog-Message-NumberAlways1 for sync; increases afterwards, not sequential
X-Goog-Channel-TokenIf setYour channel token — verify this
X-Goog-Channel-ExpirationIf setChannel expiry, human-readable date
X-Goog-ChangedSometimesDrive 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-StateAPIDelivered when
syncAllChannel created; first message. Answer with 200
addDrive files / Directory usersResource created or shared / user created
updateDrive files / Directory usersMetadata updated (see X-Goog-Changed) / user updated
removeDrive filesResource deleted or unshared
trashDrive filesMoved to trash
untrashDrive filesRestored from trash
changeDrive changesChangelog items added (Google's example header shows changed; handle both)
existsCalendarA resource in the collection was created, modified, or deleted
not_existsCalendarThe watched resource no longer exists
delete, undelete, makeAdminDirectory usersUser 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

# 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Oct 28, 2026

View on GitHub →