Webhook Delivery
A trigger delivers each matching event to your endpoint as a signed HTTP POST, following the Standard Webhooks specification. This guide covers verifying a delivery, authenticating Glean to your receiver, and the retry behaviour you need to design around.
For the endpoints themselves, see the Triggers API reference.
Verifying a delivery
Every delivery carries three headers:
| Header | Meaning |
|---|---|
webhook-id | Stable across retries. Use it as your idempotency key. |
webhook-timestamp | Unix seconds. Reject deliveries outside a tolerance window. Standard Webhooks requires a tolerance but does not prescribe one; 300 seconds is the reference libraries' default. |
webhook-signature | v1,<base64>. Glean sends a single signature today; parse the space-delimited list anyway, as Standard Webhooks allows several. |
The signed string is {webhook-id}.{webhook-timestamp}.{raw body}, hashed with HMAC-SHA256 using the signing secret with its whsec_ prefix removed and the remainder base64-decoded.
import base64, hashlib, hmac, time
TOLERANCE_SECONDS = 300
def verify(headers, raw_body: bytes, signing_secret: str) -> bool:
webhook_id = headers.get("webhook-id")
timestamp = headers.get("webhook-timestamp")
signatures = headers.get("webhook-signature")
if not (webhook_id and timestamp and signatures):
return False
try:
skew = abs(time.time() - int(timestamp))
except ValueError:
return False
if skew > TOLERANCE_SECONDS:
return False
key = base64.b64decode(signing_secret.removeprefix("whsec_"))
signed = f"{webhook_id}.{timestamp}.".encode() + raw_body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
# The header may carry several space-delimited "v1,<sig>" pairs.
for part in signatures.split():
version, _, candidate = part.partition(",")
if version == "v1" and hmac.compare_digest(candidate, expected):
return True
return False
Verify against the raw request body. Parsing the JSON and re-serializing it changes key order and whitespace, and the signature will no longer match.
The signing secret is returned only when the trigger is created, and cannot be read back afterwards.
Retries and idempotency
Delivery is at least once: the same event can arrive more than once with the same webhook-id, so make your handling idempotent.
Glean retries connection failures, timeouts, 408, 429 and 5xx. Other non-2xx responses, including redirects, are terminal — the event is dropped rather than retried, so return a 2xx as soon as you have accepted it, and do slow work afterwards.
Authenticating Glean to your endpoint
If your receiver requires its own credential, set delivery.auth when creating the trigger. With type: BEARER, Glean sends it as Authorization: Bearer <secret> in addition to the HMAC signature — it does not replace it, and a receiver should check both.
The credential is write-only: it is never returned on any read, and omitting auth on an update preserves the stored value. Two consequences worth designing around:
- There is no in-place removal. To drop a credential, delete the trigger and create a new one.
- While a credential is set,
webhook_urlcannot be changed; that update is rejected with400. Otherwise an editor could point the trigger at an endpoint they control and read the secret off the wire. Recreate the trigger to move it.
Notes
- Use an
https://webhook URL. The signature proves integrity and origin, not confidentiality — the payload itself is only protected by transport security. - Deliveries are filtered per event against the subscribing user's document permissions, so a trigger never surfaces a document that user could not already open.