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

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