> ## Documentation Index
> Fetch the complete documentation index at: https://docs.flowpayroll.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Standard Webhooks

> What Flow Payroll implements from the Standard Webhooks specification, point by point, and what it adds.

<a href="https://www.standardwebhooks.com" target="_blank" rel="noopener noreferrer">
  <img className="block dark:hidden" src="https://mintcdn.com/flowpayroll-1ca487e0/eqkE8PD3Dnpmucfn/images/standard-webhooks/standard-webhooks-light.svg?fit=max&auto=format&n=eqkE8PD3Dnpmucfn&q=85&s=5f16454b169ee94f3b273ca1e44939e1" alt="Standard Webhooks" width="200" noZoom data-path="images/standard-webhooks/standard-webhooks-light.svg" />

  <img className="hidden dark:block" src="https://mintcdn.com/flowpayroll-1ca487e0/eqkE8PD3Dnpmucfn/images/standard-webhooks/standard-webhooks-dark.svg?fit=max&auto=format&n=eqkE8PD3Dnpmucfn&q=85&s=fa9cd3bdc75224c1d8533dc32af6ff52" alt="Standard Webhooks" width="200" noZoom data-path="images/standard-webhooks/standard-webhooks-dark.svg" />
</a>

Flow Payroll's webhooks follow version 1.0.0 of the open [Standard Webhooks specification](https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md). The specification sets out a signature scheme and three headers, then a series of recommendations; it does not use formal MUST and SHOULD wording. This page takes its points in turn and says what we do. **Yes** means we do what the specification says, and the last column says how.

## Signatures and headers

| The specification says | Us | What Flow Payroll does |
| - | - | - |
| Send a `webhook-id` header, unique per event and the same on every retry | Yes | `evt_` followed by a time-sortable code. The same on every retry, resend and replay. |
| Send a `webhook-timestamp` header, in Unix seconds, for the attempt | Yes | Set fresh on every attempt. When the event happened is `timestamp` in the body. |
| Send a `webhook-signature` header, a space-separated list of signatures | Yes | Every `v1` entry first, then every `v1a` entry. |
| Prefix the webhook headers with `webhook-` | Yes | `webhook-id`, `webhook-timestamp` and `webhook-signature`. Flow Payroll's own extra headers start with `X-Flow-` (plus `X-Request-Id`), so they never clash. |
| Keep the id and timestamp out of users' control, or at least free of `.` | Yes | Flow Payroll generates both. The id is `evt_` followed by letters and digits, and the timestamp is digits only, so neither can contain a full stop. |
| Sign `{id}.{timestamp}.{body}` | Yes | Over the exact bytes sent. The body is byte-identical on every attempt. |
| Symmetric signatures: `v1`, HMAC-SHA256 | Yes | On every delivery. |
| Symmetric secret: 24 to 64 random bytes, base64 with a `whsec_` prefix | Yes | 32 random bytes (256 bits), shown once as `whsec_…` when the endpoint is created or its secret is rotated. |
| Asymmetric signatures: `v1a`, ed25519, keys written `whpk_` (public) and `whsk_` (secret) | Yes | On every delivery. You get the `whpk_…` public key. The `whsk_` private key never leaves Flow Payroll. |
| Use a different key for each endpoint | Yes | Each endpoint has its own secret and key pair, and every rotation issues new ones. |
| Prefer asymmetric signatures | Yes | Every delivery carries both. Our guides lead with `v1a`, checked with the endpoint's public key, so your receiver never holds anything that could sign an event. `v1`, checked with the signing secret, is documented as the alternative. |
| During rotation, sign with the old and the new key | Yes | Both sign for 24 hours after a rotation, or until you revoke the old one. |
| Receivers reject timestamps outside a tolerance | Yes | We advise five minutes. Every code sample on [Verify signatures](/api-reference/webhooks/verify-signatures) applies it, and so do the official libraries it shows. |
| Receivers compare `v1` signatures in constant time, and check `v1a` with a maintained ed25519 library | Yes | Every `v1` sample compares in constant time, and so do the official libraries. Every `v1a` sample uses a maintained ed25519 library: the built-in `crypto` module in Node.js, `cryptography` in Python and BouncyCastle in C#. |
| Receivers trust only known public keys, never one sent with the request | Yes | Read the public key from `GET /endpoints/{endpointId}/secrets`. No delivery carries one, in its headers or its body. |
| Receivers use `webhook-id` to avoid processing an event twice | Yes | See [Handle duplicates](/api-reference/webhooks/best-practices#handle-duplicates). |

## Payload

| The specification says | Us | What Flow Payroll does |
| - | - | - |
| Send the payload as JSON in the body of a `POST` | Yes | `Content-Type: application/json; charset=utf-8`. |
| Minimise the JSON | Yes | Bodies are sent without whitespace, whatever format the producing service used. |
| Document each event type with a formal schema and examples | Yes | Every event has a reference page with its full OpenAPI 3.1 schema and an example body. Each example is written by the same code that writes real deliveries, and our tests fail if an event has none or if one does not match its schema. |
| Top-level `type`, `timestamp` (ISO 8601, when it happened) and `data` | Yes | All three, plus the extra top-level fields listed under [What Flow Payroll adds](#what-flow-payroll-adds), which the specification allows. |
| Choose between thin and full payloads (the specification describes both and recommends neither) | Yes | Each endpoint chooses `Full` (the whole record) or `Minimal` (the resource type, its id and the employee's id). See [Full and Minimal payloads](/api-reference/webhooks/event-types#full-and-minimal-payloads). |
| Keep payloads small, usually under 20 KB | Yes | Our example `Full` payslip is about 8 KB and `Minimal` payloads are under 1 KB, and files are sent as links. A payslip with many lines can pass 20 KB. A `Full` payload over 256 KB is sent as `Minimal` instead, with `payloadDowngraded: true`. |
| Send large files as a link | Yes | `payslip.created`, `payslip.updated` and `payslip.finalised`, and the P45 and P60 `issued` and `replaced` events, carry `metadata.documentUrl`, an API address you call with your own credentials. |

## Event types

| The specification says | Us | What Flow Payroll does |
| - | - | - |
| Name types hierarchically, separated by full stops, using `[a-zA-Z0-9_]` | Yes | For example `payslip.finalised` and `employee.leaver.recorded`, in lower case. |
| Keep one schema per event type | Yes | Within an [API version](/api-reference/webhooks/event-types#versions), a type's schema only gains optional fields, and fields documented as open-ended may gain new values. Anything that could break a receiver comes in a new version. |
| Let receivers choose event types, and filter before sending | Yes | Per endpoint: exact types, families such as `payslip.*`, or `*`. |

## Delivery

| The specification says | Us | What Flow Payroll does |
| - | - | - |
| Retry over several days with exponential backoff and random jitter | Yes | The specification's example schedule: 10 attempts over about 75.6 hours, each wait varied at random by up to 20% either way. See [Retry schedule](/api-reference/webhooks/delivery-and-retries#retry-schedule). |
| `2xx` is success, anything else is failure | Yes | Any status from 200 to 299. |
| Treat a failed or reset connection as a failure | Yes | It is retried on the same schedule as a non-`2xx` answer. |
| Treat `3xx` as failure rather than following redirects | Yes | Redirects are never followed. A `3xx` is retried like any other failure. |
| On `410 Gone`, disable the endpoint and stop sending | Yes | The delivery is not retried, and the endpoint is disabled straight away with status `DisabledBySystem` and `statusReason` `endpoint_gone`. The people who get [alert emails](/api-reference/webhooks/manage-endpoints#alert-emails) are emailed. |
| Throttle on `429`, `502` and `504` | Yes | Also on `503` and on timeouts. Each one halves the rate we send to that endpoint (starting from 100 a second, or your `rateLimitPerSecond` if you set one), and each successful delivery then adds one a second back until the full rate is restored. |
| Take `Retry-After` into account when scheduling the next attempt | Yes | On any failed attempt that sends it, the next retry waits for whichever is longer: your `Retry-After`, capped at one hour, or the next wait in the retry schedule. `Retry-After` never brings a retry forward. |
| Time requests out after 15 to 30 seconds | Yes | 15 seconds for the whole request, from connecting to reading your response, with at most 5 of those to connect. |
| Tell the customer another way, such as email, when an endpoint keeps failing | Yes | An email after 24 hours of unbroken failure, and another when the endpoint is disabled, to the people who get [alert emails](/api-reference/webhooks/manage-endpoints#alert-emails). |
| Disable an endpoint that keeps failing | Yes | After five days of unbroken failure, with `statusReason` `failing_for_5_days`. |

## Security

| The specification says | Us | What Flow Payroll does |
| - | - | - |
| Consider requiring HTTPS | Yes | Required: `https://` on port 443, with a certificate from a public certificate authority. |
| Consider sending from static IP addresses | Yes | Deliveries leave from fixed addresses, listed in [Best practices](/api-reference/webhooks/best-practices#allow-list-our-sending-addresses): production `52.18.20.191` and `52.212.106.116`, sandbox `40.180.96.229`. |
| Protect against server-side request forgery, with a filtering proxy and an isolated network | Yes | We refuse URLs that are IP addresses or local names such as `localhost`, and any host that resolves to a private, loopback, link-local, shared, multicast or other reserved address. We then connect only to the address we checked. Deliveries leave from a separate network whose firewall lets nothing out but HTTPS and blocks private address ranges. We filter in the sender itself rather than through a separate proxy. |

## Additional functionality

| The specification says | Us | What Flow Payroll does |
| - | - | - |
| Allow several endpoints | Yes | 20 per organisation, and 20 account-wide endpoints per account. |
| Show failures and allow manual retries | Yes | The [delivery log](/api-reference/webhooks/replay-and-recovery#the-delivery-log), resending one delivery, and replaying any window within the last 90 days. |
| Offer an API to manage endpoints | Yes | The [Webhooks API](/api-reference/webhooks/manage-endpoints). |

## What Flow Payroll adds

None of these change anything the specification requires, so the official verification libraries check our `v1` signatures as they are.

* **Headers**: `X-Flow-Event-Type`, `X-Flow-Organisation-Id`, `X-Flow-Customer-Id`, `X-Flow-Delivery-Id`, `X-Flow-Delivery-Attempt` and `X-Request-Id`, so you can route before parsing the body and quote ids to support.
* **Envelope fields**: `id`, `apiVersion`, `organisationId`, `customerId`, `livemode` and `metadata` (`resourceUrl`, `documentUrl`, `correlationId`, `payloadStyle`, `payloadDowngraded` and the date formats).
* **Stopping one delivery**: answer with a non-`2xx` status and the header `webhook-delivery: abort-message` to stop retrying that delivery without disabling the endpoint.
* **Pausing a failing endpoint**: ten failures in a row pause deliveries for one minute, doubling each time up to 30 minutes, without using up attempts.
* **Operational events**: `webhook.endpoint.failing`, `webhook.endpoint.disabled` and `webhook.delivery.exhausted`, so you can act on failures in code. See [Operational events](/api-reference/webhooks/delivery-and-retries#operational-events).
* **Test events**: `webhook.test`, sent on demand. See [Testing webhooks](/api-reference/webhooks/testing).
* **Account-wide endpoints**: one endpoint for every organisation on an account, with organisations included or excluded. See [Bureaux and many organisations](/api-reference/webhooks/account-wide-endpoints).
* **Endpoint settings**: strict `Fifo` ordering, a `rateLimitPerSecond` cap, custom headers (stored encrypted) and alert contacts. See [Manage endpoints](/api-reference/webhooks/manage-endpoints).
* **Versioned payloads**: an endpoint keeps its `apiVersion` until you move it. See [Versions](/api-reference/webhooks/event-types#versions).
* **Retry-safe API calls**: an `Idempotency-Key` header on create, rotate, resend and replay.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.