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

# Webhooks

> Listen for real-time payment, payout, and account events with signed HTTP POST deliveries.

# Webhooks

Inflow uses webhooks to notify your application in real time when events happen across your organization (such as payments progressing to settled or payouts completing).

## Delivery Signing & Secret Management

Deliveries can be signed, but only if a secret is configured on the endpoint. If no secret is set on the endpoint (`hasSigningSecret: false`), webhooks are sent unsigned.

* **Secret Creation & Rotation**: When creating a webhook endpoint, a signing secret can be set or automatically generated (`secret: "whsec_..."`). Platform-issued secrets are returned only once at creation time. Calling `POST /organizations/{id}/webhooks/{endpointId}/rotate-secret` invalidates the previous secret immediately and returns a new replacement secret.
* **Signature Verification**: When a signing secret is active, outbound deliveries include the following HTTP headers:
  * `X-Webhook-Signature`: Format `sha256=<hex_digest>` (or `v1=<hex_digest>`).
  * `X-Webhook-Timestamp`: Unix timestamp (in seconds) of the delivery attempt.
* **HMAC Calculation**: The signature is computed using HMAC-SHA256 over the exact raw request payload string (`{timestamp}.{rawBody}` or exact JSON string). Do not parse and re-serialize JSON prior to verification.

### Node.js Signature Verification Example

```js theme={null}
import crypto from "node:crypto";

export function verifyWebhook({ rawBody, headers, secret }) {
  const timestamp = headers["x-webhook-timestamp"];
  const signatureHeader = headers["x-webhook-signature"];

  if (!timestamp || !signatureHeader) return false;

  // Reject stale requests older than 5 minutes to prevent replay attacks
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - Number(timestamp)) > 300) return false;

  const hash = signatureHeader.replace(/^sha256=|^v1=/, "");
  const expectedHash = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`, "utf8")
    .digest("hex");

  return crypto.timingSafeEqual(Buffer.from(hash), Buffer.from(expectedHash));
}
```

***

## Event Envelope & Payload Schemas

All outbound webhooks use a standardized envelope structure:

```json theme={null}
{
  "id": "evt_9876543210",
  "type": "payment.settled",
  "createdAt": "2026-07-25T08:30:00.000Z",
  "data": {
    "id": "c1f7b8a2-3e4d-4f1a-b2c3-5d6e7f8a9b0c",
    "reference": "ORD-2026-0042",
    "sourceAmount": 100.00,
    "sourceCurrency": "EUR",
    "targetAmount": 160000.00,
    "targetCurrency": "NGN",
    "exchangeRate": 1600.00,
    "status": "settled",
    "previousStatus": "executed"
  }
}
```

### Payment Data Fields

For payment status events, the `data` object contains:

* `id`: Unique Payment ID (`uuid`)
* `reference`: Your unique payment reference
* `sourceAmount`: Source currency amount
* `sourceCurrency`: Source ISO 4217 currency code
* `targetAmount`: Target currency amount
* `targetCurrency`: Target ISO 4217 currency code
* `exchangeRate`: Currency conversion exchange rate
* `status`: Current status (`created`, `initiated`, `executed`, `settled`, `failed`, `cancelled`)
* `previousStatus` *(optional)*: Prior payment status before this state transition

***

## Payment Lifecycle: `payment.executed` vs `payment.settled`

> \[!IMPORTANT]
> `payment.succeeded` is **not** an implemented outbound event name.
> The actual success events emitted by the platform are **`payment.executed`** and **`payment.settled`**.

* **`payment.executed`**: The upstream provider confirmed that funds were received. Funds are credited to your organization's pending balance.
* **`payment.settled`**: Funds have completed settlement processing and are available in your wallet balance.

**Best Practice**: Order fulfillment and digital goods release can be performed upon **`payment.executed`**, but automated payouts or withdrawals can only be triggered after funds are settled (**`payment.settled`**).

***

## Outbound Event Catalog

The platform defines the following supported outbound webhook event types:

| Category      | Event Type           | Description                                                    |
| ------------- | -------------------- | -------------------------------------------------------------- |
| **Payments**  | `payment.created`    | A payment request was created.                                 |
|               | `payment.initiated`  | The payer initiated the payment flow.                          |
|               | `payment.executed`   | Provider confirmed payment receipt (funds in pending balance). |
|               | `payment.settled`    | Payment funds cleared and available in wallet balance.         |
|               | `payment.failed`     | Payment failed or was rejected.                                |
|               | `payment.cancelled`  | Payment request was cancelled.                                 |
| **Payouts**   | `payout.created`     | A payout was created and is pending processing or approval.    |
|               | `payout.processing`  | Payout was submitted to the underlying payout rail.            |
|               | `payout.completed`   | Payout confirmed completed.                                    |
|               | `payout.failed`      | Payout reached a confirmed failed state.                       |
|               | `payout.cancelled`   | Payout was cancelled by an administrator.                      |
| **Transfers** | `transfer.completed` | Internal ledger transfer completed.                            |
|               | `transfer.failed`    | Internal ledger transfer failed.                               |
| **Customers** | `customer.created`   | Customer profile was created.                                  |
|               | `customer.updated`   | Customer profile details were updated.                         |

> \[!NOTE]
> Outbound events use explicit event types like `transfer.completed`. Generic `transaction.created` or `transaction.completed` events are not emitted because internal ledger `Transaction` objects cover multiple movement types (fees, refunds, withdrawals).

***

## Retry Policy & Deduplication

### Exponential Backoff Retry Schedule

If your endpoint returns a non-2xx HTTP status or fails to respond within 30 seconds, delivery attempts are retried using an exponential backoff schedule:

* **1 hour** after first failure
* **2 hours** after second failure
* **4 hours** after third failure
* **8 hours** after fourth failure
* **16 hours** after fifth failure
* Deliveries cease after 5 retries (as the next backoff interval would exceed 24 hours).

### Replay & Deduplication Protection

Every webhook payload includes a globally unique event `id` and `createdAt` ISO timestamp in the body. Receivers can safely store processed event IDs in a cache/database to ignore duplicate deliveries and protect against replay attacks.

***

## Testing & Manual Resends

* **Manual Resend**: Webhook delivery logs maintain execution history. You can manually resend failed or historical events using `POST /organizations/{id}/webhooks/{endpointId}/deliveries/{deliveryId}/resend`.
* **Signature Verification Testing**: You can test signature verification using stored test credentials or synthetic payload verification in your integration test suite.
