Financial System-nexgate-service(3)
Collection API
Base URL: https://api.nextgate.co.tz/api/v1
Short Description: The Collection API handles wallet top-up flows for NextGate users. It allows users to deposit money into their NextGate wallet via mobile money (USSD push) or card payment through Selcom. All subsequent platform payments (events, products) are made from the wallet balance.
Hints:
- Always provide a unique
idempotencyKeyper request to prevent duplicate charges - For USSD channels the user receives a PIN prompt on their phone — poll
/status/{id}to track completion - For CARD channel the response contains a
paymentUrl— redirect the user to complete payment - Selcom webhooks automatically credit the wallet on confirmation — no manual step needed
Security & Client Guidelines
Authentication
All endpoints in this API are protected. The client must include a valid JWT Bearer token in every request header:
Authorization: Bearer <your_token>
The token is obtained from the NextGate authentication API after login. Tokens have an expiry time — when a request returns 401 UNAUTHORIZED, the client must silently refresh the token using the refresh token endpoint and then retry the original request once. If the refresh also fails, redirect the user to the login screen. Never prompt the user manually to re-enter credentials on a token expiry.
Never store the JWT token in a place accessible to third-party scripts. On mobile, use secure device storage. On web, prefer memory or HttpOnly cookies over localStorage.
Idempotency Key
The idempotencyKey field protects against duplicate top-up requests caused by network retries. Without it, a user tapping the pay button while the network is slow could trigger two charges.
How to generate it correctly:
Generate the key exactly once when the user initiates the action — for example, when they tap the "Top Up" button — and store it in memory for the duration of that action. If the request fails due to a network error and your app retries, send the exact same key. The server will detect the duplicate and return the existing request instead of creating a new one.
Do not generate a new key on every retry. Do not use a random value that changes between attempts. A good key combines something unique to the user and something unique to the moment they initiated the action, for example combining the user ID with a timestamp captured at the moment the button was tapped.
The key must be unique per top-up attempt. Once a top-up completes or fails, do not reuse the same key for a future top-up — generate a fresh one for the next action.
Polling Strategy (USSD & Card)
After calling /initiate, the server returns immediately with status AWAITING_CUSTOMER_ACTION. The actual payment happens asynchronously — the user either enters their PIN on their phone (USSD) or pays on the card page. Your app must poll /status/{id} to know when the wallet has been credited.
Recommended polling behavior:
Poll every 5 seconds for the first 2 minutes. If still not completed after 2 minutes, slow down to every 15 seconds. Stop polling after 30 minutes total — this matches the server-side expiry window. If you reach 30 minutes without a COMPLETED status, show the user a message informing them the request has expired and they should try again.
Stop polling immediately when the status becomes COMPLETED, FAILED, or EXPIRED. Do not continue polling terminal states.
Card Payment Flow
When the channel is CARD, the response includes a paymentUrl. Open this URL in the device browser or a webview so the user can complete the card payment on Selcom's hosted page. Do not attempt to embed or replicate the card form in your app.
After the user returns from the payment page, resume polling /status/{id} — do not assume success just because the user returned. The wallet is only credited after Selcom sends a confirmation webhook to the server, which may take a few seconds after the card payment completes.
Network Error Handling
A network timeout or connection error on /initiate does not mean the request failed on the server. The server may have received and processed the request before the connection dropped. Always retry with the same idempotencyKey — the server will return the existing request if it was already created, preventing a duplicate.
On a timeout for /status, simply retry the poll on the next interval. Status polling is a read-only operation and is always safe to retry.
| HTTP Status | What it means | Client action |
|---|---|---|
400 |
Invalid request or business rule violation | Show message to user, do not retry automatically |
401 |
Token expired or invalid | Refresh token silently, retry once |
403 |
Forbidden — permission issue | Show error, do not retry |
422 |
Validation error | Show field errors to user, fix and resubmit |
500 |
Server error | Show generic error, allow user to retry manually |
| Network timeout | No response received | Retry with same idempotency key |
Standard Response Format
Success Response
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2026-03-06T10:30:45",
"data": {}
}
Error Response
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Error description",
"action_time": "2026-03-06T10:30:45",
"data": "Error description"
}
Standard Response Fields
| Field | Type | Description |
|---|---|---|
success |
boolean | true for success, false for errors |
httpStatus |
string | HTTP status name |
message |
string | Human-readable result description |
action_time |
string | ISO 8601 timestamp |
data |
object/string | Response payload or error details |
Flow Diagram
User
|
|--- POST /collection/initiate --------> CollectionController
| (channel, amount, msisdn) |
| Validate request
| Check idempotency
| Save CollectionRequest (PENDING)
| |
| .----------------.
| | |
| USSD CARD
| | |
| Push USSD to Create Selcom
| Selcom API Checkout Order
| | |
| Status: AWAITING Status: AWAITING
| CUSTOMER_ACTION CUSTOMER_ACTION
| | |
|<-- Response (requestId) ------'----------------'
|
| [USSD: User enters PIN on phone]
| [CARD: User pays on paymentUrl ]
|
| Selcom Webhook ---------> /api/selcom/webhook
| (payment confirmed) |
| creditWallet()
| Status: COMPLETED
|
|--- GET /collection/status/{id} -------> Status: COMPLETED
|<-- { status, transactionRef, ... }
Endpoints
1. Initiate Collection
Purpose: Initiates a wallet top-up request via mobile money (USSD) or card payment.
Endpoint: POST /collection/initiate
Access Level: 🔒 Protected — Requires Bearer Token
Authentication: Authorization: Bearer <token>
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer JWT token |
| Content-Type | string | Yes | application/json |
Request JSON Sample (USSD):
{
"channel": "MPESA",
"amount": 50000,
"msisdn": "255712345678",
"idempotencyKey": "usr-123-topup-1741234567"
}
Request JSON Sample (CARD):
{
"channel": "CARD",
"amount": 50000,
"idempotencyKey": "usr-123-topup-1741234568"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
channel |
string | Yes | Payment channel | enum: MPESA, AIRTEL, TIGO, HALOPESA, SELCOM_PESA, CARD |
amount |
number | Yes | Amount to top up in TZS | Min: 1000 |
msisdn |
string | Conditional | Mobile phone number | Required for all channels except CARD. Format: 255XXXXXXXXX (12 digits) |
idempotencyKey |
string | Yes | Unique key to prevent duplicate requests | Max 200 chars, unique per request |
Success Response JSON Sample (USSD):
{
"success": true,
"httpStatus": "OK",
"message": "Collection initiated successfully",
"action_time": "2026-03-06T10:30:45",
"data": {
"collectionRequestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"channel": "MPESA",
"amount": 50000,
"currency": "TZS",
"status": "AWAITING_CUSTOMER_ACTION",
"msisdnDisplay": "2557****678",
"paymentUrl": null,
"message": "Please enter your PIN on your phone to complete payment."
}
}
Success Response JSON Sample (CARD):
{
"success": true,
"httpStatus": "OK",
"message": "Collection initiated successfully",
"action_time": "2026-03-06T10:30:45",
"data": {
"collectionRequestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"channel": "CARD",
"amount": 50000,
"currency": "TZS",
"status": "AWAITING_CUSTOMER_ACTION",
"msisdnDisplay": null,
"paymentUrl": "https://checkout.selcom.net/pay/abc123",
"message": "Redirect user to payment URL."
}
}
Success Response Fields:
| Field | Description |
|---|---|
collectionRequestId |
UUID — use this to poll /status/{id} |
channel |
Channel used for this collection |
amount |
Amount in TZS |
currency |
Always TZS |
status |
Current status — see status table below |
msisdnDisplay |
Masked phone number e.g. 2557****678 |
paymentUrl |
Card payment URL — redirect user here (null for USSD) |
message |
Human-readable instruction for the user |
Collection Status Values:
| Status | Description |
|---|---|
PENDING |
Request created, not yet sent to Selcom |
AWAITING_CUSTOMER_ACTION |
Sent to Selcom, waiting for user PIN or card payment |
COMPLETED |
Payment confirmed, wallet credited |
FAILED |
Payment failed or rejected |
EXPIRED |
Request expired before user acted (30 min window) |
Error Responses:
Missing phone for USSD (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Phone number is required for MPESA payments.",
"action_time": "2026-03-06T10:30:45",
"data": "Phone number is required for MPESA payments."
}
Invalid phone format (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Invalid phone number format.",
"action_time": "2026-03-06T10:30:45",
"data": "Invalid phone number format."
}
Selcom rejection (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Payment initiation failed: Subscriber not found",
"action_time": "2026-03-06T10:30:45",
"data": "Payment initiation failed: Subscriber not found"
}
2. Get Collection Status
Purpose: Returns the current status of a collection request. Poll this after initiating to know when the wallet has been credited.
Endpoint: GET /collection/status/{collectionRequestId}
Access Level: 🔒 Protected — Requires Bearer Token
Authentication: Authorization: Bearer <token>
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
collectionRequestId |
UUID | Yes | ID returned from /initiate |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Collection status retrieved",
"action_time": "2026-03-06T10:30:45",
"data": {
"collectionRequestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"channel": "MPESA",
"amount": 50000,
"currency": "TZS",
"status": "COMPLETED",
"msisdnDisplay": "2557****678",
"failureReason": null,
"transactionRef": "NXT-TXN-20260306-ABCD1234",
"createdAt": "2026-03-06T10:30:00",
"completedAt": "2026-03-06T10:31:45"
}
}
Success Response Fields:
| Field | Description |
|---|---|
collectionRequestId |
UUID of this collection request |
channel |
Channel used |
amount |
Amount in TZS |
currency |
Always TZS |
status |
Current status — see status table above |
msisdnDisplay |
Masked phone number |
failureReason |
Populated if status is FAILED |
transactionRef |
Wallet transaction reference — available when COMPLETED |
createdAt |
When the request was created |
completedAt |
When the wallet was credited — null if not yet completed |
Error Responses:
Not found or not owned by user (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Collection request not found",
"action_time": "2026-03-06T10:30:45",
"data": "Collection request not found"
}
Quick Reference
| Method | Endpoint | Description |
|---|---|---|
| POST | /collection/initiate |
Start a top-up via USSD or card |
| GET | /collection/status/{id} |
Check top-up status |
Wallet & Transactions
Author: Josh S. Sakweli, Backend Lead Team
Last Updated: 2025-10-02
Version: v1.0
Base URL:
Short Description: The Financial System manages all monetary transactions using double-entry bookkeeping. It handles user wallets, escrow for secure payments, transaction tracking, and payment processing with complete auditability.
Core Components
1. Ledger System
Double-entry bookkeeping where every transaction has equal debits and credits. Maintains user wallets, escrow accounts, platform revenue, and external flow tracking.
2. Escrow Management
Holds buyer payments until delivery confirmation. Protects both parties and automatically splits funds: 95% to seller, 5% platform fee.
3. Transaction History
User-facing records of all financial activity including purchases, refunds, and wallet operations.
Payment Flow
- Validate checkout and payment method
- Move funds to escrow via ledger
- Create transaction records
- Generate order on success
- Release funds after delivery (seller 95%, platform 5%)
Payment Methods
Active: WALLET, CASH_ON_DELIVERY
Planned: Mobile money (M-Pesa, Tigo Pesa, Airtel Money), cards, bank transfers
Technical Specifications
- Currency: TZS (Tanzanian Shillings)
- Precision: BigDecimal (15 digits, 2 decimals)
- All operations are atomic transactions
- Complete audit trail maintained
Wallet
Base URL: https://apinexgate.glueauth.com/api/v1/wallet
Short Description: The Wallet API manages user digital wallets for storing and using funds within the platform. It handles wallet creation, balance queries, top-ups from external sources, withdrawals to external accounts, and wallet status management. All balances are maintained through the underlying ledger system ensuring accurate double-entry bookkeeping.
Hints:
- Wallets are automatically created for users on first access
- All monetary values are in TZS (Tanzanian Shillings)
- Actual balance is stored in the ledger system, not the wallet entity
- Wallets can be deactivated for security reasons
- Minimum top-up/withdrawal amount is 1 TZS
- All operations create transaction history entries
- Top-ups represent money entering the platform from external sources (M-Pesa, bank transfer, etc.)
- Withdrawals represent money leaving the platform to external accounts
- Each user can have only one wallet
Standard Response Format
All API responses follow a consistent structure using our Globe Response Builder pattern:
Success Response Structure
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2025-10-02T10:30:45",
"data": {
// Actual response data goes here
}
}
Error Response Structure
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Error description",
"action_time": "2025-10-02T10:30:45",
"data": "Error description"
}
Standard Response Fields
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true for successful operations, false for errors |
httpStatus |
string | HTTP status name (OK, CREATED, BAD_REQUEST, NOT_FOUND, etc.) |
message |
string | Human-readable message describing the operation result |
action_time |
string | ISO 8601 timestamp of when the response was generated |
data |
object/string | Response payload for success, error details for failures |
HTTP Method Badge Standards
For better visual clarity, all endpoints use colored badges for HTTP methods with the following standard colors:
- GET - GET - Green (Safe, read-only operations)
- POST - POST - Blue (Create new resources)
- PUT - PUT - Yellow (Full updates)
Endpoints
1. Get My Wallet
Purpose: Retrieves the authenticated user's wallet information including wallet ID, account details, current balance, and status. If wallet doesn't exist, it is automatically created.
Endpoint: GET {base_url}/my-wallet
Access Level: 🔒 Protected (Requires Authentication)
Authentication: Bearer Token required in Authorization header
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authenticated user |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Wallet retrieved successfully",
"action_time": "2025-10-02T10:30:45",
"data": {
"walletId": "w1a2l3l4-e5t6-7890-abcd-ef1234567890",
"accountId": "a1c2c3o4-u5n6-7890-abcd-ef1234567890",
"accountUserName": "john_doe",
"currentBalance": 150000.00,
"isActive": true,
"createdAt": "2025-09-15T08:20:30",
"updatedAt": "2025-10-02T10:15:20"
}
}
Success Response Fields:
| Field | Description |
|---|---|
| walletId | Unique identifier for the wallet |
| accountId | User account UUID this wallet belongs to |
| accountUserName | Username of the wallet owner |
| currentBalance | Current wallet balance in TZS (from ledger system) |
| isActive | Whether the wallet is currently active |
| createdAt | ISO 8601 timestamp when wallet was created |
| updatedAt | ISO 8601 timestamp of last wallet update |
Error Response Examples:
{
"success": false,
"httpStatus": "UNAUTHORIZED",
"message": "Authentication token is required",
"action_time": "2025-10-02T10:30:45",
"data": "Authentication token is required"
}
2. Get My Balance
Purpose: Retrieves only the current balance of the authenticated user's wallet. This is a lightweight endpoint for quick balance checks.
Endpoint: GET {base_url}/balance
Access Level: 🔒 Protected (Requires Authentication)
Authentication: Bearer Token required in Authorization header
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authenticated user |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Balance retrieved successfully",
"action_time": "2025-10-02T10:35:45",
"data": {
"balance": 150000.00,
"currency": "TZS"
}
}
Success Response Fields:
| Field | Description |
|---|---|
| balance | Current wallet balance |
| currency | Currency code (always TZS) |
Error Response Examples:
{
"success": false,
"httpStatus": "UNAUTHORIZED",
"message": "Authentication token is required",
"action_time": "2025-10-02T10:35:45",
"data": "Authentication token is required"
}
3. Get Wallet by ID (Admin)
Purpose: Retrieves detailed information about any wallet by its ID. This is an administrative endpoint requiring SUPER_ADMIN or STAFF_ADMIN role, or wallet ownership.
Endpoint: GET {base_url}/{walletId}
Access Level: 🔒 Protected (Requires Authentication + Admin Role or Ownership)
Authentication: Bearer Token required in Authorization header
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authenticated admin or owner |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| walletId | string (UUID) | Yes | Unique identifier of the wallet | Valid UUID format |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Wallet retrieved successfully",
"action_time": "2025-10-02T10:50:45",
"data": {
"walletId": "w1a2l3l4-e5t6-7890-abcd-ef1234567890",
"accountId": "a1c2c3o4-u5n6-7890-abcd-ef1234567890",
"accountUserName": "john_doe",
"currentBalance": 150000.00,
"isActive": true,
"createdAt": "2025-09-15T08:20:30",
"updatedAt": "2025-10-02T10:15:20"
}
}
Error Response Examples:
Not Found - No Permission (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "You do not have permission to access this wallet",
"action_time": "2025-10-02T10:50:45",
"data": "You do not have permission to access this wallet"
}
4. Activate Wallet (Admin)
Purpose: Activates a previously deactivated wallet, allowing the user to perform transactions again. Requires SUPER_ADMIN role or wallet ownership.
Endpoint: PUT {base_url}/{walletId}/activate
Access Level: 🔒 Protected (Requires Authentication + SUPER_ADMIN Role or Ownership)
Authentication: Bearer Token required in Authorization header
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authenticated admin or owner |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| walletId | string (UUID) | Yes | Unique identifier of the wallet to activate | Valid UUID format |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Wallet activated successfully",
"action_time": "2025-10-02T10:55:45",
"data": null
}
Error Response Examples:
Not Found - No Permission (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "You do not have permission to activate this wallet",
"action_time": "2025-10-02T10:55:45",
"data": "You do not have permission to activate this wallet"
}
5. Deactivate Wallet (Admin)
Purpose: Deactivates a wallet for security or administrative reasons. Once deactivated, the wallet cannot perform any transactions until reactivated. Requires SUPER_ADMIN or STAFF_ADMIN role, or wallet ownership.
Endpoint: PUT {base_url}/{walletId}/deactivate
Access Level: 🔒 Protected (Requires Authentication + Admin Role or Ownership)
Authentication: Bearer Token required in Authorization header
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authenticated admin or owner |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| walletId | string (UUID) | Yes | Unique identifier of the wallet to deactivate | Valid UUID format |
Query Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| reason | string | Yes | Reason for deactivation | Required, cannot be empty |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Wallet deactivated successfully",
"action_time": "2025-10-02T11:00:45",
"data": null
}
Error Response Examples:
Not Found - No Permission (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "You do not have permission to deactivate this wallet",
"action_time": "2025-10-02T11:00:45",
"data": "You do not have permission to deactivate this wallet"
}
Integration Examples
Example 1: Check Balance Before Purchase
Step 1: Get Current Balance
GET /api/v1/wallet/balance
Authorization: Bearer {token}
Response:
{
"success": true,
"httpStatus": "OK",
"message": "Balance retrieved successfully",
"action_time": "2025-10-02T11:10:45",
"data": {
"balance": 150000.00,
"currency": "TZS"
}
}
Step 2: Proceed to Checkout if Sufficient If balance >= purchase amount, continue to checkout session.
Example 2: Top-up and Purchase Flow
Step 1: Check Insufficient Balance
GET /api/v1/wallet/balance
Authorization: Bearer {token}
Response shows: 50000 TZS (insufficient for 100000 TZS purchase)
Step 2: Top-up Wallet
POST /api/v1/wallet/topup
Authorization: Bearer {token}
Content-Type: application/json
{
"amount": 60000.00,
"description": "M-Pesa top-up"
}
Step 3: Confirm New Balance
GET /api/v1/wallet/balance
Authorization: Bearer {token}
Response shows: 110000 TZS (now sufficient)
Example 3: Withdrawal Flow
Step 1: Get Wallet Info
GET /api/v1/wallet/my-wallet
Authorization: Bearer {token}
Step 2: Request Withdrawal
POST /api/v1/wallet/withdraw
Authorization: Bearer {token}
Content-Type: application/json
{
"amount": 50000.00,
"description": "Withdraw to CRDB Bank - Account 1234567890"
}
Step 3: Verify New Balance
GET /api/v1/wallet/balance
Authorization: Bearer {token}
Rate Limiting
Rate Limits:
- Get Wallet/Balance: 60 requests per minute per user
- Top-up: 10 requests per minute per user
- Withdraw: 10 requests per minute per user
- Admin Operations: 30 requests per minute per admin
Rate Limit Headers:
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 7
X-RateLimit-Reset: 1696258800
Rate Limit Exceeded:
{
"success": false,
"httpStatus": "TOO_MANY_REQUESTS",
"message": "Rate limit exceeded. Please try again later.",
"action_time": "2025-10-02T11:20:45",
"data": "Rate limit exceeded. Please try again later."
}
Transaction History
Base URL: https://apinexgate.glueauth.com/api/v1/transaction-history
Short Description: The Transaction History API provides user-facing records of all financial activities within the platform. It tracks purchases, refunds, wallet operations, sales earnings, and platform fees with complete details for each transaction. All transactions are linked to underlying ledger entries for full auditability.
Hints:
- All monetary values are in TZS (Tanzanian Shillings)
- Each transaction has a unique reference number (format: #YYYYTNNNNNN)
- Transactions are read-only - they cannot be modified or deleted
- DEBIT transactions represent money leaving the user's wallet (negative)
- CREDIT transactions represent money entering the user's wallet (positive)
- All transactions are linked to ledger entries for audit purposes
- Transactions are automatically created by the system during financial operations
- Pagination is supported with default 20 items per page
- Transactions can be filtered by type, direction, and date range
Standard Response Format
All API responses follow a consistent structure using our Globe Response Builder pattern:
Success Response Structure
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2025-10-02T10:30:45",
"data": {
// Actual response data goes here
}
}
Error Response Structure
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Error description",
"action_time": "2025-10-02T10:30:45",
"data": "Error description"
}
Standard Response Fields
| Field | Type | Description |
|---|---|---|
success |
boolean | Always true for successful operations, false for errors |
httpStatus |
string | HTTP status name (OK, CREATED, BAD_REQUEST, NOT_FOUND, etc.) |
message |
string | Human-readable message describing the operation result |
action_time |
string | ISO 8601 timestamp of when the response was generated |
data |
object/string | Response payload for success, error details for failures |
HTTP Method Badge Standards
For better visual clarity, all endpoints use colored badges for HTTP methods:
- GET - GET - Green (Safe, read-only operations)
Transaction Types
| Type | Description | Direction |
|---|---|---|
| WALLET_TOPUP | Money added to wallet from external source | CREDIT |
| WALLET_WITHDRAWAL | Money withdrawn from wallet to external account | DEBIT |
| PURCHASE | Payment made for product purchase | DEBIT |
| PURCHASE_REFUND | Refund received for cancelled purchase | CREDIT |
| SALE | Earnings received from selling products | CREDIT |
| SALE_REFUND | Refund issued to buyer for sale | DEBIT |
| PLATFORM_FEE_COLLECTED | Platform fee collected (admin tracking) | CREDIT |
| GROUP_PURCHASE | Payment for group buying deal | DEBIT |
| GROUP_REFUND | Refund for failed group deal | CREDIT |
| INSTALLMENT_PAYMENT | One installment payment (future) | DEBIT |
| INSTALLMENT_REFUND | Refund for cancelled installment (future) | CREDIT |
| ESCROW_HOLD | Money held in escrow (admin tracking) | DEBIT |
| ESCROW_RELEASE | Money released from escrow (admin tracking) | CREDIT |
| ESCROW_REFUND | Money refunded from escrow | CREDIT |
Transaction Directions
| Direction | Description | Display |
|---|---|---|
| DEBIT | Money leaving wallet | Negative amount (-) |
| CREDIT | Money entering wallet | Positive amount (+) |
Endpoints
1. Get My Transactions
Purpose: Retrieves all transactions for the authenticated user with pagination support. Returns transactions ordered by creation date (newest first).
Endpoint: GET {base_url}
Access Level: 🔒 Protected (Requires Authentication)
Authentication: Bearer Token required in Authorization header
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authenticated user |
Query Parameters:
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| page | integer | No | Page number (0-indexed) | 0 |
| size | integer | No | Number of items per page | 20 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Transactions retrieved successfully",
"action_time": "2025-10-02T10:30:45",
"data": {
"content": [
{
"id": "t1r2a3n4-s5a6-7890-abcd-ef1234567890",
"transactionRef": "#2025T000123",
"type": "PURCHASE",
"direction": "DEBIT",
"amount": 150000.00,
"displayAmount": -150000.00,
"currency": "TZS",
"title": "Purchase Payment",
"description": "Payment for order (Escrow: ESC-2025-000045)",
"status": "COMPLETED",
"createdAt": "2025-10-02T10:15:30",
"referenceType": "ESCROW",
"referenceId": "e1s2c3r4-o5w6-7890-abcd-ef1234567890"
},
{
"id": "t2r3a4n5-s6a7-8901-bcde-f12345678901",
"transactionRef": "#2025T000122",
"type": "WALLET_TOPUP",
"direction": "CREDIT",
"amount": 50000.00,
"displayAmount": 50000.00,
"currency": "TZS",
"title": "Wallet Topup",
"description": "M-Pesa top-up from +255712345678",
"status": "COMPLETED",
"createdAt": "2025-10-02T09:30:15",
"referenceType": "WALLET",
"referenceId": "w1a2l3l4-e5t6-7890-abcd-ef1234567890"
}
],
"pageable": {
"pageNumber": 0,
"pageSize": 20,
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
},
"offset": 0,
"paged": true,
"unpaged": false
},
"totalElements": 47,
"totalPages": 3,
"last": false,
"size": 20,
"number": 0,
"sort": {
"sorted": true,
"unsorted": false,
"empty": false
},
"numberOfElements": 20,
"first": true,
"empty": false
}
}
Success Response Fields:
| Field | Description |
|---|---|
| id | Unique identifier for the transaction |
| transactionRef | Unique transaction reference number |
| type | Transaction type (see Transaction Types table) |
| direction | DEBIT or CREDIT |
| amount | Absolute transaction amount |
| displayAmount | Amount with sign (negative for DEBIT, positive for CREDIT) |
| currency | Currency code (TZS) |
| title | Short transaction title |
| description | Detailed transaction description |
| status | Transaction status (COMPLETED, PENDING, FAILED) |
| createdAt | ISO 8601 timestamp when transaction was created |
| referenceType | Type of related entity (WALLET, ESCROW, ORDER, etc.) |
| referenceId | UUID of related entity |
Error Response Examples:
{
"success": false,
"httpStatus": "UNAUTHORIZED",
"message": "Authentication token is required",
"action_time": "2025-10-02T10:30:45",
"data": "Authentication token is required"
}
2. Get Transaction by ID
Purpose: Retrieves detailed information about a specific transaction by its ID. Only the transaction owner can access their transactions.
Endpoint: GET {base_url}/{id}
Access Level: 🔒 Protected (Requires Authentication and Ownership)
Authentication: Bearer Token required in Authorization header
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authenticated user |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| id | string (UUID) | Yes | Unique identifier of the transaction | Valid UUID format |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Transaction retrieved successfully",
"action_time": "2025-10-02T10:35:45",
"data": {
"id": "t1r2a3n4-s5a6-7890-abcd-ef1234567890",
"transactionRef": "#2025T000123",
"type": "PURCHASE",
"direction": "DEBIT",
"amount": 150000.00,
"displayAmount": -150000.00,
"currency": "TZS",
"title": "Purchase Payment",
"description": "Payment for order (Escrow: ESC-2025-000045)",
"status": "COMPLETED",
"createdAt": "2025-10-02T10:15:30",
"referenceType": "ESCROW",
"referenceId": "e1s2c3r4-o5w6-7890-abcd-ef1234567890"
}
}
Error Response Examples:
Not Found (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Transaction not found",
"action_time": "2025-10-02T10:35:45",
"data": "Transaction not found"
}
3. Get Transaction by Reference
Purpose: Retrieves a transaction by its unique reference number (e.g., #2025T000123).
Endpoint: GET {base_url}/ref/{transactionRef}
Access Level: 🔒 Protected (Requires Authentication and Ownership)
Authentication: Bearer Token required in Authorization header
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authenticated user |
Path Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
| transactionRef | string | Yes | Unique transaction reference number | Format: #YYYYTNNNNNN |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Transaction retrieved successfully",
"action_time": "2025-10-02T10:40:45",
"data": {
"id": "t1r2a3n4-s5a6-7890-abcd-ef1234567890",
"transactionRef": "#2025T000123",
"type": "PURCHASE",
"direction": "DEBIT",
"amount": 150000.00,
"displayAmount": -150000.00,
"currency": "TZS",
"title": "Purchase Payment",
"description": "Payment for order (Escrow: ESC-2025-000045)",
"status": "COMPLETED",
"createdAt": "2025-10-02T10:15:30",
"referenceType": "ESCROW",
"referenceId": "e1s2c3r4-o5w6-7890-abcd-ef1234567890"
}
}
Error Response Examples:
Not Found (404):
{
"success": false,
"httpStatus": "NOT_FOUND",
"message": "Transaction not found: #2025T000123",
"action_time": "2025-10-02T10:40:45",
"data": "Transaction not found: #2025T000123"
}
4. Get Transactions by Type
Purpose: Retrieves all transactions of a specific type for the authenticated user with pagination support.
Endpoint: GET {base_url}/filter/type
Access Level: 🔒 Protected (Requires Authentication)
Authentication: Bearer Token required in Authorization header
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authenticated user |
Query Parameters:
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| type | string (enum) | Yes | Transaction type to filter by | - |
| page | integer | No | Page number (0-indexed) | 0 |
| size | integer | No | Number of items per page | 20 |
Valid Type Values:
- WALLET_TOPUP
- WALLET_WITHDRAWAL
- PURCHASE
- PURCHASE_REFUND
- SALE
- SALE_REFUND
- PLATFORM_FEE_COLLECTED
- GROUP_PURCHASE
- GROUP_REFUND
- INSTALLMENT_PAYMENT
- INSTALLMENT_REFUND
- ESCROW_HOLD
- ESCROW_RELEASE
- ESCROW_REFUND
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Transactions retrieved successfully",
"action_time": "2025-10-02T10:45:45",
"data": {
"content": [
{
"id": "t1r2a3n4-s5a6-7890-abcd-ef1234567890",
"transactionRef": "#2025T000123",
"type": "PURCHASE",
"direction": "DEBIT",
"amount": 150000.00,
"displayAmount": -150000.00,
"currency": "TZS",
"title": "Purchase Payment",
"description": "Payment for order (Escrow: ESC-2025-000045)",
"status": "COMPLETED",
"createdAt": "2025-10-02T10:15:30",
"referenceType": "ESCROW",
"referenceId": "e1s2c3r4-o5w6-7890-abcd-ef1234567890"
}
],
"totalElements": 15,
"totalPages": 1,
"size": 20,
"number": 0
}
}
Error Response Examples:
Bad Request - Invalid Type (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Invalid transaction type",
"action_time": "2025-10-02T10:45:45",
"data": "Invalid transaction type"
}
5. Get Transactions by Direction
Purpose: Retrieves all transactions of a specific direction (DEBIT or CREDIT) for the authenticated user with pagination support.
Endpoint: GET {base_url}/filter/direction
Access Level: 🔒 Protected (Requires Authentication)
Authentication: Bearer Token required in Authorization header
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authenticated user |
Query Parameters:
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| direction | string (enum) | Yes | Transaction direction (DEBIT or CREDIT) | - |
| page | integer | No | Page number (0-indexed) | 0 |
| size | integer | No | Number of items per page | 20 |
Valid Direction Values:
- DEBIT (money out)
- CREDIT (money in)
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Transactions retrieved successfully",
"action_time": "2025-10-02T10:50:45",
"data": {
"content": [
{
"id": "t2r3a4n5-s6a7-8901-bcde-f12345678901",
"transactionRef": "#2025T000122",
"type": "WALLET_TOPUP",
"direction": "CREDIT",
"amount": 50000.00,
"displayAmount": 50000.00,
"currency": "TZS",
"title": "Wallet Topup",
"description": "M-Pesa top-up from +255712345678",
"status": "COMPLETED",
"createdAt": "2025-10-02T09:30:15",
"referenceType": "WALLET",
"referenceId": "w1a2l3l4-e5t6-7890-abcd-ef1234567890"
}
],
"totalElements": 23,
"totalPages": 2,
"size": 20,
"number": 0
}
}
Error Response Examples:
Bad Request - Invalid Direction (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Invalid transaction direction",
"action_time": "2025-10-02T10:50:45",
"data": "Invalid transaction direction"
}
6. Get Transactions by Date Range
Purpose: Retrieves all transactions within a specified date range for the authenticated user with pagination support.
Endpoint: GET {base_url}/filter/date-range
Access Level: 🔒 Protected (Requires Authentication)
Authentication: Bearer Token required in Authorization header
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authenticated user |
Query Parameters:
| Parameter | Type | Required | Description | Default |
|---|---|---|---|---|
| startDate | string (ISO 8601) | Yes | Start date and time (inclusive) | - |
| endDate | string (ISO 8601) | Yes | End date and time (inclusive) | - |
| page | integer | No | Page number (0-indexed) | 0 |
| size | integer | No | Number of items per page | 20 |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Transactions retrieved successfully",
"action_time": "2025-10-02T10:55:45",
"data": {
"content": [
{
"id": "t1r2a3n4-s5a6-7890-abcd-ef1234567890",
"transactionRef": "#2025T000123",
"type": "PURCHASE",
"direction": "DEBIT",
"amount": 150000.00,
"displayAmount": -150000.00,
"currency": "TZS",
"title": "Purchase Payment",
"description": "Payment for order (Escrow: ESC-2025-000045)",
"status": "COMPLETED",
"createdAt": "2025-10-02T10:15:30",
"referenceType": "ESCROW",
"referenceId": "e1s2c3r4-o5w6-7890-abcd-ef1234567890"
}
],
"totalElements": 8,
"totalPages": 1,
"size": 20,
"number": 0
}
}
Error Response Examples:
Bad Request - Invalid Date Format (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Invalid date format. Use ISO 8601 format",
"action_time": "2025-10-02T10:55:45",
"data": "Invalid date format. Use ISO 8601 format"
}
7. Get Transaction Count
Purpose: Retrieves the total count of all transactions for the authenticated user.
Endpoint: GET {base_url}/count
Access Level: 🔒 Protected (Requires Authentication)
Authentication: Bearer Token required in Authorization header
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer token for authenticated user |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Transaction count retrieved successfully",
"action_time": "2025-10-02T11:00:45",
"data": 47
}
Error Response Examples:
{
"success": false,
"httpStatus": "UNAUTHORIZED",
"message": "Authentication token is required",
"action_time": "2025-10-02T11:00:45",
"data": "Authentication token is required"
}
Integration Examples
Example 1: Get Recent Transactions
GET /api/v1/transaction-history?page=0&size=10
Authorization: Bearer {token}
Response: Returns 10 most recent transactions
Example 2: View All Purchases
GET /api/v1/transaction-history/filter/type?type=PURCHASE&page=0&size=20
Authorization: Bearer {token}
Response: Returns all purchase transactions
Example 3: Check Money Received (Credits)
GET /api/v1/transaction-history/filter/direction?direction=CREDIT&page=0&size=20
Authorization: Bearer {token}
Response: Returns all credit (incoming) transactions
Example 4: Get Monthly Statement
GET /api/v1/transaction-history/filter/date-range?startDate=2025-09-01T00:00:00Z&endDate=2025-09-30T23:59:59Z&page=0&size=100
Authorization: Bearer {token}
Response: Returns all transactions for September 2025
Example 5: Track Specific Transaction
Step 1: Get Transaction by Reference
GET /api/v1/transaction-history/ref/#2025T000123
Authorization: Bearer {token}
Step 2: Get Full Details by ID
GET /api/v1/transaction-history/t1r2a3n4-s5a6-7890-abcd-ef1234567890
Authorization: Bearer {token}
Rate Limiting
Rate Limits:
- All Endpoints: 60 requests per minute per user
Rate Limit Headers:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1696258800
Rate Limit Exceeded:
{
"success": false,
"httpStatus": "TOO_MANY_REQUESTS",
"message": "Rate limit exceeded. Please try again later.",
"action_time": "2025-10-02T11:05:45",
"data": "Rate limit exceeded. Please try again later."
}
Best Practices
For Developers
- Use pagination for large result sets
- Cache transaction lists with short TTL (1-2 minutes)
- Filter by type or direction for specific views
- Use date ranges for generating statements
- Display displayAmount field for user-friendly amounts with signs
- Show transaction reference for user tracking and support
- Link to related entities using referenceType and referenceId
For UI/UX
- Color-code transactions: Green for CREDIT, Red for DEBIT
- Group by date: Show transactions by day/month
- Provide filters: Type, direction, date range
- Show running balance: Calculate balance after each transaction
- Enable search: By reference number or description
- Export capability: Allow CSV/PDF export of statements
- Infinite scroll or pagination: For better mobile experience
Disbursement
Disbursement Channel API
Base URL: https://api.nextgate.co.tz/api/v1
Description: Manages withdrawal destinations for NextGate users. A channel is a verified mobile money or bank account to which a user can withdraw their wallet balance. Channels require OTP verification to add and have a cooling period before first use.
Key notes:
- Adding a channel is a three-step flow:
/lookup→/add→/add/confirm - The
/lookupstep returns a signedconfirmationTokenrequired by/add— expires in 10 minutes - Selcom name lookup is performed at both
/lookupand/add— verified twice server-side - The first channel on an account activates immediately — 24-hour cooling applies to all subsequent channels
- Deleting a channel requires OTP confirmation — two-step:
DELETE /{channelId}→DELETE /{channelId}/confirm - Deleted channels cannot be used for new withdrawals — in-flight disbursements snapshot channel data at initiation time and are unaffected
Security & Client Guidelines
Authentication
All endpoints require a valid JWT Bearer token:
Authorization: Bearer <your_token>
Never store the JWT in localStorage — use secure device storage on mobile, or HttpOnly cookies on web.
OTP Token Handling
Both the add and delete flows are two-step. The first step returns an otpToken. Store this in memory and pass it to the confirm step along with the SMS OTP code. Do not log it or persist it beyond the confirmation flow.
The OTP expires after ~5 minutes. If the user is too slow, the confirm step returns an error — prompt them to restart the entire flow from the beginning. The server must clean up the previous pending channel record before a new one can be created.
If the wrong OTP is entered too many times, the OTP is locked and the flow is terminated. The user must start over.
Name Lookup and Confirmation Token
The /lookup step returns a signed confirmationToken that encodes the destination, channel type, and account ID. It is valid for 10 minutes.
The client must pass this token to /add. The server rejects the request if the token is missing, expired, or does not match the destination and channel type in the request body.
Always display accountHolderName prominently to the user and require explicit confirmation before calling /add. This protects users from accidentally adding a wrong destination.
Cooling Period UX
After OTP confirmation, isUsable is false until activatesAt. Show a message like:
"This channel will be ready for withdrawals on {activatesAt date}"
Visually distinguish usable channels from those still in the cooling period when listing.
Exception: The first channel added to an account activates immediately (isUsable: true, activatesAt set to now).
Deleting a Channel
Deletion is irreversible. Show a clear confirmation dialog before initiating the delete flow.
If the deleted channel was the primary channel, the server automatically promotes another active channel as primary.
Pending disbursements that reference a deleted channel are not affected — the disbursement holds a snapshot of the channel data from initiation time.
Network Error Handling
| HTTP Status | Meaning | Client action |
|---|---|---|
400 |
Business rule violation | Show message to user, do not retry |
401 |
Token expired or invalid | Refresh token silently, retry once |
403 |
Forbidden | Show error, do not retry |
422 |
Validation error | Show field errors, fix and resubmit |
500 |
Server error | Show generic error, allow manual retry |
| Network timeout | No response | Lookup and list: safe to retry. Initiate/confirm: check status first |
Standard Response Format
Success
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2026-03-06T10:30:45",
"data": {}
}
Error
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Error description",
"action_time": "2026-03-06T10:30:45",
"data": "Error description"
}
Flow Diagram
ADD CHANNEL FLOW
----------------
User
|
|--- POST /disbursement/channels/lookup -------> Selcom Name Lookup
| { channelType, destination, bankCode? } |
|<-- { accountHolderName, destinationDisplay, Returns verified name
| channelType, confirmationToken }
| <-- store confirmationToken (expires 10 min)
| [User reviews name, confirms]
|
|--- POST /disbursement/channels/add ----------> Validate confirmationToken
| { channelType, destination, Re-verify name with Selcom
| bankCode?, confirmationToken } Check for duplicate
| Save channel record
| Generate + send OTP
|<-- { otpToken }
|
|--- POST /disbursement/channels/add/confirm --> Verify OTP
| ?otpToken=...&otpCode=... Activate channel
|<-- { channelId, isUsable, activatesAt, ... } (cooling 24h, except first channel)
|
| [Channel becomes usable after activatesAt]
|
|--- GET /disbursement/channels ---------------> List active channels
|<-- [ { channelId, isUsable, activatesAt, ... } ]
DELETE CHANNEL FLOW
-------------------
User
|
|--- DELETE /disbursement/channels/{channelId} ---------> Generate + send OTP
|<-- { otpToken }
|
|--- DELETE /disbursement/channels/{channelId}/confirm --> Verify OTP
| ?otpToken=...&otpCode=... Soft delete channel
|<-- 200 OK Reassign primary if needed
Channel Status Reference
| Status | Description |
|---|---|
PENDING_ACTIVATION |
Channel OTP confirmed, within 24-hour cooling period — not yet usable |
ACTIVE |
Channel verified and activated |
DELETED |
Channel removed by user |
A cleanup job runs nightly and soft-deletes any channel records whose OTP was never verified (i.e. the add flow was abandoned before confirm).
Endpoints
1. Lookup Account Name
Purpose: Verifies a destination account and returns the registered account holder name from Selcom, along with a signed confirmationToken required for the next step. Always call this before /add.
Endpoint: POST /disbursement/channels/lookup
Access: 🔒 Protected — Bearer Token required
Request Body:
{
"channelType": "MPESA",
"destination": "255712345678",
"bankCode": null
}
| Parameter | Type | Required | Description |
|---|---|---|---|
channelType |
string | Yes | MPESA, AIRTEL, TIGOPESA, HALOPESA, SELCOM_PESA, BANK |
destination |
string | Yes | Phone number (255XXXXXXXXX) or bank account number |
bankCode |
string | Conditional | Required when channelType is BANK |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Account verified successfully",
"action_time": "2026-03-06T10:30:45",
"data": {
"accountHolderName": "JOHN DOE",
"destinationDisplay": "2557****678",
"channelType": "MPESA",
"confirmationToken": "<signed-token>"
}
}
| Field | Description |
|---|---|
accountHolderName |
Verified name from Selcom — display to user for confirmation |
destinationDisplay |
Masked destination |
channelType |
The channel type |
confirmationToken |
Signed token required by /add. Valid for 10 minutes. Store in memory |
Errors:
| Scenario | Message |
|---|---|
| Account not found | "Account not found. Please check the number and try again." |
| Phone not verified | "Your phone number must be verified before adding a withdrawal channel." |
| Destination already added | "This destination is already added as a withdrawal channel." |
| Bank code missing | "Bank code is required for bank channels." |
| Selcom lookup failed | "Could not verify account. Please check the details and try again." |
2. Initiate Add Channel
Purpose: Initiates adding a new channel. Validates the confirmationToken from /lookup, re-verifies the name with Selcom server-side, saves the channel record, and sends an OTP to the user's verified phone.
Endpoint: POST /disbursement/channels/add
Access: 🔒 Protected — Bearer Token required
Request Body (Mobile Money):
{
"channelType": "MPESA",
"destination": "255712345678",
"bankCode": null,
"confirmationToken": "<token from /lookup>"
}
Request Body (Bank):
{
"channelType": "BANK",
"destination": "0012345678901",
"bankCode": "CRDB",
"confirmationToken": "<token from /lookup>"
}
| Parameter | Type | Required | Description |
|---|---|---|---|
channelType |
string | Yes | MPESA, AIRTEL, TIGOPESA, HALOPESA, SELCOM_PESA, BANK |
destination |
string | Yes | Phone or account number |
bankCode |
string | Conditional | Required when channelType is BANK |
confirmationToken |
string | Yes | Token from /lookup. Must match destination, channel type, and account |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "OTP sent to your verified phone number",
"action_time": "2026-03-06T10:30:45",
"data": {
"otpToken": "otp-token-uuid-here"
}
}
| Field | Description |
|---|---|
otpToken |
Pass to /add/confirm with the SMS OTP code |
Errors:
| Scenario | Message |
|---|---|
| Token expired | "Confirmation token expired. Please look up the account again." |
| Token invalid/mismatch | "Invalid confirmation token." |
| Channel already active | "This destination is already an active withdrawal channel." |
| Max channels reached | "Maximum of {n} withdrawal channels allowed." |
| Add window closed | "Withdrawal channel add window has closed." |
3. Confirm Add Channel
Purpose: Confirms adding a new channel by verifying the OTP. On success the channel is activated with a cooling period before first use (first channel activates immediately).
Endpoint: POST /disbursement/channels/add/confirm
Access: 🔒 Protected — Bearer Token required
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
otpToken |
string | Yes | Token returned from /add |
otpCode |
string | Yes | 6-digit OTP received via SMS |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Channel added successfully",
"action_time": "2026-03-06T10:30:45",
"data": {
"channelId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"channelType": "MPESA",
"destinationDisplay": "2557****678",
"accountHolderName": "JOHN DOE",
"bankName": null,
"isPrimary": false,
"status": "ACTIVE",
"isUsable": false,
"activatesAt": "2026-03-07T10:31:00"
}
}
| Field | Description |
|---|---|
channelId |
UUID — use this when initiating withdrawals |
channelType |
Channel type |
destinationDisplay |
Masked destination |
accountHolderName |
Verified name from Selcom |
bankName |
Bank name — only for BANK channels, null otherwise |
isPrimary |
True if this is now the user's primary channel |
status |
ACTIVE or PENDING_ACTIVATION |
isUsable |
false if still in cooling period |
activatesAt |
When the channel becomes usable for withdrawals |
Errors:
| Scenario | Message |
|---|---|
| Invalid OTP | "Invalid OTP code." |
| OTP locked | "OTP locked — max attempts exceeded." |
| Channel no longer valid | "This channel is no longer valid. Please add it again." |
4. List My Channels
Purpose: Returns all active withdrawal channels for the authenticated user.
Endpoint: GET /disbursement/channels
Access: 🔒 Protected — Bearer Token required
Only
ACTIVEchannels are returned. Deleted and pending-activation channels that have not yet been OTP-confirmed are excluded.
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Channels retrieved successfully",
"action_time": "2026-03-06T10:30:45",
"data": [
{
"channelId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"channelType": "MPESA",
"destinationDisplay": "2557****678",
"accountHolderName": "JOHN DOE",
"bankName": null,
"isPrimary": true,
"status": "ACTIVE",
"isUsable": true,
"activatesAt": "2026-03-05T10:31:00"
},
{
"channelId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"channelType": "BANK",
"destinationDisplay": "0012****789",
"accountHolderName": "JOHN DOE",
"bankName": "CRDB Bank",
"isPrimary": false,
"status": "ACTIVE",
"isUsable": false,
"activatesAt": "2026-03-07T08:00:00"
}
]
}
| Field | Description |
|---|---|
channelId |
UUID of the channel |
channelType |
Channel type |
destinationDisplay |
Masked destination |
accountHolderName |
Verified name from Selcom |
bankName |
Bank name — only for BANK channels, null otherwise |
isPrimary |
Whether this is the user's primary channel |
status |
ACTIVE |
isUsable |
true if cooling period has passed and channel can be used now |
activatesAt |
When the cooling period ends |
5. Initiate Delete Channel
Purpose: Initiates deletion of a withdrawal channel. Sends OTP to user's verified phone for confirmation.
Endpoint: DELETE /disbursement/channels/{channelId}
Access: 🔒 Protected — Bearer Token required
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
channelId |
UUID | Yes | ID of the channel to delete |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "OTP sent to your verified phone number",
"action_time": "2026-03-06T10:30:45",
"data": {
"otpToken": "otp-token-uuid-here"
}
}
Errors:
| Scenario | Message |
|---|---|
| Channel not found / not owned | "Channel not found." |
| Channel not active | "Only active channels can be deleted." |
| Phone not verified | "Your phone number must be verified to delete a withdrawal channel." |
6. Confirm Delete Channel
Purpose: Confirms channel deletion by verifying the OTP. Channel is permanently removed. If it was the primary channel, another active channel is automatically promoted.
Endpoint: DELETE /disbursement/channels/{channelId}/confirm
Access: 🔒 Protected — Bearer Token required
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
channelId |
UUID | Yes | ID of the channel to delete |
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
otpToken |
string | Yes | Token returned from initiate delete |
otpCode |
string | Yes | 6-digit OTP received via SMS |
Success Response:
{
"success": true,
"httpStatus": "OK",
"message": "Channel deleted successfully",
"action_time": "2026-03-06T10:30:45",
"data": null
}
Errors:
| Scenario | Message |
|---|---|
| Invalid OTP | "Invalid OTP code." |
| OTP does not match channel | "OTP does not match this channel." |
| Channel not active | "Only active channels can be deleted." |
Quick Reference
| Method | Endpoint | Description |
|---|---|---|
POST |
/disbursement/channels/lookup |
Verify account name, get confirmationToken |
POST |
/disbursement/channels/add |
Start add channel flow (requires confirmationToken) |
POST |
/disbursement/channels/add/confirm |
Complete add channel with OTP |
GET |
/disbursement/channels |
List all active user channels |
DELETE |
/disbursement/channels/{channelId} |
Start delete channel flow |
DELETE |
/disbursement/channels/{channelId}/confirm |
Complete delete with OTP |
Disbursement API
Base URL: https://api.nextgate.co.tz/api/v1
Short Description: The Disbursement API handles wallet withdrawals for NextGate users. Users can withdraw their wallet balance to a verified mobile money account or bank account. Each withdrawal requires OTP confirmation and deducts a platform fee plus Selcom transfer fee on top of the requested amount.
Hints:
- The withdrawal channel must be added and fully active (cooling period passed) before use — see Disbursement Channel API
- Total wallet debit =
requestedAmount + platformFee + selcomFee - The recipient receives exactly
requestedAmount— fees are charged on top - If Selcom returns
INPROGRESS, the polling job resolves the final status automatically every 3 minutes — no frontend action needed - Always provide a unique
idempotencyKeyper withdrawal request - Poll
/status/{id}after confirming to track final delivery
Security & Client Guidelines
Authentication
All endpoints in this API are protected. The client must include a valid JWT Bearer token in every request header:
Authorization: Bearer <your_token>
The token is obtained from the NextGate authentication API after login. When a request returns 401 UNAUTHORIZED, the client must silently refresh the token and retry the original request once. If the refresh also fails, redirect the user to the login screen.
Never store the JWT token in a place accessible to third-party scripts. On mobile, use secure device storage. On web, prefer memory or HttpOnly cookies over localStorage.
Idempotency Key
The idempotencyKey field protects against duplicate withdrawals caused by network retries. This is critical for financial operations — without it, a network timeout could cause the user's wallet to be debited twice if the app retries blindly.
How to generate and use it correctly:
Generate the key exactly once at the moment the user initiates the withdrawal action — for example, when they tap the "Withdraw" button. Store this key in memory for the duration of that withdrawal flow. If the /initiate request fails due to a network error and your app retries, send the exact same key. The server will detect the duplicate and return the existing request rather than creating a new one.
Never generate a new key on each retry attempt. Never reuse a key from a previous completed or failed withdrawal. A new key must be generated fresh for each new withdrawal the user starts.
The key only protects /initiate. The /confirm step is protected by the otpToken and the pessimistic database lock on the disbursement record — a duplicate confirm call on the same request will be rejected because the status will no longer be PENDING_OTP.
OTP Token Handling
After calling /initiate, you receive an otpToken. This token must be held in memory and passed to /confirm along with the OTP code the user receives via SMS. Do not ask the user to copy or type the token — it is handled entirely by the app.
The OTP code expires after a short window (typically 5 minutes). If the user does not confirm in time, the confirm step will return an error and the withdrawal request will remain in PENDING_OTP status — no money has moved at this point. The user can restart the flow with a new idempotency key.
If the user enters the wrong OTP code too many times, the OTP is locked. The disbursement will be marked FAILED and the flow is terminated. No money has moved because the wallet is only debited after OTP verification. The user must start a new withdrawal request with a fresh idempotency key.
What Confirmation Actually Means
A 200 OK response from /confirm means the OTP was verified, the wallet was debited, and the Selcom API was called. It does not mean the money has arrived at the recipient. Selcom processes transfers asynchronously and may take seconds to minutes to confirm delivery.
Always redirect the user to a status screen after confirm and poll /status/{id} to show the final outcome. Do not show a "withdrawal successful" message based solely on the confirm response.
Status Polling After Confirm
After a successful /confirm, begin polling /status/{id} to track delivery.
Recommended polling behavior:
Poll every 5 seconds for the first minute. If still not in a terminal status, slow down to every 15 seconds for the next 5 minutes. After 6 minutes total with no terminal status, show the user a message explaining that the transfer is being processed and may take a little longer — this is the AWAITING_CONFIRMATION state where the server polling job is working in the background every 3 minutes.
Stop polling when status reaches any of these terminal states: COMPLETED, REFUNDED, FAILED, MANUAL_REVIEW.
Handling Each Terminal Status
| Status | What to show the user |
|---|---|
COMPLETED |
Success — show amount sent, recipient name, transaction reference |
REFUNDED |
Transfer failed — show failure reason, confirm wallet has been refunded, suggest they try again |
FAILED |
OTP was locked — no money moved, suggest they start a new withdrawal |
MANUAL_REVIEW |
Show a message that the transfer is under review and they will be notified. Do not show this as a failure or a success. Provide the supportRef for the user to reference if they contact support |
Fee Transparency
Before the user confirms the withdrawal, always display the full fee breakdown clearly so they know exactly how much will leave their wallet. Never show only the requestedAmount without also showing the fees.
A clear display should show: amount to recipient, platform fee, transfer fee, and total to be deducted from wallet. This prevents confusion when the user checks their wallet balance after the withdrawal and sees more deducted than they expected.
Network Error Handling
| HTTP Status | What it means | Client action |
|---|---|---|
400 |
Invalid request or business rule violation | Show message to user, do not retry automatically |
401 |
Token expired or invalid | Refresh token silently, retry once |
403 |
Forbidden — permission issue | Show error, do not retry |
422 |
Validation error | Show field errors to user, fix and resubmit |
500 |
Server error | Show generic error, allow user to retry manually |
Network timeout on /initiate |
No response received | Retry with the same idempotency key — server handles duplicates safely |
Network timeout on /confirm |
No response received | Do not retry automatically — check /status/{id} first to see if the confirm was already processed before retrying |
Network timeout on /status |
No response received | Safe to retry on next poll interval |
Standard Response Format
Success Response
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2026-03-06T10:30:45",
"data": {}
}
Error Response
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Error description",
"action_time": "2026-03-06T10:30:45",
"data": "Error description"
}
Money Flow Diagram
User requests withdrawal of 10,000 TZS
|
POST /disbursement/initiate
|
.-------+----------.
| |
Validate Calculate fees
channel platformFee = 500 TZS
balance selcomFee = 1,500 TZS
totalDebited = 12,000 TZS
disbursedAmt = 10,000 TZS
|
Save DisbursementRequest
Status: PENDING_OTP
|
Send OTP to verified phone
|
Return { otpToken }
|
POST /disbursement/confirm
(otpToken + otpCode)
|
Verify OTP
|
Wallet debited 12,000 TZS
Status: PROCESSING
|
Call Selcom API
(sends 10,000 TZS to recipient)
|
.--------+-------------------.
| | |
SUCCESS INPROGRESS FAIL
| | |
COMPLETED | REFUNDED
AWAITING wallet credited
CONFIRMATION 12,000 back
|
Polling job
(every 3 min)
|
.---------+---------.
| |
SUCCESS FAIL
| |
COMPLETED REFUNDED
wallet credited
12,000 back
Fee Structure
| Component | Amount | Description |
|---|---|---|
requestedAmount |
User defined | What the recipient receives |
platformFee |
500 TZS | NextGate service charge |
selcomFee |
1,500 TZS | Selcom per-disbursement fee |
totalDebited |
requestedAmount + 2,000 TZS | Actual amount leaving wallet |
disbursedAmount |
= requestedAmount | Amount Selcom sends to recipient |
Disbursement Status Reference
| Status | Description |
|---|---|
PENDING_OTP |
Initiated, waiting for OTP confirmation — wallet not debited yet |
PROCESSING |
OTP confirmed, wallet debited, Selcom call in progress |
COMPLETED |
Selcom confirmed successful transfer |
AWAITING_CONFIRMATION |
Selcom returned INPROGRESS — polling job resolving every 3 min |
REFUNDED |
Selcom failed — wallet refunded full totalDebited automatically |
FAILED |
Failed due to OTP lock — no wallet debit occurred |
MANUAL_REVIEW |
Polling exhausted max retries — admin review required |
Endpoints
1. Initiate Withdrawal
Purpose: Initiates a wallet withdrawal request. Validates channel, calculates fees, checks wallet balance, and sends OTP to user's verified phone number. No money moves at this step.
Endpoint: POST /disbursement/initiate
Access Level: 🔒 Protected — Requires Bearer Token
Authentication: Authorization: Bearer <token>
Request Headers:
| Header | Type | Required | Description |
|---|---|---|---|
| Authorization | string | Yes | Bearer JWT token |
| Content-Type | string | Yes | application/json |
Request JSON Sample:
{
"channelId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"amount": 10000,
"idempotencyKey": "usr-123-withdraw-1741234567"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
channelId |
UUID | Yes | ID of the verified withdrawal channel | Must belong to authenticated user and be active (cooling period passed) |
amount |
number | Yes | Amount to send to recipient in TZS | Min: 1000 TZS |
idempotencyKey |
string | Yes | Unique key to prevent duplicate withdrawals | Max 200 chars, unique per request |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "OTP sent to your verified phone number",
"action_time": "2026-03-06T10:30:45",
"data": {
"otpToken": "dfe7e16f-cd8f-4c00-b0d0-8cff09201936"
}
}
Success Response Fields:
| Field | Description |
|---|---|
data.otpToken |
OTP token string — pass this along with OTP code to /confirm |
Error Responses:
Insufficient balance (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Insufficient balance. You need 12000 TZS (10000 + 500 platform fee + 1500 transfer fee).",
"action_time": "2026-03-06T10:30:45",
"data": "Insufficient balance. You need 12000 TZS (10000 + 500 platform fee + 1500 transfer fee)."
}
Channel still in cooling period (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "This withdrawal channel is not yet active.",
"action_time": "2026-03-06T10:30:45",
"data": "This withdrawal channel is not yet active."
}
Phone not verified (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Your phone number must be verified before withdrawing.",
"action_time": "2026-03-06T10:30:45",
"data": "Your phone number must be verified before withdrawing."
}
Duplicate request (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Duplicate request — this withdrawal is already being processed.",
"action_time": "2026-03-06T10:30:45",
"data": "Duplicate request — this withdrawal is already being processed."
}
Amount below minimum (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Minimum withdrawal amount is 1000 TZS.",
"action_time": "2026-03-06T10:30:45",
"data": "Minimum withdrawal amount is 1000 TZS."
}
2. Confirm Withdrawal
Purpose: Confirms the withdrawal by verifying the OTP. Debits the wallet and calls Selcom to initiate the transfer. This is when money actually leaves the wallet.
Endpoint: POST /disbursement/confirm
Access Level: 🔒 Protected — Requires Bearer Token
Authentication: Authorization: Bearer <token>
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
otpToken |
string | Yes | Token returned from /initiate |
otpCode |
string | Yes | 6-digit OTP received via SMS |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Withdrawal processed successfully",
"action_time": "2026-03-06T10:30:45",
"data": null
}
Important: A
200 OKhere means the Selcom call was made. It does not guarantee the money has arrived at the recipient yet. Poll/status/{id}to confirm final delivery status.
Error Responses:
Invalid OTP (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Invalid OTP code.",
"action_time": "2026-03-06T10:30:45",
"data": "Invalid OTP code."
}
OTP locked — max attempts exceeded (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "OTP locked — max attempts exceeded.",
"action_time": "2026-03-06T10:30:45",
"data": "OTP locked — max attempts exceeded."
}
Already processing (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "This withdrawal is already being processed.",
"action_time": "2026-03-06T10:30:45",
"data": "This withdrawal is already being processed."
}
3. Get Disbursement Status
Purpose: Returns the current status and full details of a withdrawal request. Poll this after confirm to track final delivery.
Endpoint: GET /disbursement/status/{disbursementRequestId}
Access Level: 🔒 Protected — Requires Bearer Token
Authentication: Authorization: Bearer <token>
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
disbursementRequestId |
UUID | Yes | ID of the disbursement request |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Disbursement status retrieved",
"action_time": "2026-03-06T10:30:45",
"data": {
"disbursementRequestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"requestedAmount": 10000,
"platformFee": 500,
"selcomFee": 1500,
"totalDebited": 12000,
"disbursedAmount": 10000,
"currency": "TZS",
"destination": "255712****78",
"accountHolderName": "JOHN DOE",
"status": "COMPLETED",
"failureReason": null,
"transactionRef": "NXT-TXN-20260306-ABCD1234",
"supportRef": null,
"createdAt": "2026-03-06T10:30:00",
"completedAt": "2026-03-06T10:31:45"
}
}
Success Response Fields:
| Field | Description |
|---|---|
disbursementRequestId |
UUID of this withdrawal request |
requestedAmount |
Amount sent to recipient in TZS |
platformFee |
NextGate fee charged on top |
selcomFee |
Selcom transfer fee charged on top |
totalDebited |
Total amount debited from wallet |
disbursedAmount |
Amount Selcom sent to recipient — equals requestedAmount |
currency |
Always TZS |
destination |
Masked destination e.g. 255712****78 |
accountHolderName |
Verified recipient name |
status |
Current status — see status table above |
failureReason |
Populated if FAILED or REFUNDED |
transactionRef |
Wallet transaction reference |
supportRef |
Support reference for escalations — populated on MANUAL_REVIEW |
createdAt |
When withdrawal was initiated |
completedAt |
When transfer was confirmed — null if not yet completed |
Error Responses:
Not found or not owned by user (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Disbursement request not found",
"action_time": "2026-03-06T10:30:45",
"data": "Disbursement request not found"
}
Quick Reference
| Method | Endpoint | Description |
|---|---|---|
| POST | /disbursement/initiate |
Start withdrawal, sends OTP |
| POST | /disbursement/confirm |
Confirm with OTP, debits wallet, calls Selcom |
| GET | /disbursement/status/{id} |
Check withdrawal status |