# GOV.UK Notify Webhooks

GOV.UK Notify posts JSON callbacks to your service for three things: **delivery receipts** (email
and text message status), **received text messages** and **returned letters**. Callbacks are
**not signed**. You give Notify a callback URL and a bearer token, and Notify sends the token as
`Authorization: Bearer <token>` on every request. Your endpoint authenticates the request by
comparing that header with the token you chose.

## When to Use This Skill

* How do I receive GOV.UK Notify callbacks?
* How do I verify the `Authorization: Bearer` token Notify sends?
* Does GOV.UK Notify sign webhook payloads? (No: shared bearer token only)
* How do I tell delivery receipts from received text messages? (There is no event field)
* Why does my Notify callback endpoint return 401?

## Verification (core)

```javascript
const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyNotifyRequest(headers, secret = process.env.GOVUK_NOTIFY_WEBHOOK_SECRET) {
  if (!secret) return false;                                  // never accept when unconfigured
  const expected = secret.toLowerCase().startsWith('bearer ') ? secret.slice(7) : secret;
  const header = headers['authorization'];
  if (!expected || typeof header !== 'string' || !header.toLowerCase().startsWith('bearer ')) return false;
  return safeEqual(header.slice(7), expected);               // token itself is case-sensitive
}

```

* Header: `Authorization: Bearer <token>`. Match the `Bearer` scheme case-insensitively (RFC 9110);
  compare the rest to the secret in constant time. Don't trim or alter the secret.
* `GOVUK_NOTIFY_WEBHOOK_SECRET` holds the **bare** token. If the configured value itself starts
  with `Bearer ` (any case), strip it, as Hookdeck does.
* Missing header, wrong scheme or mismatch: `401`. Body that isn't a JSON object: `400`. Handled: `200`.
* Read the body raw (`express.raw({ type: '*/*' })`) and authenticate before parsing it.
* **A static shared credential authenticates the sender, not the body, and gives no replay
  protection** (no signature, no timestamp). Use HTTPS and rotate the token in Notify if it leaks.

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

## Callback Types

Payloads have no `event` field. Tell the kinds apart by shape (documented field tables):

| Callback | Identifying fields | Sent When |
| --- | --- | --- |
| Delivery receipt | `status`, `notification_type`, `id`, `reference`, `to` | An email or text message changes status |
| Received text message | `source_number`, `destination_number`, `message`, `date_received` | Someone texts your Notify number |
| Returned letter | `notification_id`, `date_sent`, `template_name` | A letter you sent is returned |

Delivery receipt `status` values: `delivered`, `permanent-failure`, `temporary-failure`,
`technical-failure`. `notification_type` is `email` or `sms`. If a request fails, Notify retries
every 5 minutes, up to 5 times, so handle duplicates idempotently.

## Environment Variables

```bash
GOVUK_NOTIFY_WEBHOOK_SECRET=your-callback-bearer-token   # the token you set for the callback

```

## Hookdeck Setup

Hookdeck's source type is **GOV.UK Notify** (`GOVUK_NOTIFY`): API key verification on the
`authorization` header with a `Bearer ` prefix. Paste the bearer token you set for the callback in GOV.UK Notify (API integration > Callbacks).

## Local Development

Notify can't reach `localhost`. Tunnel with the Hookdeck CLI (no account required):

```bash
npx hookdeck-cli listen 3000 govuk-notify --path /webhooks/govuk-notify

```

Use the printed URL as the callback URL in Notify.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/govuk-notify-webhooks/references/overview.md) - What Notify callbacks are, payload shapes
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/govuk-notify-webhooks/references/setup.md) - Configure the callback and bearer token
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/govuk-notify-webhooks/references/verification.md) - Bearer check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/govuk-notify-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests