FluxBilling
APIs & Integrations

Webhooks

Outbound event deliveries to a partner endpoint: which events fire, the payload and headers, how to verify a signature, and how retries and back-off work.

Updated · 2026-09-03

What webhooks are for

Webhooks are the outbound half of the partner integration: your platform posts a small JSON document to a partner's own endpoint whenever something happens to one of their orders or services. They exist so a partner's shop does not have to poll. Ordering is asynchronous — a service is created some time after the order is accepted — and a webhook is how the partner learns the outcome without asking every few seconds.

Webhooks belong to the reseller programme. They are configured per partner and are signed with that partner's signing secret. There is no equivalent on the Customer REST API: an end customer's integration polls.

Note: Deliveries are only made while the partner's profile is active. While it is pending or suspended, nothing goes out.

Setting the endpoint

There are three ways to put a URL on file, and they all write the same setting:

  • The partner, in the client portal. ResellerAPI Settings, the Webhooks panel, field Endpoint URL, then Save URL.
  • The partner, through the API. PUT /api/v1/reseller/account/webhook with { "webhookUrl": "https://…" }. It needs the account scope and a signature. This is how a partner's own platform can register itself.
  • You, in the admin panel. Open the partner in Resellers, go to the API Settings tab, and set Webhook URL under Webhook Configuration. Leaving the field empty clears it.

The URL must carry an explicit scheme (http or https) and point at a publicly reachable host. Loopback and private addresses are refused when the URL is saved, and the host is resolved and checked again at delivery time — a name that resolves to a private or link-local address is blocked, and that attempt counts against the delivery's retry budget.

Warning: Set the endpoint before the first order. An event raised while no URL is on file is dropped, not queued — it will never arrive, even after a URL is added.

Sending a test delivery

POST /api/v1/reseller/webhooks/test queues a webhook.test event to whatever URL is on file. It needs the account scope and a signature, exactly like every other change. It is the right first call once an implementation compiles: it exercises the URL, the signature verification and the response handling without touching an order.

Which events fire

Every event type
EventWhat it does
order.paidAn order was charged successfully. Also raised after a successful payment retry.
order.invoicedAn invoice or proforma was raised instead of an immediate charge. Nothing provisions until it is paid.
order.payment_failedThe charge did not go through. The order exists and can be retried.
service.provisionedA service reached active. This is the “it is ready” signal.
service.provisioning_failedCreation or provisioning failed after payment — including a failure that happens in the background long after the order was accepted.
service.suspendedA service was suspended, whether by the partner or by you.
service.unsuspendedA suspension was lifted.
service.terminatedA service was terminated, whether by the partner or by you.
webhook.testThe partner called the test route.

Note: A charge that is followed by a provisioning failure raises service.provisioning_failed, not order.paid. A partner's shop that heard “paid” would stop waiting and show an end customer a working service that does not exist.

There are no events for renewals or unpaid invoices. A partner watches their own billing in the client portal; the first machine-readable sign of an unpaid wholesale balance is a service.suspended event.

The payload

Every delivery has the same three-key envelope:

{ "event": "service.provisioned", "timestamp": "2026-09-03T10:15:42.113Z", "data": { … } }

The timestamp is the moment of this attempt, not of the original event, and it is the same value as the X-Webhook-Timestamp header. A retry therefore carries a new timestamp, a new body and a new signature.

What is inside data

Order events carry orderId, orderNumber, externalOrderId, externalCustomerId, status and paymentMode, plus, where they are known, amount, invoiceId, paymentError and provisioningError. A service.provisioning_failed raised after the order was already accepted also lists the affected services.

Service events carry serviceId, orderId, externalOrderId, externalCustomerId, status, serverIp and hostname, plus a reason where one is known.

The partner's own identifiers are echoed back deliberately: they are what a downstream shop keys its records on, and they are the only way to correlate an event to an order placed before the platform's identifier was ever recorded.

How a delivery is signed

Each delivery carries these headers:

Delivery headers
HeaderWhat it does
X-Webhook-Signaturehmac-sha256= followed by the hexadecimal digest over the raw delivery body.
X-Webhook-EventThe event type, for example service.provisioned.
X-Webhook-IdThe delivery identifier. It is stable across retries — use it to deduplicate.
X-Webhook-TimestampThe time of this attempt, in ISO 8601. Matches the timestamp in the body.
User-AgentFluxBilling-Reseller-Webhook/1.0
Content-Typeapplication/json

The signature is HMAC-SHA256 keyed on the partner's signing secret, computed over the exact bytes of the delivery body. Nothing else is folded in.

Warning: This is not the same recipe as an outbound request to the reseller API. When a partner signs a request they send, the timestamp is part of the signed content ("<timestamp>." + body). When they verify a delivery they receive, only the body is signed. Using the request recipe to verify a delivery produces a mismatch on every single one.

Verifying a delivery

Read the raw request body before any JSON parsing, recompute the digest and compare in constant time.

In PHP:

$raw = file_get_contents('php://input'); $expected = 'hmac-sha256=' . hash_hmac('sha256', $raw, $secret); $provided = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? ''; if (!hash_equals($expected, $provided)) { http_response_code(401); exit; } $payload = json_decode($raw, true);

In Node.js:

const expected = 'hmac-sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.get('X-Webhook-Signature') || ''))) return res.sendStatus(401);

Use a constant-time comparison — an ordinary string comparison leaks timing information. Re-serialising the parsed body and hashing that will not work: the bytes have to be the ones that arrived.

Because delivery is at-least-once, record the X-Webhook-Id of every delivery handled and ignore repeats. A network timeout on the partner's side can produce a second copy of a delivery that was in fact processed.

The signing secret and how it is rotated

Deliveries and the partner's own signed requests share one secret, held on their profile. It exists from the moment they apply for the programme, so an automatically approved partner never has to generate one first.

  • The partner rotates it in ResellerAPI Settings, in the Webhooks panel: reveal it with the show control, copy it, or press Rotate. The confirmation is explicit — requests signed with the current secret are rejected the moment the new one is issued.
  • You can rotate it from the partner's record in the admin panel, on the API Settings tab.
  • Either way the new value is shown once, at the moment it is generated.

There is no overlap window: the old secret stops working immediately, in both directions. Rotate only when the partner is ready to update the value on their side straight away, and expect a short burst of failed verifications on their endpoint if they are not.

Note: A profile with no signing secret cannot deliver at all. Each attempt is refused with an error recorded as “webhook secret not configured”, and the delivery burns its retries and ends as failed. If a partner reports that no event ever arrives, check the secret exists before anything else.

Delivery, retries and back-off

Events are queued, not sent inline with the request that caused them. A background pass runs once a minute and works through the queue, spreading its batch across partners so that one partner's backlog cannot starve everyone else's events.

What counts as success

The endpoint must answer with a 2xx status within 30 seconds. Anything else is a failed attempt:

  • Any 4xx or 5xx status.
  • A timeout, a connection failure or a TLS failure.
  • A redirect. Redirects are never followed — the endpoint has to be a final URL. A 3xx answer is recorded as “Endpoint returned a redirect — not followed”.
  • A URL whose host resolves to a private or loopback address.

Answer 2xx as soon as the delivery is stored, and do the real work afterwards. An endpoint that finishes its own processing before replying will start timing out as soon as that processing gets slow.

The back-off schedule

A failed delivery is rescheduled with exponential back-off: the base delay, then double, then double again. With the defaults — a base delay of 60 seconds and 5 attempts in total — the retries fall at roughly 60 s, 120 s, 240 s and 480 s after each preceding failure. Because the queue is only scanned once a minute, each of those is a floor rather than an exact time. After the last attempt the delivery is marked failed permanently and is never retried again.

Both numbers are yours to set, for the whole programme, in the admin panel: SettingsReseller Program, in the Webhook Settings block. Max Retry Attempts accepts 1 to 10, and Base Retry Delay (seconds) accepts 10 to 3600.

Ordering

Deliveries for one partner are attempted in the order they were created, and a partner's queue stops at a delivery that fails so a later event cannot overtake an earlier one in the same pass. But a delivery that has been rescheduled for a later retry will be overtaken by newer events in the meantime. Treat ordering as best-effort. Key the handling on the event type and on the current state of the service, never on arrival order — and re-read the order or service through the API when the two disagree.

Inspecting deliveries

The delivery record lives with you, the supplier. In the admin panel open the partner in Resellers, then the Activity tab. Two tables sit there:

  • API Request Logs — every call the partner's integration made, with the method, endpoint, status code, duration and source address. This is where a signature or scope problem shows up as a run of 401s or 403s.
  • Webhook Delivery Logs — one row per event, with columns Time, Event, Status, Attempts, Response and Delivered.
Delivery status values
StatusWhat it does
pendingQueued, or waiting for its next retry.
deliveredThe endpoint answered 2xx. The delivery time is on the row.
failedEvery attempt was used. This is terminal — the platform will not try again.
cancelledThe delivery was withdrawn before it succeeded.

The Response column carries the last status code the endpoint returned, which is usually enough to separate “the partner's server is down” from “the partner's handler rejected our signature”. Records are kept for 30 days and then removed automatically.

Recovering a missed event

Deliveries are not re-sent by hand: once a delivery has used its attempts, its status is final. Recover the state instead of waiting for another delivery — the API is the source of truth and the partner can reconcile from it at any time:

  • GET /api/v1/reseller/orders?externalOrderId=… finds the order by the partner's own identifier, which is the right move after any timed-out create.
  • GET /api/v1/reseller/orders/{orderId} carries the payment status, the provisioning status and, on a failure, the reason.
  • Every order row carries the services it produced, with their status, hostname and address, so a shop can link a service without a second call.
  • GET /api/v1/reseller/services gives the current state of everything the partner holds.

A well-built integration reconciles on a schedule anyway, and treats webhooks as a way to react quickly rather than as the only source of truth.

What can go wrong

  • No event ever arrives. Check, in order: the URL is on file; the profile is active; a signing secret exists; and the event was raised after the URL was saved — events raised beforehand were dropped.
  • Every delivery fails verification. Almost always the wrong recipe: a delivery is signed over the body alone, with no timestamp prefix. Second most common is hashing a re-serialised body instead of the bytes received.
  • Deliveries stopped after a rotation. The secret was rotated on one side only. Copy the new value into the receiving endpoint.
  • Everything is recorded as a redirect. The endpoint is behind a rule that redirects, for example plain HTTP to HTTPS, or a trailing-slash rewrite. Configure the final URL directly.
  • Deliveries succeed but time out under load. The handler is doing its work before replying. Store and acknowledge first.
  • The same order is processed twice. The handler is not deduplicating. Record the delivery identifier and ignore repeats.
  • An order shows as paid but no ready signal follows. Either the endpoint was configured after the order, or it is not answering 2xx within 30 seconds. Check the delivery log and the order's provisioning status.

Related articles