Integrations and Technology

Webhooks Explained: How Business Systems Stay in Sync

A webhook is a message one system sends another the moment something happens. How webhooks work, why they beat polling, and how to handle retries, duplicates and out-of-order events safely.

Illustrative example with fictional data.

A webhook is an automatic message one system sends to another the moment something happens: an invoice was approved, a payment arrived, a customer changed. Instead of your system asking "anything new?" every few minutes, the other system tells it. This guide explains how webhooks work and, more usefully, the four things that go wrong with them and how to handle each.

Polling versus webhooks

There are two ways for your system to find out that a customer has paid.

Polling Webhooks
How it works Your system asks the API every few minutes The other system sends a message when it happens
Speed As slow as your polling interval Usually seconds
Wasted requests Most checks find nothing One message per event
Rate limits Polling often uses them up Not affected
What you build A scheduled job A web address that accepts messages

Polling is simpler to start with and still useful as a safety net. Webhooks are what make an integration feel immediate: the learner's course opens while they're still on the payment page, not twenty minutes later.

What a webhook looks like

A webhook is an HTTPS POST request to an address you choose, carrying a small JSON document. In Flux, each delivery has an envelope with an event id, a type such as invoice.paid, a timestamp, and the object itself:

{
  "id": "evt_01JC8Z5T6Q1W3M4N5P6R7S8T9V",
  "type": "invoice.paid",
  "created": "2026-11-03T14:02:09.4121933Z",
  "data": { "object": { "number": "INV-2026-0418", "currency": "NGN" } }
}

The object here is shortened. Flux publishes 22 event types across invoices, payments, receipts, customers and enrolments, from invoice.created and invoice.approved through payment.succeeded to enrollment.cycle_due.

Your job on receiving one is short: check it's genuine, record it, reply quickly, then do the real work.

The four things that go wrong

1. Your system is down or slow

If your endpoint doesn't reply in time, the sender treats the delivery as failed and tries again later. Flux expects any 2xx reply within 10 seconds, then retries on a widening schedule:

Attempt When
1 At once
2 1 minute later
3 5 minutes later
4 30 minutes later
5 2 hours later
6 6 hours later
7 12 hours later
8–10 1 day apart

After the 10th failure the delivery is marked dead, and it can be sent again from the delivery log. After 50 failures in a row across an endpoint's deliveries, Flux switches that endpoint off and emails the person who registered it.

What to do: reply fast. Put the event on a queue and return 200, then process it. Never make the sender wait while you generate a PDF or call another service.

2. The same event arrives twice

Retries mean duplicates are normal, not a bug. A delivery can succeed on your side but the reply can be lost on the way back, so the sender tries again.

What to do: keep a record of event ids you've processed, and skip any you've seen. Flux sends the id in a Flux-Event-Id header for exactly this. Our guide to avoiding duplicate records goes further.

3. Events arrive out of order

invoice.paid can arrive before invoice.approved, because each delivery is retried independently.

What to do: don't build logic that assumes order. When the current state matters, such as whether an invoice is paid right now, read the invoice from the API rather than trusting the sequence of messages.

4. Someone else sends you a fake event

Your webhook address is a public URL. Anyone who finds it can post a message saying "invoice paid".

What to do: verify the signature on every delivery. Flux signs each one with a secret only you and Flux know, in a Flux-Signature header of the form t=<timestamp>,v1=<signature>. The signature is an HMAC-SHA256 of the timestamp, a dot, and the raw request body. Reject anything that doesn't match, and anything whose timestamp is more than a few minutes old. API security basics explains why both checks matter.

A receiving checklist

  • The endpoint uses HTTPS and is reachable from the internet.
  • It reads the raw body before parsing JSON, so the signature check sees exactly what was signed.
  • It verifies the signature and the timestamp, and rejects failures with a 400.
  • It deduplicates on the event id.
  • It replies 200 within a few seconds and does slow work afterwards.
  • It ignores event types and fields it doesn't recognise.
  • Someone gets told when deliveries start failing.

How Flux helps

Flux sends signed webhooks for the events finance teams care about, from approval through payment to receipt, retries failed deliveries on the schedule above, and keeps a delivery log you can redeliver from. You can send a webhook.test event from the app or the API to check your endpoint before going live. The Developers page summarises the events and security model; the full receiving guide, with verification code in several languages, is at developers.flux-erp.app.

From the blog