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

# Webhooks

> Receive signed HTTPS POSTs on every money-flow status change.

RemitFlex can POST signed JSON events to your HTTPS endpoints whenever a swap, payment route, collection, local fiat order, or cNGN object is created or changes status.

Manage endpoints in the [merchant app](https://app.remitflex.io/webhooks) under **Webhooks**, or via the API with JWT or an API key that includes `api:read` / `api:write`.

## Quick setup

<Steps>
  <Step title="Create an endpoint">
    `POST /v1/webhooks` with an HTTPS `url`. Optionally set `events` to an allowlist; omit or pass `null` to receive all event types.
  </Step>

  <Step title="Store the signing secret">
    The create (and rotate) response includes `secret` (`whsec_…`) **once**. Use it to verify `X-Remitflex-Signature`.
  </Step>

  <Step title="Verify and acknowledge">
    Validate the signature, then return `2xx` within \~10 seconds. RemitFlex retries failed deliveries with exponential backoff (up to 8 attempts).
  </Step>
</Steps>

## Delivery headers

| Header                  | Meaning                          |
| ----------------------- | -------------------------------- |
| `Content-Type`          | `application/json`               |
| `X-Remitflex-Event`     | Event type (e.g. `swap.updated`) |
| `X-Remitflex-Delivery`  | Delivery id                      |
| `X-Remitflex-Timestamp` | Unix timestamp (seconds)         |
| `X-Remitflex-Signature` | `t={ts},v1={hmac_hex}`           |

### Signature verification

```text theme={null}
signed_payload = `${timestamp}.${rawBody}`
expected = HMAC_SHA256(signing_secret, signed_payload)  // hex
```

Compare `v1` from the header to `expected` (constant-time). Reject if the timestamp is too far from your clock (e.g. ±5 minutes) to limit replay.

## Payload shape

```json theme={null}
{
  "id": "event-uuid",
  "type": "swap.updated",
  "created": "2026-07-23T00:00:00.000Z",
  "data": {
    "id": "resource-uuid",
    "status": "completed",
    "previousStatus": "swapping"
  }
}
```

`data` is a resource snapshot. Status transitions include `previousStatus` and `status` (`previousStatus` is `null` on create).

## Event catalog

| Family         | Types                                                                                                                                                               |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Swaps          | `swap.created`, `swap.updated`                                                                                                                                      |
| Payment routes | `payment_route.created`, `payment_route.updated`, `payment_route.deposit.updated`                                                                                   |
| Collections    | `collection.created`, `collection.updated`                                                                                                                          |
| Local fiat     | `offramp.created`, `offramp.updated`, `onramp.created`, `onramp.updated`                                                                                            |
| cNGN           | `cngn.convert.*`, `cngn.withdrawal.*`, `cngn.payout.*`, `cngn.payin.*`, `cngn.deposit.*` (`.created` / `.updated`); `cngn.customer.enabled`, `cngn.customer.failed` |
| Test           | `webhook.test` (from `POST /v1/webhooks/{webhookEndpointId}/test` — delivered to that endpoint even if it is not on the allowlist)                                  |

Every subsequent status change emits `*.updated` (including cancel/expire), not only terminal states. `cngn.customer.enabled` fires once when KYC finishes and names match; `cngn.customer.failed` fires once on KYC failure or `customer_name_bvn_mismatch`.

## API

`{webhookEndpointId}` is the RemitFlex webhook endpoint UUID from `POST` or `GET /webhooks`.

| Method   | Path                                             | Scope                     |
| -------- | ------------------------------------------------ | ------------------------- |
| `POST`   | `/v1/webhooks`                                   | `api:write`               |
| `GET`    | `/v1/webhooks`                                   | `api:read`                |
| `GET`    | `/v1/webhooks/{webhookEndpointId}`               | `api:read`                |
| `PATCH`  | `/v1/webhooks/{webhookEndpointId}`               | `api:write`               |
| `POST`   | `/v1/webhooks/{webhookEndpointId}/rotate-secret` | `api:write`               |
| `DELETE` | `/v1/webhooks/{webhookEndpointId}`               | `api:write` (soft revoke) |
| `GET`    | `/v1/webhooks/{webhookEndpointId}/deliveries`    | `api:read`                |
| `POST`   | `/v1/webhooks/{webhookEndpointId}/test`          | `api:write`               |
| `GET`    | `/v1/webhooks/event-types`                       | `api:read`                |

Dashboard JWT sessions can call these routes without scopes. API keys need the matching `api:read` / `api:write` scope.

## Security notes

* URL must be **HTTPS**. Private, loopback, and link-local targets are rejected (SSRF protection).
* Signing secrets are stored server-side so RemitFlex can sign outbound deliveries; the raw value is only shown on create/rotate.
