Delivery
Delivery expectations for Flo webhooks.
Delivery
Flo webhooks are delivered asynchronously.
What To Assume
- delivery is asynchronous
- delivery is retry-backed
- delivery should be treated as at-least-once
- strict ordering is not guaranteed across all event families
Subscribing To Events
Create an endpoint with POST /v1/webhooks and list the events it should receive. Authenticate with Client Basic credentials, using the client identifier as the username and an active API token as the password, or with an OAuth2 bearer token.
curl -X POST https://api.flopay.com/v1/webhooks \
-u "$FLOPAY_CLIENT_ID:$FLOPAY_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/flopay",
"events": [
"dispute.created",
"dispute.updated",
"dispute.won",
"dispute.lost",
"pre_dispute_alert.created",
"pre_dispute_alert.updated"
]
}'events accepts any event type from the event catalog. It must not be empty, must not repeat a name, and rejects unknown names with 400. The 201 response returns the endpoint id, the stored url, events, and filter, the signingSecret used for the Flo-Signature header, and the failedSendAt and disabledAt markers described under Retries.
Existing endpoints are not subscribed to new event types automatically. To adopt a new family such as dispute.* or pre_dispute_alert.*, send PATCH /v1/webhooks/{id} with the complete events list you want; the array you send replaces the stored one. Omitting events leaves the stored subscriptions unchanged, so a PATCH sent only to re-enable an endpoint never unsubscribes it. Any update also clears the endpoint's failed and disabled markers. GET /v1/webhooks lists your endpoints, GET /v1/webhooks/{id} reads one, and DELETE /v1/webhooks/{id} removes one together with its delivery records.
To limit an endpoint to events for selected products or product tags, add a filter, as described in Webhook configuration.
Ordering
Ordering is best-effort and should not be treated as a strict guarantee across all event families.
Consumers should:
- treat webhook delivery as asynchronous
- fetch current resource state when absolute ordering matters
- make handlers idempotent
- apply dispute and Pre-Dispute Alert state by
sequencerather than by arrival order, as described under Idempotency
Idempotency
Consumers should implement idempotency in their handlers.
In practice, that means your endpoint should be able to safely process the same Flo event more than once without duplicating side effects.
Use the top-level eventId as the delivery key you persist for deduping. Automatic retries reuse the same eventId for the same delivery, and a resend replays the delivery under its original eventId as well, so a handler keyed on eventId treats both as duplicates of one business event.
Dispute and Pre-Dispute Alert events add a second guard. Each payload carries an integer sequence that increases with every accepted transition of the same dispute.id or preDisputeAlert.id. After deduplicating by eventId, apply the payload only when its sequence is higher than the last one you stored for that id. A lower or equal sequence that arrives later is stale and must not overwrite newer state, which keeps your record correct when HTTP deliveries arrive out of order. Compare sequence values, but never assume a starting value: it does not start at one, and it is only comparable within one id.
Retries
When delivery fails, Flo retries with backoff before eventually marking the delivery as failed. Every retry reuses the same delivery record and the same eventId, and retryCount on the record shows how many attempts were made.
Flo classifies your endpoint's HTTP response as follows:
| Response | Outcome |
|---|---|
2xx | Delivered. No further attempts are made. |
408, 425, 429, 5xx | Retried with backoff until the retry budget is exhausted. |
Any other 4xx | Failed permanently. Flo does not retry, so reserve these for requests you never want redelivered, such as a signature mismatch. |
| Timeout, connection failure, or any other status | Retried with backoff. |
Your handler should therefore:
- return a
2xxas soon as the event is durably recorded, and do the work asynchronously - perform idempotent writes
- avoid assuming a single delivery attempt
- be tolerant of delays between the billing activity and webhook arrival
An endpoint whose deliveries keep failing for a sustained period is disabled automatically: failedSendAt records the first failure, and disabledAt is set once failures have continued for more than three days. A successful delivery before the endpoint is disabled clears failedSendAt. Once disabledAt is set, new deliveries are recorded as skipped rather than sent, so the endpoint cannot recover on its own: send any PATCH /v1/webhooks/{id} to clear both markers and re-enable it, then replay the skipped deliveries with the resend routes below.
Signature
Each webhook delivery includes a Flo-Signature header with this format:
Flo-Signature: t=1712841600,v1=abcdef123456...To verify a delivery:
- read the raw request body before JSON parsing
- compute an HMAC SHA-256 using your webhook signing secret
- sign the string
${timestamp}.${rawRequestBody} - compare the result to the
v1value inFlo-Signature - reject timestamps outside a short tolerance window (the Node quick start uses five minutes)
The signing secret is the signingSecret returned when the endpoint is created. Verify the signature before you deduplicate by eventId or apply any lifecycle state, and reject a mismatch with a 4xx so that Flo does not retry it.
Delivery Records, Replay, And Resend
Every event and endpoint pair is recorded as a delivery, including deliveries Flo decided not to send. Read them with:
GET /v1/webhooks/eventslists deliveries across all of your endpoints, newest first, in the shared paginated envelope{ data, page, limit, pages, total }. Filter byeventType,status,skipReason,retryCount,responseCode,sentAt,createdAt, or by the related Flo record:userUuid,transactionUuid,subscriptionUuid,invoiceUuid,disputeUuid, orpreDisputeAlertUuid.GET /v1/webhooks/events/{id}returns one delivery, includingrequestBody(the exact payload sent, with itseventId),responseBody,responseCode,status,skipReason,retryCount,sentAt, and the related record ids.
status is pending, retrying, success, failed, or skipped. A skipped delivery records why in skipReason: endpoint_created_after_event, endpoint_disabled, event_not_subscribed, or, for an endpoint with a product filter, filter_not_matched or filter_context_missing. skipReason is null for every other status. A skipped delivery also keeps its reason in responseBody.reason for compatibility, but read skipReason. preDisputeAlertUuid is set when Flo retains the alert as a durable record; a delivery for an alert that never linked to a Flo payment or customer can omit it, so filter those by eventType instead.
To replay deliveries:
PUT /v1/webhooks/events/{id}/resendre-queues one delivery and returns202with the delivery record reset topendingandretryCountat zero. It returns404for a delivery your client does not own and409when the endpoint is disabled or a product filter skipped the delivery.POST /v1/webhooks/events/resendre-queues every delivery that matches the same filters as the list route and returns202withresentIds,skippedIdsfor deliveries whose endpoint is disabled or that a product filter skipped, andfailedIdsfor deliveries that could not be re-queued and can be resent again, each with a matching count. A disabled endpoint is skipped rather than aborting the batch.
A resend replays the delivery under its original eventId, payload, and source. It is a deliberate duplicate of the same business event, not a new event, so an idempotent handler keyed on eventId will ignore it. To reprocess a delivery on purpose, clear your own processed marker for that eventId before resending.