Documentation
Webhooks

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 sequence rather 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:

ResponseOutcome
2xxDelivered. No further attempts are made.
408, 425, 429, 5xxRetried with backoff until the retry budget is exhausted.
Any other 4xxFailed 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 statusRetried with backoff.

Your handler should therefore:

  • return a 2xx as 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 v1 value in Flo-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/events lists deliveries across all of your endpoints, newest first, in the shared paginated envelope { data, page, limit, pages, total }. Filter by eventType, status, skipReason, retryCount, responseCode, sentAt, createdAt, or by the related Flo record: userUuid, transactionUuid, subscriptionUuid, invoiceUuid, disputeUuid, or preDisputeAlertUuid.
  • GET /v1/webhooks/events/{id} returns one delivery, including requestBody (the exact payload sent, with its eventId), 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}/resend re-queues one delivery and returns 202 with the delivery record reset to pending and retryCount at zero. It returns 404 for a delivery your client does not own and 409 when the endpoint is disabled or a product filter skipped the delivery.
  • POST /v1/webhooks/events/resend re-queues every delivery that matches the same filters as the list route and returns 202 with resentIds, skippedIds for deliveries whose endpoint is disabled or that a product filter skipped, and failedIds for 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.

On this page