# Guide to GOV.UK Pay Webhooks: Features and Best Practices

GOV.UK Pay is the payment platform the Government Digital Service runs for central government, councils, the NHS and other public bodies. A service takes card payments for a licence, a fine or council tax through a GOV.UK Pay payment page, and the money is handled by the service's payment service provider. Webhooks tell the service's own systems when each payment is authorised, captured, settled or refunded, without polling the API.

In this guide: the callback URL rules for live services, the four payment events, how to verify `Pay-Signature`, and how to build a receiver that tolerates duplicates and events in the wrong order.

## What are GOV.UK Pay webhooks?

Each GOV.UK Pay service can have webhooks, created in the admin tool with a callback URL, a description and the payment events to send. When a subscribed payment event happens, GOV.UK Pay posts a JSON message describing the payment as it stands, in the same shape the API returns when you get a payment.

Webhooks only cover payments today: `resource_type` is always `payment`. The admin tool keeps a history of each webhook's events and shows every delivery attempt, which helps when a payment's state in your system doesn't match GOV.UK Pay.

## GOV.UK Pay webhook features

| Feature | Details |
| --- | --- |
| Signature | hex HMAC-SHA256 |
| Signature header | `pay-signature` |
| Body | JSON |
| Event routing | Body field `event_type` |

Rules from the [GOV.UK Pay webhooks documentation](https://docs.payments.service.gov.uk/webhooks/):

* Live callback URLs must use HTTPS with TLS 1.2 and end in `.gov.uk`. Other domains need an exception from GOV.UK Pay support, granted only for domains owned by the service or its parent organisation.
* Sandbox and "not live yet" services can use any HTTPS URL.
* The secret is shown by selecting **View** next to the webhook.
* Delivery is at least once, messages can arrive out of order, and retries stop after "several attempts", with no published schedule or timeout.

## Events and payloads

Route on the top-level `event_type`. GOV.UK Pay currently documents payment webhooks, with `resource_type` set to `payment`. Use `webhook_message_id` to identify repeated deliveries.

Some documented events are listed below; see the provider documentation for the full list.

| Event | Meaning |
| --- | --- |
| `card_payment_succeeded` | your payment service provider has authorised the payment |
| `card_payment_captured` | GOV.UK Pay has taken ('captured') the payment from the user's bank account |
| `card_payment_settled` | your payment service provider has sent the payment to your bank account. |
| `card_payment_refunded` | the refund has been sent to the user's bank account by your payment service provider |

The introduction to the GOV.UK Pay docs also mentions updates when a payment fails or expires, but no `event_type` value for either is documented. Until one is, check `resource.state` from the Get payment API for payments that never reach `card_payment_succeeded`.

This is the `card_payment_captured` sample from the [webhook samples library](https://github.com/hookdeck/webhook-samples/blob/main/providers/govuk-pay/latest/card_payment_captured.json):

```json
{
  "webhook_message_id": "123abc",
  "api_version": 1,
  "created_date": "2019-07-11T10:36:26.988Z",
  "resource_id": "hu20sqlact5260q2nanm0q8u93",
  "resource_type": "payment",
  "event_type": "card_payment_captured",
  "resource": {
    "amount": 5000,
    "description": "Pay your council tax",
    "reference": "12345",
    "language": "en",
    "email": "sherlock.holmes@example.com",
    "state": {
      "status": "success",
      "finished": true
    },
    "payment_id": "hu20sqlact5260q2nanm0q8u93",
    "payment_provider": "stripe",
    "created_date": "2021-10-19T10:05:45.454Z",
    "refund_summary": {
      "status": "available",
      "amount_available": 5000,
      "amount_submitted": 0
    },
    "settlement_summary": {},
    "card_details": {
      "last_digits_card_number": "1234",
      "first_digits_card_number": "123456",
      "cardholder_name": "Sherlock Holmes",
      "expiry_date": "04/24",
      "billing_address": {
        "line1": "221 Baker Street",
        "line2": "Flat b",
        "postcode": "NW1 6XE",
        "city": "London",
        "country": "GB"
      },
      "card_brand": "Visa",
      "card_type": "debit"
    },
    "delayed_capture": false,
    "moto": false,
    "provider_id": "10987654321",
    "return_url": "https://your.service.gov.uk/completed"
  }
}

```

`resource_id` is the same as `resource.payment_id`, and `amount` is in pence. `reference` is the reference your service set when it created the payment, which is usually how you find the application or invoice it belongs to. GOV.UK Pay's field table names the message ID `id`, while its example uses `webhook_message_id`, so read whichever is present.

## Setting up GOV.UK Pay webhooks

1. In the GOV.UK Pay admin tool, pick your service from **My services**.
2. Go to **Settings**, then **Developers**, then **Webhooks**, and select **Create a new webhook**.
3. Enter a **Callback URL**, such as `https://payments.example-council.gov.uk/webhooks/govuk-pay`, and a **Description**.
4. Select the payment events to send and **Save**.
5. Select **View** next to the webhook to reveal its signing secret, and store it in your server's configuration, for example as `GOVUK_PAY_WEBHOOK_SECRET`.

**Update** changes the URL, description or events later, and **Deactivate webhook** on the View page pauses delivery until you reactivate it.

A Hookdeck source URL is not a `.gov.uk` domain, so on a live service it needs the same exception as any other non-`.gov.uk` URL. In sandbox, create a source with the GOV.UK Pay type, paste the signing secret as its verification secret and use the source URL as the callback. To test against a local server:

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

```

Then make a test payment with one of the sandbox test cards.

## Securing GOV.UK Pay webhooks

`Pay-Signature` holds a lower-case hex HMAC-SHA256 of the request body, keyed with the signing secret. GOV.UK Pay specifies that both the body and the key are UTF-8 with no byte order mark, so hash the bytes exactly as received. The [verification section](https://docs.payments.service.gov.uk/webhooks/#verifying-webhook-messages-from-gov-uk-pay) has examples in several languages.

### Node.js verification

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

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['pay-signature'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const message = rawBody;
  const candidates = [signature];
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
    const received = Buffer.from(candidate, 'hex');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

```

GOV.UK Pay asks for a `2xx` before you process the message, and warns that messages can arrive in any order. The handler stores the message and lets a worker apply it:

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

const app = express();

app.post('/webhooks/govuk-pay', express.raw({ type: '*/*' }), async (req, res) => {
  const signingSecret = process.env.GOVUK_PAY_WEBHOOK_SECRET;
  if (!signingSecret) return res.sendStatus(500);
  if (!verifySignature(req.body, req.headers, signingSecret)) return res.sendStatus(401);

  let message;
  try {
    message = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  if (!message || typeof message !== 'object' || Array.isArray(message)) return res.sendStatus(400);
  try {
    await savePaymentMessage({
      messageId: message.webhook_message_id ?? message.id,
      paymentId: message.resource_id,
      eventType: message.event_type,
      occurredAt: message.created_date,
      body: message,
    });
  } catch {
    return res.sendStatus(503);
  }

  res.sendStatus(200);
});

```

`savePaymentMessage` inserts the message only if its ID is new. The worker then decides what to do from the payment's progress rather than arrival order: a `card_payment_settled` that arrives before `card_payment_captured` still means the money has been taken. If you need certainty, call the Get payment API for `resource_id` and use the state it returns.

No timestamp is signed, so a captured message would still verify if resent later. Deduplicating on the message ID handles that. Hookdeck's GOV.UK Pay source checks `Pay-Signature` the same way, replies `200` to GOV.UK Pay, and records a mismatched signature as failed verification.

> **Make GOV.UK Pay webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies `Pay-Signature`, keeps a searchable record of every payment message and retries delivery to your case management or finance system when it is down.

## GOV.UK Pay webhook limitations and pain points

### Live callbacks must be on .gov.uk

**The Problem:** You can't point a live service at a third-party endpoint, a cloud function URL or a webhook gateway without approval.

**Why It Happens:** GOV.UK Pay requires live callback URLs to end in `.gov.uk`, and grants exceptions only for domains belonging to the service or its parent organisation.

**Workarounds:**

* Host the receiver on a `.gov.uk` subdomain that your organisation controls.
* Email GOV.UK Pay support early if you need an exception, since approval can take time.

**How Hookdeck Can Help:** In sandbox and "not live yet" modes, a Hookdeck source works without any exception, so you can build and test against real GOV.UK Pay deliveries before your live domain is ready.

### Retry policy is not specified

**The Problem:** You can't tell how long an outage your receiver can survive.

**Why It Happens:** GOV.UK Pay says only that it retries when it doesn't get a `2xx` quickly and stops "after several attempts". It publishes no count, interval or timeout.

**Workarounds:**

* Respond before processing so a slow downstream system never causes a timeout.
* After an incident, compare the webhook's event history in the admin tool with your records, and fetch missing payments from the API.

**How Hookdeck Can Help:** Hookdeck accepts each message immediately and applies a retry policy you define, with alerts when delivery to your service keeps failing.

### Out-of-order and repeated messages

**The Problem:** A payment can appear settled before it appears captured, or the same message can arrive twice.

**Why It Happens:** GOV.UK Pay documents at-least-once delivery and warns that order is not guaranteed. A `2xx` that GOV.UK Pay doesn't receive also leads to a resend.

**Workarounds:**

* Deduplicate on the message ID.
* Never move a payment backwards to an earlier state, and confirm with the API when in doubt.

**How Hookdeck Can Help:** Hookdeck's deduplication can drop repeated `webhook_message_id` values, and its event log shows the full sequence for each payment.

### No events for failed or expired payments

**The Problem:** You don't hear about payments that never complete.

**Why It Happens:** The docs say you can be told when a payment fails or expires, but the documented `event_type` values cover only succeeded, captured, settled and refunded.

**Workarounds:**

* Poll the API for payments that are still unfinished after the payment link's lifetime.
* Capture any unexpected `event_type` your receiver logs, in case GOV.UK Pay adds these events.

**How Hookdeck Can Help:** Hookdeck keeps every request, including any with an `event_type` your code didn't expect, so a new event type can be inspected and replayed once you handle it.

## Best practices

### Store, acknowledge, then process

GOV.UK Pay explicitly asks for an immediate `2xx`. Put the message in a table or queue and do the rest in a worker, following [our guide to asynchronous webhook processing](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### Treat the message ID as an idempotency key

Record `webhook_message_id` with a unique constraint alongside the change it caused. [Implementing webhook idempotency](/webhooks/guides/implement-webhook-idempotency) walks through the pattern.

### Protect cardholder data

Payloads include the cardholder's name, billing address, email and partial card numbers. Restrict who can read stored messages and leave `resource` out of routine logs.

### Use the admin tool's event history

**View details** on each event shows when it was sent, the body and every previous attempt. Check it first when a payment's status looks wrong.

## Conclusion

GOV.UK Pay webhooks deliver four payment events, each signed in `Pay-Signature` with a hex HMAC of the body. Verify the raw bytes, acknowledge before doing anything slow, deduplicate on the message ID, and don't assume the events arrive in payment order. Plan for the `.gov.uk` rule early if your receiver lives anywhere else.

[Hookdeck Event Gateway](https://hookdeck.com) records each GOV.UK Pay message and delivers it to your back-office systems with retries you control.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle GOV.UK Pay webhooks reliably in minutes.