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

# Webhooks

> Subscribe to payout and wallet notifications at your URL, and verify incoming webhook requests.

## Overview

You can receive **payout status change** notifications at a URL you provide. When a payout’s status changes, we send an HTTP POST to your endpoint with the event payload. We use six consolidated statuses: `PENDING`, `ADDITIONAL_INFO_REQUIRED`, `PROCESSING`, `SUCCESSFUL`, `FAILED`, and `REJECTED`.

The **same webhook URL** also receives **wallet credit and debit** notifications for your business (see [Wallet credit and debit](#wallet-credit-and-debit)). You do not register a second URL.

* **One webhook URL per business** — you can register a single HTTPS endpoint; we send payout status events and wallet movement events for your business to that URL.
* **Optional signing** — you can provide a secret when registering; we then sign each request so you can verify it came from Yala. **We strongly recommend setting a secret and verifying the signature** to protect your endpoint against spoofed webhooks.
* Use the same **API key** (and base URL) as for the rest of the Payout API when managing your webhook subscription.

## Subscribe via API

Use the Payout API with your existing `x-api-key` to manage your webhook URL.

**Base path:** Same as payouts — `https://gateway.staging.useyala.com/v1/payout-api` (sandbox) or `https://gateway.useyala.com/v1/payout-api` (production). Webhook endpoints live under `/webhooks`.

| Action               | Method   | Path            |
| -------------------- | -------- | --------------- |
| Register URL         | `POST`   | `/webhooks`     |
| List subscription    | `GET`    | `/webhooks`     |
| Get one              | `GET`    | `/webhooks/:id` |
| Update URL or secret | `PATCH`  | `/webhooks/:id` |
| Remove               | `DELETE` | `/webhooks/:id` |

### Register a webhook URL

```http theme={null}
POST /v1/payout-api/webhooks HTTP/1.1
Host: gateway.staging.useyala.com
Content-Type: application/json
x-api-key: <YOUR_API_KEY>
```

**Request body:**

| Field        | Required         | Description                                                                                                                                                                                                                                                                                                                                                        |
| ------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `url`        | Yes              | Your HTTPS endpoint. We POST the event payload to this URL.                                                                                                                                                                                                                                                                                                        |
| `secret`     | No (recommended) | Shared secret used to sign requests. If set, we send `X-Yala-Signature` so you can verify the payload. **Recommended** to protect against spoofed webhooks.                                                                                                                                                                                                        |
| `eventTypes` | No               | Filters which **payout status** event types you receive. Omit or leave empty to receive all payout status events. Use `payout.status.changed` (recommended) or legacy `payout.status.changed.v1`; both match the same notifications. **Wallet** credit and debit notifications are always sent to your URL for your business; they are not filtered by this field. |

Example:

```json theme={null}
{
  "url": "https://your-server.com/webhooks/payout",
  "secret": "whsec_your_secret_for_signing"
}
```

Only one active webhook URL is allowed per business. To change it, update the existing subscription (PATCH) or delete and create a new one.

## Payload we send

Each notification is an HTTP **POST** with:

* **Content-Type:** `application/json`
* **Body:** JSON object with the shape below.
* **Headers we add:**
  * `X-Yala-Delivery-Id` — unique ID for this delivery (use it to deduplicate if you receive the same event more than once).
  * `X-Yala-Signature` — only present if you set a `secret`; format `sha256=<hex>`. See [Verifying the signature](#verifying-the-signature).

### Payload shape

```json theme={null}
{
  "eventType": "payout.status.changed",
  "deliveryId": "550e8400-e29b-41d4-a716-446655440000",
  "occurredAt": "2025-02-10T12:00:00.000Z",
  "data": {
    "payoutId": "payout-uuid",
    "businessId": "business-uuid",
    "oldStatus": "PENDING",
    "newStatus": "ADDITIONAL_INFO_REQUIRED",
    "changedAt": "2025-02-10T12:00:00.000Z",
    "reason": "Additional information requested",
    "informationRequests": [
      {
        "title": "Invoice copy",
        "description": "Please provide a clearer invoice showing beneficiary name",
        "type": "FILE"
      }
    ]
  }
}
```

When additional information has been submitted and is under review, `newStatus` is `PENDING` and `newStatusDetail` is `ADDITIONAL_INFO_UNDER_REVIEW`:

```json theme={null}
{
  "eventType": "payout.status.changed",
  "data": {
    "payoutId": "payout-uuid",
    "oldStatus": "ADDITIONAL_INFO_REQUIRED",
    "newStatus": "PENDING",
    "newStatusDetail": "ADDITIONAL_INFO_UNDER_REVIEW"
  }
}
```

**Status values** — We send exactly six statuses so you can integrate simply:

| Status                     | Meaning                                                                                                                                                                                                                 |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PENDING`                  | Payout created or awaiting approval; additional info submitted and under review; not yet sent for processing.                                                                                                           |
| `ADDITIONAL_INFO_REQUIRED` | Additional information has been requested for this payout. Check `data.informationRequests` in the webhook (or GET payout) for title and description of each item. Upload documents via PATCH `/documents` when needed. |
| `PROCESSING`               | Payout is being processed (ops, bank, or treasury).                                                                                                                                                                     |
| `SUCCESSFUL`               | Completed; funds have been sent.                                                                                                                                                                                        |
| `FAILED`                   | Processing failed (e.g. bank failure).                                                                                                                                                                                  |
| `REJECTED`                 | Rejected (e.g. compliance, initiation checks, or bank).                                                                                                                                                                 |

| Field                      | Description                                                                                                                                                                            |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `eventType`                | Event name in the POST body: `payout.status.changed`. (You may still use `payout.status.changed.v1` in `eventTypes` when registering; it matches the same deliveries.)                 |
| `deliveryId`               | Unique ID for this delivery. Use for idempotency (e.g. ignore duplicates with the same `deliveryId`).                                                                                  |
| `occurredAt`               | When the status change occurred (ISO 8601).                                                                                                                                            |
| `data.payoutId`            | Payout ID (same as the `id` returned from Initiate Payout).                                                                                                                            |
| `data.businessId`          | Your business ID.                                                                                                                                                                      |
| `data.oldStatus`           | Previous status (one of the six above).                                                                                                                                                |
| `data.newStatus`           | New status (one of the six above).                                                                                                                                                     |
| `data.newStatusDetail`     | Optional. Present when `newStatus` is `PENDING` and submitted additional information is under review (`ADDITIONAL_INFO_UNDER_REVIEW`).                                                 |
| `data.oldStatusDetail`     | Optional. Same detail values for the previous status when applicable.                                                                                                                  |
| `data.informationRequests` | Optional. Present when `newStatus` is `ADDITIONAL_INFO_REQUIRED`. Array of requested items (title, description, type). Same shape as [GET payout](/api-reference/endpoint/get-payout). |
| `data.changedAt`           | Timestamp of the change (ISO 8601).                                                                                                                                                    |
| `data.reason`              | Optional; human-readable reason.                                                                                                                                                       |

## Wallet credit and debit

When a wallet for your business is **credited** or **debited**, we POST to the **same** registered URL as payout webhooks. These messages are separate from payout status updates—check `eventType` and `data.transactionType` to route them in your handler.

### Event names (`eventType`)

| `eventType`       | When                                                   |
| ----------------- | ------------------------------------------------------ |
| `wallet.credited` | Funds were credited to a wallet (e.g. pay-in, top-up). |
| `wallet.debited`  | Funds were debited from a wallet (e.g. payout, fee).   |

### Direction (`transactionType`)

Use `data.transactionType` so you do not rely only on `eventType`:

| Value | Meaning            |
| ----- | ------------------ |
| `cr`  | Credit (money in). |
| `dr`  | Debit (money out). |

### Example payload (wallet)

```json theme={null}
{
  "eventType": "wallet.credited",
  "deliveryId": "550e8400-e29b-41d4-a716-446655440000",
  "occurredAt": "2025-02-10T12:00:00.000Z",
  "data": {
    "businessId": "business-uuid",
    "amount": 1000.0,
    "currency": "USD",
    "transactionId": "transaction-uuid",
    "createdAt": "2025-02-10T12:00:00.000Z",
    "transactionType": "cr",
    "reference": "REF-123",
    "narration": "wallet top up resolution - funding",
    "balance": 5000.0,
    "totalBalance": 5200.0
  }
}
```

### Wallet `data` fields

| Field             | Description                                                                                                                     |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `businessId`      | Your business ID.                                                                                                               |
| `amount`          | Amount of this movement (always positive).                                                                                      |
| `currency`        | Wallet currency.                                                                                                                |
| `transactionId`   | Identifier for this wallet transaction.                                                                                         |
| `createdAt`       | When the transaction was recorded (ISO 8601).                                                                                   |
| `transactionType` | `cr` (credit) or `dr` (debit).                                                                                                  |
| `reference`       | Stable reference for the movement (e.g. payout ref, batch id). Empty string if none.                                            |
| `narration`       | Human-readable narration (e.g. wallet top-up funding resolution, payout description). Empty string if none.                     |
| `balance`         | **Available** balance after this transaction. It does **not** include funds held for pending operations (e.g. pending payouts). |
| `totalBalance`    | **Total** balance including active holds for pending transactions (available balance plus amounts held).                        |

Headers (`X-Yala-Delivery-Id`, `X-Yala-Signature`), retries, and verification work the same as for [payout payloads](#payload-shape).

## Verifying the signature

If you registered a **secret**, we sign each request with **HMAC-SHA256** of the **raw request body** (the exact JSON string we send). We send the hex-encoded digest in the header:

```http theme={null}
X-Yala-Signature: sha256=a1b2c3...
```

**How to verify:**

1. Read the raw request body (do not parse and re-serialize; use the exact bytes received).
2. Compute `HMAC-SHA256(rawBody, yourSecret)`.
3. Hex-encode the result.
4. Compare with the value in `X-Yala-Signature` after the `sha256=` prefix.

If they match, the request came from Yala and the body was not modified in transit.

<Warning>
  **Use the raw body.** Small differences in JSON formatting (e.g. key order or spacing) will change the signature. Always verify against the exact bytes you receive.
</Warning>

## Best practices

* **Respond quickly** — Return HTTP `2xx` as soon as you have accepted the payload. Process the event asynchronously if needed so we don’t time out.
* **Deduplicate** — Store `deliveryId` and ignore requests you have already processed so duplicate deliveries do not trigger duplicate actions.
* **Set a secret and verify the signature** — We strongly recommend setting a secret when registering and always verifying `X-Yala-Signature` before trusting the payload. Without this, your endpoint could accept spoofed webhooks from third parties.
* **Use HTTPS only** — We only send to HTTPS URLs. Keep your endpoint and secret secure.

## Delivery retries

If your endpoint does not return HTTP `2xx`, we retry delivery automatically.

* **Total attempts:** 5
* **Backoff schedule:** 1 minute, 5 minutes, 15 minutes, 1 hour, 4 hours
* **Stop condition:** Any HTTP `2xx` response marks the delivery as successful and stops retries
* **Final state:** After the 5th failed attempt, the delivery is marked as failed

## Managing your subscription

* **List:** `GET /v1/payout-api/webhooks` returns your current subscription(s). The response does not include the secret.
* **Update:** `PATCH /v1/payout-api/webhooks/:id` with `url`, `secret`, `eventTypes`, or `isActive` as needed.
* **Remove:** `DELETE /v1/payout-api/webhooks/:id` stops notifications to that URL.

All of these use the same authentication as the rest of the Payout API (`x-api-key`).

<Tip>
  You can also manage your webhook URL from the Yala app under **Settings → Payout API Keys** (webhook section). The API and the app stay in sync.
</Tip>
