Skip to main content
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

  1. In the Merchant Portal, open Developers → Webhooks and add an endpoint URL (HTTPS required, publicly reachable).
  2. Select the events you want to receive (or all of them).
  3. 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

EventFires when
payment.succeededA payment reaches SUCCEEDED.
payment.failedA payment reaches FAILED.
payment.canceledA payment reaches CANCELED.
payment.expiredA payment reaches EXPIRED.
refund.succeededA refund reaches SUCCEEDED.
refund.failedA 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:
  1. Split the header into t (Unix timestamp) and v1 (hex signature).
  2. Compute HMAC-SHA256(signing_secret, "{t}.{raw_request_body}").
  3. Compare your result to v1 with a constant-time comparison.
  4. 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}.
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.).