Handle duplicates
Delivery is at least once. A network blip after your server answered, a retry or a resend can all bring the same event again. Every copy has the samewebhook-id.
Record each webhook-id you have processed and skip any you have seen before. A unique index on the id in your database does this in one step. Keep ids for at least as long as the retry schedule runs, and longer if you replay older events.
Answer quickly
Answer2xx as soon as you have checked the signature and stored the event, then do the real work afterwards, for example from a queue. A receiver that updates payroll records before answering can pass the 15-second limit under load, and a timed-out delivery is retried even though your work succeeded.
Process in the background
Pay runs happen in bursts. Finalising a pay run for 5,000 employees sends 5,000payslip.finalised events within minutes. Put deliveries on a queue and process them at a rate your system handles. If your server needs to slow Flow Payroll down, answer 429 or 503 with a Retry-After header (502 and 504 work too): deliveries slow down for you and nothing is lost.
Don’t rely on order
Unless an endpoint is set to"ordering": "Fifo", events can arrive in a different order from the one they happened in, and a retry or a replay can bring an older event after a newer one. See Ordering.
Wherever you store state from an event, store the event’s timestamp with it. When another event for the same record arrives, compare the two and skip the event if it is older than the one you already applied, so older data never overwrites newer data. timestamp is when the change happened, not when it was sent, so it stays the same on every retry and replay.
Treat a Full payload as a snapshot
AFull payload is the record as it was when the event happened, not as it is now. A retry or a replay sends that same snapshot again, however much has changed since. When you need the current state, fetch metadata.resourceUrl with your own credentials: it returns the record as it is at the moment you call it.
Start leaver workflows from the leaver event
To remove a leaver’s access to your systems, or to start any other leaver process, act onemployee.leaver.recorded. It is sent as soon as an employee is marked as leaving, and carries the leaving date, which can be in the future: schedule the change for that date rather than making it straight away. If the leaver marking is undone, employee.leaver.reverted follows, so reverse what you did.
p45.issued comes later. It is sent only when the pay run with the leaver’s final payslip is finalised and the P45 is produced, which can be days or weeks after the leaver was recorded, so it is the wrong trigger for removing access. Use it to collect the P45 from metadata.documentUrl. If a leaver is reverted after their P45 was issued, p45.revoked withdraws it.
Fetch documents with your own credentials
A payslip, P45 or P60 event carriesmetadata.documentUrl. Download the PDF from it when you need the file, authenticating as you do for any other API call. Don’t forward the event to someone who cannot sign in: the link only works for a caller with access to the organisation.
Verify every delivery
Check the signature and the timestamp on every request before trusting its body. See Verify signatures. Reject anything that fails with400, and never log or store the unverified body as if it came from Flow Payroll.
Keep your secret safe
Verify with the public key where you can: it cannot sign, so a leak of your receiver’s configuration cannot be used to forge a delivery. If you verify with the signing secret, store it in a secrets manager, not in code or a repository. Rotate it straight away if you think it has leaked: rotation keeps both secrets signing for 24 hours, so your receiver never misses a delivery while you switch.Protect what you receive
Full events carry employees’ personal and pay details: see Personal data in payloads.
- Store deliveries as securely as the rest of your payroll records, and limit who can read them.
- Keep them only as long as you need them.
- Log the
webhook-idandX-Flow-Delivery-Id, not the whole body. - To hold less, choose
Minimalpayloads and fetch only the fields you use.
Expect new events and fields
Subscribe to exact event types, so a type added later does not reach code that cannot handle it. Ignore fields you do not recognise. Both are how Flow Payroll adds to events without a new version.Use HTTPS with a valid certificate
Endpoints must behttps:// on port 443 with a certificate issued by a public authority. Self-signed certificates, private IP addresses and addresses such as localhost are refused. To test locally, use a tunnelling tool; see Testing webhooks.
Allow-list our sending addresses
If your firewall only accepts known senders, allow these addresses. Every delivery, test send and replay leaves from them, and they do not change.
An allow-list is an extra layer, not a replacement: always verify the signature as well.
Exempt the route from CSRF protection
Frameworks such as Rails and Django check a CSRF token on everyPOST, which a webhook cannot carry. Exempt your webhook route; the signature check replaces it.
Make registration retry-safe
Send anIdempotency-Key header when you create an endpoint, rotate a secret, start a replay or resend a delivery. If your request times out and you send it again with the same key and the same body within 24 hours, you get the first answer back instead of a second endpoint or a second resend. Use a new key for each new request: reusing one with a different body is a 409 with idempotency_key_reused.