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

# Payout Callbacks

> Handling asynchronous payout callbacks, understanding terminal payment_step 2 notifications, verifying callback signatures, and querying payout transaction status.

## Overview

Because disbursement processing across African mobile money switches and interbank clearing systems is asynchronous, Niobi notifies your server of the final settlement outcome by sending an HTTP POST callback request to the `client_callback_url` specified under `params.client_callback_url` in your disbursement request.

```mermaid theme={null}
%%{init: {'themeVariables': {'fontSize': '23px'}, 'sequence': {'width': 220, 'height': 95, 'actorMargin': 80, 'messageMargin': 60, 'boxMargin': 18, 'noteMargin': 18}}}%%
sequenceDiagram
    autonumber
    participant TelcoBank as Telco / Interbank Switch
    participant Niobi as Niobi API
    participant Merchant as Your Callback Server

    TelcoBank->>Niobi: Report payout execution outcome (Success / Failed)
    Note over Niobi: Compute SHA-256 signature<br/>Assemble payload (payment_step: 2)
    alt Payout Succeeded
        Niobi->>Merchant: POST to client_callback_url (payment_step: 2, status: "success", statusCode: "000000")
        Note over Merchant: 1. Verify signature<br/>2. Update ledger & notify recipient<br/>3. Reconcile idempotently
    else Payout Failed / Rejected
        Niobi->>Merchant: POST to client_callback_url (payment_step: 2, status: "failed", statusCode: "000005")
        Note over Merchant: 1. Verify signature<br/>2. Unlock funds & mark failed<br/>3. Reconcile idempotently
    end
    Merchant-->>Niobi: HTTP 200 OK
```

***

## Terminal Callback Guarantee (`payment_step: 2`)

<Warning>
  **Terminal Settlement Rule:**\
  Only mark payouts as cleared and notify recipients when the callback delivers `payment_step: 2` with `status: "success"` and `statusCode: "000000"`. Never assume clearance on initial dispatch (`payment_step: 1`). If `status: "failed"` (`statusCode: "000005"`) is received, the payout failed at the destination switch and merchant funds are unlocked.
</Warning>

* The callback is delivered for both **successful** ([`status: "success"`, `statusCode: "000000"`](/errors/000000-success)) and **failed** ([`status: "failed"`, `statusCode: "000005"`](/errors/000005-failed)) disbursements.
* Update your application's payout records based on both the `status` and `statusCode` fields delivered inside `params`. See our [Transaction Status Codes Reference](/errors/transaction-status-codes) for details.

***

## Callback Payload Structure

Incoming payout callbacks from Niobi follow the standard signed envelope structure:

### 1. Successful Payout Callback (`payment_step: 2`)

```json theme={null}
{
  "client_id": "YOUR_CLIENT_ID",
  "sender": "YOUR_INTEGRATION_NAME",
  "salt": "random_salt_value",
  "timestamp": 1724835630,
  "signature": "a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef0",
  "params": {
    "amount": 1000,
    "currency": "KES",
    "mobile": "254647647649",
    "payment_method_type": "send money",
    "payment_reference": "PAYOUT-REF-1001",
    "payment_step": 2,
    "reference": "NIO-PAYOUT-789012",
    "status": "success",
    "statusCode": "000000",
    "third_party_reference_1": "TX-DISB-9901"
  }
}
```

### 2. Failed Payout Callback (`payment_step: 2`)

```json theme={null}
{
  "client_id": "YOUR_CLIENT_ID",
  "sender": "YOUR_INTEGRATION_NAME",
  "salt": "random_salt_value",
  "timestamp": 1724835635,
  "signature": "e5f6a7b8c90123456789abcdef0123456789abcdef0123456789abcdef012345",
  "params": {
    "amount": 1000,
    "currency": "KES",
    "mobile": "254647647649",
    "payment_method_type": "send money",
    "payment_reference": "PAYOUT-REF-1001",
    "payment_step": 2,
    "reference": "NIO-PAYOUT-789012",
    "status": "failed",
    "statusCode": "000005",
    "failureReason": {
      "failureCode": "DS-008",
      "failureMessage": "Recipient mobile money wallet is inactive or unregistered"
    },
    "third_party_reference_1": "TX-DISB-9901"
  }
}
```

### Callback Payload Parameters (`params` Object)

| Parameter                        | Type      | Description                                                      |
| :------------------------------- | :-------- | :--------------------------------------------------------------- |
| `params.amount`                  | `integer` | Disbursed amount in major currency units.                        |
| `params.currency`                | `string`  | 3-letter currency code (e.g. `KES`).                             |
| `params.mobile`                  | `string`  | Recipient mobile phone number.                                   |
| `params.payment_method_type`     | `string`  | Payout channel used.                                             |
| `params.payment_reference`       | `string`  | Your internal payout reference.                                  |
| `params.payment_step`            | `integer` | Set to `2` for terminal disbursement callbacks.                  |
| `params.reference`               | `string`  | Niobi unique system transaction ID.                              |
| `params.status`                  | `string`  | Outcome status: `"success"` or `"failed"`.                       |
| `params.statusCode`              | `string`  | System status code: `"000000"` (success) or `"000005"` (failed). |
| `params.failureReason`           | `object`  | Detailed diagnostic object (present on failed callbacks).        |
| `params.third_party_reference_1` | `string`  | Your primary merchant idempotency tracking reference.            |

***

## Verifying Callback Signatures

Always verify incoming payout callbacks using HMAC SHA-256 before applying ledger balance updates:

1. **Extract Root Fields**: Extract `client_id`, `sender`, `salt`, `timestamp`, and `params` from the callback payload.
2. **Inject Secret Key**: Add `"senderKey": "YOUR_SECRET_KEY"` into the payload object.
3. **Sort Keys (Recursive K-Sort)**: Alphabetically sort all keys recursively.
4. **Flatten & Hash**: Convert to key-value query string format concatenated with `&`, and hash with SHA-256.
5. **Compare**: Compare your computed hash with the `signature` from Step 1. If they match, the callback is authentic.

***

## Dual-Layer Confirmation: Querying Payout Status

As an additional layer of confirmation, Niobi encourages querying the transaction status alongside listening for callbacks. Combining the incoming asynchronous callback with an on-demand status query gives your system independent, double-verified confirmation before closing batch payout runs. The full recipe is three steps: rely on the callback as your primary signal, fall back to a status query if it hasn't arrived, and only close out the payout once that confirmed status is reconciled against your own internal payout records.

### Endpoint

[`POST /api/v3/get-unified-transaction-status`](/api-reference/get-transaction-status)

### Request Parameters (`params` Object)

| Field      | Requirement  | Type     | Description                                                                                                          |
| :--------- | :----------- | :------- | :------------------------------------------------------------------------------------------------------------------- |
| **`id`**   | **Required** | `string` | The payout transaction reference to query. Pass your `reference`, `payment_reference`, or `third_party_reference_1`. |
| **`type`** | **Required** | `string` | Must be set to `"payout"` for disbursement and payout transactions.                                                  |

### Sample Signed Status Query Request

```json theme={null}
{
  "client_id": "YOUR_CLIENT_ID",
  "sender": "YOUR_INTEGRATION_NAME",
  "timestamp": 1724835600,
  "salt": "random_salt_12345",
  "signature": "b1c2d3e4f5a60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef0",
  "params": {
    "id": "PAYOUT-REF-1001",
    "type": "payout"
  }
}
```

### Sample Status Query Response

```json theme={null}
{
  "success": true,
  "message": "Transaction data fetched successfully.",
  "data": {
    "amount": 1000,
    "fee": 25,
    "currency": "KES",
    "status": "success",
    "mobile": "254647647649",
    "transaction_id": "DR-NIO-P98765432",
    "ref": "PAYOUT-REF-1001",
    "payment_method_type": "send money",
    "third_party_reference_1": "TX-DISB-9901",
    "created_at": "2026-08-28 12:35:00"
  },
  "status_code": "000000"
}
```

### Reconciling Query Results:

* **`status: "success"` (`status_code: "000000"`):** Funds were successfully credited to the recipient. Mark the payout order as completed in your ledger.
* **`status: "failed"` (`status_code: "000005"`):** The disbursement failed; funds remain in (or have been refunded to) your merchant wallet. Mark the payout as failed.
* **`status: "pending"` (`status_code: "000001"`):** The transfer is still processing with the destination network switch. **Do not take action on this state** (do not retry the transfer or adjust recipient balances). Keep the payout in a pending state and await the asynchronous callback (`payment_step: 2`) or re-query after a brief delay.

***

## Callback Receiver Implementation Guidelines

<AccordionGroup>
  <Accordion title="1. Respond Promptly with HTTP 200">
    Your callback listener should respond with an HTTP `200 OK` status as quickly as possible (ideally within 3 to 5 seconds). Perform long-running background tasks (e.g. sending SMS receipts or updating external ERPs) asynchronously in a background job queue after returning 200 OK.
  </Accordion>

  <Accordion title="2. Implement Idempotent Processing">
    In rare cases of network timeouts between Niobi and your server, callback notifications may be retried. Ensure your callback handler uses `params.reference` or `params.payment_reference` to verify whether a disbursement has already been marked as complete before applying balance adjustments.
  </Accordion>

  <Accordion title="3. Avoid Aggressive Polling Loops">
    Rely on the `payment_step: 2` callback as the primary notification mechanism. Do not continuously poll the status API in a tight loop while waiting for a callback; if a callback has not been received after a while, query the [Get Transaction Status API](/api-reference/get-transaction-status) (`POST /api/v3/get-unified-transaction-status`) using spaced intervals.
  </Accordion>
</AccordionGroup>

***

## Resending Callbacks via the Payment Portal

You can resend callbacks in case of a delivery failure through the payment portal. To do this, access the specific transaction and click on **Resend Callback**.

<Frame caption="Resending a failed callback delivery directly from the transaction details modal in the payment portal">
  <img src="https://mintcdn.com/niobi/AMhGjRoBC7yfXgRW/images/resendcallback.png?fit=max&auto=format&n=AMhGjRoBC7yfXgRW&q=85&s=0bb8a0f63a78016f4d8a7b72883e990e" alt="Resend Callback from Niobi Payment Portal" width="720" height="874" data-path="images/resendcallback.png" />
</Frame>

<Note>
  **When can a callback be resent?**\
  The **Resend Callback** option can only be triggered via the payment portal for a deposit or payout request in cases where the callback was not delivered correctly to your server (for example, if your listener experienced downtime, timed out, or returned an HTTP error). It is not displayed for callbacks that were already acknowledged with HTTP 200.
</Note>

***

## Next Steps

Follow these guides to complete and secure your payout flow:

<div className="next-steps-flow">
  <a href="/making-payments/best-practices" className="next-step-card">
    <div className="next-step-badge">Step 3</div>
    <h3>Payout Best Practices</h3>
    <p>Wallet balance management, recipient phone validation, idempotency, and error handling.</p>
  </a>

  <div className="next-step-arrow">
    <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
      <path d="M5 12h14M12 5l7 7-7 7" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  </div>

  <a href="/api-reference/get-transaction-status" className="next-step-card">
    <div className="next-step-badge">Reconciliation</div>
    <h3>Get Transaction Status API</h3>
    <p>Query payout transaction status as an independent fallback verification mechanism.</p>
  </a>
</div>
