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

# Collection Callbacks

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

## Overview

Because payment processing across African mobile networks and banking systems is asynchronous, Niobi notifies your server of the final transaction outcome by sending an HTTP POST callback request to the `callback_url` specified in your collection request.

```mermaid theme={null}
%%{init: {'themeVariables': {'fontSize': '23px', 'actorBorder': '#0D9373', 'actorLineColor': '#0D9373', 'signalColor': '#0D9373', 'noteBorderColor': '#0D9373'}, 'sequence': {'width': 220, 'height': 95, 'actorMargin': 80, 'messageMargin': 60, 'boxMargin': 18, 'noteMargin': 18}}}%%
sequenceDiagram
    autonumber
    participant Customer as Customer (Payer)
    participant TelcoBank as Telco / Bank Switch
    participant Niobi as Niobi API
    participant Merchant as Your Callback Server

    Customer->>TelcoBank: Authorize or Reject payment
    TelcoBank->>Niobi: Report settlement outcome
    Note over Niobi: Compute SHA-256 callback signature<br/>Assemble payload (payment_step: 2)
    alt Payment Succeeded
        Niobi->>Merchant: POST to callback_url (payment_step: 2, status: "success", statusCode: "000000")
        Note over Merchant: 1. Verify signature<br/>2. Credit user & fulfill order<br/>3. Reconcile idempotently
    else Payment Failed / Canceled
        Niobi->>Merchant: POST to callback_url (payment_step: 2, status: "failed", statusCode: "000005")
        Note over Merchant: 1. Verify signature<br/>2. Mark failed (Do NOT credit)<br/>3. Reconcile idempotently
    end
    Merchant-->>Niobi: HTTP 200 OK
```

***

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

<Warning>
  **Terminal Settlement Rule:**\
  Only credit user balances or fulfill orders when the callback delivers `payment_step: 2` with `status: "success"` and `statusCode: "000000"`. Never credit on `status: "failed"` (`statusCode: "000005"`) or on initial dispatch responses (`payment_step: 1`).
</Warning>

* The callback is delivered for both **successful** ([`status: "success"`, `statusCode: "000000"`](/errors/000000-success)) and **failed** ([`status: "failed"`, `statusCode: "000005"`](/errors/000005-failed)) payments.
* Update your application's order state 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 callbacks from Niobi follow the standard signed envelope structure:

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

```json theme={null}
{
  "client_id": "YOUR_CLIENT_ID",
  "sender": "YOUR_INTEGRATION_NAME",
  "salt": "random_salt_value",
  "timestamp": 1724835620,
  "signature": "a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef0",
  "params": {
    "amount": 10000,
    "depositId": "NIO-D123456789",
    "mobile": "254161166649",
    "name": "Jane Doe",
    "payment_step": 2,
    "reference": "TX-COLL-1001",
    "status": "success",
    "statusCode": "000000",
    "third_party_reference_1": "TX-COLL-1001",
    "third_party_reference_2": "CUST-450"
  }
}
```

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

```json theme={null}
{
  "client_id": "YOUR_CLIENT_ID",
  "sender": "YOUR_INTEGRATION_NAME",
  "salt": "random_salt_value",
  "timestamp": 1724835625,
  "signature": "e5f6a7b8c90123456789abcdef0123456789abcdef0123456789abcdef012345",
  "params": {
    "amount": 10000,
    "depositId": "NIO-D123456789",
    "mobile": "254161166649",
    "name": "Customer Name",
    "payment_step": 2,
    "reference": "TX-COLL-1001",
    "status": "failed",
    "statusCode": "000005",
    "failureReason": {
      "failureCode": "DS-004",
      "failureMessage": "User cancelled transaction / Request timed out"
    },
    "third_party_reference_1": "TX-COLL-1001",
    "third_party_reference_2": "CUST-450"
  }
}
```

### Callback Parameter Specification

| Parameter                        | Type                 | Description                                                                                  |
| :------------------------------- | :------------------- | :------------------------------------------------------------------------------------------- |
| `params.amount`                  | `integer` / `number` | The final deposited amount.                                                                  |
| `params.depositId`               | `string`             | Unique deposit reference returned in collection callbacks.                                   |
| `params.mobile`                  | `string`             | Payer mobile phone number.                                                                   |
| `params.name`                    | `string`             | Registered name of the account holder / payer.                                               |
| `params.payment_step`            | `integer`            | Set to `2` for terminal collection callbacks.                                                |
| `params.reference`               | `string`             | Unique transaction reference.                                                                |
| `params.status`                  | `string`             | Terminal transaction status: `"success"` or `"failed"`.                                      |
| `params.statusCode`              | `string`             | `"000000"` for success, `"000005"` for failure.                                              |
| `params.failureReason`           | `object`             | Detailed failure diagnostics (`failureCode` and `failureMessage`) if `status` is `"failed"`. |
| `params.third_party_reference_1` | `string`             | Your original merchant reference passed during initiation.                                   |
| `params.third_party_reference_2` | `string`             | Your second optional reference passed during initiation.                                     |

***

## Verifying Inbound Callback Signatures

To ensure that an incoming callback genuinely originated from Niobi and has not been forged or tampered with:

1. **Extract Signature**: Extract `signature` from the incoming root JSON object and remove it.
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 Transaction 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 updating customer balances or fulfilling high-value orders.

### 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 transaction reference to query. Pass your original `third_party_reference_1`, `payment_reference`, or Niobi's `reference`. |
| **`type`** | **Required** | `string` | Must be set to `"payin"` for collection and deposit 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": "TX-COLL-1001",
    "type": "payin"
  }
}
```

### Sample Status Query Response

```json theme={null}
{
  "success": true,
  "message": "Transaction data fetched successfully.",
  "data": {
    "amount": 10000,
    "fee": 100,
    "currency": "KES",
    "status": "success",
    "mobile": "254161166649",
    "transaction_id": "CR-NIO-S12345678",
    "ref": "TX-COLL-1001",
    "payment_method_type": "send money",
    "third_party_reference_1": "TX-COLL-1001",
    "third_party_reference_2": "CUST-450",
    "created_at": "2026-08-28 12:30:00"
  },
  "status_code": "000000"
}
```

### Reconciling Query Results:

* **`status: "success"` (`status_code: "000000"`):** The collection was successfully authorized and credited to your merchant wallet. You can safely fulfill the order or credit the user's account.
* **`status: "failed"` (`status_code: "000005"`):** The collection failed or was declined by the payer. Mark the payment as failed.
* **`status: "pending"` (`status_code: "000001"`):** Authorization is still in progress with the upstream provider. **Do not take action on this state** (do not fulfill orders or mark as failed). Keep the transaction in a pending state and await the asynchronous callback (`payment_step: 2`) or re-query after a brief delay.

The status query response above returns `status_code` (snake\_case) at its top level. The terminal callback's `params.statusCode` field, along with `params.failureReason` (`failureCode`/`failureMessage`) on failed collections, shown earlier on this page, are camelCase.

***

## 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 customer confirmation emails or generating PDFs) 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.third_party_reference_1` to verify whether a transaction has already been processed and credited before applying balance updates.
  </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, you can 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 collection flow:

<div className="next-steps-flow">
  <a href="/collecting-payments/best-practices" className="next-step-card">
    <div className="next-step-badge">Step 3</div>
    <h3>Collection Best Practices</h3>
    <p>Operational guidance, reconciliation recipes, status polling guidelines, 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 payin transaction status as an independent fallback verification mechanism.</p>
  </a>
</div>
