Webhooks
Receiving webhooks
Flux sends an HTTPS POST to your endpoint when something happens: an invoice is approved, a payment arrives, a customer changes. Respond quickly, verify the signature and process each event once.
Set up an endpoint#
Register your URL in the Flux app (Settings → Developers → Webhooks) or with POST /webhook-endpoints. Choose the events it receives, or "*" for all of them, including event types added later. Store the signing secret: it's shown once.
- The URL must be
https, at most 500 characters, and publicly reachable. Flux won't send to loopback, private, link-local or carrier-grade NAT addresses, and checks again on every delivery. - Flux doesn't follow redirects. A
3xxcounts as a failure. - Webhooks are sent only to organizations with at least one active endpoint. Events from before an endpoint existed aren't replayed to it.
The request#
Each delivery is a POST with a JSON body.
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | FluxERP-Webhooks/1 |
Flux-Event-Id | The event id, evt_…. Also in the body as id. |
Flux-Event-Type | The event type, such as invoice.paid. Also in the body as type. |
Flux-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256>. See Verify signatures. |
{
"id": "evt_01JC8Z5T6Q1W3M4N5P6R7S8T9V",
"type": "invoice.paid",
"created": "2026-11-01T10:20:00.4121933Z",
"data": {
"object": {
"…": "the invoice object"
}
}
}| Field | Description |
|---|---|
| idstring | evt_ followed by a ULID: unique and sortable by time. Deduplicate on it. |
| typestring | The event type, such as invoice.paid. |
| createdtimestamp (UTC) | When the event happened. UTC, with Z. |
| dataobject | { "object": … }: the object for this event type. |
The object in data.object depends on the event type. See the Event catalogue.
Responding, retries and ordering#
Respond within 10 seconds#
Return any 2xx status within 10 seconds. Anything else, a redirect, or a timeout is a failure. Do slow work after you respond: put the event on a queue and return 200.
Retries#
After a failure, Flux retries on this schedule. After the 10th failed attempt, the delivery is marked dead. You can send it again from the delivery log, in the app or with POST /webhook-deliveries/{id}/redeliver.
| Attempt | When | Time since the event |
|---|---|---|
| 1 | At once | 0 |
| 2 | 1 minute later | 1 min |
| 3 | 5 minutes later | 6 min |
| 4 | 30 minutes later | 36 min |
| 5 | 2 hours later | 2 h 36 min |
| 6 | 6 hours later | 8 h 36 min |
| 7 | 12 hours later | 20 h 36 min |
| 8 | 1 day later | 1 d 20 h 36 min |
| 9 | 1 day later | 2 d 20 h 36 min |
| 10 | 1 day later | 3 d 20 h 36 min |
Auto-disable#
After 50 failed attempts in a row, across all its deliveries, Flux switches the endpoint off (active: false, with a disabledReason). It emails the person who registered the endpoint (for an endpoint registered through the API, the admin who created the key) and the organization's support email. Once you've fixed it, re-enable it with PATCH {"active": true} or in the app; that also resets the failure count. Then redeliver what you missed.
At least once, in any order#
- Duplicates happen. A delivery can arrive more than once, and Flux re-sends invoice events whose sending was interrupted (it looks back 48 hours). Deduplicate on
Flux-Event-Id. - Order isn't guaranteed.
invoice.paidcan arrive beforeinvoice.approved. When the current state matters, re-read the object from the API. - Ignore event types you don't handle, and fields you don't know.
Testing#
- Send a
webhook.testevent withPOST /webhook-endpoints/{id}/test, or the Test button in the app. It's signed and retried like any other event. - Check each attempt's status code and error in the delivery log.
- To receive webhooks on your laptop, expose your local server through an HTTPS tunnel and register the tunnel's URL on a staging organization.
- Use the signature checker to sign a sample body and send it to your endpoint yourself.