Webhooks push payment and refund state changes to your server the moment they
happen, so you can fulfil orders, update your database, and notify customers
without polling.
Setup
- In the Merchant Portal, open Developers → Webhooks and add an endpoint
URL (HTTPS required, publicly reachable).
- Select the events you want to receive (or all of them).
- Copy the endpoint’s signing secret (
whsec_...). You will use it to
verify that events really come from Flowlix.
Endpoints are configured per mode: test-mode events go to your test
endpoints, live-mode events to your live endpoints.
Events
| Event | Fires when |
|---|
payment.succeeded | A payment reaches SUCCEEDED. |
payment.failed | A payment reaches FAILED. |
payment.canceled | A payment reaches CANCELED. |
payment.expired | A payment reaches EXPIRED. |
refund.succeeded | A refund reaches SUCCEEDED. |
refund.failed | A refund reaches FAILED. |
Event payload
Every delivery is a POST with a JSON envelope; data contains the full
current object exactly as GET /v1/payments/{id} would return it:
{
"id": "evt_8Xq2Lw5Rt9Yc3Vn7Bm4Kd6Pa",
"type": "payment.succeeded",
"created_at": 1719792042,
"livemode": false,
"data": {
"payment": {
"id": "pay_q7Mk2Np8Vr4Xt6Yz9Ab3Cd5E",
"status": "SUCCEEDED",
"amount": 2500,
"currency": "EUR",
"...": "..."
}
}
}
Refund events carry data.refund (and data.refund.payment_id links back to
the payment).
Verifying signatures
Every delivery includes a Flowlix-Signature header:
Flowlix-Signature: t=1719792042,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
To verify:
- Split the header into
t (Unix timestamp) and v1 (hex signature).
- Compute
HMAC-SHA256(signing_secret, "{t}.{raw_request_body}").
- Compare your result to
v1 with a constant-time comparison.
- Reject the event if the signature does not match or
t is more than
5 minutes old (replay protection).
import hashlib, hmac, time
def verify(header: str, body: bytes, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t, v1 = parts["t"], parts["v1"]
if abs(time.time() - int(t)) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
Always verify the signature before trusting an event. Anyone can send JSON
to a public URL.
Delivery semantics
- Respond fast with
2xx. Acknowledge within 10 seconds — persist the
event and process it asynchronously if your handling is slow. Any non-2xx
response (or a timeout) counts as a failed delivery.
- Retries. Failed deliveries are retried with exponential backoff for up
to 24 hours. After that the delivery is marked failed; you can inspect
and replay deliveries from the Merchant Portal.
- At-least-once. The same event can be delivered more than once. Make your
handler idempotent — deduplicate by the event
id.
- Ordering is not guaranteed. Events may arrive out of order. Don’t apply
state from the event blindly; either rely on the full object in
data
(which is always the current state at delivery time) or re-fetch via
GET /v1/payments/{id}.
Recommended handler skeleton
1. Verify the Flowlix-Signature header → 400 if invalid
2. Already processed this event id? → 200, stop
3. Persist the event id + payload → respond 200
4. Asynchronously: update your order state from data.payment / data.refund
Webhooks complement, not replace, polling: keep a slow reconciliation loop
with GET /v1/payments for payments that somehow missed an event (your
endpoint was down longer than the retry window, a network partition, etc.).