# Glean Webhooks

Glean Triggers subscribe your application to content events (a review requested
from you, a new call you took part in, the half hour before a meeting starts)
and deliver each one to your HTTPS endpoint as a signed webhook that follows the
Standard Webhooks specification.

## When to Use This Skill

* How do I receive Glean trigger webhooks?
* How do I verify `webhook-id` / `webhook-timestamp` / `webhook-signature` from Glean?
* Where do I get the Glean signing secret? (Only returned once, by `POST /api/triggers`.)
* Why is my Glean webhook signature verification failing?
* Why are retried or replayed Glean deliveries rejected after 5 minutes?
* How do I handle a `CONTENT_SCHEDULE` trigger delivery?

## Verification (core)

Each delivery carries three headers:

```
webhook-id: msg_...           # stable across retries: use as idempotency key
webhook-timestamp: 1614265330 # Unix seconds
webhook-signature: v1,g0hM9SsE...  # space-delimited "v1,<base64>" entries

```

The signed string is `{webhook-id}.{webhook-timestamp}.{raw body}`, HMAC-SHA256,
keyed with the base64-decoded signing secret **after** removing the `whsec_`
prefix, base64-encoded. Use the official `standardwebhooks` library (the `svix`
library works too) and pass the **raw** body:

```javascript
const { Webhook } = require('standardwebhooks');

const wh = new Webhook(process.env.GLEAN_SIGNING_SECRET); // "whsec_..." — library decodes it
wh.verify(rawBody, {                  // rawBody: raw Buffer/string, never re-serialized JSON
  'webhook-id': req.headers['webhook-id'],
  'webhook-timestamp': req.headers['webhook-timestamp'],
  'webhook-signature': req.headers['webhook-signature'],
});                                   // throws on a bad signature or a timestamp >5 min off
const event = JSON.parse(rawBody.toString('utf8'));

```

```python
from standardwebhooks.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["GLEAN_SIGNING_SECRET"])
wh.verify(raw_body, {"webhook-id": ..., "webhook-timestamp": ..., "webhook-signature": ...})

```

If you set `delivery.auth` with `type: BEARER` when creating the trigger, Glean
also sends `Authorization: Bearer <secret>`. Treat it as an extra check on top of
the signature, never a replacement.

### Retries and the 5-minute window

The `standardwebhooks` and `svix` libraries **always** enforce a fixed 5-minute
timestamp window. Retries and replays through Hookdeck (and any other relay)
resend the **original** `webhook-timestamp`, so the library rejects any delivery
that reaches your handler more than 5 minutes after Glean signed it. Glean treats
a 4xx (other than 408/429) as terminal, so a rejected late delivery is dropped.
If you receive through Hookdeck or need replays to work, use the manual verifier
in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/glean-webhooks/references/verification.md): it recomputes the
HMAC over `{id}.{timestamp}.{raw body}` and lets you drop or widen the window,
comparing every space-separated `v1,<base64>` entry in constant time.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/glean-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/glean-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/glean-webhooks/examples/fastapi/).

## Trigger Deliveries

Triggers are created from per-datasource **presets**. Preset ids are per
deployment (discover them with `GET /api/trigger-presets`), so there is no fixed
event list. Glean's public docs don't publish the delivery body; the fields below come from
the test fixture in Glean's customer-meeting-prep cookbook recipe, so treat them as illustrative
and confirm them against a real delivery:

| Field | Example | Meaning |
| --- | --- | --- |
| `event_type` | `CONTENT_SCHEDULE` | What kind of trigger fired (`CONTENT_SCHEDULE` for "a meeting starts in 30 minutes") |
| `trigger_id` / `trigger_name` | `spec-7d2f...` | Which trigger produced it |
| `datasource` / `doc_type` | `GOOGLECALENDAR` / `event` | Source content |
| `doc_id`, `title`, `view_url` |  | The matched document |
| `event_time`, `time_offset_seconds` | `2026-08-25T18:30:00Z` / `1800` | Schedule triggers only |

Always keep a default branch for `event_type` values you do not handle.

## Environment Variables

```bash
# Returned only once by POST /api/triggers (cannot be read back); starts with whsec_
GLEAN_SIGNING_SECRET=whsec_xxxxx
# Optional: the delivery.auth BEARER secret, if you configured one
GLEAN_DELIVERY_BEARER=

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 glean --path /webhooks/glean

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/glean-webhooks/references/overview.md) - Glean Triggers concepts, payload, retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/glean-webhooks/references/setup.md) - Creating a trigger and capturing the signing secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/glean-webhooks/references/verification.md) - Library and manual verification, replay handling