> ## Documentation Index
> Fetch the complete documentation index at: https://docs.flowlix.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time notifications when payments and refunds change state

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

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

```json theme={null}
{
  "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:

```text theme={null}
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).

```python theme={null}
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)
```

<Warning>
  Always verify the signature before trusting an event. Anyone can send JSON
  to a public URL.
</Warning>

## 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

```text theme={null}
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.).
