Skip to main content

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 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) then confirm-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