# API Overview
Source: https://docs.niobi.co/api-reference/api-overview
REST API for collecting payments, sending payouts, and managing multi-currency wallets across Africa and global corridors.
Niobi's REST API gives you a single interface for collections, disbursements, and balance queries across 17+ African markets.
## Before You Begin
Two requirements must be met before any API call will succeed in either environment:
1. **Whitelist your server IP.** Requests from non-whitelisted IPs are blocked before authentication is checked. See the [IP Whitelisting Guide](/ip-whitelisting).
2. **Sign every request.** Each call requires a unique HMAC signature computed from your credentials, timestamp, salt, and payload. Signatures cannot be reused. See the [Authentication Guide](/authentication/basics).
## Environments
Both environments enforce IP whitelisting and signature validation independently. Credentials are not shared between environments.
## Endpoints
Generate and verify the HMAC signature required on every request.
Accept payments via mobile money, bank transfer, and payment links.
Send payouts to mobile wallets and bank accounts across supported markets.
Retrieve live wallet balances across all currencies in your account.
Start Quickstart
# Generate Signature
Source: https://docs.niobi.co/api-reference/authentication/generate-signature
post /api/niobi-signature
This endpoint is for integration with Niobi. It generates signature used in the requests.
For the full signature generation walkthrough, including sandbox testing and backend signing, see the [Authentication Guide](/authentication/basics).
# Verify Signature
Source: https://docs.niobi.co/api-reference/authentication/verify-signature
post /api/niobi-verify
This endpoint verifies the signature of a request to ensure its integrity and authenticity.
For the full signature verification walkthrough, including webhook callback validation, see the [Authentication Guide](/authentication/basics).
# Unified Collections
Source: https://docs.niobi.co/api-reference/collections/unified-collections
post /api/v4/niobi-unified-collections
One endpoint for all collection methods across all supported markets. Accepts mobile money, bank transfer, virtual accounts, and redirect-based rails through a single unified interface. Each request must carry a freshly generated signature, signatures cannot be reused across requests. Conditional `params` fields vary by payment method and country. See the [Collections Guide](/collecting-payments/basics) and [Payment Methods](/collecting-payments/methods) for the full field matrix per market.
### Integration Quick Reference
| Requirement | Standard | Rule / Action |
| :------------------------------------------------------------------------------------------ | :--------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [**Settlement Rule**](/collecting-payments/basics#understanding-payment-steps-payment_step) | `payment_step: 2` only | Never credit goods or user accounts on `payment_step: 1` (initiation). Only credit upon receiving [payment\_step: 2](/collecting-payments/callbacks) with `status: "success"` (`statusCode: "000000"`). |
| [**Signatures**](/authentication/signature-generation) | Fresh per request | Compute a fresh SHA-256 signature for every API call. Signatures cannot be reused. See [Signature Generation](/authentication/signature-generation). |
| [**Amount Format**](/collecting-payments/basics#request-parameters) | Whole integers | Pass whole numbers in major currency units (for example, `1000` KES). No decimals allowed. |
| [**Idempotency**](/idempotency) | Unique merchant ref | Pass a distinct `third_party_reference_1` per request to prevent duplicate charges. See the [Idempotency Guide](/idempotency). |
| [**Delayed Webhook**](/transaction-status) | Query status | If a callback is delayed, query the [Get Transaction Status API](/transaction-status) before taking action. |
| [**Sandbox Testing**](/testing) | Mapped test MSISDNs | Use designated [Sandbox Test Numbers](/testing) mapped to outcomes (success, timeout, failed) instead of real phone numbers. |
| [**Production Prep**](/go-live-checklist) | Technical checklist | Verify public HTTPS `callback_url`, country limits, and credentials in the [Go-Live Checklist](/go-live-checklist#technical-checklist). |
## List of Supported Countries
**Coming Soon**
| Country | Code | Currency | Payment Methods |
| ----------------------------------- | ---- | -------- | ----------------------- |
| [Ethiopia](/countries/ethiopia) | 251 | ETB | teleBirr, mBirr |
| [Malawi](/countries/malawi) | 265 | MWK | Airtel, tnm |
| [Egypt](/countries/egypt) | 20 | EGP | Vodafone, Orange |
| [Mozambique](/countries/mozambique) | 258 | MZN | Vodacom, Movitel, Tmcel |
For NGN bank and virtual account deposits, the expected amount is processed and any overpayment is automatically reversed to the customer on successful deposit. Send exact amounts to prevent unnecessary reversals.
### NGN Virtual Account - Supported Bank Codes
Pass one of the following codes as `bank_code` in your request to select the bank that will issue the virtual account.
| Bank Code | Bank Name |
| --------- | -------------------------- |
| 17001 | 78 Finance Company Limited |
| 17002 | Fidelity Bank Limited |
| 17003 | Globus Bank Limited |
For Senegal Orange deposits, the end user receives an SMS with a shortcode after the deposit is initiated. They dial the shortcode, triggering a USSD prompt where they input their PIN to approve the transaction.
### Special Collection Flows
Some corridors don't follow the standard STK-push flow above: they require redirect URLs, customer national ID numbers, dynamic virtual bank accounts, or a secondary OTP verification step.
For complete architectural walkthroughs, UX screenshots, and lifecycle diagrams, see:
* [**Collection Methods Guide**](/collecting-payments/methods): Step-by-step guides for all payment collection channels.
* [**Country Guides Index**](/supported-countries): Country-specific corridors, telco channel rules, and test MSISDNs.
There is no `"eft"` payment method value. South African EFT collections use `payment_method_type: "bank_eft"`, shown below.
`payment_method_type`: `"bank_eft"`
Returns an `authorization_url` where the customer approves the payment directly with their bank (ABSA, FNB, Standard Bank, Nedbank, Investec, Bidvest, TymeBank).
**Documentation & Guides:**
* [Collection Methods: South Africa Bank EFT](/collecting-payments/methods#3-south-africa-bank-transfers--capitec-zar)
* [South Africa Country Guide: Bank EFT Flow](/countries/south-africa#1-eft-bank-collections-payment_method_type-bank_eft)
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"salt": "random_salt_12345",
"signature": "c6b98e1f5d6a7890bc4e123456789abcdef0123456789abcdef0123456789abc",
"params": {
"amount": 500,
"currency": "ZAR",
"country_id": 15,
"mobile": "2763456789",
"name": "John Doe",
"bank_code": "0001",
"payment_method_type": "bank_eft",
"successful_url": "https://yourdomain.com/payment-success",
"failure_url": "https://yourdomain.com/payment-failed",
"callback_url": "https://yourdomain.com/niobi/callback",
"third_party_reference_1": "ZAR-DEPOSIT-01"
}
}
```
**Initial Response:** Returns `"authorization_url": "https://..."` and `"payment_step": 1`. Redirect the customer to this URL. For a full walkthrough of redirect handling and bank selection, see the [South Africa Country Guide](/countries/south-africa).
`payment_method_type`: `"capitec"`
Capitec Bank collections authenticate directly against the payer's national identity. You must pass the customer's full 13-digit South African ID number in `params.id_number` (e.g. `"9001015800083"`) for the request to be valid.
**Documentation & Guides:**
* [Collection Methods: Capitec Collections](/collecting-payments/methods#3-south-africa-bank-transfers--capitec-zar)
* [South Africa Country Guide: Capitec Pay](/countries/south-africa#2-capitec-collections-payment_method_type-capitec)
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"salt": "random_salt_12345",
"signature": "c6b98e1f5d6a7890bc4e123456789abcdef0123456789abcdef0123456789abc",
"params": {
"amount": 500,
"currency": "ZAR",
"country_id": 15,
"mobile": "2763456789",
"name": "John Doe",
"id_number": "9001015800083",
"payment_method_type": "capitec",
"successful_url": "https://yourdomain.com/payment-success",
"failure_url": "https://yourdomain.com/payment-failed",
"callback_url": "https://yourdomain.com/niobi/callback",
"third_party_reference_1": "ZAR-CAPITEC-01"
}
}
```
`payment_method_type`: `"Wave"`
Wave uses a redirect-based browser authorization flow, not a USSD push. You must include `successful_url` and `failure_url`. After initiating the collection, redirect the customer to the returned `authorization_url` to complete payment.
**Documentation & Guides:**
* [Collection Methods: Wave Flow & UI Walkthrough](/collecting-payments/methods#4-wave-collections-senegal--cote-divoire)
* [Senegal Country Guide: Wave Checkout](/countries/senegal#2-wave-redirect-flow-payment_method_type-wave)
* [Côte d'Ivoire Country Guide: Wave Collections](/countries/cote-d-ivoire#2-wave-redirect-flow-payment_method_type-wave)
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"salt": "random_salt_12345",
"signature": "c6b98e1f5d6a7890bc4e123456789abcdef0123456789abcdef0123456789abc",
"params": {
"amount": 5000,
"currency": "XOF",
"country_id": 9,
"mobile": "221763456789",
"payment_method_type": "Wave",
"successful_url": "https://yourdomain.com/checkout/success",
"failure_url": "https://yourdomain.com/checkout/cancel",
"callback_url": "https://yourdomain.com/niobi/callback",
"third_party_reference_1": "WAVE-SN-001"
}
}
```
`payment_method_type`: `"Orange"`
Orange payins in Côte d'Ivoire require a two-step OTP process instead of a single request.
**Documentation & Guides:**
* [Collection Methods: OTP-Verified Payins](/collecting-payments/methods#5-otp-verified-payins-cote-divoire-orange)
* [Côte d'Ivoire Country Guide: Orange Money OTP](/countries/cote-d-ivoire#3-orange-money-otp-flow-payment_method_type-orange)
**Step 1: Initiate.** Submit the collection request as usual. The customer receives an SMS OTP, and the response returns a `payment_token` instead of a terminal status:
```json theme={null}
{
"success": true,
"message": "We've sent a verification prompt to your phone. Please approve it to complete your payment.",
"data": {
"status": "success",
"amount": 2000,
"currency": "XOF",
"country_id": 3,
"payment_token": "NIO-S0000001",
"payment_step": 1
},
"status_code": "000000"
}
```
**Step 2: Verify.** Collect the OTP from the customer and send a signed request to [`POST /api/v4/niobi-unified-process-transaction`](/api-reference/collections/verify-otp):
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"salt": "random_salt_12345",
"signature": "3a3cf7f2e3ff8d4ee991d7e1323312887dfda8536b8c2e197d0d4ef764013658",
"params": {
"otp": "000000",
"payment_token": "NIO-S0000001"
}
}
```
In production, the OTP is valid for 2 minutes. In sandbox, use OTP `000000` to simulate successful verification.
`payment_method_type`: `"virtual_account"`
Generates a dedicated NGN virtual account for the customer to pay into. `account_name` is required. Pass a `bank_code` from the [NGN Virtual Account bank codes](#ngn-virtual-account---supported-bank-codes) above to choose the issuing bank; `first_name` and `last_name` are optional customer details.
**Documentation & Guides:**
* [Collection Methods: Dynamic Virtual Accounts](/collecting-payments/methods#2-dynamic-virtual-accounts-nigeria---ngn)
* [Nigeria Country Guide: Dynamic Virtual Accounts](/countries/nigeria#deposits-collections-via-dynamic-virtual-accounts)
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"params": {
"account_name": "John Doe",
"amount": 10000,
"bank_code": "17001",
"callback_url": "https://example.com/callback",
"country_id": 17,
"currency": "NGN",
"first_name": "John",
"last_name": "Doe",
"mobile": "2349034567899",
"payment_method_type": "virtual_account",
"third_party_reference_1": "TX-COLL-1001",
"third_party_reference_2": "CUST-501"
},
"salt": "random_salt_12345",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"signature": "a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef0"
}
```
**Response:** Returns `account_number` and `bank_name` for the generated virtual account, plus `"payment_step": 1`. Display these to the customer, this is the initial acknowledgement, not confirmation of payment. Await the terminal callback (`payment_step: 2`). See the full walkthrough in the [Nigeria Country Guide](/countries/nigeria).
# Verify OTP
Source: https://docs.niobi.co/api-reference/collections/verify-otp
post /api/v4/niobi-unified-process-transaction
Completes step 2 of the OTP-verified collection flow, currently required for Cote D'Ivoire Orange payins. Submit the OTP the customer received by SMS along with the `payment_token` returned from the initial [Unified Collections](/api-reference/collections/unified-collections) request. See the [OTP-Verified Payins guide](/collecting-payments/methods#5-otp-verified-payins-c%C3%B4te-divoire-orange) for the full two-step flow.
This is step 2 of the OTP-verified collection flow. Step 1 is the standard [Unified Collections](/api-reference/collections/unified-collections) request, which returns a `payment_token` instead of a terminal status when OTP verification is required. See the [OTP-Verified Payins guide](/collecting-payments/methods#5-otp-verified-payins-c%C3%B4te-divoire-orange) for the full walkthrough.
# Unified Disbursements
Source: https://docs.niobi.co/api-reference/disbursement/unified-disbursement
post /api/v4/niobi-unified-payments
One endpoint for all disbursement methods across all supported markets. Sends payouts via mobile money, bank transfer, or other supported rails. Each request must carry a freshly generated signature, signatures cannot be reused across requests. See the [Making Payments Guide](/making-payments/basics) and [Payout Methods](/making-payments/methods) for the full field matrix per market.
### Integration Quick Reference
| Requirement | Standard | Rule / Action |
| :-------------------------------------------------------------------------------------- | :--------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [**Settlement Rule**](/making-payments/basics#understanding-payment-steps-payment_step) | `payment_step: 2` only | Funds remain in transit during `payment_step: 1`. Update your ledger and notify recipients only upon receiving [payment\_step: 2](/making-payments/callbacks) with `status: "success"` (`statusCode: "000000"`). |
| [**Signatures**](/authentication/signature-generation) | Fresh per request | Compute a fresh SHA-256 signature for every API call. Signatures cannot be reused. See [Signature Generation](/authentication/signature-generation). |
| [**Wallet Balance**](/api-reference/get-account-balance) | Available balance | Always check [`available_balance`](/api-reference/get-account-balance) (not total balance) before disbursing. See [Payout Best Practices](/making-payments/best-practices). |
| [**Amount Format**](/making-payments/basics#request-parameters) | Whole integers | Pass whole numbers in major currency units (for example, `1000` KES). No decimals allowed. |
| [**Idempotency**](/idempotency) | Unique payout ref | Pass a distinct `payment_reference` / `third_party_reference_1` per payout to prevent duplicate disbursements. See the [Idempotency Guide](/idempotency). |
| [**Delayed Webhook**](/transaction-status) | Query status | If a callback is delayed, query the [Get Transaction Status API](/transaction-status) before re-attempting or failing a transfer. |
| [**Sandbox Testing**](/testing) | Mapped test MSISDNs | Use designated [Sandbox Test Numbers](/testing) mapped to outcomes (success, invalid wallet, timeout) instead of real phone numbers. |
| [**Production Prep**](/go-live-checklist) | Technical checklist | Fund your merchant payout wallet, verify public HTTPS callback URL, and review corridor limits in the [Go-Live Checklist](/go-live-checklist#technical-checklist). |
## List of Supported Countries
For Senegal Orange deposits, the end user receives an SMS with a shortcode after the deposit is initiated. They dial the shortcode, triggering a USSD prompt where they input their PIN to approve the transaction.
# Get Payment Links
Source: https://docs.niobi.co/api-reference/payment-links/list
get /api/payment-link-api/list
This endpoint allows you to access a list of all the payment links created under your Niobi account.
# Authentication Basics
Source: https://docs.niobi.co/authentication/basics
Overview of Niobi's cryptographic request signing model, required credentials, signature generation options, and dynamic endpoint parameters.
## Overview
To maintain the highest level of security and data integrity across all transactions, Niobi requires **cryptographic request signing** for all API communication.
Unlike APIs that use static API tokens or reusable session headers, **every individual request sent to any Niobi endpoint must be signed uniquely**.
Because each signature is cryptographically computed from the request's exact timestamp, a random salt, and the specific payload parameters (`params`), **signatures cannot be reused across multiple requests**. If any parameter changes, or when initiating a new transaction, a fresh signature must be generated.
```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 Merchant as Your Server
participant Niobi as Niobi API
Note over Merchant: 1. Assemble endpoint params, timestamp, and salt 2. Compute unique signature with Secret Key 3. Ensure Secret Key is not in final request
Merchant->>Niobi: Send signed request envelope (client_id, sender, signature, params)
Note over Niobi: Look up Secret Key for client_id Recalculate signature for this specific payload
alt Signature matches
Niobi-->>Merchant: 200 OK (payment initiated / processed)
else Signature mismatch
Niobi-->>Merchant: 403 Forbidden (request not verified)
end
```
**Pre-requisite: IP Whitelisting**\
Before sending any API requests to Niobi servers or endpoints (in Sandbox or Production), your server's outgoing public IP address must be whitelisted in the Niobi Dashboard under **Workspace -> IP Whitelisting**. Requests sent from non-whitelisted IPs will be blocked. See our [IP Whitelisting Guide](/ip-whitelisting) for setup steps.
***
## API Environments & Base URLs
Niobi provides dedicated Sandbox and Production environments. Configure your API client with the appropriate base URL:
| Environment | Base URL | Purpose |
| :------------- | :------------------------------- | :----------------------------------------------------- |
| **Sandbox** | `https://sandbox.users.niobi.co` | Development, testing, and simulator verification. |
| **Production** | `https://users.niobi.co` | Live transaction processing and real money settlement. |
Prepend the appropriate base URL to all endpoint paths described in the documentation (e.g. `https://sandbox.users.niobi.co/api/v4/niobi-unified-collections` for sandbox testing).
***
## Core Credentials
When you create an API integration in your Niobi Dashboard (under [**Workspace -> Integrations**](/getting-credentials)), you will receive three credentials:
| Credential | Field in Payload | Requirement | Description |
| :-------------------- | :------------------------------- | :---------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Client ID** | `client_id` | **Required** in every API request | The unique identifier assigned to your integration in the Niobi Dashboard. Identifies your merchant account. |
| **Integration Title** | `sender` | **Required** in every API request | The exact name you gave to your integration when setting it up in the dashboard. Identifies the integration entity. |
| **Secret Key** | `senderKey` *(used for signing)* | **Required for signing only** (Do not transmit) | Your private cryptographic key used on your backend server to compute the SHA-256 signature. **Never include this key in your final API request.** |
**Need to generate your API credentials first?**\
If you have not created an integration in your dashboard yet, follow our step-by-step walkthrough in [Getting Your API Credentials](/getting-credentials) (includes UI video guide and key security practices) before proceeding with signature generation.
**Never expose your Secret Key in client-side code** (such as React, Vue, iOS, or Android apps) or send it in raw HTTP requests. Request signing must always occur on your secure backend server.
***
## How Credentials and Request Parameters (`params`) Connect
Whenever your system interacts with a Niobi endpoint (for example, collecting a payment or initiating a payout):
1. **Package Target Parameters**: Place all the specific fields required by that destination endpoint inside a nested `"params": { ... }` object (such as `amount`, `currency`, `mobile`, `payment_method_type`, etc.).
2. **Attach Authentication Metadata**: Bundle `params` with your integration credentials (`client_id`, `sender`), the UNIX `timestamp` (in seconds) at the time of signing, and a random `salt` (a unique string generated on your server as an additional layer of security and entropy).
3. **Generate the Signature**: Sign that specific combination using your private Secret Key (either [in your code](/authentication/signature-generation#method-1-in-code) or via the [Niobi Signature Generation API](/authentication/signature-generation#method-2-signature-api)).
4. **Dispatch the Request**: Send the signed envelope to the destination endpoint. Niobi recalculates the hash and confirms that the payload was created by you and has not been altered in transit.
***
## Two Ways to Generate Signatures
Niobi gives you two flexible ways to generate request signatures:
Implement our deterministic SHA-256 signing algorithm in your backend (Node.js, Python, PHP, Java, Go, etc.). Zero extra network latency.
Call Niobi's dedicated `POST /api/niobi-signature` endpoint to automatically generate and return a signed payload. Ideal for rapid prototyping and Postman testing.
***
## Response & Callback Verification
Security is bidirectional. When Niobi sends a synchronous response or an asynchronous webhook callback to your server, the payload includes a `signature` computed by Niobi.
You can verify this signature on your server to guarantee that the incoming callback genuinely originated from Niobi and was not intercepted or altered in transit. Detailed steps are covered in [Response Signature Verification](/authentication/signature-generation#verifying-response-signatures).
***
## Next Steps
Follow these two guides in order to complete your integration:
# How to Pass the Signature in Requests
Source: https://docs.niobi.co/authentication/passing-signature
How to structure signed JSON request envelopes, configure HTTP headers, pass dynamic endpoint parameters, and avoid 403 verification errors.
## Overview
Once you have generated your signature, either locally via [the SHA-256 algorithm](/authentication/signature-generation#method-1-in-code) or using the [Niobi Signature Generation API](/authentication/signature-generation#method-2-signature-api), the final step is formatting and dispatching the HTTP request. Every Niobi API request follows a standardized **envelope structure** that bundles your authentication metadata with your specific payment parameters.
**Pre-requisite: IP Whitelisting**\
Before sending any API requests to Niobi servers or endpoints (in Sandbox or Production), your server's outgoing public IP address must be whitelisted in the Niobi Dashboard under **Workspace -> IP Whitelisting**. Requests sent from non-whitelisted IPs will be blocked. See our [IP Whitelisting Guide](/ip-whitelisting) for setup steps.
***
## Request Envelope Structure
Every signed request to Niobi contains two tiers:
1. **Root-Level Authentication Metadata**: Identifies your integration, timestamp, salt, and cryptographic signature.
2. **Nested `params` Object**: Contains **whatever specific parameters are required by the destination endpoint** you are invoking.
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"salt": "random_salt_string",
"signature": "c6b98e1f5d6a7890bc4e123456789abcdef0123456789abcdef0123456789abc",
"params": {
/* Destination endpoint-specific parameters go here */
}
}
```
### Root Fields Specification
| Field | Type | Required | Description |
| :---------- | :------------------- | :------- | :------------------------------------------------------------------------------------------------------------------------ |
| `client_id` | `string` | **Yes** | The unique identifier assigned to your integration in the Niobi Dashboard. |
| `sender` | `string` | **Yes** | The exact name of your integration entity as created in the dashboard. |
| `timestamp` | `integer` / `string` | **Yes** | The exact UNIX timestamp (in seconds) used when the request was signed. |
| `salt` | `string` | **Yes** | A unique random string generated on your server providing an additional layer of security and entropy. |
| `signature` | `string` | **Yes** | 64-character hexadecimal SHA-256 hash computed using your Secret Key (or returned by the Niobi Signature Generation API). |
| `params` | `object` | **Yes** | The nested parameters required by the specific target endpoint. |
**The `params` Object is Dynamic:**\
The outer fields (`client_id`, `sender`, `timestamp`, `salt`, `signature`) never change between endpoints. But the contents of `"params": { ... }` change to match the schema of the endpoint you are calling.
***
## Complete Request Examples Across Endpoints
**Endpoint:** `POST /api/v4/niobi-unified-collections`
```json theme={null}
{
"client_id": "client_id",
"sender": "Integration_name",
"timestamp": 1724835600,
"salt": "salt_value",
"signature": "c6b98e1f5d6a7890bc4e123456789abcdef0123456789abcdef0123456789abc",
"params": {
"amount": 10000,
"currency": "KES",
"country_id": 1,
"mobile": "254161166649",
"payment_method_type": "send money",
"callback_url": "https://yourdomain.com/callbacks",
"third_party_reference_1": "TX-COLL-001",
"third_party_reference_2": "CUST-450"
}
}
```
[View Unified Collections API Reference ->](/api-reference/collections/unified-collections)
**Endpoint:** `POST /api/v4/niobi-unified-payments`
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"salt": "random_salt_67890",
"signature": "e5f6a7b8c90123456789abcdef0123456789abcdef0123456789abcdef012345",
"params": {
"amount": 5000,
"currency": "KES",
"country_id": 1,
"payment_method_type": "send money",
"payment_reference": "PAYOUT-REF-1001",
"first_name": "Jane",
"last_name": "Doe",
"mobile": "254647647649",
"email": "jane.doe@example.com",
"city": "Nairobi",
"postal_code": "00100",
"address": "Kilimani, Nairobi",
"client_callback_url": "https://yourdomain.com/payout-callbacks",
"third_party_reference_1": "PAYOUT-REF-1001",
"third_party_reference_2": "BATCH-AUG",
"sendmoney": [
{ "phone_number": "254647647649" }
],
"sub_merchant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
}
```
[View Unified Payments API Reference ->](/api-reference/disbursement/unified-disbursement)
***
## Sub-Merchant Parameter Placement
If you are operating multiple distinct brands or merchants under one master account:
* Pass the approved sub-merchant UUID in `params.sub_merchant_id`.
* The `sub_merchant_id` field **must be included in the pre-signature object** when generating the signature (either in code or via the Niobi Signature Generation API) so that the hash remains valid.
* For more information on creating and managing sub-merchants, see [Sub-Merchant Management](/submerchant/sub-merchant-management).
***
## Common Pitfalls & How to Avoid Them
**Error:** Security hazard and potential rejection.\
**Fix:** The `senderKey` is strictly used on your server to compute the SHA-256 hash. Always delete `senderKey` before sending the payload over the wire.
**Error:** `403 - Request was not verified`.\
**Fix:** If any field in `params` or the root envelope changes (e.g. updating an amount or reference), the signature must be recomputed.
**Error:** Signature mismatch on the server.\
**Fix:** When stringifying the pre-signature object, ensure `callback_url` remains unencoded (e.g. `https://yourdomain.com/callback` rather than `https%3A%2F%2F...`).
**Error:** Signature mismatch.\
**Fix:** Ensure parameter types remain consistent (e.g. if `amount` is a number `10000`, do not send `"10000"` as a string).
**Error:** Request blocked or connection refused.\
**Fix:** Ensure your server's outgoing public IP address is whitelisted in your Niobi Dashboard under **Workspace -> IP Whitelisting**. See our [IP Whitelisting Guide](/ip-whitelisting).
**Error:** `403 - Request was not verified`.\
**Fix:** Trailing or leading whitespace can occur on your credentials (`client_id`, `sender`, `senderKey`) when copied from the dashboard, as well as on string parameter values inside `params` (e.g. `callback_url`, `mobile`, `email`, `payment_reference`). Always trim string values across all fields before computing the hash to prevent signature mismatches.
***
## Next Steps
You are now ready to make authenticated API requests:
# How to Create the Signature
Source: https://docs.niobi.co/authentication/signature-generation
Step-by-step guide to generating signatures by implementing the SHA-256 algorithm in code (Node.js, Python, PHP) or using Niobi's Signature Generation API.
## Overview
Niobi supports two methods for generating request signatures:
1. **In Code (Custom Implementation)**: Run our deterministic SHA-256 signing algorithm directly on your backend server. Recommended for production.
2. **Via the Niobi Signature Generation API (`POST /api/niobi-signature`)**: Call Niobi's helper endpoint to automatically compute and return the signed payload.
**Important: `params` contains whatever the destination endpoint requires**\
In both methods, the inner `"params": { ... }` object is dynamic. It is not limited to a fixed set of fields; it must contain **whatever specific parameters are required by the target API endpoint** you are invoking (e.g., Collections parameters for collections or Payout parameters for disbursements).
***
## Method 1: The Request Signing Algorithm (In Code)
For production environments where you want to minimize network latency and generate signatures entirely within your backend infrastructure, implement the following 10-step algorithm:
1. **Obtain Credentials**: Log into your Niobi Dashboard, navigate to [**Workspace -> Integrations**](/getting-credentials), and create an integration entity. Note your `client_id`, `sender` (integration title), and `senderKey` (Secret Key).
2. **Assemble the Pre-Signing Object**: Create a JSON object containing your target endpoint parameters inside a nested `params` object, along with your metadata fields:
| Field | Requirement | Description |
| :---------- | :---------------------------- | :------------------------------------------------------------------ |
| `client_id` | **Required** | Your integration Client ID from the dashboard. |
| `sender` | **Required** | Your exact integration title from the dashboard. |
| `senderKey` | **Required for signing only** | Your private Secret Key (must be deleted before HTTP transmission). |
| `timestamp` | **Required** | The UNIX timestamp (in seconds) at the time the request is signed. |
| `salt` | **Required** | Random entropy string generated on your server. |
| `params` | **Required** | Object holding the target endpoint's parameter payload. |
```json theme={null}
{
"sender": "Integration_title",
"timestamp": 1724835600,
"salt": "random_salt_value",
"client_id": "your_client_key_here",
"senderKey": "your_secret_key_here",
"params": {
"amount": 10000,
"currency": "KES",
"country_id": 1,
"mobile": "254161166649",
"payment_method_type": "send money",
"callback_url": "https://yourdomain.com/niobi/callback",
"third_party_reference_1": "REF-001",
"third_party_reference_2": "REF-002"
}
}
```
3. **Add a Salt (`salt`)**: Provide your own random string (for example, 16 to 32 alphanumeric characters). This is not provided by Niobi; it is an additional security and entropy layer you control on your server to guarantee that every payment request produces a unique signature.
4. **Include Client ID & Secret Key**: Set `"client_id"` with your integration's Client ID. **Temporarily** add your private Secret Key as `"senderKey"`.
5. **Add Integration Title (`sender`)**: Pass the exact integration name registered in your dashboard as `"sender"`.
6. **Sort Alphabetically (Recursive K-Sort)**: Sort all keys alphabetically in ascending order, applying the sort recursively for nested objects (such as `params`).
7. **Stringify Key-Value Pairs**: Convert the sorted structure into a single string formatted as `key=value`, concatenated with `&`. Nested properties are represented using dot notation (e.g. `params.amount=10000`).
**Important URL Formatting**: The `callback_url` (or `client_callback_url`) must **not** be URL-encoded in the stringified hash representation. For example:
`...¶ms.callback_url=https://yourdomain.com/niobi/callback&...`
8. **Hash the String with SHA-256**: Apply the SHA-256 hashing algorithm to the concatenated string. This produces a 64-character hexadecimal hash string.
9. **Attach the Signature**: Insert the resulting hash into the root JSON object as the `"signature"` field.
10. **Remove the Secret Key (`senderKey`)**: Delete the `"senderKey"` field from the payload completely before dispatching the request over the network.
**Watch Out for Whitespace & Trailing Spaces:**\
Ensure that your credentials (`client_id`, `senderKey`, `sender`) and string parameter values do not contain inadvertent leading or trailing whitespace, newlines, or extra spaces. A single accidental trailing space when copying a key will alter the computed hash and cause your request to fail with a `403 Request was not verified` error. We recommend trimming all string values before hashing.
***
## Detailed Payload Walkthrough
### 1. Before Signing (Pre-Signature Payload)
```json theme={null}
{
"sender": "Integration_title",
"timestamp": 1724835600,
"salt": "random_salt_value",
"client_id": "your_client_key_here",
"senderKey": "your_secret_key_here",
"params": {
"amount": 10,
"city": "Nairobi",
"client_callback_url": "https://your-domain.com/niobi/result",
"country": "KEN",
"currency": "KES"
}
}
```
### 2. Flattened, Sorted and Stringified Format
```
client_id=your_client_key_here¶ms.amount=10¶ms.city=Nairobi¶ms.client_callback_url=https://your-domain.com/niobi/result¶ms.country=KEN¶ms.currency=KES&salt=random_salt_value&sender=Integration_title&senderKey=your_secret_key_here×tamp=1724835600
```
### 3. Final Signed Request Payload (Ready to Send to the Matching API Endpoint)
```json theme={null}
{
"sender": "Integration_title",
"timestamp": 1724835600,
"salt": "random_salt_value",
"client_id": "your_client_key_here",
"params": {
"amount": 10,
"city": "Nairobi",
"client_callback_url": "https://your-domain.com/niobi/result",
"country": "KEN",
"currency": "KES"
},
"signature": "219d12d1b0b9c8e66e84ee08cefafe05eaad68745a9e82cbcbc153131333c640"
}
```
***
## Implementation Code Examples
```javascript theme={null}
const crypto = require('crypto');
function recursiveSort(value) {
if (Array.isArray(value)) {
return value.map(recursiveSort);
}
if (value !== null && typeof value === 'object') {
const sorted = {};
for (const key of Object.keys(value).sort()) {
sorted[key] = recursiveSort(value[key]);
}
return sorted;
}
return value;
}
function stringifyValue(value) {
if (typeof value === 'boolean') return value ? '1' : '';
if (value === null || value === undefined) return '';
if (typeof value === 'string') return value.trim();
return String(value);
}
function convertToQueryString(value, parentKey = null) {
let entries;
if (Array.isArray(value)) {
entries = value.map((v, i) => [i, v]);
} else if (value !== null && typeof value === 'object') {
entries = Object.entries(value);
} else {
return stringifyValue(value);
}
const parts = entries.map(([key, subValue]) => {
const currentKey = parentKey !== null ? `${parentKey}.${key}` : String(key);
if (subValue !== null && typeof subValue === 'object') {
return convertToQueryString(subValue, currentKey);
}
return `${currentKey}=${stringifyValue(subValue)}`;
});
return parts.join('&');
}
function signRequest(payload, secretKey) {
// 1. Create a clone and inject senderKey temporarily
const prePayload = {
...payload,
senderKey: secretKey.trim()
};
// 2. Sort keys recursively, then flatten into the key=value string
const sortedPayload = recursiveSort(prePayload);
const queryString = convertToQueryString(sortedPayload);
// 3. Compute SHA-256 hash
const signature = crypto
.createHash('sha256')
.update(queryString)
.digest('hex');
// 4. Return final payload without senderKey
const finalPayload = { ...payload, signature };
return finalPayload;
}
// Example Usage: params contains whatever is needed for your target endpoint.
// Method arrays (e.g. sendmoney) are flattened by index, not JSON-stringified:
// this becomes params.sendmoney.0.phone_number=254647647649.
const requestPayload = {
client_id: "YOUR_CLIENT_ID",
sender: "MyIntegration",
salt: "random_salt_12345",
timestamp: Math.floor(Date.now() / 1000),
params: {
amount: 10000,
currency: "KES",
country_id: 1,
payment_method_type: "send money",
callback_url: "https://example.com/callback",
third_party_reference_1: "REF-001",
third_party_reference_2: "REF-002",
sendmoney: [
{ phone_number: "254647647649" }
]
}
};
const signedPayload = signRequest(requestPayload, "YOUR_SECRET_KEY");
console.log(JSON.stringify(signedPayload, null, 2));
```
```python theme={null}
import hashlib
import json
import time
def recursive_ksort(value):
if isinstance(value, dict):
return {k: recursive_ksort(value[k]) for k in sorted(value.keys())}
if isinstance(value, list):
return [recursive_ksort(v) for v in value]
return value
def stringify_value(value) -> str:
if isinstance(value, bool):
return '1' if value else ''
if value is None:
return ''
if isinstance(value, str):
return value.strip()
return str(value)
def convert_to_query_string(value, parent_key=None) -> str:
if isinstance(value, dict):
items = list(value.items())
elif isinstance(value, list):
items = list(enumerate(value))
else:
return stringify_value(value)
parts = []
for key, sub_value in items:
current_key = f"{parent_key}.{key}" if parent_key is not None else str(key)
if isinstance(sub_value, (dict, list)):
parts.append(convert_to_query_string(sub_value, current_key))
else:
parts.append(f"{current_key}={stringify_value(sub_value)}")
return "&".join(parts)
def sign_request(payload: dict, secret_key: str) -> dict:
pre_payload = dict(payload)
pre_payload["senderKey"] = secret_key.strip()
# Sort keys recursively, then flatten into the key=value string
sorted_payload = recursive_ksort(pre_payload)
query_string = convert_to_query_string(sorted_payload)
# Compute SHA-256 hash
signature = hashlib.sha256(query_string.encode('utf-8')).hexdigest()
# Build final payload without senderKey
final_payload = dict(payload)
final_payload["signature"] = signature
return final_payload
# Example Usage: params contains whatever is needed for your target endpoint.
# Method arrays (e.g. sendmoney) are flattened by index, not JSON-encoded:
# this becomes params.sendmoney.0.phone_number=254647647649.
payload = {
"client_id": "YOUR_CLIENT_ID",
"sender": "MyIntegration",
"salt": "random_salt_12345",
"timestamp": int(time.time()),
"params": {
"amount": 10000,
"currency": "KES",
"country_id": 1,
"payment_method_type": "send money",
"callback_url": "https://example.com/callback",
"third_party_reference_1": "REF-001",
"third_party_reference_2": "REF-002",
"sendmoney": [
{"phone_number": "254647647649"}
]
}
}
signed_request = sign_request(payload, "YOUR_SECRET_KEY")
print(json.dumps(signed_request, indent=2))
```
```php theme={null}
$value) {
$currentKey = $parentKey !== null ? $parentKey . '.' . $key : (string) $key;
if (is_array($value)) {
$parts[] = convertToQueryString($value, $currentKey);
} else {
$parts[] = $currentKey . '=' . stringifyValue($value);
}
}
return implode('&', $parts);
}
function signRequest(array $payload, string $secretKey): array {
$prePayload = $payload;
$prePayload['senderKey'] = trim($secretKey);
recursiveKsort($prePayload);
$queryString = convertToQueryString($prePayload);
$signature = hash('sha256', $queryString);
$finalPayload = $payload;
$finalPayload['signature'] = $signature;
return $finalPayload;
}
// Example Usage: params contains whatever is needed for your target endpoint.
// Method arrays (e.g. sendmoney) are flattened by index, not JSON-encoded:
// this becomes params.sendmoney.0.phone_number=254647647649.
$requestPayload = [
'client_id' => 'YOUR_CLIENT_ID',
'sender' => 'MyIntegration',
'salt' => 'random_salt_12345',
'timestamp' => time(),
'params' => [
'amount' => 10000,
'currency' => 'KES',
'country_id' => 1,
'payment_method_type' => 'send money',
'callback_url' => 'https://example.com/callback',
'third_party_reference_1' => 'REF-001',
'third_party_reference_2' => 'REF-002',
'sendmoney' => [
['phone_number' => '254647647649']
]
]
];
$signed = signRequest($requestPayload, 'YOUR_SECRET_KEY');
echo json_encode($signed, JSON_PRETTY_PRINT);
```
```java theme={null}
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
public class NiobiSignature {
private static String stringifyValue(Object value) {
if (value == null) return "";
if (value instanceof Boolean) return ((Boolean) value) ? "1" : "";
if (value instanceof String) return ((String) value).trim();
return String.valueOf(value);
}
@SuppressWarnings("unchecked")
private static String convertToQueryString(Object value, String parentKey) {
List> entries = new ArrayList<>();
if (value instanceof Map) {
List keys = new ArrayList<>(((Map) value).keySet());
Collections.sort(keys);
for (String key : keys) {
entries.add(new AbstractMap.SimpleEntry<>(key, ((Map) value).get(key)));
}
} else if (value instanceof List) {
List
```go theme={null}
package main
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
"time"
)
func stringifyValue(value interface{}) string {
if value == nil {
return ""
}
switch v := value.(type) {
case bool:
if v {
return "1"
}
return ""
case string:
return strings.TrimSpace(v)
default:
return fmt.Sprintf("%v", v)
}
}
func isContainer(value interface{}) bool {
switch value.(type) {
case map[string]interface{}, []interface{}:
return true
default:
return false
}
}
func convertToQueryString(value interface{}, parentKey string) string {
switch v := value.(type) {
case map[string]interface{}:
keys := make([]string, 0, len(v))
for k := range v {
keys = append(keys, k)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, k := range keys {
currentKey := k
if parentKey != "" {
currentKey = parentKey + "." + k
}
subValue := v[k]
if isContainer(subValue) {
// Recurse into BOTH maps and slices the same way. An empty
// map/slice contributes an empty string here, not "[]" or "key=".
parts = append(parts, convertToQueryString(subValue, currentKey))
} else {
parts = append(parts, currentKey+"="+stringifyValue(subValue))
}
}
return strings.Join(parts, "&")
case []interface{}:
parts := make([]string, 0, len(v))
for i, item := range v {
currentKey := strconv.Itoa(i)
if parentKey != "" {
currentKey = parentKey + "." + currentKey
}
if isContainer(item) {
parts = append(parts, convertToQueryString(item, currentKey))
} else {
parts = append(parts, currentKey+"="+stringifyValue(item))
}
}
return strings.Join(parts, "&")
default:
return stringifyValue(value)
}
}
func SignRequest(payload map[string]interface{}, secretKey string) string {
prePayload := make(map[string]interface{}, len(payload)+1)
for k, v := range payload {
prePayload[k] = v
}
prePayload["senderKey"] = strings.TrimSpace(secretKey)
// Sort keys recursively (inline, at each level) and flatten into the
// key=value string in the same pass.
queryString := convertToQueryString(prePayload, "")
hash := sha256.Sum256([]byte(queryString))
return hex.EncodeToString(hash[:])
}
func main() {
// Method arrays (e.g. sendmoney) are flattened by index, not
// JSON-marshaled: this becomes params.sendmoney.0.phone_number=254647647649.
params := map[string]interface{}{
"amount": 10000,
"currency": "KES",
"country_id": 1,
"payment_method_type": "send money",
"callback_url": "https://example.com/callback",
"third_party_reference_1": "REF-001",
"third_party_reference_2": "REF-002",
"sendmoney": []interface{}{
map[string]interface{}{"phone_number": "254647647649"},
},
}
payload := map[string]interface{}{
"client_id": "YOUR_CLIENT_ID",
"sender": "MyIntegration",
"salt": "random_salt_12345",
"timestamp": time.Now().Unix(),
"params": params,
}
signature := SignRequest(payload, "YOUR_SECRET_KEY")
payload["signature"] = signature
out, _ := json.MarshalIndent(payload, "", " ")
fmt.Println(string(out))
}
```
```csharp theme={null}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
public class NiobiSignature
{
private static string StringifyValue(object value)
{
if (value == null) return "";
if (value is bool b) return b ? "1" : "";
if (value is string s) return s.Trim();
return Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture);
}
private static bool IsContainer(object value) =>
value is IDictionary || value is IEnumerable;
private static string ConvertToQueryString(object value, string parentKey)
{
if (value is IDictionary dict)
{
var sortedKeys = dict.Keys.OrderBy(k => k, StringComparer.Ordinal).ToList();
var parts = new List();
foreach (var key in sortedKeys)
{
string currentKey = string.IsNullOrEmpty(parentKey) ? key : $"{parentKey}.{key}";
var subValue = dict[key];
// Recurse into BOTH dictionaries and lists the same way. An empty
// dictionary/list contributes an empty string, not "[]" or "key=".
parts.Add(IsContainer(subValue)
? ConvertToQueryString(subValue, currentKey)
: $"{currentKey}={StringifyValue(subValue)}");
}
return string.Join("&", parts);
}
if (value is IEnumerable list)
{
var parts = new List();
int i = 0;
foreach (var item in list)
{
string currentKey = string.IsNullOrEmpty(parentKey) ? i.ToString() : $"{parentKey}.{i}";
parts.Add(IsContainer(item)
? ConvertToQueryString(item, currentKey)
: $"{currentKey}={StringifyValue(item)}");
i++;
}
return string.Join("&", parts);
}
return StringifyValue(value);
}
public static string GenerateSignature(Dictionary payload, string secretKey)
{
var prePayload = new Dictionary(payload)
{
["senderKey"] = secretKey.Trim()
};
// Sort keys recursively (inline, at each level) and flatten into the
// key=value string in the same pass.
string queryString = ConvertToQueryString(prePayload, null);
using var sha256 = SHA256.Create();
byte[] bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(queryString));
return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
}
public static void Main()
{
// Method arrays (e.g. sendmoney) are flattened by index, not
// serialized: this becomes params.sendmoney.0.phone_number=254647647649.
var paramsDict = new Dictionary
{
{ "amount", 10000 },
{ "currency", "KES" },
{ "country_id", 1 },
{ "payment_method_type", "send money" },
{ "callback_url", "https://example.com/callback" },
{ "third_party_reference_1", "REF-001" },
{ "third_party_reference_2", "REF-002" },
{ "sendmoney", new List {
new Dictionary { { "phone_number", "254647647649" } }
} }
};
var payload = new Dictionary
{
{ "client_id", "YOUR_CLIENT_ID" },
{ "sender", "MyIntegration" },
{ "salt", "random_salt_12345" },
{ "timestamp", DateTimeOffset.UtcNow.ToUnixTimeSeconds() },
{ "params", paramsDict }
};
string signature = GenerateSignature(payload, "YOUR_SECRET_KEY");
payload["signature"] = signature;
Console.WriteLine(JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true }));
}
}
```
***
## Method 2: Generating Signatures via the Niobi Signature Generation API
If you prefer not to write custom cryptographic sorting and hashing functions, or if you want to quickly test requests in Postman or Sandbox, Niobi provides a dedicated Signature Generation endpoint:
**Endpoint:** [`POST /api/niobi-signature`](/api-reference/authentication/generate-signature)
### Step 1: Send Your Request Parameters to the Signature Endpoint
Construct a request containing the following fields:
| Field | Requirement | Description |
| :---------- | :----------- | :----------------------------------------------------------------------------------------- |
| `client_id` | **Required** | Your integration Client ID from the Niobi Dashboard. |
| `sender` | **Required** | Your exact integration title from the dashboard. |
| `salt` | **Required** | A unique random string generated on your server for entropy. |
| `params` | **Required** | Object containing the specific parameters for the destination endpoint you intend to call. |
```json theme={null}
{
"sender": "Integration_title",
"salt": "random_salt_value",
"client_id": "your_client_key_here",
"params": {
"amount": 10000,
"mobile": "254161166649",
"country_id": 1,
"currency": "KES",
"payment_method_type": "send money",
"callback_url": "https://yourdomain.com/callback",
"third_party_reference_1": "REF-001",
"third_party_reference_2": "REF-002"
}
}
```
### Step 2: Receive the Complete Signed Payload
Niobi validates your `client_id`, calculates the SHA-256 signature using your account's registered Secret Key, and returns the full signed envelope inside the `data` object:
```json theme={null}
{
"success": true,
"message": "Signature was generated successfully.",
"data": {
"client_id": "your_client_key_here",
"params": {
"amount": 10000,
"mobile": "254161166649",
"country_id": 1,
"currency": "KES",
"payment_method_type": "send money",
"callback_url": "https://yourdomain.com/callback",
"third_party_reference_1": "REF-001",
"third_party_reference_2": "REF-002"
},
"salt": "random_salt_value",
"sender": "Integration_title",
"timestamp": 1724835600,
"signature": "c6b98e1f5d6a7890bc4e123456789abcdef0123456789abcdef0123456789abc"
}
}
```
### Step 3: Pass the Signed Payload to the Target API
You can now take the exact JSON object inside `data` and send it directly to your target endpoint (e.g. `POST /api/v4/niobi-unified-collections` or `POST /api/v4/niobi-unified-payments`) **without changing or modifying anything as it is**.
**Do Not Modify the Returned Payload:**\
You must transmit the JSON object inside `data` exactly as returned, without adding, altering, or removing any fields. Any modification to parameters or values after generation will invalidate the signature and cause the target endpoint to reject your request with a `403 Request was not verified` error.
***
## Verifying Response & Webhook Signatures
When receiving a synchronous response or asynchronous webhook callback from Niobi, you can verify its authenticity using either code or the Verification API:
### 1. In Code (5 Steps):
1. **Extract Signature**: Store the `"signature"` field from the incoming JSON and remove it from the object.
2. **Add Secret Key**: Add `"senderKey": "your_secret_key_here"` into the object.
3. **Sort and Stringify**: Alphabetically sort keys recursively (K-sort) and format into `key=value` concatenated with `&`.
4. **Compute SHA-256 Hash**: Hash the string with SHA-256.
5. **Compare Signatures**: Compare your computed hash with the signature received in Step 1.
### 2. Via the Verification Helper API:
Send the payload to [`POST /api/niobi-verify`](/api-reference/authentication/verify-signature):
```json theme={null}
{
"success": true,
"message": "Request verified successfully."
}
```
***
## Next Steps
Now that you can generate valid signatures, proceed to construct the signed request envelope:
# Collection Basics
Source: https://docs.niobi.co/collecting-payments/basics
Core fundamentals of collecting payments across Africa: payment steps, request parameters, synchronous dispatch responses, and asynchronous terminal callbacks.
## Overview
Niobi's **Unified Collections API** allows your application to collect payments from customers across Africa through a single standardized integration. Whether your customer pays via Mobile Money (STK Push, OTP, or USSD), Bank Transfer, Dynamic Virtual Account, or Browser Redirect, you send a signed request to the unified collections endpoint:
**Endpoint:** [`POST /api/v4/niobi-unified-collections`](/api-reference/collections/unified-collections)
```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 Merchant as Your Backend
participant Niobi as Niobi API
participant TelcoBank as Telco / Bank Switch
Merchant->>Niobi: POST /api/v4/niobi-unified-collections (Signed Payload)
Niobi->>TelcoBank: Dispatch collection request
Niobi-->>Merchant: Initial HTTP Response (On success: payment_step: 1, status_code: 000000)
Note over Merchant: Transaction initiated (Pending customer authorization)
TelcoBank->>Customer: Trigger STK push / Prompt / Issue Virtual Account
Customer->>TelcoBank: Authorize payment (Enter PIN / Complete Transfer)
TelcoBank->>Niobi: Confirm payment outcome
alt Payment Succeeded
Niobi->>Merchant: Webhook (payment_step: 2, status: "success", statusCode: "000000")
Note over Merchant: Only now: Credit user balance & fulfill order
else Payment Failed / Canceled
Niobi->>Merchant: Webhook (payment_step: 2, status: "failed", statusCode: "000005")
Note over Merchant: Mark order failed, do not credit user
end
```
Step 1Request Initiation
payment\_step: 1
Your backend sends a signed POST /api/v4/niobi-unified-collections request, which Niobi dispatches to the telco or bank switch.
Niobi returns an initial HTTP response confirming the transaction was initiated and is pending customer authorization.
The switch triggers an STK push, prompt, or issues a virtual account, and the customer authorizes payment (PIN entry or transfer).
→
Step 2Terminal Outcome
payment\_step: 2
The switch confirms the payment outcome back to Niobi.
Niobi sends a webhook callback to your callback\_url with the final status.
Reconcile the transaction and credit the user based on the final terminal state.
***
## API Environments & Base URLs
Niobi provides separate environments for integration testing and live transaction processing:
| Environment | Base URL | Purpose |
| :------------- | :------------------------------- | :------------------------------------------------------------ |
| **Sandbox** | `https://sandbox.users.niobi.co` | Integration testing, mock transactions, and sandbox accounts. |
| **Production** | `https://users.niobi.co` | Live payment collections and real customer funds. |
All collection endpoints referenced throughout this guide are relative to your target environment's base URL.
***
## Understanding Payment Steps (`payment_step`)
A critical concept in Niobi's collection architecture is the two-step payment lifecycle:
Step 1Request Initiation
payment\_step: 1
Initial HTTP response to the request when it's successful, telling you the transaction was accepted by the telco/bank switch.
→
Step 2Terminal Outcome
payment\_step: 2
Asynchronous webhook callback delivering the final result (success or failed).
### 1. `payment_step: 1` (Initial API Response)
* **Delivery:** Synchronous HTTP response returned immediately when calling `POST /api/v4/niobi-unified-collections`.
* **Meaning:** The request passed validation, authentication was verified, and the transaction was accepted and dispatched to the upstream payment processor or telco switch.
* **Important:** This is an acknowledgement that the process has begun, **not the final payment outcome**. The customer has not yet entered their PIN or completed the transfer.
**Absence of `payment_step` on Initiation Failures:**\
If a collection request fails during initial submission (for example, due to an invalid phone number format, unsupported method, or immediate upstream processor rejection returning `status_code: "000005"`), the `"payment_step"` field is **not present** in the response. Your backend must inspect `status_code` (`"000000"` vs `"000005"`) rather than assuming `payment_step` is always returned.
### 2. `payment_step: 2` (Terminal Callback)
* **Delivery:** Asynchronous callback POST request sent to the `callback_url` passed in your request.
* **Meaning:** The transaction has reached its **final terminal state**.
* **Action:** When `"payment_step": 2` is present in the `params` object of the callback, treat this as the final outcome. Reconcile your records based on `status: "success"` (`status_code: "000000"`) or `status: "failed"` (`status_code: "000005"`). Full details are covered in [Collection Callbacks](/collecting-payments/callbacks).
**Rules of Transaction Settlement (Never Credit on `payment_step: 1`):**\
Niobi's collection flows follow a strict two-step lifecycle. An initial synchronous API response with `"payment_step": 1` is an acknowledgement that the payment request has been delivered to the telecom provider or banking switch; **it is NOT a confirmation of payment**.
* **Never credit customer balances or release goods on `payment_step: 1`**:\
Wait for the asynchronous terminal webhook callback (`payment_step: 2`) containing `"status": "success"` (`status_code: "000000"`).
* **Handle Failure States (`payment_step: 2` with `"status": "failed"` / `"000005"`):**\
If the callback returns `status: "failed"`, the customer canceled the prompt, entered an incorrect PIN, or lacked funds. Mark the invoice as unpaid.
* **Missing or Delayed Webhook?**\
If a callback hasn't arrived after a reasonable wait, query the [Get Transaction Status API](/transaction-status) (`POST /api/v3/get-unified-transaction-status`) to verify the state before taking any action.
***
## Request Parameters
The unified collections endpoint accepts standard parameters wrapped in the signed envelope:
| Parameter | Type | Required | Description |
| :---------------------------- | :------------------- | :--------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`amount`** | `integer` | **Yes** | The amount to collect in standard major currency units (e.g. `1000` = 1,000 KES). **Must be a whole integer with no decimal points.** |
| **`mobile`** | `string` | **Yes** | The customer's mobile phone number in international format without `+` (e.g. `254700000000`, `234xxxxxxxxx`). |
| **`country_id`** | `integer` / `string` | **Yes** | The country ID where the deposit originates (e.g. `1` for Kenya, `17` for Nigeria, `15` for South Africa). See [Country Coverage](/supported-countries). |
| **`currency`** | `string` | **Yes** | The 3-letter ISO currency code (e.g. `KES`, `NGN`, `ZAR`, `GHS`, `UGX`, `TZS`, `XOF`, `XAF`). |
| **`payment_method_type`** | `string` | **Yes** | The collection channel (e.g. `send money`, `virtual_account`, `bank_eft`, `Wave`, `capitec`). See [Collection Methods](/collecting-payments/methods). |
| **`callback_url`** | `string` | **Required (for callbacks)** | The endpoint URL on your server where real-time terminal callbacks (`payment_step: 2`) are delivered. Essential for automated transaction settlement. |
| **`third_party_reference_1`** | `string` | **Recommended** | Your unique merchant transaction reference. Strongly recommended for automated reconciliation and idempotency defense. |
| **`third_party_reference_2`** | `string` | Optional | Additional secondary reference or custom customer identifier. |
| **`id_number`** | `string` | **ZAR Capitec only** | Customer's full 13-digit South African ID number (required for Capitec payments to be valid). |
| **`successful_url`** | `string` | **Redirects only** | URL to redirect customer upon payment approval (required for Wave Senegal/Ivory Coast and South Africa Bank EFT/Capitec). |
| **`failure_url`** | `string` | **Redirects only** | URL to redirect customer upon payment cancellation or failure (required for Wave and South Africa Bank EFT/Capitec). |
| **`name`** | `string` | **ZAR only** | Customer's full name (required for South Africa Bank EFT and Capitec collections). |
| **`first_name`** | `string` | **NGN only** | Customer's first name (required for Nigerian dynamic virtual accounts). |
| **`last_name`** | `string` | **NGN only** | Customer's last name (required for Nigerian dynamic virtual accounts). |
| **`account_name`** | `string` | **NGN only** | Account holder name to appear when funds are reconciled (Nigerian virtual accounts). |
| **`bank_code`** | `string` | **NGN / ZAR** | Code of the bank issuing the account (Nigeria: `17001`, `17002`, `17003`) or handling transfer (South Africa: `0001`, etc.). |
| **`sub_merchant_id`** | `string` (UUID) | Optional | Sub-merchant identifier if collecting on behalf of a managed sub-merchant. |
***
## Front-End Integration Pathways
Niobi gives you **2** ways to deliver payment collections to your customers:
### Option 1: Niobi Hosted Checkout (Fastest & Zero UI Code)
If you want to start accepting payments immediately without building and maintaining custom checkout forms, input validation, or tokenization UI, use **Niobi Hosted Checkout** powered by Payment Links.
#### How It Works:
1. **Create via Dashboard (No-Code) or API:**
* **Dashboard (No-Code):** Log into your Niobi Dashboard, navigate to **Payments -> Payment Links**, click **Create Payment Link**, configure your business details and amount (fixed or customer-entered), and copy your generated checkout URL immediately.
* **API (Programmatic):** Call the [Create Payment Link API](/api-reference/payment-links/create) (`POST /api/v3/payment-link-api/create`) to dynamically generate customized payment links on demand from your backend.
2. **Manage & Retrieve:** View, edit, filter, or retrieve active links in the dashboard or via the [Payment Link List API](/api-reference/payment-links/list) (`GET /api/payment-link-api/list`).
3. **Share or Redirect:** Share the link via WhatsApp, SMS, invoices, emails, or redirect customers directly from your web or mobile checkout button.
4. **Automated Settlement & Callback:** The customer completes payment on the secure hosted checkout page. Niobi confirms the transaction, credits your merchant wallet, and dispatches an asynchronous webhook callback (`payment_step: 2`) to your server.
For full implementation details, see [Payment Links Guide](/collecting-payments/payment-links) and [Hosted Checkout & Payment Links](/collecting-payments/methods#hosted-checkout).
***
### Option 2: Direct API Integration (Custom UI / White-Label)
If you require 100% white-labeled control over every UI element and payment form, integrate directly with our collections endpoint:
Send a signed `POST` request to `/api/v4/niobi-unified-collections`:
#### Sample Request
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"salt": "random_salt_12345",
"signature": "c6b98e1f5d6a7890bc4e123456789abcdef0123456789abcdef0123456789abc",
"params": {
"amount": 10000,
"currency": "KES",
"country_id": 1,
"mobile": "254161166649",
"payment_method_type": "send money",
"callback_url": "https://yourdomain.com/niobi/callback",
"third_party_reference_1": "TX-COLL-1001",
"third_party_reference_2": "CUST-450"
}
}
```
#### Sample Initial API Responses
```json theme={null}
{
"success": true,
"message": "We've sent a verification prompt to your phone. Please approve it to complete your payment.",
"data": {
"amount": 10000,
"callback_url": "https://yourdomain.com/niobi/callback",
"country_id": 1,
"currency": "KES",
"mobile": "254161166649",
"payment_method_type": "send money",
"third_party_reference_1": "TX-COLL-1001",
"third_party_reference_2": "CUST-450",
"status": "success",
"payment_step": 1,
"payment_token": null
},
"status_code": "000000"
}
```
```json theme={null}
{
"success": false,
"message": "Payment process initiation failed. Kindly check the details and try again.",
"status_code": "000005"
}
```
If an initiation request is rejected outright (e.g. missing fields, invalid phone number, unsupported currency), refer to the [400 Client Errors Guide](/errors/client-errors).
**Understanding the Two "Success" Indicators:**\
You will notice two different success indicators in the initial response:
* **Outer `"success": true` (Root Level):** Indicates the **HTTP API Request Status** ([Request Successfully Initiated](/errors/200-success)). It confirms that your API call was authenticated, structurally valid, and successfully received by Niobi servers.
* **Inner `"status": "success"` (Inside `data`):** Indicates the **Transaction Initiation State**. Used together with `"status_code": "000000"` and `"payment_step": 1`, it confirms that the upstream provider or telco switch accepted the collection request and triggered the authorization prompt on the payer's device. It signifies successful initiation, not final fund settlement.
**The `message` Field:**\
The `message` string returned in the initial response describes the action the payer must take to complete the transaction. Its wording varies depending on the upstream payment processor or telco switch. Common examples include:
* `"Verification token was sent to your mobile number. Kindly verify to proceed payment."`
* `"We've sent a verification prompt to your phone. Please approve it to complete your payment."`
We recommend displaying this returned message directly in your application UI so payers know to check their device for the authorization prompt.
Depending on the selected payment method:
* **Mobile Money (STK Push):** The customer receives an on-screen prompt on their mobile phone and enters their PIN.
* **OTP Payins (e.g. Ivory Coast Orange):** The customer receives an SMS OTP, which your UI collects and submits to `/api/v4/niobi-unified-process-transaction`.
* **Virtual Account (e.g. Nigeria):** The initial response returns an `account_number` and `bank_name`. Display these details to the customer to complete a standard bank transfer within 30 minutes.
* **Browser Redirect (e.g. Wave, South Africa Bank EFT):** The response includes an `authorization_url`. Redirect the customer to open the URL in their browser to complete payment.
When the transaction concludes, Niobi sends an asynchronous callback containing `"payment_step": 2` to the `callback_url` passed under `params.callback_url` in your collection request.
#### Sample Terminal Callbacks (payment\_step: 2)
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"sender": "YOUR_INTEGRATION_NAME",
"salt": "random_salt_12345",
"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"
}
}
```
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"sender": "YOUR_INTEGRATION_NAME",
"salt": "random_salt_12345",
"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"
}
}
```
**Determine Outcome Using `status` and `statusCode`:**\
When `"payment_step": 2` arrives, inspect both `params.status` and `params.statusCode` inside the payload:
* [`status: "success"` with `statusCode: "000000"`](/errors/000000-success): The payment was successfully authorized and credited. Fulfill the order or credit the user account.
* [`status: "failed"` with `statusCode: "000005"`](/errors/000005-failed): The payment failed or was declined. Inspect `params.failureReason` for specific provider diagnostics.
`payment_step: 2` always carries one of these two outcomes, there is no separate timeout callback shape. See our [Transaction Status Codes Guide](/errors/transaction-status-codes) for the complete list of system status codes.
**Handling Collection Timeouts (`000006`):**\
`000006` is not a callback outcome, it only appears when you query the [Transaction Status API](/api-reference/get-transaction-status) while a collection is still awaiting customer authorization past the standard 2-minute window. Timeouts can be triggered by several factors:
* **Customer Inaction:** The payer did not enter their PIN or respond to the prompt before the session expired.
* **Mobile Operator / SMS Latency:** Delays across the telco network in delivering the STK or USSD push to the user's handset.
* **Upstream Processor Congestion:** Temporary clearing latency with partner banking switches or mobile money providers.
If a status query returns `000006`, do not immediately prompt the customer to pay again. Wait 2 to 3 minutes and query again, the transaction will eventually resolve to `success` (`000000`) or `failed` (`000005`), and that final outcome is what arrives in the `payment_step: 2` callback.
**On-Demand Status Verification:**\
If you ever need to confirm or query the current status of a collection on demand (for example, if a network disruption delayed your webhook callback), call our [Get Transaction Status API](/api-reference/get-transaction-status) (`POST /api/v3/get-unified-transaction-status`) by passing `"type": "payin"` and your transaction reference in `"id"`.
***
## Next Steps
Follow these guides in order to complete your collection integration:
# Collection Best Practices
Source: https://docs.niobi.co/collecting-payments/best-practices
Operational guidance, phone number validation rules, reconciliation recipes, and status polling best practices for payment collections.
## Overview
Building a resilient, production-ready payment collection flow requires proper error handling, disciplined polling behavior, strict input validation, and reliable transaction reconciliation. Follow these best practices to ensure seamless transaction processing and avoid common production pitfalls.
***
## 1. When to Query Status vs. Waiting for Callbacks
Niobi processes transactions asynchronously with regional payment switches and mobile network operators.
Primary FlowCollection Callback
Rely on the incoming payment\_step: 2 payload.
→
Fallback FlowTransaction Status Query API
Query with spaced intervals if the callback isn't received.
### Best Practices for Status Handling:
* **Rely on Callbacks as the Primary Source:**\
The callback delivered to your `callback_url` is the primary and most efficient notification mechanism.
* **Avoid Aggressive Polling Loops:**\
Do not poll the status API immediately in a tight loop after initiating a collection. Aggressive polling consumes unnecessary bandwidth and may trigger rate limits.
* **Query Status Using Spaced Intervals:**\
If your server has not received a callback 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 (e.g. progressive backoff) to reconcile pending collection transactions.
***
## 2. The Double-Verification Reconciliation Recipe
For mission-critical workflows where releasing value prematurely or acting on an unverified signal creates financial risk, Niobi recommends implementing a **double-verification reconciliation recipe**: rely on the `payment_step: 2` callback as your primary signal, fall back to a status query if it hasn't arrived, and only close out the order once that confirmed status is reconciled against your own internal order records.
### When to Implement Double-Verification:
* **Instant Digital Value & Wallet Credits:** Crypto on-ramps, forex trading platforms, and digital wallet top-ups where credited funds can be immediately withdrawn.
* **Automated Chain Reactions (Collection to Payout):** Systems where collection completion automatically triggers a downstream disbursement (e.g. cross-border remittances or escrow releases).
* **High-Value B2B Invoicing & Automated Dispatch:** Wholesalers or e-commerce platforms triggering automated warehouse dispatch upon payment confirmation.
### The 4-Step Technical Reconciliation Flow:
```mermaid theme={null}
%%{init: {'themeVariables': {'fontSize': '17px'}}}%%
flowchart TD
A["Initiate Collection (POST /api/v4/niobi-unified-collections)"] --> B["Receive payment_step: 1 (Initiated)"]
B --> C["Wait for Incoming Callback"]
C --> D{"payment_step: 2 Callback Received?"}
D -- Yes --> E["Reconciliation Step 1: Verify Inbound Signature"]
D -- "No (after a while)" --> F["Call Status API (POST /api/v3/get-unified-transaction-status)"]
E --> G["Reconciliation Step 2: Acquire DB Lock on Reference"]
F --> G
G --> H{"Reconciliation Step 3: Triple-Point Match? 1. status == 'success' & statusCode == '000000' 2. amount & currency match order 3. third_party_reference_1 matches"}
H -- "Match Confirmed" --> I["
Reconciliation Step 4: Atomic DB Update (Set status = 'SETTLED') & Credit Balance
"]
H -- "Mismatch / Failed" --> J["
Mark Transaction Failed / Flag for Review
"]
style I fill:#0D9373,stroke:#0D9373,color:#ffffff
style J fill:#b91c1c,stroke:#b91c1c,color:#ffffff
```
### Reconciliation Checklist:
1. **Inbound Signature Verification:** Always verify the incoming SHA-256 callback signature using your integration Secret Key (`senderKey`) before parsing the payload.
2. **Concurrency Lock & Idempotency Check:** Acquire a row-level database lock on `third_party_reference_1` before processing balance updates. If both a callback and a status query arrive concurrently, only the first thread processes the credit while the second safely exits.
3. **Triple-Point Data Match:** Confirm that all three attributes match your internal order record:
* **Status & Code:** Both `status: "success"` and `statusCode: "000000"` ([Success Code 000000](/errors/000000-success)).
* **Amount & Currency:** The received `amount` matches the exact order total (prevents underpayment fulfillment).
* **Reference:** The `third_party_reference_1` matches the specific customer order ID.
4. **Atomic Ledger Settlement:** Execute the balance credit and update the local order status to `SETTLED` inside a single atomic database transaction.
***
## 3. Phone Number & Amount Formatting Rules
### Strict Integer Rule for `amount` (No Decimal Points)
All collection amounts submitted in API requests **MUST be whole integers** (e.g. `1000` for 1,000 KES/NGN/ZAR). Do not send decimal points (such as `1000.50` or `1000.00`). Submitting decimal amounts will cause the request to fail validation.
### Phone Number Formatting Rules:
* **Digits Only:** Strip all spaces, plus signs (`+`), hyphens, parentheses, or symbols.
* **Length:** Between 8 and 15 digits (including country code).
* **International Prefix without Plus:** Always include the country code, but omit the leading `+` or local trunk `0`.
| Country | Correct Format | Incorrect Format |
| :---------------- | :-------------- | :----------------------------------- |
| **Kenya** | `254712345678` | `+254 712 345 678` or `0712345678` |
| **Nigeria** | `2348012345678` | `+234 801 234 5678` or `08012345678` |
| **Ghana** | `233501234567` | `+233 50 123 4567` or `0501234567` |
| **South Africa** | `27600000008` | `+27 60 000 0008` or `0600000008` |
| **Senegal** | `221770000000` | `+221 77 000 0000` |
| **Côte d'Ivoire** | `225070000000` | `+225 07 00 00 00` |
***
## 4. Error Handling & Retry Policies
Design your integration to distinguish between permanent client errors and temporary upstream issues:
**Examples:** [`400 Bad Request`](/errors/client-errors), [`403 Request not verified`](/errors/authentication-errors), `404 Not Found`.\
**Action:** Do not retry these requests automatically with the same payload. Log the error message, inspect your parameter validation, verify your signature generation algorithm, and correct the payload before resending. See [400 Client Errors](/errors/client-errors) for code breakdowns.
**Examples:** `500 Internal Server Error`, `502 Bad Gateway`, `504 Gateway Timeout`.\
**Action:** Implement exponential backoff (e.g. retry after 2s, 4s, 8s, 16s) up to a maximum of 3 to 5 attempts. If timeouts persist, check transaction status before re-initiating.
For Nigerian NGN virtual account collections, always collect the exact amount. Any excess payment is automatically reversed by the banking system, which may cause customer confusion if not handled cleanly.
***
## Next Steps
Complete your integration and explore developer references:
# Collection Callbacks
Source: https://docs.niobi.co/collecting-payments/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 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 2. Credit user & fulfill order 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 2. Mark failed (Do NOT credit) 3. Reconcile idempotently
end
Merchant-->>Niobi: HTTP 200 OK
```
***
## Terminal Callback Guarantee (`payment_step: 2`)
**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`).
* 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
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.
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.
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.
***
## 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**.
**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.
***
## Next Steps
Follow these guides to complete and secure your collection flow:
# Collection Methods
Source: https://docs.niobi.co/collecting-payments/methods
Comprehensive guide to supported collection methods across Africa: Mobile Money USSD push, OTP verification, Bank Transfers, Virtual Accounts, and Redirect flows.
## Overview
Niobi supports diverse payment collection channels across Africa. Depending on the destination country, your application can collect funds via Mobile Money, Dynamic Virtual Accounts, Bank Transfers, or Browser Redirects.
### Master Country Collection Matrix
| ID | Country | Currency | Supported Methods (`payment_method_type`) |
| :----: | :------------------------------------------------------------------------------------------------------ | :------: | :----------------------------------------------------- |
| **1** | [Kenya](/countries/kenya) | `KES` | `send money` (M-Pesa), `airtel` |
| **2** | [Benin](/countries/benin) | `XOF` | `mtn`, `moov` |
| **3** | [Côte d'Ivoire](/countries/cote-d-ivoire) | `XOF` | `Orange` (OTP), `mtn`, `moov`, `Wave` |
| **4** | [Cameroon](/countries/cameroon) | `XAF` | `mtn`, `Orange` |
| **5** | [DRC](/countries/drc) | `CDF` | `airtel`, `Orange`, `vodacom` |
| **8** | [Rwanda](/countries/rwanda) | `RWF` | `mtn`, `airtel` |
| **9** | [Senegal](/countries/senegal) | `XOF` | `Orange` (USSD PIN), `free`, `Wave` |
| **10** | [Tanzania](/countries/tanzania) | `TZS` | `airtel`, `halotel`, `tigo`, `vodacom` |
| **11** | [Uganda](/countries/uganda) | `UGX` | `mtn`, `airtel` |
| **12** | [Zambia](/countries/zambia) | `ZMW` | `mtn`, `zamtel`, `airtel` |
| **13** | [Sierra Leone](/countries/sierra-leone) | `SLE` | `Orange` |
| **15** | [South Africa](/countries/south-africa) | `ZAR` | `bank_eft` (EFT), `capitec` (Capitec Pay) |
| **16** | [Ghana](/countries/ghana) | `GHS` | `mtn`, `At` (AirtelTigo), `vodafone` |
| **17** | [Nigeria](/countries/nigeria) | `NGN` | `virtual_account` (Bank Transfer) |
| **18** | [Mali](/countries/mali) | `XOF` | `Orange`, `moov` |
| **19** | [Burkina Faso](/countries/burkina-faso) | `XOF` | `Orange`, `moov` |
| **20** | [Togo](/countries/togo) | `XOF` | `moov`, `tmoney` |
| N/A | **[Hosted Checkout](/collecting-payments/basics#option-1-niobi-hosted-checkout-fastest--zero-ui-code)** | *All* | Pre-built payment links across all supported countries |
### Upcoming Collection Corridors
| Country | Code | Currency | Planned Payment Methods |
| :---------------------------------- | :--: | :------: | :----------------------------- |
| [Ethiopia](/countries/ethiopia) | 251 | `ETB` | teleBirr, mBirr |
| [Malawi](/countries/malawi) | 265 | `MWK` | Airtel Money, TNM Mpamba |
| [Egypt](/countries/egypt) | 20 | `EGP` | Vodafone Cash, Orange Money |
| [Mozambique](/countries/mozambique) | 258 | `MZN` | Vodacom M-Pesa, Movitel, Tmcel |
***
## 1. Mobile Money (USSD STK Push)
USSD Push (STK Push) is the standard mobile money collection flow across East and West Africa.
### How It Works:
1. Your backend dispatches `POST /api/v4/niobi-unified-collections` with the corridor-specific `payment_method_type` for that mobile money network.
2. The telco switch sends an instant SIM toolkit prompt to the customer's mobile phone.
3. The customer enters their PIN to authorize the payment.
4. Niobi dispatches a terminal callback (`payment_step: 2`) to the `callback_url` passed under `params.callback_url` in your request.
**`payment_method_type` Matches the Channel and Country:**\
The value passed in `payment_method_type` is specific to the local payment provider and country:
* **Kenya (Country ID: 1):** `payment_method_type` = `"send money"` (M-Pesa STK Push) or `"airtel"` (Airtel Money)
* **Ghana (Country ID: 16):** `payment_method_type` = `"mtn"`, `"At"`, or `"vodafone"`
* **Côte d'Ivoire (Country ID: 3):** `payment_method_type` = `"Orange"`, `"mtn"`, `"moov"`, or `"Wave"`
* **Senegal (Country ID: 9):** `payment_method_type` = `"Orange"`, `"free"`, or `"Wave"`
* **Nigeria (Country ID: 17):** `payment_method_type` = `"virtual_account"` (Dynamic Virtual Account)
* **South Africa (Country ID: 15):** `payment_method_type` = `"bank_eft"` (Bank EFT) or `"capitec"` (Capitec Pay)
Always check the individual [Country Guides](/supported-countries) for the exact method strings accepted in each market.
### Sample Request: Kenya M-Pesa STK Push (Country ID: 1)
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"salt": "random_salt_12345",
"signature": "c6b98e1f5d6a7890bc4e123456789abcdef0123456789abcdef0123456789abc",
"params": {
"amount": 1000,
"currency": "KES",
"country_id": 1,
"mobile": "254700000000",
"payment_method_type": "send money",
"callback_url": "https://yourdomain.com/niobi/callback",
"third_party_reference_1": "ORDER-991"
}
}
```
***
## 2. Dynamic Virtual Accounts (Nigeria - NGN)
For Nigerian collections, Niobi provides **dynamic virtual bank accounts** that expire within 30 minutes.
**Send Exact Amounts:**\
For NGN virtual account deposits, the expected amount is processed and any overpayment is automatically reversed to the customer upon successful deposit. Always instruct customers to transfer the exact amount.
### Required Parameters for Nigerian Virtual Accounts:
* `payment_method_type`: `"virtual_account"` or `"bank"`
* `first_name`: Customer's first name
* `last_name`: Customer's last name
* `account_name`: Customer's full bank account name
* `bank_code`: Code of the issuing bank:
* `17001`: 78 Finance Company Limited
* `17002`: Fidelity Bank Limited
* `17003`: Globus Bank
### Sample Request:
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"salt": "random_salt_12345",
"signature": "c6b98e1f5d6a7890bc4e123456789abcdef0123456789abcdef0123456789abc",
"params": {
"amount": 10000,
"currency": "NGN",
"country_id": 17,
"mobile": "2348012345678",
"first_name": "Joseph",
"last_name": "Mike",
"account_name": "Joseph Mike",
"bank_code": "17001",
"payment_method_type": "virtual_account",
"callback_url": "https://yourdomain.com/niobi/callback",
"third_party_reference_1": "NGN-COLL-001"
}
}
```
### Initial API Response (Returns Virtual Account Details):
```json theme={null}
{
"success": true,
"message": "Please use the following bank details for making a deposit via your preferred banking application",
"data": {
"account_name": "Joseph Mike",
"account_number": "7500123497",
"bank_name": "78 FINANCE COMPANY LIMITED",
"amount": 10000,
"currency": "NGN",
"country_id": 17,
"mobile": "2348012345678",
"depositId": "NIO-S12345678",
"status": "success",
"payment_step": 1,
"callback_url": "https://yourdomain.com/niobi/callback"
},
"status_code": "000000"
}
```
Display the returned `account_number` and `bank_name` to your customer to complete their transfer. See the [Nigeria Country Guide](/countries/nigeria) for full details.
***
## 3. South Africa Bank Transfers & Capitec (ZAR)
South African collections support both generic bank transfers (EFT) and direct Capitec authorization. There is no `"eft"` value, EFT collections use `payment_method_type: "bank_eft"`, shown below.
**`payment_method_type`**: `"bank_eft"`\
Returns an `authorization_url` where the customer approves the payment directly with their bank (ABSA, FNB, Standard Bank, Nedbank, Investec, Bidvest, TymeBank).
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"salt": "random_salt_12345",
"signature": "c6b98e1f5d6a7890bc4e123456789abcdef0123456789abcdef0123456789abc",
"params": {
"amount": 500,
"currency": "ZAR",
"country_id": 15,
"mobile": "27600000008",
"name": "John Doe",
"bank_code": "0001",
"payment_method_type": "bank_eft",
"successful_url": "https://yourdomain.com/payment-success",
"failure_url": "https://yourdomain.com/payment-failed",
"callback_url": "https://yourdomain.com/niobi/callback",
"third_party_reference_1": "ZAR-DEPOSIT-01"
}
}
```
**Initial Response:** Returns `"authorization_url": "https://..."` and `"payment_step": 1`. Redirect the customer to this URL.
**`payment_method_type`**: `"capitec"`\
Capitec Bank collections authenticate directly against the payer's national identity. You must pass the customer's full **13-digit South African ID number** in `params.id_number` (e.g. `"9001015800083"`) for the request to be valid.
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"salt": "random_salt_12345",
"signature": "c6b98e1f5d6a7890bc4e123456789abcdef0123456789abcdef0123456789abc",
"params": {
"amount": 500,
"currency": "ZAR",
"country_id": 15,
"mobile": "27600000008",
"name": "John Doe",
"id_number": "9001015800083",
"payment_method_type": "capitec",
"successful_url": "https://yourdomain.com/payment-success",
"failure_url": "https://yourdomain.com/payment-failed",
"callback_url": "https://yourdomain.com/niobi/callback",
"third_party_reference_1": "ZAR-CAPITEC-01"
}
}
```
***
## 4. Wave Collections (Senegal & Côte d'Ivoire)
Wave uses a redirect-based authorization flow, not a USSD push:
Submit the request with `successful_url` and `failure_url` included in the parameters.
Niobi returns an `authorization_url` (`payment_step: 1`). Redirect the customer to this URL. If they open it on their phone, it launches the Wave app directly to approve payment; if they open it on a desktop browser, the page displays a QR code they can scan with the Wave app on their phone to authorize instead.
Once the customer approves or cancels the payment in Wave, they're redirected to your `successful_url` or `failure_url` accordingly, and Niobi delivers the terminal webhook callback (`payment_step: 2`) to your `callback_url`.
### Sample Request (Senegal - XOF):
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"salt": "random_salt_12345",
"signature": "c6b98e1f5d6a7890bc4e123456789abcdef0123456789abcdef0123456789abc",
"params": {
"amount": 5000,
"currency": "XOF",
"country_id": 9,
"mobile": "221763456789",
"payment_method_type": "Wave",
"successful_url": "https://yourdomain.com/checkout/success",
"failure_url": "https://yourdomain.com/checkout/cancel",
"callback_url": "https://yourdomain.com/niobi/callback",
"third_party_reference_1": "WAVE-SN-001"
}
}
```
***
## 5. OTP-Verified Payins (Côte d'Ivoire Orange)
For Orange payins in Côte d’Ivoire, payment authorization requires a two-step OTP process:
### Step 1: Initiate Deposit
Submit the collection request to `POST /api/v4/niobi-unified-collections`. The customer receives an SMS OTP, and the response returns a `payment_token`:
```json theme={null}
{
"success": true,
"message": "We've sent a verification prompt to your phone. Please approve it to complete your payment.",
"data": {
"status": "success",
"amount": 2000,
"currency": "XOF",
"country_id": 3,
"payment_token": "NIO-S0000001",
"payment_step": 1
},
"status_code": "000000"
}
```
### Step 2: Verify the Transaction with OTP
Collect the OTP from the customer and send a signed request to `POST /api/v4/niobi-unified-process-transaction`:
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"salt": "random_salt_12345",
"signature": "3a3cf7f2e3ff8d4ee991d7e1323312887dfda8536b8c2e197d0d4ef764013658",
"params": {
"otp": "000000",
"payment_token": "NIO-S0000001"
}
}
```
* **OTP Validity:** In production, the authorization OTP is valid for **2 minutes**.
* **Sandbox Testing:** Use OTP `000000` to simulate successful verification when testing in the sandbox environment.
***
## Hosted Checkout & Payment Links
If you want to accept payments without building and maintaining a custom front-end payment interface, Niobi provides **Payment Links** (Hosted Checkout).
This is the fastest and easiest way to start collecting payments across Africa with zero UI code required on your part. For full architectural walkthroughs, dashboard setup, and API parameters, see the [Payment Links Guide](/collecting-payments/payment-links).
### Why Use Hosted Checkout?
* **Zero Front-End Engineering:** You do not need to build complex payment forms, input masks, or error-handling UI. Niobi hosts the entire checkout experience for you.
* **Multi-Currency & Country Support:** Payer details, country selection, and currency conversions are handled automatically on the page.
* **Flexible Distribution:** Share links over WhatsApp, SMS, invoices, emails, or redirect customers directly from your website or mobile app checkout button.
* **Instant Reconciliation:** When a payment is completed on the hosted checkout page, Niobi dispatches an asynchronous webhook callback to your server and credits your merchant wallet in real time.
### How Hosted Checkout Works:
* **Dashboard (No-Code):** Log into your Niobi Dashboard, navigate to **Payments -> Payment Links**, click **Create Payment Link**, enter your item name, currency, and amount (fixed or customer-entered), and copy your generated checkout URL immediately.
* **API (Programmatic):** Dynamically generate payment links on demand from your backend server using the [Create Payment Link API](/api-reference/payment-links/create) (`POST /api/v3/payment-link-api/create`). You can retrieve, search, and manage active links via the [Payment Link List API](/api-reference/payment-links/list) (`GET /api/payment-link-api/list`). Pass `first_name`, `last_name`, `email`, and `phone` in the request to pre-fill the payer's details on the checkout page, see [Create Payment Link](/api-reference/payment-links/create) for details.
Provide the generated checkout URL to your customer. When clicked, the customer is presented with the clean, branded Niobi checkout interface shown above.
The customer confirms their name, email, country, and currency (pre-filled automatically if you supplied them at creation), then chooses their preferred local payment channel (Mobile Money, Bank Transfer, or Card) to complete authorization.
Once the transaction settles, Niobi sends a terminal callback (`payment_step: 2`) to your configured webhook URL and automatically updates your transaction records.
***
## Next Steps
Follow these guides in order to complete your collection integration:
***
## Country-Specific Guides
For deep dives into local provider configurations, test phone numbers, and country limits, explore our country guides:
Virtual accounts (78 Finance, Fidelity, Globus), NGN limits, and sample callbacks.
Bank EFT, Capitec Pay, ZAR settlement, and redirect handling.
M-Pesa STK Push, Airtel Money, KES channels, and callbacks.
Wave browser redirect and Orange SMS shortcode authorization.
Wave redirect and Orange two-step OTP authorization.
Complete coverage matrix across all 29 supported African markets.
# Payment Links
Source: https://docs.niobi.co/collecting-payments/payment-links
Collect payments with zero front-end code using Niobi's hosted checkout. Learn the two ways to create a payment link, when to use each, and how the customer experience works end to end.
## Overview
Niobi gives you two ways to collect payments: integrate directly with the [Unified Collections API](/collecting-payments/basics) for full control over your own checkout UI, or generate a **Payment Link**, a Niobi-hosted checkout page you can share with a customer without writing any front-end code.
This page covers Payment Links, how to create one, and how the customer completes payment on it.
### Why Payment Links?
* **Zero front-end engineering:** Niobi hosts the entire checkout page, so you don't need to build payment forms, input masks, or error-handling UI.
* **Multi-currency & country support:** Payer details, country selection, and currency are handled on the page itself.
* **Flexible distribution:** Share the link over WhatsApp, SMS, invoices, email, or a website or app checkout button, or embed it as a QR code for a customer to scan in person.
* **Instant reconciliation:** When the customer completes payment, Niobi credits your merchant wallet and dispatches the same webhook callback (`payment_step: 2`) used across every other collection method.
***
## Two Ways to Create a Payment Link
A payment link can be created in one of two ways, depending on who is creating it and why:
```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 You as You (Dashboard or API)
participant Niobi as Niobi
participant Customer as Customer
alt Via Dashboard (no-code)
You->>Niobi: Payments -> Payment Links -> Create
else Via API (programmatic)
You->>Niobi: POST /api/v3/payment-link-api/create
end
Niobi-->>You: Checkout URL generated
You->>Customer: Share the link (WhatsApp, SMS, email, invoice, website, or QR code)
Customer->>Niobi: Opens the hosted checkout page
Customer->>Niobi: Selects payment method and completes payment
Niobi->>You: Webhook callback (payment_step: 2)
Note over You: Reconcile and credit the merchant wallet
```
Built for a person creating a link by hand: sales, support, or finance staff sending a one-off link to a specific customer, invoice, or order, with no engineering work involved.
Built for your own systems creating links automatically: for example, generating a unique link every time an order is placed on your platform, tied to your own order reference, with no manual step in between.
Neither method replaces the other. Which one you use depends on whether a **person** is triggering link creation, or a **system event** in your own backend is.
### Method 1: Via the Dashboard
Navigate to **Payments → Payment Links**.
Enter the item name, currency, and amount. The amount can be fixed, or left for the customer to enter themselves.
The link is ready to share immediately, no further setup required.
### Method 2: Via the API
Send a signed request to the [Create Payment Link API](/api-reference/payment-link/create-a-payment-link) (`POST /api/v3/payment-link-api/create`) from your backend.
Pass `first_name`, `last_name`, `email`, and `phone` at creation time so the checkout page loads with the customer's details already filled in, they only need to pick a payment method and confirm.
Use the [Payment Link List API](/api-reference/payment-link/payment-link-list) (`GET /api/payment-link-api/list`) to view, filter, or retrieve links you've created.
***
## How the Customer Completes Payment
Regardless of how the link was created, the customer experience and the settlement flow are identical:
Whether the customer clicked the link directly or scanned it as a QR code, they land on the same clean, branded Niobi checkout page shown above.
Their name, email, country, and currency are pre-filled automatically if you supplied them at creation. The customer then picks their preferred local payment channel (Mobile Money, Bank Transfer, or Card) and completes authorization.
Once the transaction settles, Niobi sends a terminal callback (`payment_step: 2`) to your configured webhook URL, the same shape you'd receive for a direct API collection. See [Collection Callbacks](/collecting-payments/callbacks) for the full payload and how to reconcile it.
***
## Next Steps
***
## Payment Flow: How It Works
### Payouts (Disbursements)
1. **Initiation:** Send a POST request to the [Unified Payments endpoint](/api-reference/disbursement/unified-disbursement) with `currency: "CAD"` and `payment_method_type: "bank"`.
2. **Recipient Bank Details:** Provide the recipient's bank account information, SWIFT/BIC code, and account name.
3. **Settlement & Callback:** Funds are routed via international wire transfer, and Niobi sends an asynchronous webhook callback (`payment_step: 2`, `status: "success"`) to your `client_callback_url` upon clearance.
***
## Sample Requests & Responses
### CAD Bank Payout Request
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"params": {
"amount": 1000,
"client_callback_url": "https://example.com/callback",
"currency": "CAD",
"first_name": "John",
"last_name": "Doe",
"payment_method_type": "bank",
"payment_reference": "YOUR-PAYOUT-REF",
"third_party_reference_1": "TX-CAD-DISB-1001",
"bank": [
{
"bank_name": "Royal Bank of Canada",
"bank_code": "ROYCCAT2",
"branch_name": "Toronto",
"account_name": "John Doe",
"account_number": "123456789"
}
]
},
"salt": "random_salt_12345",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"signature": "a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef0"
}
```
***
## Try it
Collections API reference for CAD deposits.
Payments API reference for CAD payouts.
***
## Transaction Limits
For the full list of minimum and maximum transaction limits for Canada (CAD) collections and payouts, please see the [Transaction Limits for Canada](/transaction-limits#canada) section on the Transaction Limits page.
# China | Supported Payment Methods & Corridors
Source: https://docs.niobi.co/countries/china
Supported collection and payout methods, request formats, and sample payloads for China on the Niobi API.
China
Supported payment methods and integration details for China
***
## Payment Flow: How It Works
### Payouts (Disbursements)
1. **Initiation:** Send a POST request to the [Unified Payments endpoint](/api-reference/disbursement/unified-disbursement) with `currency: "CNY"` and `payment_method_type: "cny_express"`.
2. **Recipient Bank Details:** Include the recipient's bank account information, CNAPS code, and account name.
3. **Settlement & Callback:** Funds are routed via CNAPS, and Niobi sends an asynchronous webhook callback (`payment_step: 2`, `status: "success"`) upon clearance.
***
## Sample Requests & Responses
### CNY Bank Payout Request
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"params": {
"amount": 5000,
"client_callback_url": "https://example.com/callback",
"currency": "CNY",
"first_name": "John",
"last_name": "Doe",
"payment_method_type": "cny_express",
"payment_reference": "YOUR-PAYOUT-REF",
"third_party_reference_1": "TX-CNY-DISB-1001",
"bank": [
{
"bank_name": "Industrial and Commercial Bank of China",
"bank_code": "ICBKCNBJ",
"branch_name": "Beijing",
"account_name": "John Doe",
"account_number": "6222021234567890"
}
]
},
"salt": "random_salt_12345",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"signature": "a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef0"
}
```
***
## Try it
Collections API reference for CNY deposits.
Payments API reference for CNY payouts.
***
## Transaction Limits
For the full list of minimum and maximum transaction limits for China (CNY) collections and payouts, please see the [Transaction Limits for China](/transaction-limits#china) section on the Transaction Limits page.
# Cote D'Ivoire | Supported Payment Methods & Corridors
Source: https://docs.niobi.co/countries/cote-d-ivoire
Supported collection and payout methods, request formats, and sample payloads for Cote D'Ivoire on the Niobi API.
Cote D'Ivoire
Supported payment methods and integration details for Cote D'Ivoire
Country Details
Country ID: 3 •
Country Code: 225 •
Currency: XOF (West African CFA Franc)
***
## Overview & Availability
Egypt payment rails are currently in development. To join the beta program or request early access for EGP collections and disbursements, please contact our team at [support@niobi.co](mailto:support@niobi.co).
***
## Transaction Limits
For the full list of minimum and maximum transaction limits for Egypt (EGP) collections and payouts, please see the [Transaction Limits for Egypt](/transaction-limits#egypt-coming-soon) section on the Transaction Limits page.
# Ethiopia | Supported Payment Methods & Corridors
Source: https://docs.niobi.co/countries/ethiopia
Supported collection and payout methods, request formats, and sample payloads for Ethiopia on the Niobi API.
Ethiopia
Coming Soon
Supported payment methods and integration details for Ethiopia
Country Details
Country Code: 251 •
Currency: ETB (Ethiopian Birr)
***
## Overview & Availability
Ethiopia payment integrations are currently in active development. If your business requires early access or custom routing for Ethiopian Birr (ETB), contact us at [support@niobi.co](mailto:support@niobi.co).
***
## Transaction Limits
For the full list of minimum and maximum transaction limits for Ethiopia (ETB) collections and payouts, please see the [Transaction Limits for Ethiopia](/transaction-limits#ethiopia-coming-soon) section on the Transaction Limits page.
# Europe | Supported Payment Methods & Corridors
Source: https://docs.niobi.co/countries/europe
Supported collection and payout methods, request formats, and sample payloads for Europe on the Niobi API.
Europe
Supported payment methods and integration details for Single Euro Payments Area (SEPA)
***
## Payment Flow: How It Works
### Payouts (Disbursements)
1. **Initiation:** Send a POST request to the [Unified Payments endpoint](/api-reference/disbursement/unified-disbursement) with `currency: "EUR"` and `payment_method_type: "bank"`.
2. **IBAN & BIC Details:** Provide the recipient's IBAN in `account_number` and BIC/SWIFT code in `bank_code`.
3. **Settlement & Callback:** Funds are routed via SEPA Credit Transfer, and Niobi delivers an asynchronous webhook callback (`payment_step: 2`, `status: "success"`) upon execution.
***
## Sample Requests & Responses
### EUR SEPA Payout Request
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"params": {
"amount": 1000,
"client_callback_url": "https://example.com/callback",
"currency": "EUR",
"first_name": "John",
"last_name": "Doe",
"payment_method_type": "bank",
"payment_reference": "YOUR-PAYOUT-REF",
"third_party_reference_1": "TX-EUR-DISB-1001",
"bank": [
{
"bank_name": "BNP Paribas",
"bank_code": "BNPAFRPP",
"branch_name": "Paris",
"account_name": "John Doe",
"account_number": "FR7630006000011234567890189"
}
]
},
"salt": "random_salt_12345",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"signature": "a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef0"
}
```
***
## Try it
Collections API reference for EUR deposits.
Payments API reference for EUR payouts.
***
## Transaction Limits
For the full list of minimum and maximum transaction limits for Europe (EUR) collections and payouts, please see the [Transaction Limits for Europe](/transaction-limits#europe) section on the Transaction Limits page.
# Ghana | Supported Payment Methods & Corridors
Source: https://docs.niobi.co/countries/ghana
Supported collection and payout methods, request formats, and sample payloads for Ghana on the Niobi API.
Ghana
Supported payment methods and integration details for Ghana
Country Details
Country ID: 16 •
Country Code: 233 •
Currency: GHS (Ghanaian Cedi)
***
## Overview & Availability
Malawi mobile money integrations are currently in development. If your business requires early access or pilot testing for Malawian Kwacha (MWK), reach out to us at [support@niobi.co](mailto:support@niobi.co).
***
## Transaction Limits
For the full list of minimum and maximum transaction limits for Malawi (MWK) collections and payouts, please see the [Transaction Limits for Malawi](/transaction-limits#malawi-coming-soon) section on the Transaction Limits page.
# Mali | Supported Payment Methods & Corridors
Source: https://docs.niobi.co/countries/mali
Supported collection and payout methods, request formats, and sample payloads for Mali on the Niobi API.
Mali
Supported payment methods and integration details for Mali
Country Details
Country ID: 18 •
Country Code: 223 •
Currency: XOF (West African CFA Franc)
***
## Overview & Availability
Mozambique payment channels are currently under integration. For updates or early enterprise boarding for Mozambican Metical (MZN), contact [support@niobi.co](mailto:support@niobi.co).
***
## Transaction Limits
For the full list of minimum and maximum transaction limits for Mozambique (MZN) collections and payouts, please see the [Transaction Limits for Mozambique](/transaction-limits#mozambique-coming-soon) section on the Transaction Limits page.
# Nigeria | Supported Payment Methods & Corridors
Source: https://docs.niobi.co/countries/nigeria
Supported collection and payout methods, request formats, and sample payloads for Nigeria on the Niobi API.
Nigeria
Supported payment methods and integration details for Nigeria
Country Details
Country ID: 17 •
Country Code: 234 •
Currency: NGN (Nigerian Naira)
***
## Payment Flow: How It Works
### Payouts (Disbursements)
1. **Initiation:** Submit a POST request to the [Unified Payments endpoint](/api-reference/disbursement/unified-disbursement) with `currency: "PKR"` and `payment_method_type: "bank"`.
2. **Recipient Bank Details:** Provide the recipient's bank account or IBAN in `account_number`, bank code in `bank_code`, and account name in `account_name`.
3. **Settlement & Callback:** Funds are processed via 1LINK/Raast network, and Niobi delivers an asynchronous webhook callback (`payment_step: 2`, `status: "success"`) upon settlement.
***
## Sample Requests & Responses
### PKR Bank Payout Request
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"params": {
"amount": 15000,
"client_callback_url": "https://example.com/callback",
"currency": "PKR",
"first_name": "John",
"last_name": "Doe",
"payment_method_type": "bank",
"payment_reference": "YOUR-PAYOUT-REF",
"third_party_reference_1": "TX-PKR-DISB-1001",
"bank": [
{
"bank_name": "Standard Chartered Bank Pakistan",
"bank_code": "SCBLPKKA",
"branch_name": "Karachi",
"account_name": "John Doe",
"account_number": "PK36SCBL0000001123456701"
}
]
},
"salt": "random_salt_12345",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"signature": "a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef0"
}
```
***
## Try it
Collections API reference for PKR deposits.
Payments API reference for PKR payouts.
***
## Transaction Limits
For the full list of minimum and maximum transaction limits for Pakistan (PKR) collections and payouts, please see the [Transaction Limits for Pakistan](/transaction-limits#pakistan) section on the Transaction Limits page.
# Rwanda | Supported Payment Methods & Corridors
Source: https://docs.niobi.co/countries/rwanda
Supported collection and payout methods, request formats, and sample payloads for Rwanda on the Niobi API.
Rwanda
Supported payment methods and integration details for Rwanda
Country Details
Country ID: 8 •
Country Code: 250 •
Currency: RWF (Rwandan Franc)
***
## Payment Flow: How It Works
### Deposits (Collections)
#### 1. EFT Bank Collections (`payment_method_type: "bank_eft"`)
1. **Initiation:** Send a POST request to the [Unified Collections endpoint](/api-reference/collections/unified-collections) with `payment_method_type: "bank_eft"`, `country_id: 15`, `currency: "ZAR"`, `bank_code`, `successful_url`, and `failure_url`.
2. **Customer Redirection:** Niobi returns an `authorization_url` in the initial response (`payment_step: 1`). Redirect the customer to this URL to log into their bank portal and approve the transfer. This URL is single-use and expires once opened, do not open it yourself to preview or test it, only the customer should open it, and only once they're ready to pay. If it expires unused, you'll need to initiate a new request to get a fresh one.
3. **Completion & Callback:** Once the customer approves or cancels the transfer on their bank's portal, they're redirected to your `successful_url` or `failure_url` accordingly, and Niobi delivers an asynchronous webhook callback (`payment_step: 2`) to your `callback_url`.
#### Supported South African Banks for EFT Collections:
* ABSA (`0001`)
* First National Bank (FNB) (`0002`)
* Investec (`0003`)
* Nedbank (`0004`)
* Standard Bank (`0005`)
* Bidvest Bank (`0006`)
* TymeBank (`0007`)
***
#### 2. Capitec Collections (`payment_method_type: "capitec"`)
1. **Initiation:** Send a POST request to the [Unified Collections endpoint](/api-reference/collections/unified-collections) with `payment_method_type: "capitec"` and the customer's 13-digit South African ID number in `id_number`.
2. **In-App Approval:** The customer receives a prompt on their Capitec banking app to approve the debit.
3. **Callback Delivery:** Niobi delivers an asynchronous webhook callback (`payment_step: 2`) to your `callback_url`.
***
### Payouts (Disbursements)
1. **Initiation:** Send a POST request to the [Unified Payments endpoint](/api-reference/disbursement/unified-disbursement) with `payment_method_type: "bank"`.
2. **Recipient Details:** Include the recipient's bank name, bank code, branch name, account name, and account number.
3. **Settlement:** Funds are routed via Real Time Clearing (RTC) or Electronic Funds Transfer (EFT).
4. **Callback Delivery:** Niobi delivers a terminal webhook callback (`payment_step: 2`) once funds clear.
***
## Sample Requests & Responses
### EFT Deposit Request
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"params": {
"amount": 500,
"bank_code": "0001",
"callback_url": "https://example.com/callback",
"country_id": 15,
"currency": "ZAR",
"failure_url": "https://example.com/payment-failed",
"mobile": "2763456789",
"name": "John Doe",
"payment_method_type": "bank_eft",
"successful_url": "https://example.com/payment-success",
"third_party_reference_1": "TX-ZAR-DEP-1001",
"third_party_reference_2": "CUST-301"
},
"salt": "random_salt_12345",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"signature": "a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef0"
}
```
### EFT Deposit Response (Initiation)
```json theme={null}
{
"success": true,
"message": "Payment process initiated successfully.",
"data": {
"amount": 500,
"authorization_url": "https://auth.bank-gateway.example.com/session/12345",
"currency": "ZAR",
"depositId": "dep_12345678",
"payment_step": 1,
"status": "success",
"third_party_reference_1": "TX-ZAR-DEP-1001"
},
"status_code": "000000"
}
```
Redirect the user to `authorization_url` to complete authentication with their bank.
***
### Capitec Deposit Request
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"params": {
"amount": 500,
"callback_url": "https://example.com/callback",
"country_id": 15,
"currency": "ZAR",
"failure_url": "https://example.com/payment-failed",
"id_number": "9001015800083",
"mobile": "27600000000",
"name": "John Doe",
"payment_method_type": "capitec",
"successful_url": "https://example.com/payment-success",
"third_party_reference_1": "TX-CAP-DEP-1002",
"third_party_reference_2": "CUST-302"
},
"salt": "random_salt_12345",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"signature": "a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef0"
}
```
***
### Payout Request (Bank Transfer)
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"params": {
"amount": 1000,
"client_callback_url": "https://example.com/callback",
"country_id": 15,
"currency": "ZAR",
"email": "customer@example.com",
"first_name": "John",
"last_name": "Doe",
"mobile": "2768856789",
"payment_method_type": "bank",
"payment_reference": "YOUR-PAYOUT-REF",
"third_party_reference_1": "TX-ZAR-DISB-2001",
"bank": [
{
"bank_name": "ABSA",
"bank_code": "0001",
"branch_name": "Sandton",
"account_name": "John Doe",
"account_number": "4081234567"
}
]
},
"salt": "random_salt_12345",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"signature": "a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef0"
}
```
***
## Webhook/Callback Examples
### Successful Deposit Callback
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"salt": "random_salt_12345",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"signature": "a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef0",
"params": {
"amount": 500,
"currency": "ZAR",
"depositId": "dep_12345678",
"email": "customer@example.com",
"mobile": "2763456789",
"name": "John Doe",
"payment_step": 2,
"status": "success",
"status_code": "000000",
"third_party_reference_1": "TX-ZAR-DEP-1001",
"third_party_reference_2": "CUST-301"
}
}
```
### Successful Payout Callback
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"salt": "random_salt_12345",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"signature": "a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef0",
"params": {
"account": "4081234567",
"amount": 1000,
"currency": "ZAR",
"fee": 10,
"message": "Payment was transferred successfully.",
"mobile": "2768856789",
"payee": "John Doe",
"payment_channel": "EFT",
"payment_reference": "YOUR-PAYOUT-REF",
"payment_step": 2,
"payoutId": "bd8f51a2-xxxx-xxxx-xxxx-xxxxx9a",
"ref": "ref_98765432",
"status": "success",
"statusCode": "000000",
"third_party_reference_1": "TX-ZAR-DISB-2001",
"third_party_reference_2": null,
"transaction_date": "2026-08-28 12:30:00",
"transaction_detail": "Disbursement made through EFT",
"type": "Pay out"
}
}
```
***
## Try it
Collections API reference for South Africa deposits.
Payments API reference for South Africa payouts.
***
## Transaction Limits
For the full list of minimum and maximum transaction limits for South Africa (ZAR) collections and payouts, please see the [Transaction Limits for South Africa](/transaction-limits#south-africa) section on the Transaction Limits page.
# Stablecoins | Supported Payment Methods & Corridors
Source: https://docs.niobi.co/countries/stablecoins
Supported stablecoin payment methods, request formats, and sample payloads for Stablecoins on the Niobi API.
Stablecoins
Supported blockchain networks and settlement details for USDT and USDC
***
## Payment Flow: How It Works
### Payouts & Collections
1. **Activation:** Contact [support@niobi.co](mailto:support@niobi.co) to enable stablecoin settlement and configure your designated deposit and payout blockchain addresses.
2. **Execution:** Initiate requests specifying the on-chain destination wallet address and chosen network.
3. **Confirmation:** Once required network confirmations are reached on-chain, Niobi delivers an asynchronous webhook callback (`payment_step: 2`, `status: "success"`).
***
## Try it
Collections API reference for digital currency.
Payments API reference for digital currency payouts.
***
## Transaction Limits
For the full list of minimum and maximum transaction limits for Stablecoins (USDT/USDC) collections and payouts, please see the [Transaction Limits for Stablecoins](/transaction-limits#stablecoins) section on the Transaction Limits page.
# Tanzania | Supported Payment Methods & Corridors
Source: https://docs.niobi.co/countries/tanzania
Supported collection and payout methods, request formats, and sample payloads for Tanzania on the Niobi API.
Tanzania
Supported payment methods and integration details for Tanzania
Country Details
Country ID: 10 •
Country Code: 255 •
Currency: TZS (Tanzanian Shilling)
***
## Payment Flow: How It Works
### Payouts (Disbursements)
1. **Initiation:** Submit a POST request to the [Unified Payments endpoint](/api-reference/disbursement/unified-disbursement) with `currency: "AED"` and `payment_method_type: "bank"`.
2. **Recipient Details:** Include the recipient's UAE IBAN in `account_number`, bank SWIFT code in `bank_code`, and account name in `account_name`.
3. **Settlement & Callback:** Funds are routed via the UAE Funds Transfer System (UAEFTS) or SWIFT wire, and Niobi sends an asynchronous webhook callback (`payment_step: 2`, `status: "success"`) upon settlement.
***
## Sample Requests & Responses
### AED Bank Payout Request
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"params": {
"amount": 2500,
"client_callback_url": "https://example.com/callback",
"currency": "AED",
"first_name": "John",
"last_name": "Doe",
"payment_method_type": "bank",
"payment_reference": "YOUR-PAYOUT-REF",
"third_party_reference_1": "TX-AED-DISB-1001",
"bank": [
{
"bank_name": "Emirates NBD",
"bank_code": "EBILAEAD",
"branch_name": "Dubai",
"account_name": "John Doe",
"account_number": "AE070331234567890123456"
}
]
},
"salt": "random_salt_12345",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"signature": "a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef0"
}
```
***
## Try it
Collections API reference for AED deposits.
Payments API reference for AED payouts.
***
## Transaction Limits
For the full list of minimum and maximum transaction limits for UAE (AED) collections and payouts, please see the [Transaction Limits for UAE](/transaction-limits#uae) section on the Transaction Limits page.
# Uganda | Supported Payment Methods & Corridors
Source: https://docs.niobi.co/countries/uganda
Supported collection and payout methods, request formats, and sample payloads for Uganda on the Niobi API.
Uganda
Supported payment methods and integration details for Uganda
Country Details
Country ID: 11 •
Country Code: 256 •
Currency: UGX (Ugandan Shilling)
***
## Payment Flow: How It Works
### Payouts (Disbursements)
1. **Initiation:** Send a POST request to the [Unified Payments endpoint](/api-reference/disbursement/unified-disbursement) with `currency: "GBP"` and `payment_method_type: "bank"`.
2. **Recipient Bank Details:** Include the UK Sort Code (in `bank_code`), 8-digit Account Number (in `account_number`), and account name. Alternatively, provide the full UK IBAN.
3. **Settlement & Callback:** Funds are routed via Faster Payments System (FPS) or CHAPS, and Niobi sends an asynchronous webhook callback (`payment_step: 2`, `status: "success"`) upon settlement.
***
## Sample Requests & Responses
### GBP Bank Payout Request
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"params": {
"amount": 500,
"client_callback_url": "https://example.com/callback",
"currency": "GBP",
"first_name": "John",
"last_name": "Doe",
"payment_method_type": "bank",
"payment_reference": "YOUR-PAYOUT-REF",
"third_party_reference_1": "TX-GBP-DISB-1001",
"bank": [
{
"bank_name": "Barclays Bank",
"bank_code": "200000",
"branch_name": "London",
"account_name": "John Doe",
"account_number": "12345678"
}
]
},
"salt": "random_salt_12345",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"signature": "a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef0"
}
```
***
## Try it
Collections API reference for GBP deposits.
Payments API reference for GBP payouts.
***
## Transaction Limits
For the full list of minimum and maximum transaction limits for United Kingdom (GBP) collections and payouts, please see the [Transaction Limits for United Kingdom](/transaction-limits#united-kingdom) section on the Transaction Limits page.
# United States | Supported Payment Methods & Corridors
Source: https://docs.niobi.co/countries/united-states
Supported collection and payout methods, request formats, and sample payloads for United States on the Niobi API.
United States
Supported payment methods and integration details for United States
Country Details
Country Code: 1 •
Currency: USD (United States Dollar)
***
## Payment Flow: How It Works
### Payouts (Disbursements)
1. **Initiation:** Send a POST request to the [Unified Payments endpoint](/api-reference/disbursement/unified-disbursement) with `currency: "USD"` and `payment_method_type: "bank"`.
2. **Recipient Bank Details:** Provide the recipient's 9-digit ABA Routing Number in `bank_code`, Account Number in `account_number`, and full account name in `account_name`.
3. **Settlement & Callback:** Funds are routed via Fedwire, ACH, or SWIFT, and Niobi sends an asynchronous webhook callback (`payment_step: 2`, `status: "success"`) upon execution.
***
## Sample Requests & Responses
### USD Bank Payout Request
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"params": {
"amount": 1000,
"client_callback_url": "https://example.com/callback",
"currency": "USD",
"first_name": "John",
"last_name": "Doe",
"payment_method_type": "bank",
"payment_reference": "YOUR-PAYOUT-REF",
"third_party_reference_1": "TX-USD-DISB-1001",
"bank": [
{
"bank_name": "JPMorgan Chase",
"bank_code": "021000021",
"branch_name": "New York",
"account_name": "John Doe",
"account_number": "1234567890"
}
]
},
"salt": "random_salt_12345",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"signature": "a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef0"
}
```
***
## Try it
Collections API reference for USD deposits.
Payments API reference for USD payouts.
***
## Transaction Limits
For the full list of minimum and maximum transaction limits for United States (USD) collections and payouts, please see the [Transaction Limits for United States](/transaction-limits#united-states) section on the Transaction Limits page.
# Zambia | Supported Payment Methods & Corridors
Source: https://docs.niobi.co/countries/zambia
Supported collection and payout methods, request formats, and sample payloads for Zambia on the Niobi API.
Zambia
Supported payment methods and integration details for Zambia
Country Details
Country ID: 12 •
Country Code: 260 •
Currency: ZMW (Zambian Kwacha)
***
## Payment Flow: How It Works
### Deposits (Collections)
1. **Initiation:** Submit a signed POST request to the [Unified Collections endpoint](/api-reference/collections/unified-collections) with `country_id: 12`, `currency: "ZMW"`, `amount`, `payment_method_type` (`"mtn"`, `"zamtel"`, or `"airtel"`), and the customer's phone number in `mobile` (format: `260XXXXXXXXX`).
2. **Handset Authorization:** The customer receives a prompt on their handset to enter their PIN and authorize the debit.
3. **Initial Response:** Niobi returns an acknowledgement (`payment_step: 1`) containing the assigned `depositId`.
4. **Callback Confirmation:** Once authorized, Niobi delivers an asynchronous webhook callback (`payment_step: 2`, `status: "success"`) to your `callback_url`.
***
### Payouts (Disbursements)
When initiating a payout to Zambia via the [Unified Payments endpoint](/api-reference/disbursement/unified-disbursement), pass the appropriate `payment_method_type` and its corresponding parameters:
* **MTN MoMo:** Pass `payment_method_type: "mtn"` with an `mtn` array containing `phone_number`.
* **Zamtel Kwacha:** Pass `payment_method_type: "Zamtel"` with a `Zamtel` array containing `phone_number`.
* **Airtel Money:** Pass `payment_method_type: "Airtel"` with an `Airtel` array containing `phone_number`.
***
## Sample Requests & Responses
### MTN MoMo Deposit Request
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"params": {
"amount": 250,
"callback_url": "https://example.com/callback",
"country_id": 12,
"currency": "ZMW",
"email": "customer@example.com",
"first_name": "John",
"last_name": "Doe",
"mobile": "260873039649",
"payment_method_type": "mtn",
"third_party_reference_1": "TX-ZMB-DEP-1001",
"third_party_reference_2": "CUST-1201"
},
"salt": "random_salt_12345",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"signature": "a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef0"
}
```
### Deposit Response (Initiation)
```json theme={null}
{
"success": true,
"message": "Payment process initiated successfully.",
"data": {
"account_name": "John Doe",
"amount": 250,
"callback_url": "https://example.com/callback",
"country_id": 12,
"currency": "ZMW",
"depositId": "dep_12345678",
"mobile": "260873039649",
"payment_method_type": "mtn",
"payment_step": 1,
"status": "success",
"third_party_reference_1": "TX-ZMB-DEP-1001"
},
"status_code": "000000"
}
```
***
### Payout Request (MTN MoMo)
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"params": {
"amount": 100,
"client_callback_url": "https://example.com/callback",
"country_id": 12,
"currency": "ZMW",
"first_name": "John",
"last_name": "Doe",
"mobile": "260771312649",
"payment_method_type": "mtn",
"payment_reference": "YOUR-PAYOUT-REF",
"third_party_reference_1": "TX-ZMB-DISB-2001",
"mtn": [
{
"phone_number": "260771312649"
}
]
},
"salt": "random_salt_12345",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"signature": "a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef0"
}
```
### Payout Response (Initiation)
```json theme={null}
{
"success": true,
"message": "Payment process initiated successfully.",
"data": {
"creation_date": "2026-08-28T12:30:00.000000Z",
"from": "YOUR_INTEGRATION_NAME",
"payment_step": 1,
"reference": "ref_98765432",
"to": "260771312649",
"total": 100,
"type": "MTN MoMo"
},
"status_code": "000000"
}
```
***
## Webhook/Callback Examples
### Successful Deposit Callback
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"salt": "random_salt_12345",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"signature": "a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef0",
"params": {
"amount": 250,
"currency": "ZMW",
"depositId": "dep_12345678",
"email": "customer@example.com",
"mobile": "260873039649",
"name": "John Doe",
"payment_step": 2,
"status": "success",
"status_code": "000000",
"third_party_reference_1": "TX-ZMB-DEP-1001",
"third_party_reference_2": "CUST-1201"
}
}
```
### Successful Payout Callback
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"salt": "random_salt_12345",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"signature": "a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef0",
"params": {
"account": "260771312649",
"amount": 100,
"currency": "ZMW",
"fee": 2,
"message": "Payment was transferred successfully.",
"mobile": "260771312649",
"payee": "John Doe",
"payment_channel": "MTN MoMo",
"payment_reference": "YOUR-PAYOUT-REF",
"payment_step": 2,
"payoutId": "bd8f51a2-xxxx-xxxx-xxxx-xxxxx9a",
"ref": "ref_98765432",
"status": "success",
"statusCode": "000000",
"third_party_reference_1": "TX-ZMB-DISB-2001",
"third_party_reference_2": null,
"transaction_date": "2026-08-28 12:30:00",
"transaction_detail": "Disbursement made through MTN MoMo",
"type": "Pay out"
}
}
```
***
## Try it
Collections API reference for Zambia deposits.
Payments API reference for Zambia payouts.
***
## Transaction Limits
For the full list of minimum and maximum transaction limits for Zambia (ZMW) collections and payouts, please see the [Transaction Limits for Zambia](/transaction-limits#zambia) section on the Transaction Limits page.
# Authentication Errors
Source: https://docs.niobi.co/errors/authentication-errors
This section describes a list of errors related to security credentials while accessing the api endpoints.
| Error Code | Error Message | Action Required |
| ---------- | ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| 403 | [You are restricted to access the site. Contact to Niobi support for further information.](/errors/403/restricted-access) | Your ip address is not yet whitelisted with us. Contact us with a list of your ips for whitelisting at [support@niobi.co](mailto:support@niobi.co) |
| 403 | [Request was not verified](/errors/403/request-not-verified) | Make sure you are using the correct payload and signature which were previously signed |
# Client Errors
Source: https://docs.niobi.co/errors/client-errors
This section describes a list of errors caused by issues in the client's request, such as invalid data, missing fields, or insufficient permissions.
| Error Code | Error Message | Action Required |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 400 | [Account details are missing.](/errors/400/account-details-missing) | Account details for the payment method are missing. Ensure that you have provided the necessary account details for the specified payment method. |
| 400 | [Country was not found](/errors/400/country-not-found) | The country id used is incorrect or missing. Make sure you are using the correct country id as listed in the [supported countries section](/supported-countries) |
| 400 | [Niobi user not found.](/errors/400/niobi-user-not-found) | User or api credentials do not exist. Ensure the integration being used is active and that a valid user is linked to it. |
| 400 | [Payment method type is wrong or missing](/errors/400/payment-method-type-missing) | The payment method in the API request is invalid or missing. Make sure you are using one of the supported payment methods found in the [collections](/api-reference/collections/unified-collections) or [disbursements](/api-reference/disbursement/unified-disbursement) page |
| 400 | [Your account has insufficient balance to complete this transaction. Please top up the account and try again.](/errors/400/insufficient-balance) | There are insufficient funds in your currency wallet. Top up your account to complete the transaction. |
| 400 | [Your transaction amount exceeded the limit. Please input an amount below \{amount}.](/errors/400/transaction-amount-exceeded-limit) | The transaction limit has been exceeded. Reduce the transaction amount to a value below the maximum limit as listed [here](/transaction-limits) |
| 400 | [Your transaction amount is not sufficient.](/errors/400/insufficient-transaction-amount) | The transaction amount is lower than the minimum transaction limit. Ensure the amount is above the minimum limit supported by a specific country or payment method as listed [here](/transaction-limits) |
| 403 | [Client id not matched](/errors/403/client-id-not-matched) | The client ID is incorrect. Ensure you are using the correct client key for the integration, which is generated when creating API credentials. Please refer to [API Credentials Section](/getting-credentials) for more information on generating credentials. |
| 403 | [Entity integration record not found](/errors/403/entity-record-not-found) | The integration name is invalid. Verify that the sender value used in signing the request matches the API integration title created. Please refer to [API Credentials Section](/getting-credentials) for more information on generating credentials. |
# HTTP Status Codes Overview
Source: https://docs.niobi.co/errors/overview
This page provides an overview of all HTTP status codes supported by our API, categorized by their general meanings. Each status code includes a brief description and a link to a detailed page for more information.
## Success Status Codes
This indicates that the request was successfully received, understood, and processed by the server.
* **200 OK**\
The request was successful, and the server returned the requested data.\
[View Details](/errors/200-success)
## Client Errors Status Codes
These codes indicate that there was an issue with the request, such as incorrect parameters or missing information.
### 400 Bad Request
The request could not be understood due to malformed syntax.
* [400 - Account details are missing.](/errors/400/account-details-missing)
* [400 - Country was not found](/errors/400/country-not-found)
* [400 - Niobi user not found.](/errors/400/niobi-user-not-found)
* [400 - Payment method type is wrong or missing](/errors/400/payment-method-type-missing)
* [400 - Your account has insufficient balance to complete this transaction. Please top up the account and try again.](/errors/400/insufficient-balance)
* [400 - Your transaction amount exceeded the limit. Please input an amount below \{amount}.](/errors/400/transaction-amount-exceeded-limit)
* [400 - Your transaction amount is not sufficient.](/errors/400/insufficient-transaction-amount)
### 403 Forbidden
The server understood the request but refuses to authorize it.
* [403 - Client id not matched](/errors/403/client-id-not-matched)
* [403 - Entity integration record not found](/errors/403/entity-record-not-found)
## Authentication Errors Status Codes
These status codes are for errors related to security credentials while accessing the api endpoints.
* [403 - Restricted access.](/errors/403/restricted-access)
* [403 - Request was not verified](/errors/403/request-not-verified)
## Transaction Status Codes
These codes indicate the transaction statuses returned after a transaction has been initiated.
* [000000 - Success](/errors/000000-success)
* [000001 - Pending](/errors/000001-pending)
* [000005 - Failed](/errors/000005-failed)
* [000006 - Timeout](/errors/000006-timeout)
## Transaction Errors
Transaction failure codes are errors that are returned by the provider when a transaction fails.
* [400 - Amount Exceeds Maximum Limit](/errors/400/amount-exceeds-maximum-limit)
* [400 - Top Up Funds](/errors/400/top-up-funds)
* [400 - Invalid Payment Details](/errors/400/invalid-payment-details)
* [400 - The payment is still being processed.](/errors/400/payment-still-processing)
* [400 - A similar payment is still being processed](/errors/400/similar-payment-being-processed)
* [400 - Insufficient Access Rights](/errors/400/no-access-rights)
* [400 - Wait for some time and try again later](/errors/400/wait-for-some-time)
* [400 - We apologise as we were not able to process your payment request.](/errors/400/error-processing-payment)
## Server Errors
These codes indicate that the server failed to process a valid request due to internal issues.
### 429 Too Many Requests
The client has sent too many requests in a given time period and has exceeded the API rate limit.
* [429 - Too Many Requests](/errors/429/too-many-requests)
### 500 Internal Server Error
The server encountered an unexpected condition that prevented it from fulfilling the request.
* [500 - Server Error](/errors/500/server-error)
## Next Steps
Click on any of the status codes above to view detailed information about the code, its causes, and troubleshooting steps.
# Server Errors
Source: https://docs.niobi.co/errors/server-errors
This section describes a list of errors caused by issues on the server-side.
| Error Code | Error Message | Action Required |
| ---------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 429 | [Too Many Requests](/errors/429/too-many-requests) | The client has sent too many requests in a given time exceeding the given api rate limit. Reduce the number of requests or consult with our support team for further assistance at [support@niobi.co](mailto:support@niobi.co) |
# Transaction-Related Errors
Source: https://docs.niobi.co/errors/transaction-errors
This section describes a list of errors that are returned by the provider when a transaction fails.
| ERROR Code | Error Message | Action Required |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400 | [This is more than maximum transaction value we allow in one transaction](/errors/400/amount-exceeds-maximum-limit) | The transaction amount exceeds the maximum allowable value. Adjust the amount and retry. |
| 400 | [Please add funds to your account to cover this payment.](/errors/400/top-up-funds) | Top-up your wallet to be able to process the transaction. |
| 400 | [One of the payment details entered is invalid. Please confirm again.](/errors/400/invalid-payment-details) | Check if you have added all the valid payment information. |
| 400 | [The payment is still being processed. We request for your patience as we work to complete the process. Once completed, we will send an email notification.](/errors/400/payment-still-processing) | Please wait patiently or contact us in case it is taking too long. |
| 400 | [A similar payment is still being processed. Please confirm to avoid duplicating the payment.](/errors/400/similar-payment-being-processed) | A similar transaction is still being processed. If it was not intentional, you can just ignore this. However, if you want to perform the same transaction you can make the request again. |
| 400 | [Your account does not have the rights to complete this payment. Please contact us for assistance.](/errors/400/no-access-rights) | This occurs when you have not fully submitted all the required KYC or the account is not verified. Please contact us for assistance. |
| 400 | [Wait for some time and try again later](/errors/400/wait-for-some-time) | This might happen because of our providers downtimes. Please contact us for further support |
| 400 | [We apologise as we were not able to process your payment request. Please try again later.](/errors/400/error-processing-payment) | Please contact us. |
# Transaction Status Codes
Source: https://docs.niobi.co/errors/transaction-status-codes
This section describes the transaction status codes you will expect once a transaction has been processed.
| Status Code | Description | Action Required |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [000000](/errors/000000-success) | The transaction was successfully completed. | No action required |
| [000001](/errors/000001-pending) | The transaction is pending and is still being processed. We request your patience as we work to complete the process. Once completed, we will send an email notification. | Please wait patiently or contact us in case it is taking too long. |
| [000005](/errors/000005-failed) | The transaction failed. This could be due to the errors described above. | Check if you have added all the valid payment information and contact us if it persists |
| [000006](/errors/000006-timeout) | The transaction timed out. The transaction time was more than the 2-minute timeout set on our end. Once completed outside the 2-minute window, it will be changed to success or failed. | Query transaction status after a few minutes to check if the status changed to success or failed. If not, please contact support for further assistance. |
# FAQs
Source: https://docs.niobi.co/faqs
## Getting Started
**1. How do I set up my API credentials?**
Log in to the payment portal and navigate to **Workspace -> API Keys**. Select **Create New Integration** and proceed to generate the credentials. Full guide: [Getting Your API Credentials](/getting-credentials).
**2. What countries are available to me?**
All countries listed in [Supported Countries](/supported-countries) are available in the sandbox environment with the supported methods. Only the countries you select during onboarding are enabled in production.
**3. How do I access test funds in the sandbox environment?**
Request a top-up of the specific currencies you want to test by emailing [support@niobi.co](mailto:support@niobi.co).
**4. Can I reuse the same signature across multiple requests?**
No. Every request must carry its own uniquely generated signature, computed from that request's exact timestamp, salt, and parameters. Signatures cannot be reused, even for retries of the same logical action. See [Authentication Basics](/authentication/basics).
## Errors & Status Codes
**5. What does status code "000005" mean, and what should I check first?**
`000005` means the request was rejected or the transaction failed. In sandbox, first confirm you're using a valid test phone number for that country ([Testing Guide](/testing)), and that your `country_id`, `currency`, and `payment_method_type` are all correct for that channel.
**6. Does a validation error mean my transaction actually failed?**
No, these are two different things. A validation error (for example, a missing required field) means Niobi rejected the request before any transaction was created, so there's nothing to reconcile. A real transaction failure is a terminal callback with `payment_step: 2`, `status: "failed"`, and `status_code: "000005"`. See [Understanding Payment Steps](/collecting-payments/basics#understanding-payment-steps-payment_step). Treat these as categorically different: one means "fix your request," the other means "the transaction ran and didn't succeed."
**7. What do the transaction status codes 000000 / 000001 / 000005 / 000006 mean, and which ones are final?**
`000000` (success) and `000005` (failed) are terminal, treat them as the final word. `000001` (pending) and `000006` (timeout) are not terminal, so don't act on them, just wait or re-query. Full breakdown: [Status Codes & Action Matrix](/transaction-status#status-codes--action-matrix).
**8. Why do some responses use `status_code` and others `statusCode`?**
It depends on where you're reading the response from, not random inconsistency. `status_code` (snake\_case) appears at the root of direct, synchronous API responses: the initial response you get back right after submitting a collection or payout, and the response from querying [transaction status](/transaction-status). `statusCode` (camelCase) appears only inside `params` in asynchronous webhook callbacks (`payment_step: 2` notifications). If you're parsing a webhook payload, check `params.statusCode`. If you're parsing a direct API response, check `status_code`.
**9. What do I do when I encounter "You are restricted to access the site. Contact Niobi support for further information."?**
Your IP address hasn't been whitelisted yet. If your account is already approved, you can whitelist your own IPs directly in the portal under **Workspace -> IP Whitelisting -> Add New**, no need to contact support. If your account hasn't been verified/approved yet, email your sandbox and production IPs to [support@niobi.co](mailto:support@niobi.co) instead. Full guide: [IP Whitelisting](/ip-whitelisting).
## Confirming a Transaction Really Completed
**10. How do I know a transaction is really done, not just acknowledged?**
`payment_step: 1` on the initial response only means "accepted for processing," don't act on it yet. Wait for the terminal `payment_step: 2` callback, then independently verify with [`POST /api/v3/get-unified-transaction-status`](/transaction-status), and only mark the transaction complete once both agree. Full recipe: [Collections Best Practices](/collecting-payments/best-practices#2-the-double-verification-reconciliation-recipe) and [Payouts Best Practices](/making-payments/best-practices#2-the-double-verification-reconciliation-recipe-for-payouts).
**11. Is `/api/v3/get-unified-transaction-status` deprecated in favor of a v4 endpoint?**
No. Unlike collections and payouts, transaction status intentionally stays on v3, there's no v4 version and none is planned. It's a synchronous, direct status check outside the async payment flow, which is also why `payment_step` doesn't appear in its response. See [Transaction Status](/transaction-status).
**12. Is the API ever going to become synchronous?**
No, this is intentional, not a current limitation. The underlying settlement process with mobile network operators and banks is inherently asynchronous, so callbacks and status polling are how you get the final result, not a workaround pending a future synchronous option.
## Payouts
**13. Do I need to approve payouts with OTP when using the API?**
No. OTP approval only applies to payments initiated manually through the payment portal. It never applies to payments sent via the API.
**14. What's the difference between M-Pesa PayBill and Till Number payouts?**
`"pay bill"` sends to a business PayBill account (requires a `paybill` array with `paybill_number` and `account_number`). `"till number"` sends to a Buy Goods till (requires a `tillnumber` array with just `till_number`). See the full method table on [Kenya's country page](/countries/kenya#supported-payment-methods--payload-requirements).
**15. Is there an API to retrieve my `sub_merchant_id`?**
No, there's currently no endpoint to list or fetch sub-merchant IDs programmatically. Look them up in the Dashboard's Sub-Merchant list after onboarding. See [Sub-Merchant Management](/submerchant/sub-merchant-management).
## Refunds
**16. Will I get a webhook callback when a refund is processed?**
No. Refunds are handled through the payment portal and confirmed by email notification, not a webhook callback, unlike collections and payouts. See [Refund Requests](/refund-requests).
# Getting Credentials
Source: https://docs.niobi.co/getting-credentials
## Types of Credentials
There are two types of credentials: **Sandbox** and **Production** credentials. The sandbox credentials are for use in the sandbox environment during testing, while the production credentials are set up when going live.
## Structure of Credentials
The API credentials include:
* **Client ID (`client_id`)**: Public identifier for your integration.
* **Integration Title (`sender`)**: The name assigned to your integration.
* **Secret Key (`senderKey`)**: The private key used strictly on your backend server to generate the cryptographic signature for each API request.
## Getting Credentials
Please follow these steps to set up your API credentials:
In the developer portal, on the sidebar menu under "Workspace", select
the option "Integrations" where you will manage integrations and API keys.
Begin by creating your first integration by clicking the "Create
Integration" button. We recommend creating one integration for each of
the projects or brands you are connecting.
You need to create separate integrations for both sandbox and live
accounts in their respective environments.
Within the Integration settings, generate the required
credentials for the integration.
These credentials consist of a Client ID, Integration Name, and Secret Key. Make
sure to save these credentials securely. Treat your Secret Key like a password. Never expose it in frontend code, client apps, or public logs.
This key serves as your authentication mechanism when signing API
requests.
You are now ready to integrate the Niobi Payments API into your application.
Proceed to the [Authentication Basics](/authentication/basics) guide to learn how to sign your first request.
***
## Next Step
Learn how to use your Client ID and Secret Key to cryptographically sign API requests.
# Go-Live Checklist
Source: https://docs.niobi.co/go-live-checklist
Essential prerequisites and configuration steps before transitioning your integration from sandbox to live production.
Once your sandbox testing is complete across all collection and payout endpoints, follow this checklist to move your integration into live production.
***
## Pre-Launch Steps
Verify that all API calls have been successfully tested in the sandbox environment:
* Test both successful transactions and edge-case error scenarios.
* Verify inbound webhook signature verification for callbacks (`payment_step: 2`).
* Test the [Transaction Status API](/api-reference/get-transaction-status) fallback query.
Ensure your live server IP addresses are whitelisted on the payment portal before sending live API calls:
* Navigate to **Settings > IP Whitelist** in the [Payment Portal](https://payments.niobi.co).
* Refer to the [IP Whitelisting Guide](/ip-whitelisting) for detailed instructions.
Update your integration environment configuration to use Niobi's live production endpoints:
| Environment | Base URL |
| :------------- | :------------------------------- |
| **Sandbox** | `https://sandbox.users.niobi.co` |
| **Production** | `https://users.niobi.co` |
Generate dedicated live credentials from the portal:
* Obtain your live `client_id` and integration Secret Key (`senderKey`).
* Ensure secrets are stored securely in environment variables (never committed to version control).
* Refer to [Getting Credentials](/getting-credentials) for a step-by-step walkthrough.
***
## Production Readiness Checklist
Ensure no sandbox test data, dummy mobile numbers, or mock webhooks are active in your production environment.
Before executing live disbursements, ensure your merchant currency wallet has a sufficient balance to cover payout amounts and associated transaction fees.
Ensure your callback listener handles retries idempotently by checking `params.reference` or `params.third_party_reference_1` before executing state updates.
All transactions processed in the live production environment are subject to agreed commercial fees.
***
## Technical Checklist
Beyond the setup steps above, confirm these implementation details before your first live transaction. Each one has caused real production issues for integrations that skipped it.
| Check | Why It Matters | Reference |
| :---------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------- |
| Idempotency keys configured on every request | Network retries without a stable `third_party_reference_1` / `payment_reference` can trigger duplicate payouts or duplicate collection prompts. | [Idempotency](/idempotency) |
| Payout reconciliation flow implemented | A callback can arrive late or not at all. Without a status-check fallback and a triple-point match (status, amount, reference), you risk crediting or refunding incorrectly. | [Payout Best Practices](/making-payments/best-practices#2-the-double-verification-reconciliation-recipe-for-payouts) |
| Transaction amounts fall within corridor limits | Each country and payment method has its own min/max. Requests outside that range are rejected, test with production-realistic amounts, not just sandbox defaults. | [Transaction Limits](/transaction-limits) |
| Rate limit handling in place | Sustained high-volume traffic without backoff can trigger `429` responses. | [429 Too Many Requests](/errors/429/too-many-requests) |
| `amount` sent as a whole integer | Decimal amounts (e.g. `1000.50`) fail validation, even though your wallet balance itself can show decimals from fee deductions. | [Payout Best Practices](/making-payments/best-practices) |
| `callback_url` is a public, reachable HTTPS endpoint | A `localhost`, internal, or expired tunnel URL (e.g. ngrok) cannot receive production callbacks, transactions will complete but you'll never be notified. | [Collection Callbacks](/collecting-payments/callbacks) · [Payout Callbacks](/making-payments/callbacks) |
| Balance checks use `available_balance`, not `total_balance` | `total_balance` includes funds that are locked or in-flight. Checking it instead of `available_balance` can make a payout appear affordable when it isn't. | [Get Account Balance](/api-reference/get-account-balance) |
# Idempotency
Source: https://docs.niobi.co/idempotency
How Niobi pairs merchant references with unique system IDs across collections and payouts, and enforces duplicate-transaction prevention on payouts.
## How Niobi Ensures Idempotency
In financial API integrations, network drops, client timeouts, and automated retries can cause the same API request to be transmitted multiple times.
**Idempotency** guarantees that initiating the same payment operation multiple times produces the exact same outcome as initiating it once. It prevents double-payouts to recipients, duplicate prompt pushes to payers, and replay attacks.
***
## The Dual-Reference Architecture
Every transaction that reaches Niobi operates on two distinct, complementary identifiers:
1. **Niobi Transaction Reference (`depositId` / `reference`):**
* Generated automatically by Niobi when an API request is initialized (`depositId` for collections, `reference` for payouts).
* Returned in the initial API response upon request initiation.
* Included in all subsequent asynchronous callbacks (`payment_step: 2`) and status queries.
2. **Merchant Reference (`third_party_reference_1` / `payment_reference`):**
* Generated on your backend before dispatching the API request (e.g. `ORDER-2026-001` or `TX-DISB-9901`).
* Represents your internal transaction or order identifier.
* Serves as your idempotency key to prevent duplicates and enable automated reconciliation.
***
## How It Works
Niobi binds your merchant reference to a unique transaction reference upon initial receipt. When a reference uniqueness flag (`is_third_party_reference_1_unique: 1` and/or `is_third_party_reference_2_unique: 1`) is enabled, Niobi enforces duplicate prevention. This active duplicate rejection is currently enforced on disbursements (payouts) only: collection requests accept `third_party_reference_1`/`third_party_reference_2` and their uniqueness flags and Niobi still stores them against the transaction, but a duplicate collection request is not currently rejected based on these flags.
```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 Merchant as Your Application
participant Niobi as Niobi API Gateway
Merchant->>Niobi: 1. POST Request (third_party_reference_1: "TX-ORD-9001", is_third_party_reference_1_unique: 1)
Note over Niobi: 2. Generates Unique Reference Records "TX-ORD-9001" Processes transaction
Niobi--xMerchant: Connection Dropped / HTTP 504 Timeout
Note over Merchant: Network timeout! Outcome unknown.
rect rgb(240, 253, 244)
Note over Merchant: SAFE RETRY: Re-send with SAME third_party_reference_1 and uniqueness flag
Merchant->>Niobi: 3. POST Retry (third_party_reference_1: "TX-ORD-9001", is_third_party_reference_1_unique: 1)
Note over Niobi: 4. Uniqueness Check (Flag = 1): Matches "TX-ORD-9001" to existing record Rejects duplicate request (Does NOT re-debit or create duplicate)
Niobi-->>Merchant: Duplicate rejected: "Duplicate third_party_reference_1 was found!"
end
```
### 1. Request Initiation & Transaction Reference Assignment
When your server submits a payout or collection request, Niobi assigns a unique transaction reference and maps your `third_party_reference_1` to the record.
### 2. Active Duplicate Prevention (`is_third_party_reference_1_unique` / `is_third_party_reference_2_unique`)
To prevent accidental double-payouts during retries or network timeouts, include `"is_third_party_reference_1_unique": 1` and/or `"is_third_party_reference_2_unique": 1` in `params`:
```json theme={null}
{
"is_third_party_reference_1_unique": 1,
"is_third_party_reference_2_unique": 1
}
```
**Flag Requirement:**\
Duplicate detection **only executes for a reference whose flag is passed as `1`** in the request payload. If a flag is omitted or set to `0`, Niobi will not check that reference and will process the retry as a new transaction. If both flags are set to `1`, Niobi checks `third_party_reference_1` first, then `third_party_reference_2`, and immediately rejects the request on whichever one first matches an existing record, without evaluating further.
When enabled, Niobi verifies uniqueness before debiting or dispatching funds. If a transaction with that reference already exists in the system, Niobi immediately rejects the duplicate submission, naming whichever reference actually matched:
```json theme={null}
{
"success": false,
"message": "We apologize as we were not able to process your payment request. Please try again later.",
"data": {
"error": true,
"message": "Duplicate third_party_reference_1 was found!"
}
}
```
***
## Terminal States & Reference Binding
Once a transaction reaches a **terminal status** ([`000000 - Success`](/errors/000000-success) or [`000005 - Failed`](/errors/000005-failed)), both your merchant reference and Niobi's transaction reference are permanently recorded:
* **Finality:** A terminal status indicates the payment has completed.
* **Duplicate Protection:** Resubmitting with the same `third_party_reference_1` when `is_third_party_reference_1_unique: 1` is passed immediately rejects duplicate execution (`Duplicate third_party_reference_1 was found!`).
* **Verifying Outcome:** To check the state of an already-submitted transaction, query the [Get Transaction Status API](/transaction-status) rather than re-initiating the request.
* **Failed Transactions:** A `000005 - Failed` status represents finality (such as invalid account details, payer decline, or timeout).
***
## Retrying Failed Transactions
If a transfer or collection fails definitively and you need to re-attempt the payment (such as after the customer provides a new phone number or corrects their account details), **you must use a new `third_party_reference_1`**.
The previous reference cannot be reused to initiate a new transfer.
### Recommended Pattern: Append a Retry Suffix
```json theme={null}
// Original attempt
"third_party_reference_1": "payout-2026-001"
// First retry attempt (e.g. after correcting phone number)
"third_party_reference_1": "payout-2026-001-retry-1"
// Second retry attempt (if needed)
"third_party_reference_1": "payout-2026-001-retry-2"
```
### Benefits of the Suffix Pattern:
* **Traceability:** Correlate all attempts related to the same underlying logical order.
* **Webhook Mapping:** Match asynchronous callback notifications (`payment_step: 2`) back to the specific retry attempt.
* **Audit Trail:** Maintain an immutable history in your database of each payment attempt and its specific outcome.
***
## Uniqueness Identifiers Reference
| Identifier | Defined By | Location | Purpose & Role |
| :---------------------------- | :----------: | :-----------------: | :---------------------------------------------------------------------------------------------------------------------------------------- |
| **`third_party_reference_1`** | **Merchant** | Request `params` | **Primary Merchant Idempotency Key.** Your unique internal order ID. Used to prevent duplicates on retries. |
| **`payment_reference`** | **Merchant** | Request `params` | Merchant tracking reference specific to disbursement instructions. |
| **`third_party_reference_2`** | **Merchant** | Request `params` | Optional secondary reference (e.g. batch ID, customer ID, or invoice number). |
| **`depositId`** | **Niobi** | Response / Callback | Unique deposit reference returned by Niobi when initiating a collection request and in collection callbacks. |
| **`reference`** | **Niobi** | Response / Callback | Unique transaction identifier returned by Niobi when initiating a request. Included in subsequent status queries and webhook callbacks. |
| **`salt`** & **`timestamp`** | **Merchant** | Root Envelope | Cryptographic nonce and Unix timestamp included in every request envelope to guarantee unique HMAC signatures and prevent replay attacks. |
***
## Webhook Callback Idempotency on Your Server
Because webhook callbacks traverse public networks, Niobi may re-deliver a terminal callback (`payment_step: 2`) if your server took longer than 5 seconds to respond with HTTP `200 OK` or encountered temporary connectivity issues.
Ensure your webhook listener handles duplicate callbacks safely:
```mermaid theme={null}
flowchart TD
A["Incoming Callback (payment_step: 2)"] --> B["1. Verify HMAC Signature"]
B --> C["2. Extract third_party_reference_1 and reference"]
C --> D{"3. Check Local Database: Is transaction already marked SETTLED?"}
D -- "Already Processed" --> E["Safely Return HTTP 200 OK (Do not duplicate balance credits)"]
D -- "Not Yet Processed" --> F["4. Acquire Row Lock (SELECT ... FOR UPDATE)"]
F --> G["5. Credit User Account / Fulfill Order"]
G --> H["6. Set status = 'SETTLED' in DB"]
H --> I["Return HTTP 200 OK"]
```
### Callback Receiver Best Practices:
1. **Verify Inbound Signature:** Always verify the incoming SHA-256 signature using your integration Secret Key (`senderKey`) before parsing data.
2. **Check State Before Crediting:** Check whether `third_party_reference_1` or Niobi's `reference` has already been credited in your database.
3. **Return HTTP 200 on Duplicates:** If a callback is received for an already settled order, acknowledge receipt immediately with HTTP `200 OK` without duplicating financial credits.
4. **Use Concurrency Locks:** Use database row-level locking or unique constraints on `third_party_reference_1` to prevent concurrent threads from processing duplicate webhooks simultaneously.
***
## Next Steps
Explore related operational and security guides:
# Initial Steps
Source: https://docs.niobi.co/initial-steps
This section outlines the initial steps needed to begin using the Niobi Payments API.
## Setup your Environment
Niobi provides both a sandbox and live environment. The sandbox lets you build and test your integration without needing production compliance approval first — live payments still require completing KYC/KYB verification and following our security requirements before go-live.
Details of Niobi's sandbox and live environments are as below
During the development process, you will use a sandbox environment where you can easily interact with our APIs while making simulated / mocked transactions.
1. To get started, follow the signup instructions using this link for the [sandbox account](https://payments.niobi.co).
2. The base URL for our sandbox APIs is ([https://sandbox.users.niobi.co](https://sandbox.users.niobi.co)).
3. The rest of the steps, including creating an account, verifying it and fetching API credentials are all same for both our sandbox and live environments.
After testing with the sandbox and ready for production, follow these steps.
1. Follow the signup instructions using this link
[Live](https://payments.niobi.co).
2. Change the API endpoints from
`https://sandbox.users.niobi.co` to `https://users.niobi.co`
3. Make sure your live account is ***verified*** by our compliance team to be ready to accept and send payments. [Account Verification](/initial-steps#account-verification).
(Please Note: Live transactions are subject to a transaction fee.)
## Setting up a Niobi Account
To access the Niobi Payments API, you need to create an account on our platform and then add a developer account in the Workspace section. Follow the steps below to get started:
[The Niobi Payment Portal](https://payments.niobi.co) gives you access to both sandbox and live environments
with Niobi so you can use it to transact with our APIs
When you register for an account you would need to submit all compliance
documents in order to have your account verified. You can choose to do this
via the portal or request for verification via our compliance team at [support@niobi.co](mailto:support@niobi.co)
You can use the same portal for sandbox and production environment access
by clicking the toggle on the top right at any time
Open your web browser and visit [https://payments.niobi.co/register](https://payments.niobi.co/register) if you
are just starting out integrating Niobi and want to register on our payment
portal
Under the sign up page, fill in the required information, such as your
name, email address, and password.
Accept the terms and conditions, and then click on "Sign Up" to proceed to
register your business.
Fill in the required information about your business and click on "Save
Business Details" to proceed.
We ask that only managers/owners of the business set up their business
account first and then invite other team members to the platform later.
Once you have created your account, you need to invite the developer who
will be working on the integration. To do this, navigate to the
"Workspace" section on the left sidebar.
Within the Workspace tab, you will see an option to manage your "Team".
Navigate to it and click the "Invite Users" button.
Once you click it, you will see the option of adding emails and roles of
the team members you want to invite.
Please add the email of the team member that needs to be invited and set
their role as "Developer".
The invited team member will now receive an email invitation to sign up.
The developer should sign up using the link provided in their
email.
Once the account has been created the user can navigate through the
sidebar and interact with the dashboard to see a snapshot of the API
activity, see transactions on the balance screen as well as generate API
keys.
## Account Verification
Before going live with Niobi Payments, it's mandatory for all businesses to complete our ***Know Your Customer (KYC)*** and ***Know Your Business (KYB)*** verification processes. This is a critical step to ensure compliance with financial regulations and to maintain the integrity and security of our platform.
During ***KYC***, we require individual users to provide valid identification documents.
For ***KYB***, businesses must submit specific registration documents, including details about the business and its shareholders.
This process helps us verify the legitimacy of your business and its stakeholders, ensuring a secure and trustworthy environment for all transactions. We are committed to maintaining the highest standards of security and compliance, and we appreciate your cooperation in providing these necessary documents to complete the verification process.
## Begin Integration
Once your account is setup, follow the steps below to start integrating with Niobi Payments API.
Setting up your API credentials
Learn more about whitelisting of ip addresses
What to check before moving your integration to production
# IP Whitelisting
Source: https://docs.niobi.co/ip-whitelisting
To securely access our APIs, your server’s public IP addresses must be whitelisted for both **Sandbox** and **Live** environments.
Whitelisting ensures only trusted systems can access our APIs and receive sensitive information like transaction or webhook data.
***
## Portal IP Whitelisting (Guide)
To manage IP whitelisting directly in the portal, follow these steps:
1. Log in to your Niobi dashboard.
2. Navigate to **Workspace**.
3. Select **IP Whitelisting**.
4. Click **Add New** to add a new IP address
5. Fill in the IP address details and save.
6. To change an entry, click **Edit**, make your updates, and save.
7. To remove an entry, click **Delete** and confirm.
Using this portal workflow avoids manual email requests and lets you maintain security in real time.
***
## How to Check Your Public IP
Here are quick ways to find your public IP:
1. Through the terminal: run `curl ifconfig.me`
2. Through online sites like [whatismyip.com](https://www.whatismyip.com) or [ipinfo.io](https://ipinfo.io)
### Dynamic IP? Here's What to Do
If your server has a dynamic IP (i.e. it changes), we recommend switching to a static IP to avoid disruptions.
## Webhook Notifications - IP to Whitelist
If you're integrating webhook notifications, make sure to whitelist our IP address to receive updates. Kindly send an email to [support@niobi.co](mailto:support@niobi.co) to get access to the necessary IPs for whitelisting.
***
## How to Request Whitelisting
As an alternative to the portal whitelisting in case of any queries, you can request IP whitelisting by contacting us via:
* **Email**: [support@niobi.co](mailto:support@niobi.co)
* **Support Chat**: Available through our current support channels on Slack, WhatsApp, or Telegram
# Disbursement Basics
Source: https://docs.niobi.co/making-payments/basics
Core fundamentals of making disbursements across Africa: payment steps, request parameters, synchronous dispatch responses, and asynchronous terminal callbacks.
## Overview
Niobi's **Unified Payments API** allows your application to disburse funds to recipients across Africa through a single standardized integration. Whether disbursing via Mobile Money (M-Pesa, MTN, Airtel, Orange, Wave), Bank Transfers, Business Paybills, or Till Numbers, you send a signed request to the unified payments endpoint:
**Endpoint:** [`POST /api/v4/niobi-unified-payments`](/api-reference/disbursement/unified-disbursement)
Disbursements follow an asynchronous lifecycle:
```mermaid theme={null}
%%{init: {'themeVariables': {'fontSize': '23px'}, 'sequence': {'width': 220, 'height': 95, 'actorMargin': 80, 'messageMargin': 60, 'boxMargin': 18, 'noteMargin': 18}}}%%
sequenceDiagram
autonumber
participant Merchant as Your Backend Server
participant Niobi as Niobi Unified Payments API
participant TelcoBank as Recipient Telco / Banking Switch
participant Recipient as Recipient (Payee)
Note over Merchant: 1. Sign request envelope (SHA-256) 2. Send POST /api/v4/niobi-unified-payments
Merchant->>Niobi: POST /api/v4/niobi-unified-payments
Note over Niobi: Validate signature, params & check merchant wallet balance
Niobi-->>Merchant: Synchronous Response (On success: payment_step: 1, status: success, statusCode: 000000)
Note over Merchant: Payout accepted & dispatched upstream
Niobi->>TelcoBank: Submit payout to network switch
TelcoBank->>Recipient: Credit recipient wallet / bank account
TelcoBank->>Niobi: Report settlement confirmation
alt Payout Succeeded
Niobi->>Merchant: Webhook (payment_step: 2, status: "success", statusCode: "000000")
Note over Merchant: Final clearance: Mark payout completed & update ledger
else Payout Failed / Rejected
Niobi->>Merchant: Webhook (payment_step: 2, status: "failed", statusCode: "000005")
Note over Merchant: Payout failed: Mark failed in ledger & notify operations
end
```
Step 1Request Initiation
payment\_step: 1
Your backend signs the request envelope (SHA-256) and sends POST /api/v4/niobi-unified-payments.
Niobi validates the signature and params, and checks your merchant wallet balance.
Niobi returns the initial HTTP response to the request when it's successful, confirming the payout was accepted and dispatched upstream.
→
Step 2Terminal Outcome
payment\_step: 2
Niobi submits the payout to the recipient's telco or banking switch.
The switch credits the recipient's wallet or bank account, then reports settlement back to Niobi.
Niobi sends an asynchronous callback to your client\_callback\_url with the final status.
***
## API Environments & Base URLs
Niobi provides separate environments for sandbox testing and live disbursement processing:
| Environment | Base URL | Purpose |
| :------------- | :------------------------------- | :---------------------------------------------------------------------- |
| **Sandbox** | `https://sandbox.users.niobi.co` | Integration testing, mock disbursements, and simulator accounts. |
| **Production** | `https://users.niobi.co` | Live disbursements, real wallet balances, and actual recipient credits. |
All payout endpoints referenced throughout this documentation are relative to your chosen environment's base URL.
***
## Understanding Payment Steps (`payment_step`)
Like collections, disbursements operate on a two-step lifecycle:
Step 1Request Initiation
payment\_step: 1
Initial HTTP response to the request when it's successful, confirming the payout was validated and dispatched to the destination telco/bank switch.
→
Step 2Terminal Outcome
payment\_step: 2
Asynchronous callback delivering the final settlement outcome (success or failed).
### 1. `payment_step: 1` (Initial API Response)
* **Delivery:** Synchronous HTTP response returned immediately when calling `POST /api/v4/niobi-unified-payments`.
* **Meaning:** The request passed signature validation, parameter formatting was accepted, merchant wallet balance was confirmed, and the disbursement instruction was dispatched to the destination telco or banking switch.
* **Important:** This confirms that the transfer has been initiated, **not that funds have settled in the recipient's wallet or account**.
**Absence of `payment_step` on Initiation Failures:**\
If a disbursement request fails during initial submission (for example, due to invalid recipient details, unsupported corridor, insufficient merchant wallet balance, or immediate processor rejection returning `status_code: "000005"`), the `"payment_step"` field is **not present** in the response. Your backend must inspect `status_code` (`"000000"` vs `"000005"`) rather than assuming `payment_step` is always returned.
### 2. `payment_step: 2` (Terminal Callback)
* **Delivery:** Asynchronous callback POST request sent to the `client_callback_url` passed under `params.client_callback_url` in your request.
* **Meaning:** The transaction has reached its **final terminal state**. Funds have either been credited to the recipient or the payout was rejected/refunded by the banking/telco switch.
* **Action:** When `"payment_step": 2` is present in the `params` object of the callback, update your internal ledger and record the outcome based on `status: "success"` (`status_code: "000000"`) or `status: "failed"` (`status_code: "000005"`). Full details are covered in [Payout Callbacks](/making-payments/callbacks).
**Rules of Transaction Settlement (Disbursement Lifecycle):**
* **`payment_step: 1` = Payout Dispatched (Funds In Transit)**\
Confirms that Niobi validated your signature, confirmed sufficient merchant wallet balance, and dispatched the payout instruction to the destination telco/bank switch.\
**CRITICAL RULE:** Funds are in transit across banking/mobile money networks. **Do NOT treat the transfer as settled or cleared at this stage.**
* **`payment_step: 2` = Terminal Outcome (Delivered via Webhook Callback)**\
Delivered asynchronously to your `client_callback_url`. Inspect both `status` and `statusCode` (or `status_code`):
* **Success (`status: "success"`, `statusCode: "000000"`):** The recipient's bank account or mobile wallet has officially confirmed receipt of the funds. **Update your ledger and notify the recipient.**
* **Failed (`status: "failed"`, `statusCode: "000005"`):** The transfer failed or was rejected by the switch (e.g. invalid account number, inactive wallet, bank network offline). Niobi automatically refunds/unlocks the held wallet balance. **Mark the payout as failed in your ledger.**
* **Missing or Delayed Webhook?**\
If a callback hasn't arrived after a reasonable wait, query the [Get Transaction Status API](/transaction-status) (`POST /api/v3/get-unified-transaction-status`) to reconcile in-doubt payouts before re-attempting or failing a transfer.
***
## Request Parameters
The unified payments endpoint accepts standard parameters wrapped in the signed envelope:
### Root Envelope Fields
| Field | Type | Required | Description |
| :-------------- | :-------- | :------: | :-------------------------------------------------------------------- |
| **`client_id`** | `string` | **Yes** | Your integration Client ID from the Niobi Dashboard. |
| **`sender`** | `string` | **Yes** | Your exact integration name as registered in the dashboard. |
| **`timestamp`** | `integer` | **Yes** | The exact UNIX timestamp (in seconds) used during signature creation. |
| **`salt`** | `string` | **Yes** | A unique random string generated on your server for entropy. |
| **`signature`** | `string` | **Yes** | The 64-character SHA-256 hash computed with your Secret Key. |
| **`params`** | `object` | **Yes** | The nested payload containing all disbursement details. |
### Parameters (`params` Object)
| Parameter | Type | Required | Description |
| :-------------------------------------- | :-------------------- | :--------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`amount`** | `integer` | **Yes** | The amount to disburse in standard major currency units (e.g. `1000` = 1,000 KES). **Must be a whole integer with no decimal points.** |
| **`currency`** | `string` | **Yes** | The 3-letter ISO currency code (e.g. `KES`, `NGN`, `ZAR`, `GHS`, `UGX`, `TZS`, `XOF`, `XAF`, `ZMW`, `RWF`, `CDF`, `SLE`). |
| **`country_id`** | `integer` | **Yes** | The country ID where the recipient is located (e.g. `1` for Kenya, `17` for Nigeria, `15` for South Africa). See [Payout Methods](/making-payments/methods). |
| **`payment_method_type`** | `string` | **Yes** | The destination payout channel (e.g. `send money`, `pay bill`, `till number`, `pesalink`, `mtn`, `airtel`, `orange`, `bank`). |
| **`first_name`** | `string` | **Yes** | Recipient's first name. |
| **`last_name`** | `string` | **Yes** | Recipient's last name. |
| **`mobile`** | `string` | **Yes** | Recipient's phone number in international format without `+` (e.g. `254700000000`). |
| **`email`** | `string` | **Yes** | Recipient's email address. |
| **`city`** | `string` | **Yes** | Recipient's city. |
| **`client_callback_url`** | `string` | **Required (for callbacks)** | The endpoint URL on your server where real-time terminal callbacks (`payment_step: 2`) are delivered. Essential for automated transaction settlement. |
| **`payment_reference`** | `string` | **Recommended** | Internal merchant reference for identifying the payout transaction and preventing duplicates. |
| **`third_party_reference_1`** | `string` | **Recommended** | Your unique merchant transaction tracking reference for ledger reconciliation and idempotency defense. |
| **`third_party_reference_2`** | `string` | Optional | Additional secondary tracking reference or batch identifier. |
| **`is_third_party_reference_1_unique`** | `boolean` / `integer` | Optional | Set to `true` or `1` to enforce strict uniqueness on `third_party_reference_1`. Prevents duplicate payouts from being initiated. |
| **`is_third_party_reference_2_unique`** | `boolean` / `integer` | Optional | Set to `true` or `1` to enforce strict uniqueness on `third_party_reference_2`. |
| **`sub_merchant_id`** | `string` (UUID) | Optional | Sub-merchant ID if disbursing on behalf of an onboarded sub-merchant. |
### Channel-Specific Parameter Objects (`params`)
Depending on your selected `payment_method_type`, supply the matching nested array inside `params`:
| Channel / `payment_method_type` | Nested Array Key | Required Fields in Array | Description |
| :--------------------------------------------------------------------------- | :------------------------------------------ | :------------------------------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------- |
| **Mobile Money** (`send money`, `mtn`, `airtel`, `Orange`, `Vodafone`, etc.) | Matches provider (`sendmoney`, `mtn`, etc.) | `phone_number` | Recipient mobile number in international format without `+`. |
| **M-Pesa Business Paybill** (`pay bill`) | `paybill` | `paybill_number`, `account_number` | Destination business Paybill and customer account number reference. |
| **M-Pesa Buy Goods Till** (`till number`) | `tillnumber` | `till_number` | Destination merchant Buy Goods till number. |
| **PesaLink (Kenya)** (`pesalink`) | `pesalink` | `bank_name`, `bank_code`, `branch_name`, `account_name`, `account_number` | Real-time bank transfer details to commercial banks in Kenya. |
| **Direct Bank Transfer** (`bank`) | `bank` | `bank_name`, `bank_code`, `branch_name`, `account_name`, `account_number` | Direct bank disbursement details (e.g. Nigeria NGN NUBAN, South Africa ZAR RTC/EFT, or International bank wires). |
| **Cameroon Compliance** (XAF) | Root `params` fields | `sender_firstname`, `sender_lastname`, `sender_phone_number` | Mandatory CEMAC sender compliance fields for Cameroon disbursements. |
**Best Practice: Generate Unique References & Enable Uniqueness Checks:**\
We strongly recommend generating `payment_reference` and `third_party_reference_1` on your server before sending the payout request (for example, using an internal transfer ID like `PAYOUT-2026-001` or `TX-DISB-9901`).
To guarantee that duplicate transactions are never processed:
* **Enable Uniqueness Enforcement:** Pass `"is_third_party_reference_1_unique": 1` in `params`. If a payout with the same reference has already been created, Niobi immediately rejects the duplicate with `"Duplicate third_party_reference_1 was found!"`.
* **Instant Webhook Reconciliation:** When Niobi sends the terminal callback (`payment_step: 2`), it includes your exact reference keys so your ledger updates automatically.
* **Direct Status Lookups:** Allows you to query the [Get Transaction Status API](/transaction-status) using your own internal ID at any time.
***
## 3-Step Direct Payout Integration Walkthrough
Your backend server signs the payload and dispatches a POST request to `/api/v4/niobi-unified-payments`.
#### Sample Request: Mobile Money Payout (Kenya M-Pesa)
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"salt": "random_salt_12345",
"signature": "d4e5f6a7b8c90123456789abcdef0123456789abcdef0123456789abcdef01",
"params": {
"amount": 1000,
"currency": "KES",
"country_id": 1,
"first_name": "Jane",
"last_name": "Doe",
"mobile": "254647647649",
"email": "jane.doe@example.com",
"city": "Nairobi",
"payment_method_type": "send money",
"sendmoney": [
{ "phone_number": "254647647649" }
],
"client_callback_url": "https://yourdomain.com/niobi/payout-callback",
"payment_reference": "PAYOUT-REF-1001",
"third_party_reference_1": "TX-DISB-9901"
}
}
```
#### Sample Initial API Responses (Synchronous)
```json theme={null}
{
"success": true,
"message": "Payment has been initiated successfully.",
"data": {
"amount": 1000,
"client_callback_url": "https://yourdomain.com/niobi/payout-callback",
"country_id": 1,
"currency": "KES",
"mobile": "254647647649",
"payment_method_type": "send money",
"payment_reference": "PAYOUT-REF-1001",
"third_party_reference_1": "TX-DISB-9901",
"third_party_reference_2": null,
"status": "success",
"payment_step": 1
},
"status_code": "000000"
}
```
```json theme={null}
{
"success": false,
"message": "Insufficient merchant wallet balance to execute payout.",
"data": null,
"status_code": "000005"
}
```
If payout dispatch is rejected outright due to invalid parameters or balance constraints, refer to the [400 Client Errors Guide](/errors/client-errors).
**Understanding the Two Success Indicators:**
* **Outer `"success": true` (Root level):** Indicates the HTTP request was valid, authenticated, and received by Niobi ([Request Successfully Initiated](/errors/200-success)).
* **Inner `"status": "success"` (Inside `data`):** Indicates the payout instruction was accepted and dispatched to the upstream network switch. It does **not** signify final delivery to the recipient.
Niobi routes the disbursement to the local payment provider or mobile network operator. The switch processes the transfer and deposits funds into the recipient's wallet or bank account.
When settlement concludes, Niobi dispatches an asynchronous callback containing `"payment_step": 2` to the `client_callback_url` passed under `params.client_callback_url` in your disbursement request.
#### Sample Terminal Callbacks (payment\_step: 2)
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"sender": "YOUR_INTEGRATION_NAME",
"salt": "random_salt_12345",
"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"
}
}
```
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"sender": "YOUR_INTEGRATION_NAME",
"salt": "random_salt_12345",
"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"
}
}
```
**Determine Outcome Using `status` and `statusCode`:**\
When `"payment_step": 2` arrives, inspect both `params.status` and `params.statusCode` inside the payload:
* [`status: "success"` with `statusCode: "000000"`](/errors/000000-success): The disbursement was successfully processed and credited to the recipient. Mark the payout order as completed in your ledger.
* [`status: "failed"` with `statusCode: "000005"`](/errors/000005-failed): The disbursement failed or was rejected by the switch. Inspect `params.failureReason` for specific provider diagnostics.
See our [Transaction Status Codes Guide](/errors/transaction-status-codes) for the complete list of system status codes.
**On-Demand Status Verification:**\
If you ever need to independently verify the status of a payout on demand, call the [Get Transaction Status API](/api-reference/get-transaction-status) (`POST /api/v3/get-unified-transaction-status`) passing `"type": "payout"` and your transaction reference in `"id"`.
***
## Next Steps
Follow these guides in order to complete your disbursement integration:
# Payout Best Practices
Source: https://docs.niobi.co/making-payments/best-practices
Operational guidance, merchant wallet balance management, recipient validation rules, idempotency defense, and error retry strategies for disbursements.
## Overview
Building a reliable, automated disbursement pipeline requires strict wallet balance monitoring, rigorous recipient data validation, idempotency controls, and disciplined error handling. Follow these operational best practices to prevent failed transfers, avoid double-payouts, and maintain ledger integrity.
***
## 1. Merchant Wallet Balance Management & Pre-Check
Unlike collections, disbursements directly debit your Niobi merchant wallet in real time.
### Pre-Checking Wallet Balance via API
Before triggering automated or batch disbursement routines, query your current wallet balance using the [Get Account Balance API](/api-reference/get-account-balance) (`POST /api/v3/get-unified-balance`):
```json theme={null}
{
"success": true,
"message": "Balance fetched successfully.",
"data": {
"currency": "KES",
"total_balance": 185943.4,
"on_hold_balance": 0,
"available_balance": 185843.4,
"unsettled_balance": 100
}
}
```
#### Key Balance Fields:
* **`available_balance` (Primary Indicator):** Always evaluate `available_balance` to determine if you have sufficient funds to cover the payout principal plus transaction fees. Do not rely on `total_balance`, as it includes funds that are currently locked or in-flight (`on_hold_balance` / `unsettled_balance`).
* **Why `available_balance` Contains Decimal Points:** You may notice fractional decimal values (e.g. `185843.4`) in your balance. These fractional values are the cumulative result of percentage-based transaction fee deductions across various payment corridors.
**Strict Integer Rule for `amount` (No Decimal Points):**\
Even though your `available_balance` may show decimal points due to fee deductions, **all `amount` values submitted in API requests (both Collections and Payouts) MUST be whole integers** (e.g. `1000` for 1,000 KES/NGN, not `1000.50` or `1000.00`). Submitting decimal amounts will cause the request to fail validation.
### Best Practices for Balance Operations:
* **Maintain Safety Buffers:** Set up automated alerts on your server when your `available_balance` approaches operational thresholds (e.g. less than 20% of estimated daily volume).
* **Handle Insufficient Balance Gracefully:** If an initiation returns `status_code: "000005"` with `"Insufficient merchant wallet balance"`, pause your payout queue, notify your treasury team to top up, and avoid flooding the endpoint with automated retries.
***
## 2. The Double-Verification Reconciliation Recipe for Payouts
For payroll disbursements, large vendor payments, or automated cross-border transfers, implement Niobi's recommended double-verification recipe: rely on the `payment_step: 2` 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:
```mermaid theme={null}
flowchart TD
A["1. Initiate Payout (POST /api/v4/niobi-unified-payments)"] --> B["Receive payment_step: 1 (Initiated)"]
B --> C["Wait for Incoming Callback"]
C --> D{"payment_step: 2 Callback Received?"}
D -- Yes --> E["Step 1: Verify Inbound Signature"]
D -- "No (after a while)" --> F["Call Status API (POST /api/v3/get-unified-transaction-status) with Spaced Intervals"]
E --> G["Step 2: Acquire DB Lock on Reference"]
F --> G
G --> H{"Step 3: Triple-Point Match? 1. status == 'success' & statusCode == '000000' 2. amount & currency match payout order 3. payment_reference matches"}
H -- "Match Confirmed" --> I["Step 4: Atomic DB Update (Set status = 'SETTLED')"]
H -- "Payout Failed / Returned" --> J["Refund Merchant Internal Ledger / Notify User"]
```
### Reconciliation Checklist:
1. **Inbound Signature Verification:** Always verify the incoming SHA-256 callback signature using your integration Secret Key (`senderKey`) before parsing the payload.
2. **Concurrency Lock & Idempotency Check:** Acquire a row-level database lock on `payment_reference` before processing balance updates. If both a callback and a status query arrive concurrently, only the first thread processes the reconciliation while the second safely exits.
3. **Triple-Point Data Match:** Confirm that all three attributes match your internal payout instruction:
* **Status & Code:** Both `status: "success"` and `statusCode: "000000"` ([Success Code 000000](/errors/000000-success)).
* **Amount & Currency:** The disbursed `amount` matches the intended transfer amount.
* **Reference:** The `payment_reference` or `third_party_reference_1` matches your internal payout ID.
4. **Atomic Ledger Settlement:** Update your local transfer record to `SETTLED` inside a single atomic database transaction.
***
## 3. Recipient Phone & Account Validation Rules
Improper MSISDN or bank account formatting is the leading cause of instant disbursement rejections.
### Formatting Rules:
* **Digits Only:** Strip all spaces, plus signs (`+`), hyphens, parentheses, or symbols from phone numbers and bank accounts.
* **Phone Number Length:** Between 8 and 15 digits (including country code).
* **International Prefix without Plus:** Always prepend the destination country code, but omit the leading `+` or local trunk `0`.
| Country | Channel | Correct Format | Incorrect Format |
| :----------- | :---------------------- | :----------------------- | :--------------------------------- |
| **Kenya** | M-Pesa / Airtel | `254712345678` | `+254 712 345 678` or `0712345678` |
| **Ghana** | MTN / Vodafone / AT | `233501234567` | `+233 50 123 4567` or `0501234567` |
| **Uganda** | MTN / Airtel | `256770000000` | `+256 77 000 0000` |
| **Tanzania** | Vodacom / Airtel / Tigo | `255750000000` | `+255 75 000 0000` |
| **Cameroon** | MTN / Orange | `237670000000` | `+237 67 000 0000` |
| **Nigeria** | NUBAN Bank Account | `0123456789` (10 digits) | `012-345-6789` |
***
## 4. Idempotency & Reference Generation Best Practices
To prevent duplicate payouts caused by network retries or transient connection errors:
### Generating References on Your Side
Generating your own tracking references (`payment_reference` and `third_party_reference_1`) directly on your backend before dispatching requests is strongly recommended:
* **Format:** Use structured, easily traceable identifiers (such as `PAYOUT-2026-USER45-001` or a UUIDv4).
* **Safe Retries:** If an API call times out before receiving a response, resubmitting the request with the **same** reference guarantees that Niobi will not execute a duplicate transfer.
* **Audit & Status Queries:** Having your own reference stored locally lets you reconcile asynchronous callbacks (`payment_step: 2`) or query the [Get Transaction Status API](/transaction-status) using your own identifier.
### Enforcing Strict Uniqueness (`is_third_party_reference_1_unique`)
To have Niobi automatically reject duplicate disbursement submissions, pass the uniqueness flag in your request `params`:
```json theme={null}
{
"is_third_party_reference_1_unique": 1,
"is_third_party_reference_2_unique": 1
}
```
When set to `1` (or `true`), Niobi verifies that no prior transaction exists with that reference. If a duplicate reference is detected, Niobi immediately blocks the transfer and returns:
```json theme={null}
{
"success": false,
"message": "We apologize as we were not able to process your payment request. Please try again later.",
"data": {
"error": true,
"message": "Duplicate third_party_reference_1 was found!"
}
}
```
### Key Rules for Payout Uniqueness:
* **Assign Unique References per Payout:** Every distinct payout instruction must carry a unique `payment_reference` or `third_party_reference_1`.
* **Generate Unique Salt per Request:** Always generate a fresh random `salt` string for each API call to ensure cryptographic uniqueness and prevent replay attacks.
* **Avoid Blind Retries with New References:** If an HTTP request times out, never generate a new reference to retry the same payout. Re-send with the original reference or query status first.
***
## 5. Error Handling & Retry Policies
**Examples:** [`400 Bad Request`](/errors/client-errors), [`403 Request not verified`](/errors/authentication-errors), validation errors.\
**Action:** Do not retry automatically. Check parameter validation, verify your signature generation algorithm, ensure required corridor fields (such as Cameroon sender details) are supplied, and correct the payload before resending. See [400 Client Errors](/errors/client-errors) for code breakdowns.
**Examples:** `500 Internal Server Error`, `502 Bad Gateway`, `504 Gateway Timeout`.\
**Action:** Implement exponential backoff (e.g. retry after 2s, 4s, 8s, 16s) up to a maximum of 3 to 5 attempts. If timeouts persist, check payout status using your reference before re-initiating.
**Examples:** `status: "failed"`, `statusCode: "000005"`, `failureReason.failureCode: "DS-008"`.\
**Action:** Funds are automatically refunded to your merchant wallet. Notify the recipient or prompt them to provide an active, registered phone number or bank account.
***
## Next Steps
Complete your integration and explore developer references:
# Payout Callbacks
Source: https://docs.niobi.co/making-payments/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 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 2. Update ledger & notify recipient 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 2. Unlock funds & mark failed 3. Reconcile idempotently
end
Merchant-->>Niobi: HTTP 200 OK
```
***
## Terminal Callback Guarantee (`payment_step: 2`)
**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.
* 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
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.
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.
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.
***
## 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**.
**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.
***
## Next Steps
Follow these guides to complete and secure your payout flow:
# Platform Overview
Source: https://docs.niobi.co/platform-overview
How Niobi routes payments, manages wallet balances, and secures every request across regional rails.
* **Sandbox access, instantly.** Sign up and start building, no approval gate.
* **No UI? No problem.** Generate a [Payment Link](/collecting-payments/payment-links) and start collecting payments in minutes.
* **One integration, every market.** Same credentials, sandbox to production.
***
## What You Can Build With Niobi
Niobi handles collections, payouts, and multi-currency wallets, along with the approvals, sub-merchant management, and reporting most growing businesses need around them, all through one API and one dashboard.
Accept mobile money, bank transfers, cards, and redirect-based payments (Wave, Capitec) through one API. Coverage varies by market, see [Supported Countries](/supported-countries) for active methods per country.
Disburse to bank accounts or mobile money wallets (M-Pesa, PesaLink), or move funds straight to another Niobi wallet, all through the same signed request format.
Generate a payment link with zero front-end code. Pre-fill it with your customer's name, email, and phone, and Niobi handles the entire [checkout experience](/collecting-payments/payment-links).
Hold balances in multiple currencies and convert between them as you need. Check your main wallet or any sub-wallet's balance independently to keep funds segmented by team, product, or client.
Upload a batch of payouts in one go, or schedule them to go out on a future date, instead of firing requests one at a time.
Get notified the moment a collection or payout settles. Niobi posts a signed callback to your server so you don't have to poll for a final status.
Set an approval rule on high-value bills or bulk payment batches, so a second team member reviews and signs off before they go out.
Onboard and manage merchants under your own parent account, without issuing separate credentials for each one.
Control who on your team can initiate, approve, or just view transactions, with role-based access across the dashboard.
Create and send invoices, manage a catalog of products, and track customers, useful if you're collecting recurring or itemized payments rather than one-off transactions.
KYB onboarding, IP whitelisting, and scoped API key management keep your integration locked down as you move from sandbox to production.
Sync transactions to QuickBooks, and export CSV or PDF reports of balances and transaction history for reconciliation.
# Refund Requests
Source: https://docs.niobi.co/refund-requests
If you need to request a refund or reverse a transaction, you can easily initiate the process through the payment portal. Below are the steps and details about how refunds are handled.
## Steps to Request a Refund
1. **Log in to the Payment Portal**\
Access your account on [the payment portal](https://payments.niobi.co).
2. **Locate the Transaction**\
Navigate to My Accounts Page and select the currency. You will be redirected to the dashboard to view your transaction history. Identify the specific transaction for which you need a refund and click on View Details.
3. **Initiate the Refund**\
A pop up with the transaction details will appear and you can proceed to click on the **"Request Refund"** button.
## What Happens Next?
* Once your refund request is submitted, you will receive an acknowledgement email notifying you that our team will review and process the transaction reversal.
* The refunded amount will be credited to your account upon successful processing.
* You will receive a notification email to confirm the refund status.
## Important Notes
* **Processing Time**: Refunds may take a few business days to reflect in your account depending on the payment provider.
* **Eligibility**: Certain transactions may not be eligible for refunds based on their type or terms of service.
* Refunds only apply for live transactions.
* If you encounter any issues while requesting a refund or have questions about the process, please contact our support team at [support@niobi.co](mailto:support@niobi.co).
# 2025-07-02 Changes
Source: https://docs.niobi.co/release-notes/02-07-2025
**Release Date:** 02/07/2025
## **Overview**
This release includes important API updates and improvements, including enhanced payment link functionality, OTP verification for Ivory Coast Orange deposits, revised transaction limits, updated country support, and a new best practices guide.
## **New Features and API Improvements**
### **Payment Links v3**
* **Prefilled Customer Data** - you can now generate payment links with prefilled customer data. By passing fields such as first\_name, last\_name, email, and mobile, the user is directed straight to the checkout page with their details already filled in.
**Previously:** Payment links required customers to manually enter their information at checkout.
**Sample V2 Payment link creation API with no customer data:**
```json theme={null}
{
"client_id": "K1PoY1WYricSpXh6Wm24twnk6ecPJOWrHGsqiKJr",
"salt": "justrandomstring",
"sender": "John.co",
"timestamp": 1709363033,
"signature": "2193d01d50d4bd5da234ac0e0add5d4ccb40907a1b1ab27a12d2c1d88a5d5807",
"params": {
"business_name": "my business",
"item_name": "item name",
"callback_url": "https://webhook.site/unique-id",
"redirection_url": "https://webhook.site/unique-id",
"amount": 5000,
"logo": "This is a brand logo. Use the base64 format to upload a file of maximum 2MB size.",
"country_id": "This is an integer value. You can get country id from the supported country table.",
"currency": "KES"
}
}
```
**Now:** Backward compatibility with v2. You can pass in the customer data.
**Sample V3 API Payment link with customer data:**
```json theme={null}
{
"client_id": "K1PoY1WYricSpXh6Wm24twnk6ecPJOWrHGsqiKJr",
"salt": "justrandomstring",
"sender": "John.co",
"timestamp": 1709363033,
"signature": "2193d01d50d4bd5da234ac0e0add5d4ccb40907a1b1ab27a12d2c1d88a5d5807",
"params": {
"business_name": "my business",
"item_name": "item name",
"callback_url": "https://webhook.site/unique-id",
"redirection_url": "https://webhook.site/unique-id",
"amount": 5000,
"logo": "This is a brand logo. Use the base64 format to upload a file of maximum 2MB size.",
"country_id": "This is an integer value. You can get country id from the supported country table.",
"currency": "KES",
"first_name": "First name",
"last_name": "Last name",
"email":"john.doe@example.com",
"mobile": "2547123xxxx7"
}
}
```
You can find more details [here](/api-reference/payment-links/create)
### **Orange OTP for Ivory Coast Deposits**
* For Orange payins in Ivory Coast, OTP verification is **now required**.
* Ensure your implementation includes this OTP step.
```json theme={null}
{
"client_id": "client_id",
"params": {
"amount": 500,
"callback_url": "https://webhook.site/",
"country_id": 3,
"currency": "XOF",
"mobile": "2257xxxxxx00",
"name": "Joseph Mike",
"payment_method_type": "Orange",
"third_party_reference_1": null,
"third_party_reference_2": null
},
"salt": "salt value",
"sender": "Integration name",
"timestamp": "",
"signature": "1xxxxxxxxxxxxxxxx8"
}
```
* Verify the transaction using our verification API with the client's OTP and payment token fetched from the response: /v4/niobi-unified-process-transaction
```json theme={null}
{
"client_id": "TLNnEXPhsFmBUbU6vwVPVAVZCLVUC7OL2Bu5HnQo",
"params": {
"otp": "1234",
"payment_token": "7cb952a7-6477-4ff0-8736-3dd9488b6aa3"
},
"salt": "niobi_salt",
"sender": "ApexVim",
"timestamp": 1744788807,
"signature": "3a3cf7f2e3ff8d4ee991d7e1323312887dfda8536b8c2e197d0d4ef764013658"
}
```
Find the full reference under [collection methods](/collecting-payments/methods).
### **Updated Transaction Limits**
* We have updated our transaction limits for some payment methods. Please refer to the full list of transaction limits here: [transaction limits](/transaction-limits).
### **Updated Supported Countries**
* Discontinued **Rwanda** temporarily. Please find the full list of supported countries [here](/supported-countries)
### **Added Best Practices Guide**
* A new guide is available to help you integrate more effectively. Find more details [here](/collecting-payments/best-practices)
# 2025-09-03 Changes
Source: https://docs.niobi.co/release-notes/03-09-2025
**Release Date:** 03/09/2025
## **Overview**
This release includes updates on external references within the Payouts API and changes to TZS transaction limits.
***
## **1. Unique Third-Party Reference Check**
You can now include flags in your disbursement request to prevent processing duplicate payouts based on `third_party_reference_1` and `third_party_reference_2`.
To enable this check, set the following fields to true `1`:
```json theme={null}
{
"is_third_party_reference_1_unique": 1,
"is_third_party_reference_2_unique": 1
}
```
When these fields are set to true(1), any payout request with duplicate third-party references will be rejected. If the flags are not included, the transaction will proceed as normal.
These fields are optional and can help improve the safety and integrity of your payouts.
Sample Disbursement Request with Duplicate Third-Party Reference
```json theme={null}
{
"client_id": "Axxxxxxxxxxxxxxxxxxxxxxxxxxxxxz",
"salt": "justrandomstring",
"sender": "John.co",
"timestamp": xxxxxxxxx,
"signature": "2193dxxxxxxxxxxxxxxxxxx5d5807",
"params": {
"Orange": [
{
"phone_number": "22178xxxxxxx"
}
],
"amount": 100,
"city": "Kenya",
"client_callback_url": "https://webhook.site/",
"country_id": 9,
"currency": "XOF",
"description": "Testing email",
"email": "johnd@example.com",
"first_name": "Test",
"is_third_party_reference_1_unique": 1,
"is_third_party_reference_2_unique": 1,
"last_name": "Tester",
"line1": "123",
"mobile": "2217xxxxxxxx",
"payment_method_type": "Orange",
"payment_reference": "SENXOFORANGEPAYOUTTEST2",
"postal_code": "000",
"third_party_reference_1": "SENXOFORANGEPAYOUTTEST",
"third_party_reference_2": "SENXOFORANGEPAYOUTTEST"
}
}
```
**Sample Response**
```json theme={null}
{
"success": false,
"message": "We apologize as we were not able to process your payment request. Please try again later.",
"data": {
"error": true,
"message": "Duplicate third_party_reference_2 was found!"
}
}
```
**2. TZS Transaction Limit Update**
We have updated the transaction limits for TZS (Tanzanian Shilling) payouts.
These new limits are now in effect and apply to all transactions made in TZS.
Please review and adjust your integration accordingly to ensure compliance and avoid failed transactions.
For full details on the updated limits, refer to the [API documentation](/transaction-limits).
# 2026-03 Changes
Source: https://docs.niobi.co/release-notes/03-2026
**Release Date:** 03/2026
## **Overview**
This release includes updates to the Payment Link v3 request payload, a development environment base URL change, South Africa flow enhancements, updated transaction limits for UGX and RWF, XAF Cameroon payout requirements, and a new Orange sandbox flow.
***
## **Breaking Changes**
> The following changes require immediate updates to your integration. Failure to update may result in rejected or failed transactions.
***
### **1. Payment Link v3 - Optional currency\_country\_id Field**
The Payment Link v3 API request payload now supports an optional currency\_country\_id field. This field identifies the country of the payer (customer) making the payment, based on the selected currency. It is used to support multi-country currency scenarios and ensures payments are routed correctly depending on the payer’s location.
**Sample Request Payload (Payment Link v3)**
```json theme={null}
{
"client_id": "Axxxxxxxxxxxxxxxxxxxxxxxxxxxxxz",
"salt": "justrandomstring",
"timestamp": "",
"signature": "2193dxxxxxxxxxxxxxxxxxx5d5807",
"params": {
"amount": 500,
"currency": "KES",
"currency_country_id": 1,
"description": "Payment for Order #1234",
"email": "customer@example.com",
"first_name": "Jane",
"last_name": "Doe",
"payment_reference": "ORDER1234",
"business_name": "my business",
"item_name": "item name",
"callback_url": "https://webhook.site/unique-id",
"redirection_url": "https://webhook.site/unique-id",
"logo": null,
"country_id": 2,
"phone": "25471xxxxxxx"
}
}
```
`currency_country_id` - **Optional.**
The numeric ID representing the country associated with the transaction currency. Refer to the [Countries reference](/supported-countries) for the full list of IDs. You can find more details on the request payload [here](/api-reference/payment-links/create)
> If omitted, the system will infer the country from `currency` and `country_id`.
***
### **2. Sandbox Environment Base URL Change**
The base URL for the sandbox environment has been updated. Please update all sandbox API calls and webhook configurations immediately.
**Previous base URL:**
```
https://dev.users.niobi.co
```
**New base URL:**
```
https://sandbox.users.niobi.co
```
> The previous URL will be deprecated. Please migrate as soon as possible.
***
### **3. XAF Cameroon Payouts - Sender Name & Sender Number Now Required**
XAF payouts to Cameroon now require `sender_firstname`, `sender_lastname` and `sender_phone_number` in the payout request to comply with local regulatory requirements.
(additional fields omitted for brevity)
```json theme={null}
{
"params": {
"country_id": 4,
"currency": "XAF",
"sender_firstname": "John",
"sender_lastname": "Doe",
"sender_phone_number": "2376XXXXXXXX",
"amount": 5000
}
}
```
* `sender_firstname`, `sender_lastname` - Full name of the sender initiating the payout.
* `sender_phone_number` - Phone number of the sender in international format.
* Refer to the full request payload [here](/making-payments/methods)
> Requests submitted without these fields will fail.
***
## **New Features & Updates**
***
### **4. South Africa Flow - successful\_url and failure\_url** *(Breaking Change)*
The South Africa payment flow now supports `successful_url` and `failure_url` redirect parameters, allowing you to redirect customers to custom pages upon payment completion or failure.
(additional fields omitted for brevity)
```json theme={null}
{
"params": {
"country_id": 15,
"currency": "ZAR",
"successful_url": "https://yoursite.com/payment/success",
"failure_url": "https://yoursite.com/payment/failure"
}
}
```
* `successful_url` - The URL to redirect the customer to upon a successful payment.
* `failure_url` - The URL to redirect the customer to if the payment fails or is cancelled.
> Both fields are required. You can find more details on the request payload [here](/collecting-payments/methods)
***
### **5. Updated UGX Transaction Limits**
We have updated the transaction limits for UGX (Ugandan Shilling) payouts. These new limits are now in effect and apply to all UGX transactions.
Please review and adjust your integration accordingly to ensure compliance and avoid failed transactions.
For full details on the updated limits, refer to the [transaction limits page](/transaction-limits#uganda).
***
### **6. New Ivory Coast Orange Sandbox Flow**
The Ivory Coast Orange Flow is now supported on the sandbox environment to simulate the full production flow, allowing for more accurate pre-production validation. Refer to the [Ivory Coast Orange Flow](/collecting-payments/methods) for full setup instructions and [testing phone numbers](/testing#côte-divoire---orange) for numbers you can use to test the flow on sandbox.
***
### **7. Portal IP Whitelisting Management**
A new portal feature is live allowing users to manage IP whitelisting directly in the dashboard. In Workspace > IP Whitelisting, you can add, edit, or delete IP entries without contacting support.
* Add new IP addresses using the “Add New” button.
* Modify existing entries with “Edit”.
* Remove entries with “Delete”.
> This feature streamlines secure API access setup and speeds up environment transitions. You can view more details [here](/ip-whitelisting#portal-ip-whitelisting-guide)
# API Version 4 Release Notes
Source: https://docs.niobi.co/release-notes/api-v4
**Release Date:** 12/03/2025
## **Overview**
This release introduces significant updates and improvements to the API, enhancing transaction processing, standardizing data formats, and expanding support for payouts and pay-ins across multiple currencies.
***
## **Breaking Changes**
The API has been upgraded to **Version 4 (V4)** to improve stability, security, and performance. This version includes some changes that should be considered when migrating from the previous version.
### **Standardized Amount Handling**
* **Updated both collection and disbursement APIs** to use absolute amounts.\
**Previously:** Payout amounts were multiplied by 100.\
**Sample V3 API Payout Request for 50 XOF:**
```json theme={null}
{
"client_id": "client_id",
"params": {
"amount": 5000,
"city": "CustomerCity",
"client_callback_url": "https://webhook.site/url",
"country_id": "2",
"currency": "XOF",
"email": "john.doe@example.com",
"first_name": "John",
"last_name": "Doe",
"line1": "CustomerStreet",
"mobile": "22991600649",
"mtn": [
{
"phone_number": "22991600649"
}
],
"payment_method_type": "mtn",
"payment_reference": "",
"postal_code": "192.0.0.1",
"third_party_reference_1": ""
},
"salt": "mySalt123",
"sender": "Integration name",
"timestamp": "Timestamp value",
"signature":"XxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxX"
}
```
**Now:** All amounts are passed in their absolute form to maintain consistency across all transactions.\
**Sample V4 API Payout Request for 50 XOF:**
```json theme={null}
{
"client_id": "client_id",
"params": {
"amount": 50,
"city": "CustomerCity",
"client_callback_url": "https://webhook.site/url",
"country_id": "2",
"currency": "XOF",
"email": "john.doe@example.com",
"first_name": "John",
"last_name": "Doe",
"line1": "CustomerStreet",
"mobile": "22991600649",
"mtn": [
{
"phone_number": "22991600649"
}
],
"payment_method_type": "mtn",
"payment_reference": "",
"postal_code": "192.0.0.1",
"third_party_reference_1": ""
},
"salt": "mySalt123",
"sender": "Integration name",
"timestamp": "Timestamp value",
"signature":"XxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxX"
}
```
### **Callback Enhancements**
* **Removed** `invoice_no` and `niobi_ref` fields from callback responses for payouts.
* **Added** a `currency` field for complete payment details.\
**Sample V3 Callback Response for Payouts:**
```json theme={null}
{
"client_id": "Your client id",
"sender": "Integration title",
"salt": "Random salt value",
"timestamp": "Timestamp",
"params": {
"account": "The account number of the account the payment was made from",
"account_number" : "Account number for paybill payment method",
"amount": "Amount paid out",
"fee": "Fees incurred for the payment",
"invoice_no": "Payout invoice number",
"niobi_ref":"Unique reference generated by Niobi for the transaction",
"message": "Message describing the transaction status",
"mobile": "Mobile number of the recipient",
"payee": "Full name of the recipient",
"payment_channel": "Payment channel",
"payment_reference": "Unique information passed by user to identify transaction",
"payoutId": "Unique transaction id of the payment",
"ref": "Transaction reference shared by provider",
"statusCode": "Status code of the transaction",
"third_party_reference_1": "External reference",
"third_party_reference_2": "External reference",
"transaction_date": "Date payment was made",
"transaction_detail": "Message with details of the transaction",
"type": "Transaction type"
},
"signature": "Request signature"
}
```
**Sample V4 Callback Response for Payouts:**
```json theme={null}
{
"client_id": "Your client id",
"sender": "Integration title",
"salt": "Random salt value",
"timestamp": "Timestamp",
"params": {
"account": "The account number of the account the payment was made from",
"account_number" : "Account number for paybill payment method",
"amount": "Amount paid out",
"currency":"Currency of the payment",
"fee": "Fees incurred for the payment",
"message": "Message describing the transaction status",
"mobile": "Mobile number of the recipient",
"payee": "Full name of the recipient",
"payment_channel": "Payment channel",
"payment_reference": "Unique information passed by user to identify transaction",
"payoutId": "Unique transaction id of the payment",
"ref": "Transaction reference shared by provider",
"statusCode": "Status code of the transaction",
"third_party_reference_1": "External reference",
"third_party_reference_2": "External reference",
"transaction_date": "Date payment was made",
"transaction_detail": "Message with details of the transaction",
"type": "Transaction type"
},
"signature": "Request signature"
}
```
**Sample V4 Callback Response for Payins:**
```json theme={null}
{
"client_id": "Your client id",
"sender": "Integration title",
"salt": "Random salt value",
"timestamp": "Timestamp",
"params": {
"amount": "Amount deposited",
"currency":"Transaction currency",
"depositId": "Transaction id of the deposit",
"email": "Your email address",
"failureReason": {
"failureCode": "Failure Code from MNO in case of failure",
"failureMessage": "Descriptive message on the failure reason in case of failure"
},
"mobile": "Mobile that made the deposit",
"message": "Message describing deposit success or failure",
"name": "Account username",
"payment_link_id": null,
"payment_link_payment_id": null,
"reference" : " Transaction id",
"status": "Status of the deposit",
"statusCode": "Status code of the transaction",
"third_party_reference_1": "TRANS1",
"third_party_reference_2": null,
},
"signature": "Request signature"
}
```
### **Payment Methods Standardization**
* **Removed case sensitivity** for payment methods (e.g., `MTN`, `TNM`, `Moov`).
**Sample Ways to Pass the Payment Methods:**
```json theme={null}
"payment_method_type": "moov"
```
```json theme={null}
"payment_method_type": "Moov"
```
```json theme={null}
"payment_method_type": "MOOV"
```
***
## **New Features and API Improvements**
### **NGN Payouts and Payins**
* Added support for Nigerian transactions.
* NGN deposits now require two fields:
* `account_name`
* `name`
```json theme={null}
{
"client_id": "client_id",
"params": {
"account_name": "BTC Bank",
"amount": 5500,
"callback_url": "https://webhook.site/",
"country_id": 17,
"currency": "NGN",
"mobile": "2347xxxxxx00",
"name": "Joseph Mike",
"payment_method_type": "bank",
"third_party_reference_1": null,
"third_party_reference_2": null
},
"salt": "salt value",
"sender": "Integration name",
"timestamp": xxxxxxx,
"signature": "1xxxxxxxxxxxxxxxx8"
}
```
* Users can now send and receive payments in Nigeria via bank transfers. See more under [collection methods](/collecting-payments/methods).
### **GHS Payouts and Payins**
* Added support for Ghanaian Cedi (GHS) transactions.
* Users can now send and receive payments in GHS via mobile money through MTN, Vodafone, and Tigo. See more under [collection methods](/collecting-payments/methods).
### **Wave Senegal Updates**
* Added support for **Wave Senegal**.
* Now includes:
* **Successful URL** for completed transactions
* **Failure URL** for failed transactions
Find the full reference under [collection methods](/collecting-payments/methods).
### **Updated Supported Countries**
* Discontinued **DRC and Malawi** temporarily due to settlement inefficiencies and FX volatility. Please find the full list of supported countries [here](/supported-countries)
### **Updated Test Phone Numbers**
* Updated test phone numbers to simulate real-world cases.
* Added a **test account number** for **KES Paybill transactions**. Refer to the full list of test phone numbers [here.](/testing)
### **Improved Error Pages for Troubleshooting**
* Updated error pages under the **Troubleshooting** section to make debugging easier for developers.
* Clearer error descriptions and recommended resolutions are now included. Please refer to the [Troubleshooting Section](/errors/overview) for more details.
# Release Notes - 10.0.0 - September 2026
Source: https://docs.niobi.co/release-notes/release-10.0.0
Adds the Verify OTP endpoint (/api/v4/niobi-unified-process-transaction) to official API References with interactive schemas and clarified two-step flow guidance.
**Release Date:** September 2026
## TL;DR
* **Added to API Reference:** The Verify OTP endpoint (`POST /api/v4/niobi-unified-process-transaction`), previously supported and announced in earlier releases, is now formally documented in the [API Reference](/api-reference/collections/verify-otp) with interactive schemas, parameter definitions, and error codes.
* **Clarified Two-Step Flow:** Streamlined end-to-end guidance and sequence diagrams for corridors requiring interactive OTP authorization (e.g. Côte d'Ivoire Orange Money), clarifying the separation between Step 1 (`payment_token` issuance) and Step 2 (OTP verification).
***
## Updates & Improvements
### Verify OTP Published in API Reference (`/api/v4/niobi-unified-process-transaction`)
Certain mobile money operators, such as **Orange Money in Côte d'Ivoire** (`country_id: 3`, `currency: "XOF"`), require the payer to generate or receive an authorization one-time password (OTP) before funds can be debited.
While this two-step capability was previously supported, Release 10.0.0 brings this endpoint into the core **API Reference** suite with full documentation, interactive testing, and clearer developer guidance:
* **Endpoint:** `POST /api/v4/niobi-unified-process-transaction`
* **API Reference:** [Verify OTP](/api-reference/collections/verify-otp)
* **Guide:** [OTP-Verified Payins](/collecting-payments/methods#5-otp-verified-payins-c%C3%B4te-divoire-orange)
***
### The Two-Step Payment Flow
```mermaid theme={null}
sequenceDiagram
autonumber
actor Customer as Customer (Handset)
participant Client as Your Backend / App
participant Niobi as Niobi API
participant Telco as Mobile Operator (e.g. Orange CI)
Note over Client,Niobi: Step 1: Collection Initiation
Client->>Niobi: POST /api/v4/niobi-unified-collections (payment_method_type: "orange")
Niobi->>Telco: Request authorization session
Telco-->>Customer: Deliver OTP via SMS (or customer dials #144*77#)
Niobi-->>Client: Return payment_step: 1 & payment_token
Note over Customer,Client: Customer enters received OTP in your checkout UI
Note over Client,Niobi: Step 2: OTP Verification
Client->>Niobi: POST /api/v4/niobi-unified-process-transaction (otp, payment_token)
Niobi->>Telco: Validate OTP & debit wallet
Telco-->>Niobi: Debit confirmed
Niobi-->>Client: Immediate response (payment_step: 2, status: "success")
Niobi-->>Client: Asynchronous webhook callback (payment_step: 2)
```
#### Step 1: Initiation (`POST /api/v4/niobi-unified-collections`)
Submit the standard signed collection payload specifying `payment_method_type: "orange"` and the customer's mobile number:
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"salt": "random_salt_12345",
"signature": "SHA256_SIGNATURE",
"params": {
"amount": 2000,
"currency": "XOF",
"country_id": 3,
"mobile": "2250700000000",
"payment_method_type": "orange",
"callback_url": "https://yourdomain.com/niobi/callback",
"third_party_reference_1": "TX-CI-COLL-1001"
}
}
```
**Step 1 Response:** Niobi returns an acknowledgement with `payment_step: 1` and a `payment_token`:
```json theme={null}
{
"success": true,
"message": "We've sent a verification prompt to your phone. Please approve it to complete your payment.",
"data": {
"status": "success",
"amount": 2000,
"currency": "XOF",
"country_id": 3,
"payment_token": "NIO-S0000001",
"payment_step": 1
},
"status_code": "000000"
}
```
In production, the authorization OTP is valid for **2 minutes**.
***
#### Step 2: Verification (`POST /api/v4/niobi-unified-process-transaction`)
Prompt the customer to enter the OTP in your application, then dispatch a signed request containing the `payment_token` from Step 1 and the customer's `otp`:
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835620,
"salt": "random_salt_67890",
"signature": "SHA256_SIGNATURE",
"params": {
"otp": "1234",
"payment_token": "NIO-S0000001"
}
}
```
**Step 2 Response:** The endpoint completes debiting and returns the synchronous result with `payment_step: 2`:
```json theme={null}
{
"success": true,
"message": "Verification token was sent to your mobile number. Kindly verify to proceed payment.",
"data": {
"status": "success",
"amount": 2000,
"currency": "XOF",
"payment_step": 2,
"third_party_reference_1": "TX-CI-COLL-1001"
},
"status_code": "000000"
}
```
A terminal asynchronous webhook callback (`payment_step: 2`, `status: "success"`) is also dispatched to your `callback_url`.
***
### Sandbox Testing
To test this flow in the sandbox environment:
* Initiate the collection with any Côte d'Ivoire test MSISDN.
* In Step 2, pass `"otp": "000000"` to simulate a successful verification.
***
### Documentation Links
* [Verify OTP API Reference](/api-reference/collections/verify-otp)
* [Unified Collections API Reference](/api-reference/collections/unified-collections)
* [Côte d'Ivoire Country Guide](/countries/cote-d-ivoire#3-orange-money-otp-flow)
* [Collection Methods Guide](/collecting-payments/methods#5-otp-verified-payins-c%C3%B4te-divoire-orange)
# Release Notes - 7.0.0 - May 2026
Source: https://docs.niobi.co/release-notes/release-7.0.0
**Release Date:** May 2026
## TL;DR
* Sub-Merchant routing - attach transactions to individually managed sub-merchants
* Sandbox base URL changed from `dev.users.niobi.co` to `sandbox.users.niobi.co`
* Cameroon XAF payouts now require sender name and phone number
* South Africa collections require `successful_url` and `failure_url`
* New Transaction Status v3 endpoint for unified payin and payout status checks
* Payment Link v3 supports optional `currency_country_id`
* UGX and RWF transaction limits have been updated
## Breaking Changes
The changes below require immediate integration updates.
### 1. Sub-Merchant Management
This feature is for marketplaces, payment service providers, and enterprises that manage multiple merchants under a single Niobi account. It lets you onboard each sub-merchant in the portal, review them through a compliance workflow, and then attach each transaction to a specific `sub_merchant_id`.
#### How it works
* Create a sub-merchant in the Niobi portal.
* The sub-merchant enters review and moves through these statuses: **Pending**, **Approved**, or **Suspended**.
* Only **Approved** sub-merchants can process live transactions.
* Include `sub_merchant_id` in the transaction payload to route payments to the selected sub-merchant.
#### Who it is for
* Marketplaces settling funds to individual vendors.
* PSPs integrating multiple merchants.
* Businesses that need separate merchant attribution for compliance, reporting, or risk controls.
#### API usage
Include `sub_merchant_id` inside `params` for eligible transactions:
(additional fields omitted for brevity)
```json theme={null}
{
"client_id": "",
"salt": "",
"timestamp": 1234567890,
"signature": "",
"params": {
"amount": 1000,
"currency": "KES",
"country_id": 1,
"sub_merchant_id": "",
"description": "Payment for Order #1234"
}
}
```
#### Status details
* **Pending**: Submitted and awaiting review by the compliance team. Do not send live transactions yet.
* **Approved**: The sub-merchant has passed review and is cleared for live processing.
* **Suspended**: The sub-merchant is blocked due to compliance or risk issues. Contact support to resolve.
#### Steps
1. Confirm whether your integration uses sub-merchant routing.
2. Add `sub_merchant_id` to all relevant transaction requests.
3. Check the sub-merchant status in the portal before sending live traffic.
4. Review the guide for onboarding, approval, and status definitions.
See: [Sub-Merchant Management](../submerchant/sub-merchant-management)
***
### 2. Sandbox Base URL Change
All sandbox API calls and sandbox webhook endpoints must be updated to use the new sandbox URL.
**Previous sandbox base URL:**
```
https://dev.users.niobi.co
```
**New sandbox base URL:**
```
https://sandbox.users.niobi.co
```
#### Steps
1. Search your sandbox integration for `dev.users.niobi.co`.
2. Replace it with `sandbox.users.niobi.co` in all API client code and webhook URLs.
3. Restart your sandbox environment and rerun tests.
4. Confirm sandbox webhook delivery to the updated callback endpoint.
> Remaining traffic to `dev.users.niobi.co` **will fail** after deprecation.
***
### 3. XAF Cameroon Payouts Now Require Sender Details
XAF payouts to Cameroon now require the sender's first name, last name, and phone number in the payout request to comply with local regulatory requirements.
**Required fields:**
* `sender_firstname`
* `sender_lastname`
* `sender_phone_number`
**Example request:**
(additional fields omitted for brevity)
```json theme={null}
{
"client_id": "",
"salt": "",
"timestamp": 1234567890,
"signature": "",
"params": {
"country_id": 4,
"currency": "XAF",
"amount": 5000,
"sender_firstname": "John",
"sender_lastname": "Doe",
"sender_phone_number": "2376XXXXXXXX"
}
}
```
**Implementation checklist:**
1. Add `sender_firstname`, `sender_lastname`, and `sender_phone_number` to all XAF/Cameroon payout requests.
2. Format `sender_phone_number` as `237XXXXXXXXX` - country code 237 followed by the local number, no `+` prefix.
3. Ensure your request builder includes these fields for all `country_id: 4` / `currency: XAF` payouts, and update any payload validation logic accordingly.
Requests without these fields will be rejected.
See: [Cameroon XAF payout sample](../making-payments/methods)
***
### 4. South Africa (ZAR) Collections Now Require Redirect URLs
South Africa EFT collections now require both `successful_url` and `failure_url` in the request payload. After the customer completes or cancels the bank transfer, they are redirected to the appropriate URL.
**Example request snippet:**
(additional fields omitted for brevity)
```json theme={null}
{
"client_id": "",
"salt": "",
"timestamp": 1234567890,
"signature": "",
"params": {
"country_id": 15,
"currency": "ZAR",
"amount": 250,
"successful_url": "https://yoursite.com/payment/success",
"failure_url": "https://yoursite.com/payment/failure"
}
}
```
**Implementation checklist:**
1. Create a success page and a failure page in your application.
2. Populate `successful_url` and `failure_url` in the request payload.
3. After the customer is redirected back to your site, query the [Transaction Status API](/api-reference/get-transaction-status) to confirm the final transaction outcome before displaying a result to the customer.
See: [South Africa ZAR Bank / EFT](../collect-v4#south-africa-zar-bank-collections)
***
## New Features
These features are available and can be integrated now.
### 1. Transaction Status v3
**New endpoint:** `POST /api/v3/get-unified-transaction-status`
This endpoint replaces the v2 status endpoint and returns a richer, unified response for both payins and payouts in a single call. Previously, payin and payout status responses had different structures; v3 normalises them.
**Purpose:**
* Returns unified metadata for both payins and payouts.
* Simplifies transaction reconciliation across all payment channels.
**Example request:**
```json theme={null}
{
"client_id": "",
"salt": "",
"timestamp": 1234567890,
"signature": "",
"params": {
"id": "",
"type": "payin"
}
}
```
**Example response:**
```json theme={null}
{
"success": true,
"message": "Transaction data fetched successfully.",
"data": {
"amount": "Transaction amount",
"fee": "Transaction fee",
"currency": "Currency code",
"status": "Transaction status",
"payee": "Payee name (payouts only)",
"invoice_no": "Invoice number (payouts only)",
"mobile": "2547xxxxxxxx",
"account_number": "Account number (where applicable)",
"ref": "NIO-SXXXXX",
"balance": "Wallet balance (where applicable)",
"transaction_detail": "Additional transaction detail",
"payment_channel": "Payment channel used",
"transaction_date": "Transaction date",
"created_at": "Record creation date",
"updated_at": "Last updated date",
"callback_url": "https://yoursite.com/callback",
"status_code": "Status code",
"failureReason": { "failureCode": null, "failureMessage": null }
}
}
```
**Integration notes:**
1. Send `params.id` with the transaction reference you want to look up.
2. Set `params.type` to `payin` for collections or `payout` for disbursements.
See: [Get Transaction Status (v3)](/api-reference/get-transaction-status)
***
### 2. Conditional SMS for KES Payouts
SMS notifications to beneficiaries can now be enabled for KES payouts. When active, the recipient receives an SMS confirming the payment after a successful disbursement.
This is an account-level configuration — it does not require any changes to your API requests.
**Steps:**
1. Contact support at [support@niobi.co](mailto:support@niobi.co) or via your Slack/WhatsApp support channel to request the feature be enabled for your account.
2. Once enabled, run a test KES payout and confirm the beneficiary receives the SMS notification.
See details [here](/making-payments/methods)
***
### 3. Portal IP Whitelisting Management
You can now manage IP whitelist entries directly in the dashboard without contacting support.
**Steps:**
1. Go to **Workspace > IP Whitelisting** in the portal.
2. Add new IP addresses using the **Add New** button.
3. Update an existing entry any time your server's IP changes to avoid losing API access.
This removes the need to raise a support request for routine IP changes and speeds up environment transitions. See more details [here](/ip-whitelisting#portal-ip-whitelisting-guide)
***
## Updates
These improvements are available now and may require optional integration updates.
### 1. Payment Link v3 — Optional `currency_country_id`
The Payment Link v3 payload now supports an optional `currency_country_id` field. Use it when the payer's physical location differs from the country implied by the `country_id` in the request — for example, when a Kenyan customer is paying via a Uganda-issued currency account.
**Example:**
```json theme={null}
{
"client_id": "",
"salt": "",
"timestamp": 1234567890,
"signature": "",
"params": {
"amount": 500,
"currency": "KES",
"currency_country_id": 1,
"description": "Payment for Order #1234",
"email": "customer@example.com",
"first_name": "Jane",
"last_name": "Doe",
"payment_reference": "ORDER1234",
"business_name": "My Business",
"item_name": "item name",
"callback_url": "https://yoursite.com/callback",
"redirection_url": "https://yoursite.com/redirect",
"country_id": 2,
"phone": "25471xxxxxxx"
}
}
```
* `currency_country_id` is optional. When provided, it explicitly identifies the payer's country.
* If omitted, the system infers the payer country from `currency` and `country_id`.
See: [Payment Link v3 payload](../api-reference/payment-links/create)
***
### 2. Ivory Coast Orange Sandbox Flow
The Ivory Coast Orange (telecom operator) OTP payment flow is now supported in the sandbox environment, allowing you to simulate the full production flow before going live.
**Steps:**
1. Set your sandbox base URL to `https://sandbox.users.niobi.co`.
2. Implement the OTP redirect flow as documented in the Ivory Coast Orange guide.
3. Use the sandbox test phone numbers from the Testing Guide to trigger the OTP flow.
4. Complete the full OTP verification cycle and confirm expected callbacks before going live.
See:
* [Ivory Coast Orange Flow](../collecting-payments/methods)
* [Testing numbers](../testing#côte-divoire---orange)
***
### 3. Updated UGX and RWF Transaction Limits
Transaction limits for UGX (Ugandan Shilling) and RWF (Rwandan Franc) payouts have been updated and are now in effect.
**Steps:**
1. Review the new limits in the transaction limits reference and compare against any hardcoded min/max values in your integration.
2. Update your payload validation logic to reflect the new limits for UGX and RWF transactions.
3. Run test payouts at the boundary values to confirm your integration handles them correctly.
See: [Uganda transaction limits](../transaction-limits#uganda)
***
## Sources
* March Release original: [2026-03 Changes](./03-2026)
* Sub-Merchant guide: [Sub-Merchant Management](../submerchant/sub-merchant-management)
* Transaction Status v3 API page: [Get Transaction Status (v3)](/api-reference/get-transaction-status)
* Portal IP Whitelisting: [Portal IP Whitelisting](../ip-whitelisting#portal-ip-whitelisting-guide)
# Release Notes - 8.0.0 - June 2026
Source: https://docs.niobi.co/release-notes/release-8.0.0
**Release Date:** June 2026
## TL;DR
* NGN collections now require `first_name` and `last_name` instead of the `name` field, plus a `bank_code` to select the issuing bank for the virtual account
* ZAR collections `payment_method_type` has changed from `eft` to `bank`
## Breaking Changes
The changes below require immediate integration updates.
### 1. NGN Collections (Virtual Accounts) — `first_name`, `last_name`, and `bank_code` Now Required
The single `name` field for Nigerian (NGN) collections has been replaced with separate `first_name` and `last_name` fields. In addition, a `bank_code` is now required to specify which bank should issue the virtual account.
**Previous required fields:**
* `name`
* `account_name`
**New required fields:**
* `first_name`
* `last_name`
* `account_name`
* `bank_code`
#### Supported bank codes
| Bank Code | Bank Name |
| --------- | -------------------------- |
| 17001 | 78 Finance Company Limited |
| 17002 | Fidelity Bank Limited |
| 17003 | Globus Bank Limited |
#### Example request
```json theme={null}
{
"client_id": "client_id",
"params": {
"account_name": "Joseph Mike",
"amount": 10000,
"bank_code": "17001",
"callback_url": "https://example.com",
"country_id": 17,
"currency": "NGN",
"first_name": "Joseph",
"last_name": "Mike",
"mobile": "234xxxxxxxxx0",
"payment_method_type": "virtual_account",
"third_party_reference_1": "",
"third_party_reference_2": ""
},
"salt": "salt_value",
"sender": "Integration_name",
"timestamp": 1234567890,
"signature": "0xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx5"
}
```
**Implementation checklist:**
1. Remove the `name` field from all NGN collection requests.
2. Add `first_name` and `last_name` with the customer's split name.
3. Add `bank_code` using one of the supported bank codes listed above.
4. Requests missing any of these fields will be rejected.
See: [NGN Virtual Account Collections](../collect-v4#ngn-virtual-account-collections)
***
### 2. ZAR Collections — `payment_method_type` Changed from `eft` to `bank`
The payment method type for South Africa (ZAR) collections has changed. Any request still passing `payment_method_type: "eft"` will be rejected.
**Previous value:**
```
"payment_method_type": "eft"
```
**New value:**
```
"payment_method_type": "bank"
```
#### Example request
```json theme={null}
{
"client_id": "your_client_id",
"params": {
"amount": 500,
"bank_code": "0001",
"callback_url": "https://webhook.site/",
"country_id": 15,
"currency": "ZAR",
"mobile": "276xxxxxxxx8",
"name": "John Doe",
"payment_method_type": "bank",
"third_party_reference_1": "ZAR_DEPOSIT",
"successful_url": "https://yoursite.com/payment/success",
"failure_url": "https://yoursite.com/payment/failure"
},
"salt": "unique_salt",
"sender": "Integration Name",
"timestamp": 1234567890,
"signature": "generated_signature"
}
```
**Implementation checklist:**
1. Find all ZAR collection requests in your integration.
2. Replace `"payment_method_type": "eft"` with `"payment_method_type": "bank"`.
3. Retest your South Africa collection flow in sandbox before deploying to production.
See: [South Africa ZAR Bank Collections](../collect-v4#south-africa-zar-bank-collections)
# Release Notes - 9.0.0 - July 2026
Source: https://docs.niobi.co/release-notes/release-9.0.0
**Release Date:** July 2026
## TL;DR
* KES Airtel is now supported for both deposits and payouts in Kenya
* Capitec is now supported as a South Africa (ZAR) collection method, requiring a new `id_number` field
* Payout responses and callbacks now include an explicit `payment_step` field to distinguish the initial acknowledgement from the final outcome
## New Features & Updates
### 1. Kenya — Airtel Money Support (Deposits & Payouts)
Airtel Money is now a supported payment method for Kenya (KES), for both deposits and payouts. Pass `Airtel` as the `payment_method_type` to use it.
**Transaction limits** are the same as KES Send Money (B2C) payouts:
| Payment Channel | Transaction Type | Minimum (KES) | Maximum (KES) |
| --------------- | ---------------- | ------------- | ------------- |
| **Airtel** | Payout, Deposit | 10 | 250,000 |
#### Example request
```json theme={null}
{
"client_id": "client_id",
"params": {
"amount": 500,
"country_id": 1,
"currency": "KES",
"mobile": "2547xxxxxxx",
"payment_method_type": "Airtel",
"third_party_reference_1": ""
},
"salt": "salt_value",
"sender": "Integration_name",
"timestamp": "",
"signature": "0xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx5"
}
```
See: [Kenya](/countries/kenya) and [Transaction Limits](/transaction-limits#kenya)
***
### 2. South Africa — Capitec Support (New `id_number` Field)
Capitec is now supported as a South Africa (ZAR) collection method. Pass `capitec` as the `payment_method_type` to use it.
Capitec accounts are linked to the customer's South African ID number rather than a bank code, so requests for this method must also include `id_number`.
**New required field for Capitec collections:**
* `id_number` — The customer's South African ID number.
#### Example request
```json theme={null}
{
"client_id": "your_client_id",
"params": {
"amount": 500,
"id_number": "9001015800083",
"callback_url": "https://webhook.site/",
"country_id": 15,
"currency": "ZAR",
"mobile": "276xxxxxxxx8",
"name": "John Doe",
"payment_method_type": "capitec",
"third_party_reference_1": "ZAR_DEPOSIT",
"successful_url": "https://yoursite.com/payment/success",
"failure_url": "https://yoursite.com/payment/failure"
},
"salt": "unique_salt",
"sender": "Integration Name",
"timestamp": "UNIX_timestamp",
"signature": "generated_signature"
}
```
See: [South Africa Capitec Collections](../collecting-payments/methods)
***
### 3. Payout `payment_step` — Distinguishing Initiation from Final Outcome
Payout responses and callbacks now include an explicit `payment_step` field so you can tell an initial acknowledgement apart from the terminal result:
* **`payment_step: 1`** — Included only in the initial API response when the payout was successfully initiated with the processor (`status_code: "000000"`). If initiation fails (`status_code: "000005"`), the response has no `payment_step` field at all. Either way, this is an acknowledgement of the request, not the final result.
* **`payment_step: 2`** — Included in the terminal callback, sent for both a successful and a failed transaction outcome. When present, treat the callback as final: act on whatever `status` and `statusCode` is in the payload.
#### Sample initial response (success)
```json theme={null}
{
"success": true,
"message": "Payment process initiated successfully.",
"data": {
"reference": "S000001234",
"type": "MPESA Send Money",
"total": 500,
"creation_date": "2026-08-12T09:53:40.000000Z",
"execution_date": "2026-08-12T09:53:44.000000Z",
"from": "John Doe",
"to": "254712345678",
"invoice_date": "2026-08-12",
"invoice_amount": 500,
"payment_step": 1
},
"status_code": "000000"
}
```
#### Sample terminal callback (failed)
```json theme={null}
{
"client_id": "Your client id",
"params": {
"account": "80xxxxx3",
"amount": 10,
"currency": "KES",
"fee": 5,
"message": "Payment was not transferred.",
"mobile": "254123456789",
"payee": "JohnDoe 123",
"payment_channel": "Send Money",
"payment_reference": "OID51",
"payment_step": 2,
"payoutId": "bd8f51a2-xxxx-xxxx-xxxx-xxxxx9a",
"ref": "SII6ZH9PU0",
"status": "failed",
"statusCode": "000005",
"transaction_date": "2024-09-18 00:00:00",
"transaction_detail": "Disbursement made through M-Pesa Send Money",
"type": "Pay out"
},
"salt": "salt value",
"sender": "Integration name",
"timestamp": 123,
"signature": "7xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxb"
}
```
The `message` field in the initial response describes the result of initiating the payout, and its exact wording may vary by processor or channel. Don't use it to determine success or failure: rely on `payment_step` and `status_code`/`status` instead.
See: [Making Payments](/making-payments/methods), [Collect Payments](/collecting-payments/methods), [Collection Callbacks](/collecting-payments/callbacks), and [Payout Callbacks](/making-payments/callbacks)
# Sub-Merchant Management
Source: https://docs.niobi.co/submerchant/sub-merchant-management
Learn how to onboard and manage sub-merchants for multi-vendor marketplaces, platforms, and aggregators using the Niobi Dashboard.
## Overview
Sub-Merchant Management allows platforms, marketplaces, aggregators, and payment service providers (PSPs) to onboard and manage distinct merchant entities under a single master Niobi account.
### Common Use Cases:
* **Online Marketplaces:** Attribute and settle transactions to specific vendors.
* **Payment Facilitators & PSPs:** Onboard individual businesses under a single master integration.
* **Multi-Brand Enterprises:** Segregate reporting, audits, and compliance tracking across regional business units.
***
## Sub-Merchant Onboarding Fields
When onboarding a sub-merchant in the Niobi Dashboard, provide the following information:
| Form Field | Identifier | Requirement | Description | Example |
| :------------------------ | :---------------------- | :----------: | :--------------------------------------------------------------- | :------------------------- |
| **Legal Name** | `legal_name` | **Required** | The registered legal corporate name of the sub-merchant entity. | `ABC Corporation Ltd` |
| **Brand Name** | `brand_name` | Optional | The trading or consumer-facing brand name. | `ABC Corp` |
| **License Type** | `license_type` | **Required** | The regulatory or operational license type held by the business. | `Money Transfer License` |
| **License Jurisdiction** | `license_jurisdiction` | **Required** | The regulatory authority or territory that issued the license. | `Central Bank of Kenya` |
| **Website** | `website` | **Required** | The sub-merchant's official corporate or e-commerce URL. | `https://example.com` |
| **Business Activity** | `business_activity` | **Required** | The primary industry category or business model. | `E-commerce, SaaS, Retail` |
| **Incorporation Country** | `incorporation_country` | **Required** | The country where the entity was legally incorporated. | `Kenya` |
| **Transaction Country** | `transaction_country` | **Required** | The country where the sub-merchant will process payments. | `1` (Kenya) |
**Compliance Verification:**\
Please ensure all onboarded sub-merchants align with Niobi's compliance policies. Once submitted, new sub-merchants enter a compliance review workflow before processing live transactions.
***
## Adding a Sub-Merchant via Dashboard
To onboard a new sub-merchant:
1. Log in to your [Niobi Dashboard](https://users.niobi.co).
2. Navigate to **Workspace -> Sub-Merchant**.
3. Click the **Add New Sub-Merchant** button.
4. Complete the required corporate, licensing, and transaction country details.
5. Click **Create Sub-Merchant** to submit the entity for compliance review.
6. Once created, the sub-merchant will appear in your **Sub-Merchant List** with its assigned unique UUID (`sub_merchant_id`).
***
## Linking Sub-Merchants to Payments & Collections
Once a sub-merchant is created, obtain their unique `id` (UUID format, e.g. `3fa85f64-5717-4562-b3fc-2c963f66afa6`) from the dashboard list.
Pass this UUID in the `sub_merchant_id` field inside `params` when initiating collections or disbursements:
### Sample Collection Request with `sub_merchant_id`
```json theme={null}
{
"client_id": "YOUR_CLIENT_ID",
"sender": "YOUR_INTEGRATION_NAME",
"timestamp": 1724835600,
"salt": "random_salt_12345",
"signature": "c6b98e1f5d6a7890bc4e123456789abcdef0123456789abcdef0123456789abc",
"params": {
"amount": 10000,
"currency": "KES",
"country_id": 1,
"mobile": "254700000000",
"payment_method_type": "send money",
"sub_merchant_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"callback_url": "https://yourdomain.com/niobi/callback",
"third_party_reference_1": "ORDER-9912",
"third_party_reference_2": "CUST-450"
}
}
```
**Include in Signature Calculation:**\
Because `sub_merchant_id` is inside the `params` object, ensure it is included in your pre-signature object when generating the SHA-256 signature.
***
## Sub-Merchant Approval & Lifecycle Statuses
The `status` field on a sub-merchant record tells you whether it can process live transactions:
| Status | Live Transactions | Description |
| :------------- | :---------------: | :-------------------------------------------------------------------------------------------------------------------- |
| **`Inreview`** | No | The sub-merchant has been submitted and is under compliance verification. Test transactions in sandbox are supported. |
| **`Active`** | **Yes** | Verification is complete. The sub-merchant can process live collections and disbursements. |
| **`Inactive`** | No | The sub-merchant is not currently active. Live transactions are rejected. |
***
## Support & Inquiries
For questions regarding sub-merchant compliance, onboarding thresholds, or documentation requirements, reach out to our team at [support@niobi.co](mailto:support@niobi.co).
# Supported Countries
Source: https://docs.niobi.co/supported-countries
This section outlines the supported countries that you can use while making a payment using Niobi's Payment API. Please note that all countries are available in the sandbox environment. For production environment, only the countries you select will be available to you.
## List of Supported Countries
***
# Testing our API
Source: https://docs.niobi.co/testing
With Niobi's payment API, you'll get access to our sandbox environment, where you can safely test your integration with all available MNOs. Use special phone numbers (MSISDNs) to simulate different scenarios without involving real money or mobile wallets.
**Only the phone numbers listed below work in sandbox.** An arbitrary correctly-formatted number (e.g. a real number in valid international format) will not fall back to a generic success, it fails deterministically: deposits are marked `failed`, and payouts return `"Test phone number was not found."` (`status_code: "000005"`). Always pick a number from the tables below for the country, network, and scenario you want to simulate.
## Test Funds
The funds available in the sandbox environment do not represent real money. You can top up your sandbox account in two ways.
Simulate a payin to your account using any of the provided test phone numbers below for the specific currency you would like to top up. ([View available methods](https://docs.niobi.co/api-reference/collection/unified-collections))
You will then receive funds you can use to test payouts in the sandbox environment.
Request a manual deposit by contacting us through [support@niobi.co](mailto:support@niobi.co)
| Status Code | Status String | State Type | Meaning | Developer Action |
| :----------: | :-----------: | :--------------: | :--------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------ |
| **`000000`** | `"success"` | **Terminal** | Transaction completed and settled. Funds credited to recipient/merchant. | Fulfill the customer order, update user wallet balance, or mark payout as complete. |
| **`000001`** | `"pending"` | **Non-Terminal** | Authorization or upstream network switch processing is in progress. | **Do NOT act on this state.** Do not fulfill orders, retry transfers, or mark as failed. Wait for callback or re-query after a delay. |
| **`000005`** | `"failed"` | **Terminal** | Transaction was declined, rejected, or reversed by the upstream provider. | Mark order/payout as failed. Inspect `failureReason` for specific provider diagnostics. |
| **`000006`** | `"timeout"` | **Non-Terminal** | Collection prompt session timed out before the customer completed authorization. **(Collections/deposits only)** | Check status again after 2 to 3 minutes before taking further action. |
**Strict Rule on `pending` (`000001`):**\
Never release value, credit user wallets, fulfill goods, or retry payments while a transaction is in `pending` status. A pending status indicates the transaction is in-flight across banking or telecommunication switches. Always wait for a terminal status (`000000` or `000005`).
***
## The Double-Verification Reconciliation Recipe
For platforms managing high financial volume, automated payroll, or instant digital withdrawals, implement Niobi's recommended double-verification recipe:
```mermaid theme={null}
flowchart TD
A["1. Transaction Initiated (POST Unified Collections / Payments)"] --> B["Receive Initial Response (payment_step: 1)"]
B --> C["Await Incoming Webhook Callback"]
C --> D{"payment_step: 2 Received?"}
D -- Yes --> E["Step 1: Verify HMAC Signature"]
D -- "Delayed / Missing" --> F["Query Status API (POST /api/v3/get-unified-transaction-status)"]
E --> G["Step 2: Check Database Lock on Reference"]
F --> G
G --> H{"Step 3: Triple-Point Match? 1. statusCode == '000000' & status == 'success' 2. amount & currency match internal order 3. third_party_reference_1 matches exactly"}
H -- "Match Confirmed" --> I["Step 4: Atomic DB Update (Mark Order SETTLED)"]
H -- "Pending (000001)" --> J["Keep Waiting (Do Not Act)"]
H -- "Failed (000005)" --> K["Mark Failed / Safe Reversal"]
```
### Reconciliation Checklist:
1. **Inbound Signature Verification:** Validate incoming webhook signatures using your Secret Key before parsing data.
2. **Concurrency Control:** Use database row-level locking (`SELECT ... FOR UPDATE` or optimistic locking on `third_party_reference_1`) to ensure duplicate callbacks or simultaneous polling queries do not process twice.
3. **Triple-Point Match:** Confirm that `statusCode`, `amount`, `currency`, and `third_party_reference_1` match your internal order record exactly.
4. **Atomic Settlement:** Update your internal ledger to `SETTLED` inside a single atomic database transaction.
***
## Polling Guidelines & Best Practices
To ensure optimal performance and avoid hitting rate limits:
* **Rely on Webhooks First:** Use callbacks as your primary notification channel.
* **Avoid Tight Polling Loops:** Never poll the status API immediately in rapid loops (e.g. every second).
* **Use Spaced Backoff Intervals:** If you must poll for a pending transaction, use progressive spaced intervals (for example: wait 5 seconds after initiation, then 15 seconds, then 30 seconds, then 60 seconds).
* **Idempotency Keys:** Always query using the same `third_party_reference_1` or `reference` originally assigned to the transaction.
***
## Next Steps
Explore related operational and security guides: