Skip to main content

Disbursement 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 API handles wallet withdrawals for NextGate users. Users can withdraw their wallet balance to a verified mobile money account or bank account. Each withdrawal requires OTP confirmation and deducts a platform fee plus Selcom transfer fee on top of the requested amount.

Hints:

  • The withdrawal channel must be added and fully active (cooling period passed) before use — see Disbursement Channel API
  • Total wallet debit = requestedAmount + platformFee + selcomFee
  • The recipient receives exactly requestedAmount — fees are charged on top
  • If Selcom returns INPROGRESS, the polling job resolves the final status automatically every 3 minutes — no frontend action needed
  • Always provide a unique idempotencyKey per withdrawal request
  • Poll /status/{id} after confirming to track final delivery

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

Money Flow Diagram

User requests withdrawal of 10,000 TZS
                |
        POST /disbursement/initiate
                |
        .-------+----------.
        |                  |
   Validate             Calculate fees
   channel              platformFee =  500 TZS
   balance              selcomFee   = 1,500 TZS
                        totalDebited = 12,000 TZS
                        disbursedAmt = 10,000 TZS
                             |
                    Save DisbursementRequest
                    Status: PENDING_OTP
                             |
                    Send OTP to verified phone
                             |
                    Return { otpToken }
                             |
        POST /disbursement/confirm
        (otpToken + otpCode)
                             |
                    Verify OTP
                             |
              Wallet debited 12,000 TZS
              Status: PROCESSING
                             |
                    Call Selcom API
                    (sends 10,000 TZS to recipient)
                             |
           .--------+-------------------.
           |        |                   |
        SUCCESS  INPROGRESS           FAIL
           |        |                   |
       COMPLETED    |               REFUNDED
                AWAITING            wallet credited
                CONFIRMATION        12,000 back
                    |
             Polling job
             (every 3 min)
                    |
          .---------+---------.
          |                   |
       SUCCESS              FAIL
          |                   |
      COMPLETED            REFUNDED
                           wallet credited
                           12,000 back

Fee Structure

Component Amount Description
requestedAmount User defined What the recipient receives
platformFee 500 TZS NextGate service charge
selcomFee 1,500 TZS Selcom per-disbursement fee
totalDebited requestedAmount + 2,000 TZS Actual amount leaving wallet
disbursedAmount = requestedAmount Amount Selcom sends to recipient

Disbursement Status Reference

Status Description
PENDING_OTP Initiated, waiting for OTP confirmation — wallet not debited yet
PROCESSING OTP confirmed, wallet debited, Selcom call in progress
COMPLETED Selcom confirmed successful transfer
AWAITING_CONFIRMATION Selcom returned INPROGRESS — polling job resolving every 3 min
REFUNDED Selcom failed — wallet refunded full totalDebited automatically
FAILED Failed due to OTP lock — no wallet debit occurred
MANUAL_REVIEW Polling exhausted max retries — admin review required

Endpoints


1. Initiate Withdrawal

Purpose: Initiates a wallet withdrawal request. Validates channel, calculates fees, checks wallet balance, and sends OTP to user's verified phone number. No money moves at this step.

Endpoint: POST /disbursement/initiate

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:

{
  "channelId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "amount": 10000,
  "idempotencyKey": "usr-123-withdraw-1741234567"
}

Request Body Parameters:

Parameter Type Required Description Validation
channelId UUID Yes ID of the verified withdrawal channel Must belong to authenticated user and be active (cooling period passed)
amount number Yes Amount to send to recipient in TZS Min: 1000 TZS
idempotencyKey string Yes Unique key to prevent duplicate withdrawals Max 200 chars, unique per request

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 along with OTP code to /confirm

Error Responses:

Insufficient balance (400):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Insufficient balance. You need 12000 TZS (10000 + 500 platform fee + 1500 transfer fee).",
  "action_time": "2026-03-06T10:30:45",
  "data": "Insufficient balance. You need 12000 TZS (10000 + 500 platform fee + 1500 transfer fee)."
}

Channel still in cooling period (400):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "This withdrawal channel is not yet active.",
  "action_time": "2026-03-06T10:30:45",
  "data": "This withdrawal channel is not yet active."
}

Phone not verified (400):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Your phone number must be verified before withdrawing.",
  "action_time": "2026-03-06T10:30:45",
  "data": "Your phone number must be verified before withdrawing."
}

Duplicate request (400):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Duplicate request — this withdrawal is already being processed.",
  "action_time": "2026-03-06T10:30:45",
  "data": "Duplicate request — this withdrawal is already being processed."
}

Amount below minimum (400):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Minimum withdrawal amount is 1000 TZS.",
  "action_time": "2026-03-06T10:30:45",
  "data": "Minimum withdrawal amount is 1000 TZS."
}

2. Confirm Withdrawal

Purpose: Confirms the withdrawal by verifying the OTP. Debits the wallet and calls Selcom to initiate the transfer. This is when money actually leaves the wallet.

Endpoint: POST /disbursement/confirm

Access Level: 🔒 Protected — Requires Bearer Token

Authentication: Authorization: Bearer <token>

Query Parameters:

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

Success Response JSON Sample:

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

Important: A 200 OK here means the Selcom call was made. It does not guarantee the money has arrived at the recipient yet. Poll /status/{id} to confirm final delivery status.

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 — max attempts exceeded (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."
}

Already processing (400):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "This withdrawal is already being processed.",
  "action_time": "2026-03-06T10:30:45",
  "data": "This withdrawal is already being processed."
}

3. Get Disbursement Status

Purpose: Returns the current status and full details of a withdrawal request. Poll this after confirm to track final delivery.

Endpoint: GET /disbursement/status/{disbursementRequestId}

Access Level: 🔒 Protected — Requires Bearer Token

Authentication: Authorization: Bearer <token>

Path Parameters:

Parameter Type Required Description
disbursementRequestId UUID Yes ID of the disbursement request

Success Response JSON Sample:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Disbursement status retrieved",
  "action_time": "2026-03-06T10:30:45",
  "data": {
    "disbursementRequestId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "requestedAmount": 10000,
    "platformFee": 500,
    "selcomFee": 1500,
    "totalDebited": 12000,
    "disbursedAmount": 10000,
    "currency": "TZS",
    "destination": "255712****78",
    "accountHolderName": "JOHN DOE",
    "status": "COMPLETED",
    "failureReason": null,
    "transactionRef": "NXT-TXN-20260306-ABCD1234",
    "supportRef": null,
    "createdAt": "2026-03-06T10:30:00",
    "completedAt": "2026-03-06T10:31:45"
  }
}

Success Response Fields:

Field Description
disbursementRequestId UUID of this withdrawal request
requestedAmount Amount sent to recipient in TZS
platformFee NextGate fee charged on top
selcomFee Selcom transfer fee charged on top
totalDebited Total amount debited from wallet
disbursedAmount Amount Selcom sent to recipient — equals requestedAmount
currency Always TZS
destination Masked destination e.g. 255712****78
accountHolderName Verified recipient name
status Current status — see status table above
failureReason Populated if FAILED or REFUNDED
transactionRef Wallet transaction reference
supportRef Support reference for escalations — populated on MANUAL_REVIEW
createdAt When withdrawal was initiated
completedAt When transfer was confirmed — null if not yet completed

Error Responses:

Not found or not owned by user (400):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Disbursement request not found",
  "action_time": "2026-03-06T10:30:45",
  "data": "Disbursement request not found"
}

Quick Reference

Method Endpoint Description
POST /disbursement/initiate Start withdrawal, sends OTP
POST /disbursement/confirm Confirm with OTP, debits wallet, calls Selcom
GET /disbursement/status/{id} Check withdrawal status