Gareth Wilson Gareth Wilson

Guide to GOV.UK Notify Webhooks: Features and Best Practices

Published


GOV.UK Notify callbacks tell your service what happened to the messages it sent through Notify. An email or text message is delivered or fails, a member of the public texts your service's number, or a letter comes back undelivered. Instead of polling Notify's API for each message, you receive the result as a POST to your own endpoint.

This guide covers the three kinds of callback Notify sends and what each contains, how to authenticate them with your bearer token, how Notify retries, and the practices that keep a callback receiver reliable.

What are GOV.UK Notify callbacks?

Notify's name for its webhooks is callbacks. You can get them when:

  • an email or text message you've sent is delivered or fails
  • your service receives a text message
  • a letter you sent is returned

To set one up, you give Notify a URL and a bearer token. Notify sends JSON in a POST with the token in the Authorization header as Bearer <token>. There is no signature, so the token is the only thing that tells your endpoint a request came from Notify.

None of the callbacks carries an event name. You tell them apart by their fields, and by which URL received them if you set up a different URL for each kind.

GOV.UK Notify callback features

FeatureDetails
ConfigurationNotify, API integration > Callbacks
KindsDelivery receipts (email and text message), received text messages, returned letters
Method and formatPOST, Content-Type: application/json
AuthenticationYour bearer token, sent as Authorization: Bearer <token>
Signature and timestamp headerNone
Event nameNone. Identify the kind by its fields
RetriesEvery 5 minutes, up to 5 times
Timeout5 seconds, in Notify's sending code
Retried responsesConnection errors, 5xx and 429, in Notify's sending code. Other 4xx aren't retried
Test keysDelivery receipts work with test API keys, but not with smoke-testing numbers or addresses
Source IPsNot published

The timeout and the retry conditions come from Notify's open-source sending code (alphagov/notifications-api), not from the documentation page, so treat them as current behaviour rather than a promise.

Callback payloads

Delivery receipts

All values are strings except template_version, which is a number.

FieldFormat
idNotify's ID for the notification (UUID)
referenceThe reference your service sent, or null
toThe recipient's email address or phone number
statusdelivered, permanent-failure, temporary-failure or technical-failure
created_atWhen your service sent the request
completed_atWhen the status last changed, or null
sent_atWhen the notification was sent, or null
notification_typeemail or sms
template_idThe template's ID (UUID)
template_versionThe template's version number

Built from that table, a receipt looks like this:

{
  "id": "740e5834-3a29-46b4-9a6f-16142fde533a",
  "reference": "12345678",
  "to": "07700912345",
  "status": "delivered",
  "created_at": "2017-05-14T12:15:30.000000Z",
  "completed_at": "2017-05-14T12:15:30.000000Z",
  "sent_at": "2017-05-14T12:15:30.000000Z",
  "notification_type": "sms",
  "template_id": "f33517ff-2a88-4f6e-b855-c550268ce08a",
  "template_version": 1
}

Received text messages

id, source_number (the sender), destination_number (your number), message and date_received (UTC). All values are strings.

Returned letters

notification_id, reference, date_sent, sent_by (the email address of the team member who sent it, or null), template_name, template_id, template_version, spreadsheet_file_name, spreadsheet_row_number and upload_letter_file_name. Notify says it can take a few weeks to hear about a returned letter.

A fourth kind in the code

Notify's sending code also has a complaint callback, with notification_id, complaint_id, reference, to and complaint_date. The documentation page doesn't mention it, so don't rely on it, but don't let it break your handler either: it shares notification_id with returned letters.

Inspect GOV.UK Notify callbacks as they arrive. Use a Hookdeck Inspect URL as your callback URL while you send with a test API key to see the exact headers and body Notify sends, with no account or setup required.

Setting up GOV.UK Notify callbacks

Generate a long random bearer token first, for example with openssl rand -hex 32.

  1. Sign in to GOV.UK Notify.
  2. Go to the API integration page.
  3. Select Callbacks.
  4. Enter a callback URL and bearer token for each kind of callback you want, and save.
  5. Send an email or text message with a test API key and confirm that a delivery receipt arrives.

With Hookdeck. GOV.UK Notify is a supported source in Hookdeck. Its setup note reads: "Paste the bearer token you set for the callback in GOV.UK Notify (API integration > Callbacks)." Create a source with the GOV.UK Notify type and use its URL as the callback URL. In Bearer Token, paste only the token, without Bearer . Notify sends Bearer <token>, and the source checks that header. The source accepts POST only, which is what Notify sends. A request with a missing or wrong token still gets a 200 from Hookdeck, but Hookdeck marks it as failed verification and doesn't deliver it to your service.

For local development, hookdeck listen 3000 govuk-notify --path /webhooks/notify gives you a public URL that forwards to your machine.

Securing GOV.UK Notify callbacks

Check that the Authorization header uses the Bearer scheme, then compare the token with the one you stored, in constant time.

const crypto = require('crypto');
const express = require('express');

const TOKEN = process.env.NOTIFY_CALLBACK_TOKEN; // the bare bearer token

function verifyBearer(header) {
  if (!TOKEN || typeof header !== 'string') return false;
  const match = /^bearer (.+)$/i.exec(header);
  if (!match) return false;
  const a = Buffer.from(match[1], 'utf8');
  const b = Buffer.from(TOKEN, 'utf8');
  // timingSafeEqual throws on unequal lengths, so check first.
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();

app.post('/webhooks/notify', express.raw({ type: '*/*' }), (req, res) => {
  if (!verifyBearer(req.get('authorization'))) {
    return res.sendStatus(401);
  }

  let callback;
  try {
    callback = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  if (callback.notification_type && callback.status) {
    // Delivery receipt: update the notification with callback.id / callback.reference.
  } else if (callback.source_number) {
    // Received text message.
  } else if (callback.complaint_id) {
    // Complaint (sent by Notify's code, not on the documentation page).
  } else if (callback.notification_id) {
    // Returned letter.
  }

  res.sendStatus(200);
});

The same check in Python:

import hmac
import os

TOKEN = os.environ.get("NOTIFY_CALLBACK_TOKEN", "")


def verify_notify_callback(headers) -> bool:
    if not TOKEN:
        return False  # never accept when unconfigured
    header = headers.get("Authorization", "")
    scheme, _, token = header.partition(" ")
    if scheme.lower() != "bearer" or not token:
        return False
    # Compare bytes: compare_digest raises TypeError on a non-ASCII str.
    return hmac.compare_digest(token.encode("utf-8"), TOKEN.encode("utf-8"))

The bearer token is static. It proves the sender knows the token, but it doesn't cover the body, so a modified payload still passes, and nothing stops a captured request from being replayed. Use HTTPS only, keep the token out of logs, and change it in Callbacks if it might have leaked. Callbacks contain personal data such as phone numbers and the text of received messages, which is another reason to keep them off plain HTTP.

Make GOV.UK Notify callbacks production-ready. Hookdeck Event Gateway checks the bearer token, answers Notify well inside its 5-second timeout, and stores every receipt so you can retry and replay delivery to your service.

GOV.UK Notify callback limitations and pain points

Five retries, five minutes apart

The Problem: An outage of half an hour or so can lose delivery receipts and received messages for good.

Why It Happens: Notify retries every 5 minutes, up to 5 times. Its sending code also gives up straight away on a 4xx other than 429.

Workarounds:

  • Don't return a 4xx for an authenticated request you could process later, such as a receipt for a notification you haven't saved yet. Return a 5xx if you need Notify to try again.
  • Reconcile with Notify's API for any notification still without a final status.

How Hookdeck Can Help: Hookdeck accepts the callback as soon as it arrives and stores it, then delivers to your service with retries you configure, over hours or days rather than 25 minutes.

A 5-second timeout

The Problem: A handler that writes to a slow database or calls another service times out and gets retried, even though it eventually succeeded.

Why It Happens: Notify's sending code waits 5 seconds for a response.

Workarounds:

  • Authenticate, queue the payload and return 200, then process in the background.

How Hookdeck Can Help: Hookdeck answers Notify immediately and then delivers to your service, with a delivery timeout you control.

No event name

The Problem: One endpoint receiving all three kinds has to guess which kind it got.

Why It Happens: Notify's callbacks carry their fields and nothing else.

Workarounds:

  • Use a different URL, or a different path, for each kind of callback. Notify stores a URL and token per kind.
  • If you share a URL, branch on distinctive fields: notification_type for receipts, source_number for received texts, notification_id for returned letters, checking complaint_id first.

How Hookdeck Can Help: Hookdeck filters can route on those fields, so each kind reaches its own handler.

Best practices

Test with a test API key

Delivery receipts work with test API keys, so you can exercise your callback endpoint without sending real messages. They don't work with Notify's smoke-testing phone numbers or email addresses.

Use reference to link receipts to your records

Set a reference when you send, and look it up when the receipt arrives. It saves storing Notify's id before the response comes back.

Deduplicate receipts

Retries mean the same callback can arrive twice. Key delivery receipts on id and status, received texts on id, and returned letters on notification_id. See our guide to webhook idempotency.

Acknowledge fast and process asynchronously

Return 200 once the token checks out and the body is queued. See why to process webhooks asynchronously.

Conclusion

GOV.UK Notify sends delivery receipts, received text messages and returned letters as JSON POST requests with your bearer token in Authorization: Bearer <token>. There is no signature and no event name, so compare the token in constant time over HTTPS, tell the kinds apart by URL or by their fields, answer within 5 seconds, and remember that five retries over 25 minutes is all Notify gives you.

Hookdeck Event Gateway checks the token, answers Notify immediately, and keeps every callback so a short outage on your side doesn't cost you receipts.

Get started with Hookdeck for free and handle GOV.UK Notify callbacks 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.