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
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": "otp-token-uuid-here"
}
Success Response Fields:
| Field | Description |
|---|---|
data |
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 |