> ## 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.

# Webhooks overview

> Get told the moment something happens in Flow Payroll: a payslip is finalised, a pay run completes, an employee leaves. Register an HTTPS endpoint, subscribe to the events you need, and verify every delivery.

Webhooks send an HTTPS `POST` to your server whenever something happens in Flow Payroll, so you react straight away instead of polling. A payslip is finalised, and your HR system has it seconds later. A P60 is issued, and your employee portal shows it. A pay run completes, and your accounting sync starts.

The Webhooks API lives at `/v1/webhooks` relative to the gateway base URLs on the [API reference introduction](/api-reference/introduction), for example `https://api.sandbox.flowpayroll.ai/v1/webhooks` in sandbox. It authenticates like every other service: see [Authentication](/api-reference/authentication).

<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>

Every delivery follows the open [Standard Webhooks](https://www.standardwebhooks.com) specification, so the official verification libraries for Node.js, Python, Go, Java, Ruby, PHP and more work without changes. [Standard Webhooks](/api-reference/webhooks/standard-webhooks) goes through the specification point by point.

## How it works

<Steps>
  <Step title="Register an endpoint">
    `POST /endpoints` with your HTTPS URL and the event types you want. The response carries the endpoint's **public key**, for checking its deliveries, and its **signing secret**, shown only this once.
  </Step>

  <Step title="Something happens in Flow Payroll">
    A payslip is finalised, an employee's tax code changes, an email bounces. Flow Payroll records the event and works out which endpoints are subscribed to it.
  </Step>

  <Step title="Flow Payroll delivers it">
    Your endpoint receives a signed JSON `POST`. Answer with any `2xx` within 15 seconds and the delivery is done.
  </Step>

  <Step title="Anything that fails is retried">
    Any other answer, or no answer, is retried on a fixed schedule for just over 75 hours (about three days). Every attempt is in the delivery log, and anything can be sent again. See [Delivery and retries](/api-reference/webhooks/delivery-and-retries).
  </Step>
</Steps>

## Quickstart

<Steps>
  <Step title="Create an endpoint">
    ```bash theme={null}
    curl -X POST https://api.sandbox.flowpayroll.ai/v1/webhooks/endpoints \
      -H "X-Auth-Token: $TOKEN" \
      -H "X-Org-Id: $ORGANISATION_ID" \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://example.com/webhooks/flow-payroll",
        "description": "HR system",
        "eventTypes": ["payslip.finalised", "employee.*"]
      }'
    ```

    Keep `publicKey` from the response in your receiver's configuration: checking deliveries with it is the [recommended way to verify](/api-reference/webhooks/verify-signatures#verify-with-the-public-key-v1a). It is not a secret, and you can read it again at any time. The response also carries `signingSecret`, which you need only if you check `v1` signatures instead: store it somewhere safe, such as your secrets manager. You cannot read it again; if you lose it, [rotate](/api-reference/webhooks/verify-signatures#rotate-a-signing-secret).
  </Step>

  <Step title="Verify each delivery">
    Check the `webhook-signature` header against the raw body before you trust anything in it. [Verify signatures](/api-reference/webhooks/verify-signatures) has copy-paste code.
  </Step>

  <Step title="Send a test event">
    `POST /endpoints/{endpointId}/test-deliveries` sends a signed `webhook.test` event straight away and tells you what your server answered. See [Testing webhooks](/api-reference/webhooks/testing).
  </Step>
</Steps>

## What a delivery looks like

```http theme={null}
POST /webhooks/flow-payroll HTTP/1.1
Host: example.com
Content-Type: application/json; charset=utf-8
User-Agent: FlowPayroll-Webhooks/1.0
webhook-id: evt_01J9ZQ8Y5W3F0R8S6T4V2X1ABC
webhook-timestamp: 1790700627
webhook-signature: v1,s5VzgF9IHMQ9XSZseYE+JVnUd4FqxLJrngB2MSRUq08= v1a,6w9TituPP9R87mQcYz3qRyH+nhA2VU5umHswQZq+vW0zC7S1u32PUEzqdzCb5+cnotNbuSBW+PVmAiKOExiSBA==
X-Flow-Event-Type: employee.created
X-Flow-Organisation-Id: 01J9ZQ7K2M4N6P8R0T2V4X6Z8B
X-Flow-Customer-Id: 01J9ZQ6H1K3M5P7R9T1V3X5Z7A
X-Flow-Delivery-Id: dlv_01J9ZQ8Y5W5B7D9F1H3K5M7P9R
X-Flow-Delivery-Attempt: 1
X-Request-Id: req_01J9ZQ8Y5W8C0E2G4J6L8N0Q2S

{
  "id": "evt_01J9ZQ8Y5W3F0R8S6T4V2X1ABC",
  "type": "employee.created",
  "timestamp": "2026-10-01T09:30:25.488Z",
  "apiVersion": "2026-10-01",
  "organisationId": "01J9ZQ7K2M4N6P8R0T2V4X6Z8B",
  "customerId": "01J9ZQ6H1K3M5P7R9T1V3X5Z7A",
  "livemode": false,
  "data": { "employeeId": "employee-001", "...": "the event's own fields" },
  "metadata": {
    "resourceUrl": null,
    "correlationId": null,
    "documentUrl": null,
    "payloadStyle": "Full",
    "payloadDowngraded": false,
    "dateFormat": "yyyy-MM-dd",
    "dateTimeFormat": "yyyy-MM-ddTHH:mm:ss.fffZ"
  }
}
```

### Headers

| Header | What it is for |
| - | - |
| `webhook-id` | The event's unique id. The same on every retry and every resend, so use it to [ignore duplicates](/api-reference/webhooks/best-practices#handle-duplicates). |
| `webhook-timestamp` | Unix seconds when this attempt was signed. Reject deliveries more than five minutes old. |
| `webhook-signature` | Signatures separated by spaces: every `v1,{signature}` (checked with your signing secret), then every `v1a,{signature}` (checked with your public key). More than one of each appears while a secret is being rotated. |
| `X-Flow-Event-Type` | The event type, so you can route without parsing the body. |
| `X-Flow-Organisation-Id` | The organisation the event belongs to. |
| `X-Flow-Customer-Id` | The account (customer) that owned the organisation when the event happened. Useful for [account-wide endpoints](/api-reference/webhooks/account-wide-endpoints). |
| `X-Flow-Delivery-Id` | Our id for this delivery. Quote it to support. |
| `X-Flow-Delivery-Attempt` | `1` for the first attempt, `2` for the first retry, and so on. |
| `X-Request-Id` | The trace id across Flow Payroll's services. Quote it to support. |

### The body

| Field | Meaning |
| - | - |
| `id` | The event id, identical to the `webhook-id` header. |
| `type` | The event type. See [Event types](/api-reference/webhooks/event-types). |
| `timestamp` | When it happened in Flow Payroll (ISO 8601, UTC), not when it was sent. |
| `apiVersion` | The payload version this event was written in. |
| `organisationId`, `customerId` | Who it belongs to. |
| `livemode` | `true` for live, `false` for sandbox. |
| `data` | The event's own fields: the full record, or a compact reference if the endpoint asked for `Minimal` payloads. |
| `metadata.resourceUrl` | Where to fetch the latest version of the thing the event is about, when there is one. |
| `metadata.documentUrl` | Where to download the payslip, P45 or P60 the event is about as a PDF, with your own credentials. `null` on every other event. |
| `metadata.payloadStyle`, `metadata.payloadDowngraded` | Which style `data` is in, and whether a `Full` payload was too large and sent as `Minimal` instead. |

## Key terms

* **Webhook**: an HTTP request Flow Payroll sends to you when something happens, instead of you asking repeatedly whether anything has.
* **Receiver**: the code on your server that accepts those requests.
* **Endpoint**: Flow Payroll's record of your receiver: its HTTPS URL, which events it wants, and the secret its deliveries are signed with. See [Manage endpoints](/api-reference/webhooks/manage-endpoints).
* **Public key**: what you check each delivery with, so you can prove a request is genuine. Flow Payroll signs every delivery with a private key only it holds, and gives you the matching public key, which can check a signature but never make one. See [Verify signatures](/api-reference/webhooks/verify-signatures).
* **Signing secret**: the other way to check a delivery. A random key only you and Flow Payroll know, which every delivery is also signed with.
* **At least once**: an event can occasionally arrive more than once (for example, when your answer was lost on the way back), but it is never silently lost. Use `webhook-id` to spot repeats.
* **Event type**: a named thing that can happen, such as `payslip.finalised`. Subscribe to an exact type, a family (`payslip.*`) or everything (`*`).
* **Delivery**: one event sent to one endpoint. It has attempts, a status (`Pending`, `Succeeded`, `Failed` or `Skipped`) and a place in the [delivery log](/api-reference/webhooks/replay-and-recovery#the-delivery-log).
* **Payload style**: `Full` sends the whole record in `data`. `Minimal` sends only its type, its id and the employee's id (when it is about one), for receivers that prefer to fetch the latest version from the API.
* **Scope**: an endpoint belongs to one organisation, or, created with `?scope=customer`, to your whole account, which is how a bureau receives every client's events at one URL. See [Account-wide endpoints](/api-reference/webhooks/account-wide-endpoints).

## Limits

| What | Limit |
| - | - |
| Endpoints | 20 per organisation, and 20 account-wide |
| Event type selectors | 100 per endpoint |
| Organisations in an account-wide endpoint's filter | 500 |
| Custom headers | 10 per endpoint, each value up to 1,024 characters |
| Your server's answer | within 15 seconds; only the first 16 KB of the body is kept |
| Test events | 5 a minute per endpoint |
| Active replays | 3 at a time |
| Events you can list or replay | the last 90 days |

## Where to go next

<CardGroup cols={2}>
  <Card title="Quickstart" icon="rocket" href="/api-reference/webhooks/quickstart">
    A working, verified receiver in about fifteen minutes.
  </Card>

  <Card title="Manage endpoints" icon="sliders" href="/api-reference/webhooks/manage-endpoints">
    Every setting, and how to pause, change and delete.
  </Card>

  <Card title="Verify signatures" icon="shield-check" href="/api-reference/webhooks/verify-signatures">
    Code for Node.js, Python and C#, and how rotation works.
  </Card>

  <Card title="Event types" icon="list" href="/api-reference/webhooks/event-types">
    Every event you can subscribe to, with its payload.
  </Card>

  <Card title="Delivery and retries" icon="rotate" href="/api-reference/webhooks/delivery-and-retries">
    The retry schedule, ordering, and what disables an endpoint.
  </Card>

  <Card title="Best practices" icon="list-check" href="/api-reference/webhooks/best-practices">
    Duplicates, fast answers, and processing in the background.
  </Card>
</CardGroup>


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