Disbursement Channel API
Disbursement Channel API
Base URL: https://api.nextgate.co.tz/api/v1
Short Description: The Disbursement Channel API manages theManages withdrawal destinations for NextGate users. A channel representsis 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.
HintsKey notes:
- Adding a channel is a
two-three-stepflow:flow:→initiate-/lookup/add(sends OTP) then→confirm-/add/confirm - The
/lookupstep returns a signedconfirmationTokenrequired by/add(verifies—OTP)expires Newlyinadded10channels have a cooling period (default 24 hours) before they can be used for withdrawalsminutes- Selcom name lookup is performed
beforeatOTPbothis/lookupsentand/add—theverified twice server-side - The first channel on an account
holderactivatesnameimmediatelyis—verified24-hourautomaticallycooling applies to all subsequent channels - Deleting a channel
alsorequires OTP confirmation — two-stepstep:flowDELETEsame/{channelId}as→addingDELETE /{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 in this API are protected. The client must includerequire a valid JWT Bearer token in every request header:token:
Authorization: Bearer <your_token>
Never store the JWT token in alocalStorage place accessible to third-party scripts. On mobile,— use secure device storage.storage Onon web, prefer memorymobile, or HttpOnly cookies overon localStorage.web.
OTP Token Handling
TheBoth OTPthe flow is two-step for both addingadd and deletingdelete aflows channel.are two-step. The first step (initiate-add or initiate delete) returns an otpToken. string.Store This token must be storedthis in memory and passedpass it to the secondconfirm 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 theSMS OTP codecode. to the specific channel action. However, treat it with care: doDo not log it,it door not storepersist it beyond the duration of the OTP confirmation flow.
The OTP code itself expires after a short window (typically ~5 minutes).minutes. If the user takesis too long to enter the code,slow, the confirm step will returnreturns an error.error In— thisprompt case,them to restart the client should prompt the user to restart theentire flow from the initiate-addorbeginning. The initiatedeleteserver —must doclean not allowup the userprevious topending requestchannel record before a new OTPone without starting over, as the previous channel record mustcan be cleaned up server-side.created.
If the user enters the wrong OTP codeis multipleentered too many times, the OTP will beis locked after the maximum attempts are exceeded. The confirm endpoint will return an OTP locked error. In this caseand the flow is terminatedterminated. —The the clientuser must start over from the beginning.over.
Name Lookup Beforeand AddConfirmation Token
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,step displayreturns a signed confirmationToken that encodes the returneddestination, 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,user and require explicit confirmation before proceeding tocalling /.initiate-add
This protects the userusers from accidentally 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.destination.
Cooling Period UX
After aOTP 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. Theconfirmation, isUsable field will beis false until thatactivatesAt. 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 orShow a message likelike:
"This channel will be ready for withdrawals on {activatesAt date}"
.When
Visually distinguish usable channels from those still in the cooling period.period when listing.
Exception: The first channel added to an account activates immediately (isUsable: true, activatesAt set to now).
Deleting a Channel
Deleting a channelDeletion is irreversibleirreversible. and also requires OTP confirmation. The client should show the userShow a clear confirmation dialog before initiating the delete flow, explaining that the channel will be permanently removed.flow.
If athe userdeleted haschannel anywas pendingthe primary channel, the server automatically promotes another active channel as primary.
Pending disbursements that reference a deleted channel they are trying to delete, those disbursements are not affected — the disbursement entity holds a snapshot of the channel data atfrom theinitiation time of initiation. The delete only prevents future withdrawals to that destination.time.
Network Error Handling
| HTTP Status | Client action | |
|---|---|---|
400 |
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 |
500 |
Server error | Show generic error, allow |
| Network timeout | No response |
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/disbursement/channels/lookup ------------> Selcom Name Lookup
API
| ({ channelType, destination)destination, bankCode? } |
|<-- { accountHolderNameaccountHolderName, }destinationDisplay, Returns verified name
| channelType, confirmationToken }
| <-- store confirmationToken (expires 10 min)
| [User reviews name, confirms]
|
|--- POST /channel/initiate-disbursement/channels/add ----------> Validate channelconfirmationToken
| ({ channelType, destination, CheckRe-verify duplicatename with Selcom
| accountHolderName,bankCode?, bankCode)confirmationToken Name} lookupCheck againfor | Generate OTPduplicate
| Save channel (PENDING_OTP)record
| Generate + send OTP
|<-- { otpToken }
Send OTP to verified phone
|
|--- POST /channel/confirm-adddisbursement/channels/add/confirm -------> Verify OTP
| (otpToken, otpCode)?otpToken=...&otpCode=... Activate channel (ACTIVE)
|<-- { channelId, isUsable, activatesAt, ... } Set (cooling period24h, (24h)except first channel)
|
| [Cooling period: 24 hours]
| [Channel becomes usable forafter withdrawals]activatesAt]
|
|--- GET /channel/my-disbursement/channels ---------------> List useractive channels
|<-- [ { channelId, isUsable, activatesAt, ... } ]
DELETE CHANNEL FLOW
-------------------
User
|
|--- DELETE /channel/disbursement/channels/{id}/initiatechannelId} ---------> Generate + send OTP
|<-- { otpToken }
Send OTP to verified phone
|
|--- DELETE /channel/disbursement/channels/{id}channelId}/confirm ----> Verify OTP
| (otpToken, otpCode)?otpToken=...&otpCode=... Soft delete channel
|<-- 200 OK Reassign primary if needed
Channel Status Reference
| Status | Description |
|---|---|
|
Channel |
ACTIVE |
Channel verified and |
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.Selcom, Callalong with a signed confirmationToken required for the next step. Always call this before initiating /add. so the user can confirm the name before proceeding.
Endpoint: POST /disbursement/channels/lookup
Access Level: 🔒 Protected — Requires Bearer Token
Authentication: requiredAuthorization: Bearer <token>
Request Headers:
|
Request JSON SampleBody:
{
"channelType": "MPESA",
"destination": "255712345678",
"bankCode": null
}
Request Body Parameters:
| Parameter | Type | Required | Description | |
|---|---|---|---|---|
channelType |
string | Yes | MPESA, AIRTEL, , HALOPESA, SELCOM_PESA, BANK |
|
destination |
string | Yes | Phone number (255XXXXXXXXX) or bank account number |
|
bankCode |
string | Conditional | Required when BANK |
Success Response JSON Sample:
{
"success": true,
"httpStatus": "OK",
"message": "Account found"verified successfully",
"action_time": "2026-03-06T10:30:45",
"data": {
"accountHolderName": "JOHN DOE",
"destination"destinationDisplay": "255712345678"2557****678",
"channelType": "MPESA",
"confirmationToken": "<signed-token>"
}
}
Success Response Fields:
| Field | Description |
|---|---|
accountHolderName |
Verified name from Selcom — |
|
|
channelType |
The channel type |
confirmationToken |
Signed token required by /add. Valid for 10 minutes. Store in memory |
Error ResponsesErrors:
Purpose: Initiates adding a new Endpoint: Access Request Request Success Response Purpose: Confirms adding a new channel by verifying the OTP. On success the channel is activated with a cooling period before first Endpoint: Access Query Parameters: Success Response Purpose: Returns all active withdrawal channels Endpoint: Access Success Response Purpose: Initiates deletion of a withdrawal channel. Sends OTP to user's verified phone for confirmation. Endpoint: Access Path Parameters: Success Response Purpose: Confirms channel deletion by verifying the OTP. Channel is permanently Endpoint: Access Path Parameters: Query Parameters: Success Response
Scenario
Message
Account not found
(400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Account not foundfound. 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 thisbank destination.channels.","action_time":"2026-03-06T10:30:45",
"data": "Account not found for this destination."
}
Selcom lookup failed
(400):"Could {not "success":verify false,
"httpStatus": "BAD_REQUEST",
"message": "Name lookup failed.account. Please check the details and try again.","action_time":"2026-03-06T10:30:45",
"data": "Name lookup failed. Please try again."
}
2. Initiate Add Channel
withdrawal channel. PerformsValidates the confirmationToken from /lookup, re-verifies the name verificationwith againSelcom server-sideside, saves the channel record, and sends an OTP to the user's verified phone number.phone.POST /disbursement/channels/add Level: 🔒 Protected — Requires Bearer TokenAuthentication: requiredAuthorization: Bearer <token>JSON SampleBody (Mobile Money):{
"channelType": "MPESA",
"destination": "255712345678",
"accountHolderName"bankCode": null,
"confirmationToken": "JOHN<token DOE",from /lookup>"bankCode": null
}
JSON SampleBody (Bank):{
"channelType": "BANK",
"destination": "0012345678901",
"accountHolderName": "JOHN DOE",
"bankCode": "CRDB",
"confirmationToken": "<token from /lookup>"
}
Request Body Parameters:
Parameter
Type
Required
Description
Validation
channelTypestring
Yes
Type of channel
enum: MPESA, AIRTEL, , TIGOTIGOPESAHALOPESA, SELCOM_PESA, BANK
destinationstring
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
bankCodestring
Conditional
Bank code
Required
only when channelType is BANK
confirmationTokenstring
Yes
Token from
/lookup. Must match destination, channel type, and account JSON Sample:{
"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"
}
}
Success Response Fields:
Field
Description
dataotpTokenOTP token string — pass thisPass to / with the SMS OTP codeconfirm-addadd/confirmError ResponsesErrors:Phone
not
verified
(400):Scenario
Message
Token expired
{"Confirmation token expired. Please look up the account again."success":false,
Token invalid/mismatch
"httpStatus":Invalid confirmation token."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
existsactive(400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "This destination is already registeredan as aactive withdrawal channel.",
Max channels reached
"action_time":Maximum "2026-03-06T10:30:45",of "data": "This destination is already registered as a{n} withdrawal channel.channels allowed."}
Add window closed
"Withdrawal channel add window has closed."Bankcode
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
use.use (first channel activates immediately).POST /disbursement/channels/add/confirm Level: 🔒 Protected — Requires Bearer TokenAuthentication: requiredAuthorization: Bearer <token>
Parameter
Type
Required
Description
otpTokenstring
Yes
Token returned from
/initiate-add
otpCodestring
Yes
6-digit OTP received via SMS
JSON Sample:{
"success": true,
"httpStatus": "OK",
"message": "Withdrawal channelChannel added successfully",
"action_time": "2026-03-06T10:30:45",
"data": {
"channelId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"channelType": "MPESA",
"destinationDisplay": "255712*2557****78"678",
"accountHolderName": "JOHN DOE",
"bankCode"bankName": null,
"isPrimary": false,
"status": "ACTIVE",
"isUsable": false,
"activatesAt": "2026-03-07T10:31:00",
"createdAt": "2026-03-06T10:31:00"
}
}
Success Response Fields:
Field
Description
channelIdUUID — use this when initiating withdrawals
channelTypeChannel type
destinationDisplayMasked destination
e.g. 255712****78
accountHolderNameVerified name from Selcom
bankCodebankNameBank
codename — only for BANK channels, null otherwise
isPrimaryTrue if this is now the user's primary channel
statusACTIVE or PENDING_ACTIVATION
isUsablefalse immediatelyif afterstill adding — becomes true afterin cooling period
activatesAtWhen the channel becomes usable for withdrawals
createdAt
When the channel was created
Error ResponsesErrors:
Scenario
Message
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.",
Channel no longer valid
"action_time":This channel is no longer valid. Please add it again."2026-03-06T10:30:45","data":"OTP
locked — max attempts exceeded."
}
4. List My Channels
belonging tofor the authenticated user.GET /disbursement/channels Level: 🔒 Protected — Requires Bearer Token required
Authentication:Only Authorization:ACTIVEBearerchannels <token>are returned. Deleted and pending-activation channels that have not yet been OTP-confirmed are excluded. 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*2557****78"678",
"accountHolderName": "JOHN DOE",
"bankCode"bankName": null,
"isPrimary": true,
"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"bankName": "CRDB"CRDB Bank",
"isPrimary": false,
"status": "ACTIVE",
"isUsable": false,
"activatesAt": "2026-03-07T08:00:00",
"createdAt": "2026-03-06T08:00:00"
}
]
}
Success Response Fields:
Field
Description
channelIdUUID of the channel
channelTypeChannel type
destinationDisplayMasked destination
accountHolderNameVerified name from Selcom
bankCodebankNameBank
codename — only for BANK channels, null otherwise
isPrimaryWhether this is the user's primary channel
statusACTIVE or DELETED
isUsabletrue if cooling period has passed and channel can be used now
activatesAtWhen the cooling period ends
createdAt
When the channel was added
5. Initiate Delete Channel
DELETE /disbursement/channels/{channelId} Level: 🔒 Protected — Requires Bearer TokenAuthentication: requiredAuthorization: Bearer <token>
Parameter
Type
Required
Description
channelIdUUID
Yes
ID of the channel to delete
JSON Sample:{
"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"
}
}
Error ResponsesErrors:
Scenario
Message
Channel not found
or/ not owned(400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Withdrawal channelChannel not found.","action_time":"2026-03-06T10:30:45",
"data": "Withdrawal channelChannel not
found.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
removedremoved. andIf canit nowas longerthe beprimary usedchannel, foranother withdrawals.active channel is automatically promoted.DELETE /disbursement/channels/{channelId}/confirm Level: 🔒 Protected — Requires Bearer TokenAuthentication: requiredAuthorization: Bearer <token>
Parameter
Type
Required
Description
channelIdUUID
Yes
ID of the channel to delete
Parameter
Type
Required
Description
otpTokenstring
Yes
Token returned from
delete/initiate
otpCodestring
Yes
6-digit OTP received via SMS
JSON Sample:{
"success": true,
"httpStatus": "OK",
"message": "Withdrawal channelChannel deleted successfully",
"action_time": "2026-03-06T10:30:45",
"data": null
}
Error ResponsesErrors:
Scenario
Message
Invalid OTP
(400):
{
"success": false,
"httpStatus": "BAD_REQUEST",
"message": "Invalid OTP code.","action_time":"2026-03-06T10:30:45",
"data": "Invalid OTP
code.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/channel/channels/lookupVerify account
namename, beforeget addingconfirmationToken
POST/disbursement/channel/initiate-channels/addStart add channel flow (requires
confirmationToken)
POST/disbursement/channel/confirm-addchannels/add/confirmComplete add channel with OTP
GET/disbursement/channel/my-channelsList all active user channels
DELETE/disbursement/channel/channels/{id}/initiatechannelId}Start delete channel flow
DELETE/disbursement/channel/channels/{id}channelId}/confirmComplete delete with OTP