Disbursement Channel API
Base URL: https://api.nextgate.co.tz/api/v1
Short Description: The Disbursement Channel API manages the withdrawal destinations for NextGate users. A channel represents a verified mobile money account 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.
Hints:
- Adding a channel is a two-step flow:
initiate-add(sends OTP) thenconfirm-add(verifies OTP) - Newly added channels have a cooling period (default 24 hours) before they can be used for withdrawals
- Selcom name lookup is performed before OTP is sent — the account holder name is verified automatically
- Deleting a channel also requires OTP confirmation — two-step flow same as adding
- Deleted channels cannot be used for new withdrawals — in-flight disbursements snapshot channel data at initiation time
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.
OTP Token Handling
The OTP flow is two-step for both adding and deleting a channel. The first step (initiate-add or initiate delete) returns an otpToken string. This token must be stored in memory and passed to the second step (confirm-add or confirm delete) along with the OTP code the user receives via SMS.
The otpToken is not a security credential on its own — it is a session reference that ties the OTP code to the specific channel action. However, treat it with care: do not log it, do not store it beyond the duration of the OTP confirmation flow.
The OTP code itself expires after a short window (typically 5 minutes). If the user takes too long to enter the code, the confirm step will return an error. In this case, the client should prompt the user to restart the flow from initiate-add or initiate delete — do not allow the user to request a new OTP without starting over, as the previous channel record must be cleaned up server-side.
If the user enters the wrong OTP code multiple times, the OTP will be locked after the maximum attempts are exceeded. The confirm endpoint will return an OTP locked error. In this case the flow is terminated — the client must start over from the beginning.
Name Lookup Before Add
The lookup step is separate from the initiate-add step intentionally. Its purpose is to let the user see and confirm the account holder name before committing to adding the channel. The client must always call /lookup first, display the returned accountHolderName prominently to the user, and require explicit confirmation before proceeding to /initiate-add.
This protects the user from adding a wrong destination by mistake — for example a typo in the phone number that happens to belong to someone else. The name displayed is fetched directly from Selcom's registry and reflects the actual registered owner of that number or account.
Cooling Period UX
After a channel is successfully added, it is not immediately usable for withdrawals. The activatesAt field in the confirm response tells you exactly when the cooling period ends. The isUsable field will be false until that time.
The client should communicate this clearly to the user — do not simply show the channel as active and let them discover it is not usable when they try to withdraw. A good UX shows a countdown or a message like "This channel will be ready for withdrawals on {activatesAt date}". When listing channels, visually distinguish usable channels from those still in the cooling period.
Deleting a Channel
Deleting a channel is irreversible and also requires OTP confirmation. The client should show the user a clear confirmation dialog before initiating the delete flow, explaining that the channel will be permanently removed.
If a user has any pending disbursements that reference a channel they are trying to delete, those disbursements are not affected — the disbursement entity holds a snapshot of the channel data at the time of initiation. The delete only prevents future withdrawals to that destination.
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 | No response received | For lookup and list: safe to retry. For initiate/confirm: retry with caution — check status first |
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"
}
Flow Diagram
ADD CHANNEL FLOW
----------------
User
|
|--- POST /channel/lookup ------------> Selcom Name Lookup API
| (channelType, destination) |
|<-- { accountHolderName } Returns verified name
|
| [User reviews name, confirms]
|
|--- POST /channel/initiate-add ------> Validate channel
| (channelType, destination, Check duplicate
| accountHolderName, bankCode) Name lookup again
| Generate OTP
| Save channel (PENDING_OTP)
|<-- { otpToken } Send OTP to verified phone
|
|--- POST /channel/confirm-add -------> Verify OTP
| (otpToken, otpCode) Activate channel (ACTIVE)
|<-- { channelId, activatesAt, ... } Set cooling period (24h)
|
| [Cooling period: 24 hours]
| [Channel becomes usable for withdrawals]
|
|--- GET /channel/my-channels --------> List user channels
|<-- [ { channelId, isUsable, ... } ]
DELETE CHANNEL FLOW
-------------------
User
|
|--- DELETE /channel/{id}/initiate ---> Generate OTP
|<-- { otpToken } Send OTP to verified phone
|
|--- DELETE /channel/{id}/confirm ----> Verify OTP
| (otpToken, otpCode) Soft delete channel
|<-- 200 OK
Channel Status Reference
| Status | Description |
|---|---|
PENDING_OTP |
Channel created, waiting for OTP confirmation |
ACTIVE |
Channel verified and usable (after cooling period) |
DELETED |
Channel removed by user |
Endpoints
1. Lookup Account Name
Purpose: Verifies a destination account and returns the registered account holder name from Selcom. Call this before initiating add so the user can confirm the name before proceeding.
Endpoint: POST /disbursement/channel/lookup
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:
{
"channelType": "MPESA",
"destination": "255712345678"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
channelType |
string | Yes | Type of channel | enum: MPESA, AIRTEL, TIGO, HALOPESA, SELCOM_PESA, BANK |
destination |
string | Yes | Phone number or bank account number | Format: 255XXXXXXXXX for mobile; account number for BANK |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Account found",
"action_time": "2026-03-06T10:30:45",
"data": {
"accountHolderName": "JOHN DOE",
"destination": "255712345678",
"channelType": "MPESA"
}
}
Success Response Fields:
| Field | Description |
|---|---|
accountHolderName |
Verified name from Selcom — show to user for confirmation |
destination |
The destination that was looked up |
channelType |
The channel type |
Error Responses:
Account not found (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Account not found for this destination.",
"action_time": "2026-03-06T10:30:45",
"data": "Account not found for this destination."
}
Selcom lookup failed (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Name lookup failed. Please try again.",
"action_time": "2026-03-06T10:30:45",
"data": "Name lookup failed. Please try again."
}
2. Initiate Add Channel
Purpose: Initiates adding a new withdrawal channel. Performs name verification again server-side and sends OTP to user's verified phone number.
Endpoint: POST /disbursement/channel/initiate-add
Access Level: 🔒 Protected — Requires Bearer Token
Authentication: Authorization: Bearer <token>
Request JSON Sample (Mobile Money):
{
"channelType": "MPESA",
"destination": "255712345678",
"accountHolderName": "JOHN DOE",
"bankCode": null
}
Request JSON Sample (Bank):
{
"channelType": "BANK",
"destination": "0012345678901",
"accountHolderName": "JOHN DOE",
"bankCode": "CRDB"
}
Request Body Parameters:
| Parameter | Type | Required | Description | Validation |
|---|---|---|---|---|
channelType |
string | Yes | Type of channel | enum: MPESA, AIRTEL, TIGO, HALOPESA, SELCOM_PESA, BANK |
destination |
string | Yes | Phone or account number | Format: 255XXXXXXXXX for mobile |
accountHolderName |
string | Yes | Name from the lookup step — shown to user for confirmation | Max 200 chars |
bankCode |
string | Conditional | Bank code | Required only when channelType is BANK |
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 to /confirm-add |
Error Responses:
Phone not verified (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Your phone number must be verified before adding a withdrawal channel.",
"action_time": "2026-03-06T10:30:45",
"data": "Your phone number must be verified before adding a withdrawal channel."
}
Channel already exists (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "This destination is already registered as a withdrawal channel.",
"action_time": "2026-03-06T10:30:45",
"data": "This destination is already registered as a withdrawal channel."
}
Bank code missing (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Bank code is required for BANK channels.",
"action_time": "2026-03-06T10:30:45",
"data": "Bank code is required for BANK channels."
}
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.
Endpoint: POST /disbursement/channel/confirm-add
Access Level: 🔒 Protected — Requires Bearer Token
Authentication: Authorization: Bearer <token>
Query Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
otpToken |
string | Yes | Token returned from /initiate-add |
otpCode |
string | Yes | 6-digit OTP received via SMS |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Withdrawal channel added successfully",
"action_time": "2026-03-06T10:30:45",
"data": {
"channelId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"channelType": "MPESA",
"destinationDisplay": "255712****78",
"accountHolderName": "JOHN DOE",
"bankCode": null,
"status": "ACTIVE",
"isUsable": false,
"activatesAt": "2026-03-07T10:31:00",
"createdAt": "2026-03-06T10:31:00"
}
}
Success Response Fields:
| Field | Description |
|---|---|
channelId |
UUID — use this when initiating withdrawals |
channelType |
Channel type |
destinationDisplay |
Masked destination e.g. 255712****78 |
accountHolderName |
Verified name from Selcom |
bankCode |
Bank code — only for BANK channels, null otherwise |
status |
ACTIVE |
isUsable |
false immediately after adding — becomes true after cooling period |
activatesAt |
When channel becomes usable for withdrawals |
createdAt |
When the channel was created |
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 (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."
}
4. List My Channels
Purpose: Returns all withdrawal channels belonging to the authenticated user.
Endpoint: GET /disbursement/channel/my-channels
Access Level: 🔒 Protected — Requires Bearer Token
Authentication: Authorization: Bearer <token>
Success Response JSON Sample:
{
"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": "255712****78",
"accountHolderName": "JOHN DOE",
"bankCode": null,
"status": "ACTIVE",
"isUsable": true,
"activatesAt": "2026-03-05T10:31:00",
"createdAt": "2026-03-04T10:31:00"
},
{
"channelId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"channelType": "BANK",
"destinationDisplay": "0012****789",
"accountHolderName": "JOHN DOE",
"bankCode": "CRDB",
"status": "ACTIVE",
"isUsable": false,
"activatesAt": "2026-03-07T08:00:00",
"createdAt": "2026-03-06T08:00:00"
}
]
}
Success Response Fields:
| Field | Description |
|---|---|
channelId |
UUID of the channel |
channelType |
Channel type |
destinationDisplay |
Masked destination |
accountHolderName |
Verified name from Selcom |
bankCode |
Bank code — only for BANK channels, null otherwise |
status |
ACTIVE or DELETED |
isUsable |
true if cooling period has passed and channel can be used now |
activatesAt |
When cooling period ends |
createdAt |
When the channel was added |
5. Initiate Delete Channel
Purpose: Initiates deletion of a withdrawal channel. Sends OTP to user's verified phone for confirmation.
Endpoint: DELETE /disbursement/channel/{channelId}/initiate
Access Level: 🔒 Protected — Requires Bearer Token
Authentication: Authorization: Bearer <token>
Path Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
channelId |
UUID | Yes | ID of the channel to delete |
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"
}
Error Responses:
Channel not found or not owned (400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Withdrawal channel not found.",
"action_time": "2026-03-06T10:30:45",
"data": "Withdrawal channel not found."
}
6. Confirm Delete Channel
Purpose: Confirms channel deletion by verifying the OTP. Channel is permanently removed and can no longer be used for withdrawals.
Endpoint: DELETE /disbursement/channel/{channelId}/confirm
Access Level: 🔒 Protected — Requires Bearer Token
Authentication: Authorization: Bearer <token>
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 JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Withdrawal channel deleted successfully",
"action_time": "2026-03-06T10:30:45",
"data": null
}
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."
}
Quick Reference
| Method | Endpoint | Description |
|---|---|---|
| POST | /disbursement/channel/lookup |
Verify account name before adding |
| POST | /disbursement/channel/initiate-add |
Start add channel flow |
| POST | /disbursement/channel/confirm-add |
Complete add channel with OTP |
| GET | /disbursement/channel/my-channels |
List all user channels |
| DELETE | /disbursement/channel/{id}/initiate |
Start delete channel flow |
| DELETE | /disbursement/channel/{id}/confirm |
Complete delete with OTP |