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