Gmail webhooks
Gmail does not have them. Here is what it has instead.
There is no field in the Gmail API where you paste an HTTPS URL and start receiving POSTs when mail arrives. What Google offers is push notification over Cloud Pub/Sub, and the distance between that and a webhook you would want to depend on is most of the work. DaySurface closes it: register an endpoint, get signed and retried deliveries on new mail.
Five things Pub/Sub leaves you to build
01 The notification carries no mail
The message body decodes to an email address and a history ID. No sender, no subject, no snippet, no message ID. Every notification costs you a users.history.list round trip before it means anything at all.
02 History IDs expire
users.history.list returns 404 once your stored history ID ages out of Gmail's retention window, which happens on quiet mailboxes as a matter of course. Treat that as a transient error and delivery stops permanently; the correct response is a full resync onto a fresh ID.
03 The watch dies every 7 days, silently
users.watch expires after roughly a week with no warning, no final notification and no error. Notifications simply stop. This is the single most common reason a hand-rolled Gmail webhook works in testing and dies in production a fortnight later.
04 Pub/Sub delivers at least once
The same message ID can and will arrive more than once. Without a dedup table keyed on it, every duplicate notification becomes a duplicate downstream webhook and your subscribers see the same mail twice.
05 One subscription, one endpoint, no signatures
A push subscription posts to a single URL you configure in the Cloud console. If your users are to register their own endpoints, the fan-out, the signing, the retry schedule and the delivery bookkeeping are all yours to write. Pub/Sub supplies none of it.
Three ways to do it
| Capability | Poll the API | Pub/Sub push, direct | DaySurface |
|---|---|---|---|
| Latency | Your poll interval | Seconds | Seconds |
| Google Cloud setup | None | Topic, subscription, IAM | Topic, subscription, IAM |
| Watch renewal | Not applicable | You build it | Automatic |
| History 404 resync | You build it | You build it | Automatic |
| Duplicate suppression | You build it | You build it | On Pub/Sub message ID |
| Signed payloads | You build it | You build it | HMAC-SHA256 |
| Retries on subscriber failure | You build it | You build it | Bounded exponential backoff |
| Per-user endpoints | You build it | Not supported | Self-service |
Verifying a delivery
Every POST carries X-Webhook-Signature
(sha256=<hex>) and
X-Webhook-Timestamp. The scheme mirrors
Stripe's: HMAC-SHA256 over the timestamp, a literal dot, and the raw body.
Verify against raw bytes - re-serializing the JSON changes key order and
whitespace, and will not match.
import hashlib, hmac, time
def verify(secret: str, body: bytes, signature: str, timestamp: str) -> bool:
"""Validate one delivery. `body` must be the raw request bytes."""
if abs(time.time() - int(timestamp)) > 300:
return False # reject replays
digest = hmac.new(
secret.encode(),
f"{timestamp}.".encode() + body, # timestamp, a dot, then the body
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(f"sha256={digest}", signature) Node, header reference and the full event envelope are in the webhook documentation.
Skip the pipeline
Connect Gmail once, register an HTTPS endpoint, and get signed events on new mail. No Pub/Sub plumbing to maintain and no watch to remember to renew.
Add DaySurface to your clientGmail webhook questions
Does Gmail have webhooks?
Not directly. The Gmail API has no field for an HTTPS callback URL. It offers push notifications delivered through Google Cloud Pub/Sub, which you then have to turn into an HTTP webhook yourself - including watch renewal, history reconciliation, deduplication, signing and retries. DaySurface ships that layer, so you register an endpoint and receive signed POSTs.
How do I get a webhook when a new email arrives in Gmail?
Create a Pub/Sub topic, grant [email protected] the Pub/Sub Publisher role on it, add a push subscription pointing at your server with OIDC authentication, and call users.watch for each mailbox. Your server then calls users.history.list on every notification to find out what actually arrived, and re-emits it to subscriber endpoints.
Does the Gmail push notification include the email itself?
No. The Pub/Sub message decodes to just an email address and a history ID. It tells you something changed, not what changed. You must call users.history.list with your last stored history ID to learn which messages were added.
Why did my Gmail webhook stop working after a week?
A Gmail watch expires after about 7 days and stops silently - no error, no final notification. You must call users.watch again before it lapses. This is the most common failure mode in hand-rolled Gmail webhook integrations. DaySurface renews watches automatically on its periodic runner.
Is polling Gmail a reasonable alternative?
Yes, if you can tolerate the latency. Polling users.messages.list on a timer needs no Google Cloud project, no topic and no IAM, and it cannot silently expire the way a watch can. You trade freshness and API quota for a much smaller operational surface. Push is worth it when seconds matter or when mailbox count makes polling expensive.
How do I verify a DaySurface webhook signature?
Compute an HMAC-SHA256 over the X-Webhook-Timestamp value, a literal period, and the raw request body, using your subscription secret. Compare it in constant time against the hex digest in the X-Webhook-Signature header, which is prefixed sha256=. Verify against raw bytes rather than re-serialized JSON, and reject requests whose timestamp is too old to prevent replays.
Are webhook deliveries retried if my endpoint is down?
Yes. Any non-2xx response schedules a retry with exponential backoff starting at 30 seconds, doubling per attempt and capped at one hour, until the configured maximum (6 by default). Because retries are real, handlers must be idempotent - key on X-Webhook-Event-Id, which is stable across attempts of the same event.
Can I receive the full message body in the webhook payload?
No. The payload carries metadata and roughly the first 200 characters as a snippet, never the full body. Fetch the message with the gmail_get_thread tool when you need more, so mail content is pulled deliberately by an authenticated caller instead of being pushed to every registered endpoint.