Skip to main content

Disbursement Channel API

Disbursement Channel API

Author: Josh Lead Backend Team
Last Updated: 2026-03-06
Version: v1.0

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-step flow:flow: initiate-/lookup → /add (sends OTP) then confirm-/add/confirm
  • The /lookup step returns a signed confirmationToken required by /add (verifies OTP)
  • expires
  • Newlyin added10 channels have a cooling period (default 24 hours) before they can be used for withdrawalsminutes
  • Selcom name lookup is performed beforeat OTPboth is/lookup sentand /addtheverified twice server-side
  • The first channel on an account holderactivates nameimmediately is verified24-hour automaticallycooling applies to all subsequent channels
  • Deleting a channel also requires OTP confirmation — two-stepstep: flowDELETE same/{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>

The token is obtained from the NextGate authentication API after login. When a request returnsOn 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.login.

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 initiate-addthe orbeginning. initiateThe deleteserver 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
listing channels, visually

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 What it meansMeaning Client action
400 Invalid request or businessBusiness 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,errors, fix and resubmit
500 Server error Show generic error, allow user tomanual retry manually
Network timeout No response received For lookupLookup and list: safe to retry. For initiate/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/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
PENDING_OTPPENDING_ACTIVATION Channel created, waiting for OTP confirmationconfirmed, within 24-hour cooling period — not yet usable
ACTIVE Channel verified and usable (after cooling period)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.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: Authorization: Bearer <token>required

Request Headers:

HeaderTypeRequiredDescription
AuthorizationstringYesBearer JWT token
Content-TypestringYesapplication/json

Request JSON SampleBody:

{
  "channelType": "MPESA",
  "destination": "255712345678",
  "bankCode": null
}

Request Body Parameters:

Parameter Type Required DescriptionValidation
channelType string Yes Type of channelenum: MPESA, AIRTEL, TIGOTIGOPESA, HALOPESA, SELCOM_PESA, BANK
destination string Yes Phone number (255XXXXXXXXX) or bank account number
Format:bankCodestringConditionalRequired when 255XXXXXXXXXchannelType for mobile; account number foris 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 — showdisplay to user for confirmation
destinationdestinationDisplay TheMasked destination that was looked up
channelType The channel type
confirmationTokenSigned token required by /add. Valid for 10 minutes. Store in memory

Error ResponsesErrors:

(400):













"action_time":"2026-03-06T10:30:45","data": "Account not found for this destination."
}

(400):

"action_time":"2026-03-06T10:30:45","data": "Name lookup failed. Please try again." }
ScenarioMessage
Account not found { "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.",
Selcom lookup failed "Could
{not "success":verify false,
  "httpStatus": "BAD_REQUEST",
  "message": "Name lookup failed.account. Please check the details and try again.",

2. Initiate Add Channel

Purpose: Initiates adding a new 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.

Endpoint: POST /disbursement/channels/add

Access Level: 🔒 Protected — Requires Bearer Token

Authentication: Authorization: Bearer <token>required

Request JSON SampleBody (Mobile Money):

{
  "channelType": "MPESA",
  "destination": "255712345678",
  "accountHolderName"bankCode": null,
  "confirmationToken": "JOHN<token DOE",from /lookup>"bankCode": null
}

Request JSON SampleBody (Bank):

{
  "channelType": "BANK",
  "destination": "0012345678901",
  "accountHolderName": "JOHN DOE",
  "bankCode": "CRDB",
  "confirmationToken": "<token from /lookup>"
}

Request Body Parameters:

Parameter Type Required DescriptionValidation
channelType string Yes Type of channelenum: MPESA, AIRTEL, TIGOTIGOPESA, HALOPESA, SELCOM_PESA, BANK
destination string Yes Phone or account number Format: 255XXXXXXXXX for mobile
accountHolderNamestringYesName from the lookup step — shown to user for confirmationMax 200 chars
bankCode string Conditional Bank codeRequired only when channelType is BANK
confirmationTokenstringYesToken from /lookup. Must match destination, channel type, and account

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": "otp-token-uuid-here"
  }
}

Success Response Fields:

Field Description
dataotpToken OTP token string — pass thisPass to /confirm-addadd/confirm with the SMS OTP code

Error ResponsesErrors:

Phone

notverified(400):







false,

"message":"Yourphone 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."
}

(400):




}

Bank

codemissing (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."
}
Scenario Message
Token expired{"Confirmation token expired. Please look up the account again."success":
Token invalid/mismatch"httpStatus":Invalid confirmation token."BAD_REQUEST",
Channel already existsactive { "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."

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.use (first channel activates immediately).

Endpoint: POST /disbursement/channels/add/confirm

Access Level: 🔒 Protected — Requires Bearer Token

Authentication: Authorization: Bearer <token>required

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 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
channelId UUID — use this when initiating withdrawals
channelType Channel type
destinationDisplay Masked destination e.g. 255712****78
accountHolderName Verified name from Selcom
bankCodebankName Bank codename — only for BANK channels, null otherwise
isPrimaryTrue if this is now the user's primary channel
status ACTIVE or PENDING_ACTIVATION
isUsable false immediatelyif afterstill adding — becomes true afterin cooling period
activatesAt When the channel becomes usable for withdrawals
createdAtWhen the channel was created

Error ResponsesErrors:

(400):

"action_time":"2026-03-06T10:30:45","data": "Invalid OTP code."
}

(400):




"data":"OTPlocked — max attempts exceeded."
}
ScenarioMessage
Invalid OTP { "success": false, "httpStatus": "BAD_REQUEST", "message": "Invalid OTP code.",
OTP locked { "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",

4. List My Channels

Purpose: Returns all active withdrawal channels belonging tofor the authenticated user.

Endpoint: GET /disbursement/channels

Access Level: 🔒 Protected — Requires Bearer Token required

Authentication:Only Authorization:ACTIVE Bearerchannels <token>are returned. Deleted and pending-activation channels that have not yet been OTP-confirmed are excluded.

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*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
channelId UUID of the channel
channelType Channel type
destinationDisplay Masked destination
accountHolderName Verified name from Selcom
bankCodebankName Bank codename — only for BANK channels, null otherwise
isPrimaryWhether this is the user's primary channel
status ACTIVE or DELETED
isUsable true if cooling period has passed and channel can be used now
activatesAt When the cooling period ends
createdAtWhen 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/channels/{channelId}

Access Level: 🔒 Protected — Requires Bearer Token

Authentication: Authorization: Bearer <token>required

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": {
    "otpToken": "otp-token-uuid-here"
  }
}

Error ResponsesErrors:

(400):

"action_time":"2026-03-06T10:30:45","data": "Withdrawal channel







ScenarioMessage
Channel not found or/ not owned { "success": false, "httpStatus": "BAD_REQUEST", "message": "Withdrawal channelChannel not found.",
Channel 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

Purpose: Confirms channel deletion by verifying the OTP. Channel is permanently removedremoved. andIf canit nowas longerthe beprimary usedchannel, foranother withdrawals.active channel is automatically promoted.

Endpoint: DELETE /disbursement/channels/{channelId}/confirm

Access Level: 🔒 Protected — Requires Bearer Token

Authentication: Authorization: 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 JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Withdrawal channelChannel deleted successfully",
  "action_time": "2026-03-06T10:30:45",
  "data": null
}

Error ResponsesErrors:

(400):

"action_time":"2026-03-06T10:30:45","data": "Invalid 







ScenarioMessage
Invalid OTP { "success": false, "httpStatus": "BAD_REQUEST", "message": "Invalid OTP code.",
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/lookup Verify account namename, beforeget addingconfirmationToken
POST /disbursement/channel/initiate-channels/add Start add channel flow (requires confirmationToken)
POST /disbursement/channel/confirm-addchannels/add/confirm Complete add channel with OTP
GET /disbursement/channel/my-channels List all active user channels
DELETE /disbursement/channel/channels/{id}/initiatechannelId} Start delete channel flow
DELETE /disbursement/channel/channels/{id}channelId}/confirm Complete delete with OTP