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

Description: Manages withdrawal destinations for NextGate users. A channel is a verified mobile money 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.

Key notes:

  • Adding a channel is a three-step flow: /lookup/add/add/confirm
  • The /lookup step returns a signed confirmationToken required by /add — expires in 10 minutes
  • Selcom name lookup is performed at both /lookup and /add — verified twice server-side
  • The first channel on an account activates immediately — 24-hour cooling applies to all subsequent channels
  • Deleting a channel requires OTP confirmation — two-step: DELETE /{channelId}DELETE /{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 require a valid JWT Bearer token:

Authorization: Bearer <your_token>

On 401 UNAUTHORIZED, silently refresh the token and retry once. If refresh fails, redirect to login.

Never store the JWT in localStorage — use secure device storage on mobile, or HttpOnly cookies on web.


OTP Token Handling

Both the add and delete flows are two-step. The first step returns an otpToken. Store this in memory and pass it to the confirm step along with the SMS OTP code. Do not log it or persist it beyond the confirmation flow.

The OTP expires after ~5 minutes. If the user is too slow, the confirm step returns an error — prompt them to restart the entire flow from the beginning. The server must clean up the previous pending channel record before a new one can be created.

If the wrong OTP is entered too many times, the OTP is locked and the flow is terminated. The user must start over.


Name Lookup and Confirmation Token

The /lookup step returns a signed confirmationToken that encodes the destination, 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 and require explicit confirmation before calling /add. This protects users from accidentally adding a wrong destination.


Cooling Period UX

After OTP confirmation, isUsable is false until activatesAt. Show a message like:

"This channel will be ready for withdrawals on {activatesAt date}"

Visually distinguish usable channels from those still in the cooling period when listing.

Exception: The first channel added to an account activates immediately (isUsable: true, activatesAt set to now).


Deleting a Channel

Deletion is irreversible. Show a clear confirmation dialog before initiating the delete flow.

If the deleted channel was the primary channel, the server automatically promotes another active channel as primary.

Pending disbursements that reference a deleted channel are not affected — the disbursement holds a snapshot of the channel data from initiation time.


Network Error Handling

HTTP Status Meaning Client action
400 Business rule violation 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 errors, fix and resubmit
500 Server error Show generic error, allow manual retry
Network timeout No response Lookup and list: safe to retry. Initiate/confirm: check status first

Standard Response Format

Success

{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2026-03-06T10:30:45",
  "data": {}
}

Error

{
  "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 /disbursement/channels/lookup -------> Selcom Name Lookup
 |       { channelType, destination, bankCode? }          |
 |<-- { accountHolderName, destinationDisplay,    Returns verified name
 |       channelType, confirmationToken }
 |                                            <-- store confirmationToken (expires 10 min)
 |   [User reviews name, confirms]
 |
 |--- POST /disbursement/channels/add ----------> Validate confirmationToken
 |       { channelType, destination,               Re-verify name with Selcom
 |         bankCode?, confirmationToken }           Check for duplicate
 |                                                  Save channel record
 |                                                  Generate + send OTP
 |<-- { otpToken }
 |
 |--- POST /disbursement/channels/add/confirm --> Verify OTP
 |       ?otpToken=...&otpCode=...                 Activate channel
 |<-- { channelId, isUsable, activatesAt, ... }    (cooling 24h, except first channel)
 |
 |   [Channel becomes usable after activatesAt]
 |
 |--- GET /disbursement/channels ---------------> List active channels
 |<-- [ { channelId, isUsable, activatesAt, ... } ]


DELETE CHANNEL FLOW
-------------------
User
 |
 |--- DELETE /disbursement/channels/{channelId} ---------> Generate + send OTP
 |<-- { otpToken }
 |
 |--- DELETE /disbursement/channels/{channelId}/confirm --> Verify OTP
 |       ?otpToken=...&otpCode=...                          Soft delete channel
 |<-- 200 OK                                                Reassign primary if needed

Channel Status Reference

Status Description
PENDING_ACTIVATION Channel OTP confirmed, within 24-hour cooling period — not yet usable
ACTIVE Channel verified and 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, along with a signed confirmationToken required for the next step. Always call this before /add.

Endpoint: POST /disbursement/channels/lookup

Access: 🔒 Protected — Bearer Token required

Request Body:

{
  "channelType": "MPESA",
  "destination": "255712345678",
  "bankCode": null
}
Parameter Type Required Description
channelType string Yes MPESA, AIRTEL, TIGOPESA, HALOPESA, SELCOM_PESA, BANK
destination string Yes Phone number (255XXXXXXXXX) or bank account number
bankCode string Conditional Required when channelType is BANK

Success Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Account verified successfully",
  "action_time": "2026-03-06T10:30:45",
  "data": {
    "accountHolderName": "JOHN DOE",
    "destinationDisplay": "2557****678",
    "channelType": "MPESA",
    "confirmationToken": "<signed-token>"
  }
}
Field Description
accountHolderName Verified name from Selcom — display to user for confirmation
destinationDisplay Masked destination
channelType The channel type
confirmationToken Signed token required by /add. Valid for 10 minutes. Store in memory

Errors:

Scenario Message
Account not found "Account not found. 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 bank channels."
Selcom lookup failed "Could not verify account. Please check the details and try again."

2. Initiate Add Channel

Purpose: Initiates adding a new channel. Validates the confirmationToken from /lookup, re-verifies the name with Selcom server-side, saves the channel record, and sends an OTP to the user's verified phone.

Endpoint: POST /disbursement/channels/add

Access: 🔒 Protected — Bearer Token required

Request Body (Mobile Money):

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

Request Body (Bank):

{
  "channelType": "BANK",
  "destination": "0012345678901",
  "bankCode": "CRDB",
  "confirmationToken": "<token from /lookup>"
}
Parameter Type Required Description
channelType string Yes MPESA, AIRTEL, TIGOPESA, HALOPESA, SELCOM_PESA, BANK
destination string Yes Phone or account number
bankCode string Conditional Required when channelType is BANK
confirmationToken string Yes Token from /lookup. Must match destination, channel type, and account

Success Response:

{
  "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"
  }
}
Field Description
otpToken Pass to /add/confirm with the SMS OTP code

Errors:

Scenario Message
Token expired "Confirmation token expired. Please look up the account again."
Token invalid/mismatch "Invalid confirmation token."
Channel already active "This destination is already an active withdrawal channel."
Max channels reached "Maximum of {n} withdrawal 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 (first channel activates immediately).

Endpoint: POST /disbursement/channels/add/confirm

Access: 🔒 Protected — Bearer Token required

Query Parameters:

Parameter Type Required Description
otpToken string Yes Token returned from /add
otpCode string Yes 6-digit OTP received via SMS

Success Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Channel added successfully",
  "action_time": "2026-03-06T10:30:45",
  "data": {
    "channelId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "channelType": "MPESA",
    "destinationDisplay": "2557****678",
    "accountHolderName": "JOHN DOE",
    "bankName": null,
    "isPrimary": false,
    "status": "ACTIVE",
    "isUsable": false,
    "activatesAt": "2026-03-07T10:31:00"
  }
}
Field Description
channelId UUID — use this when initiating withdrawals
channelType Channel type
destinationDisplay Masked destination
accountHolderName Verified name from Selcom
bankName Bank name — only for BANK channels, null otherwise
isPrimary True if this is now the user's primary channel
status ACTIVE or PENDING_ACTIVATION
isUsable false if still in cooling period
activatesAt When the channel becomes usable for withdrawals

Errors:

Scenario Message
Invalid OTP "Invalid OTP code."
OTP locked "OTP locked — max attempts exceeded."
Channel no longer valid "This channel is no longer valid. Please add it again."

4. List My Channels

Purpose: Returns all active withdrawal channels for the authenticated user.

Endpoint: GET /disbursement/channels

Access: 🔒 Protected — Bearer Token required

Only ACTIVE channels are returned. Deleted and pending-activation channels that have not yet been OTP-confirmed are excluded.

Success Response:

{
  "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": "2557****678",
      "accountHolderName": "JOHN DOE",
      "bankName": null,
      "isPrimary": true,
      "status": "ACTIVE",
      "isUsable": true,
      "activatesAt": "2026-03-05T10:31:00"
    },
    {
      "channelId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
      "channelType": "BANK",
      "destinationDisplay": "0012****789",
      "accountHolderName": "JOHN DOE",
      "bankName": "CRDB Bank",
      "isPrimary": false,
      "status": "ACTIVE",
      "isUsable": false,
      "activatesAt": "2026-03-07T08:00:00"
    }
  ]
}
Field Description
channelId UUID of the channel
channelType Channel type
destinationDisplay Masked destination
accountHolderName Verified name from Selcom
bankName Bank name — only for BANK channels, null otherwise
isPrimary Whether this is the user's primary channel
status ACTIVE
isUsable true if cooling period has passed and channel can be used now
activatesAt When the cooling period ends

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: 🔒 Protected — Bearer Token required

Path Parameters:

Parameter Type Required Description
channelId UUID Yes ID of the channel to delete

Success Response:

{
  "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"
  }
}

Errors:

Scenario Message
Channel not found / not owned "Channel not found."
Channel not 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 removed. If it was the primary channel, another active channel is automatically promoted.

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

Access: 🔒 Protected — 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:

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

Errors:

Scenario Message
Invalid OTP "Invalid OTP code."
OTP 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/channels/lookup Verify account name, get confirmationToken
POST /disbursement/channels/add Start add channel flow (requires confirmationToken)
POST /disbursement/channels/add/confirm Complete add channel with OTP
GET /disbursement/channels List all active user channels
DELETE /disbursement/channels/{channelId} Start delete channel flow
DELETE /disbursement/channels/{channelId}/confirm Complete delete with OTP