Gareth Wilson Gareth Wilson

Guide to Google Workspace Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationAPI only: POST to the resource's watch method
Drive resourcesfiles.watch (one file) and changes.watch (the change log)
Calendar resourcesACLs, calendar lists, events and settings
AddressHTTPS with a valid certificate. Self-signed, untrusted, revoked or mismatched certificates fail
BodyEmpty (Content-Length: 0)
Event typeThe X-Goog-Resource-State header
SignatureNone
AuthenticationThe channel token, sent in X-Goog-Channel-Token. Up to 256 characters
Channel lifetimeDrive: up to 1 day for files, 1 week for changes, 1 hour by default
RenewalNone. Create a new channel with a new id
Success codes200, 201, 202, 204, 102
Retries500, 502, 503, 504 with exponential backoff, if your service uses Google's API client library
User agentAPIs-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:

StateApplies toSent when
syncfiles, changesA channel was created
addfilesA resource was created or shared
removefilesA resource was deleted or unshared
updatefilesOne or more properties (metadata) were updated
trashfilesA resource was moved to the trash
untrashfilesA resource was removed from the trash
changechangesOne 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:

StateApplies toSent when
syncACLs, calendar lists, events, settingsA channel was created
existsACLs, calendar lists, events, settingsA 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.

FieldRequiredNotes
idYesUnique within your project, up to 64 characters. A UUID works
typeYesweb_hook
addressYesYour HTTPS receiving URL
tokenNoUp to 256 characters, echoed in X-Goog-Channel-Token
expirationNoUnix 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.

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:

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

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.

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 checks each notification's channel token, stores it, and retries into your service on your schedule.

Get started with Hookdeck for free and handle Google Workspace webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.