API Reference

Disbursements API

Send money out of your Infinity Africa balance — to a Selcom Pesa wallet, a mobile money number, or a bank account. Available balance is validated before anything is created, and every withdrawal is reviewed by an Infinity Africa Super Admin before it reaches Selcom.

info

Withdrawals vs. Disbursements

In the Infinity Africa dashboard, merchants see this feature as Withdrawals. In the API, the technical endpoint may use disbursements for payment-provider compatibility. Internally, withdrawal approvals call the Selcom Business Disbursement API — that name only ever appears in backend/internal documentation, never in the dashboard or in merchant-facing copy.
warning

Every withdrawal needs Super Admin approval — no exceptions

Submitting a withdrawal never calls Selcom. It always comes back PENDING_ADMIN_APPROVAL, regardless of amount or method. Selcom is only ever contacted once an Infinity Africa Super Admin approves the request in the dashboard.

Endpoints

POST/v1/merchant/withdrawals/quote

Calculate charges before submitting — no withdrawal is created, no funds are reserved.

dashboard
POST/v1/merchant/withdrawals

Submit a withdrawal request. Always PENDING_ADMIN_APPROVAL.

dashboard, Idempotency-Key required
GET/v1/merchant/withdrawals

List your own withdrawals.

dashboard
POST/v1/disbursements/selcom-pesa

Payout to a Selcom Pesa wallet — direct API-key integration.

dashboard or API key, Idempotency-Key required
POST/v1/disbursements/mobile-money

Payout to a mobile money number — direct API-key integration.

dashboard or API key, Idempotency-Key required
POST/v1/disbursements/bank-account

Payout to a bank account (bank_name required) — direct API-key integration.

dashboard or API key, Idempotency-Key required
GET/v1/disbursements

List disbursements (merchant_id required as a query param).

dashboard or API key
GET/v1/disbursements/{id}

Get a disbursement.

dashboard or API key

Approval itself (approve/reject/request-info) is a Super Admin action, not something a merchant or API key ever calls — see the Super Admin console, not this API.

Phone number format

Any phone-based destination (Selcom Pesa, mobile money) must be a Tanzanian number in the form 255XXXXXXXXX — country code, no leading zero, no plus sign. Infinity Africa normalizes0747730270, 747730270, and +255747730270 to the same canonical value automatically, but it's simplest to send it correctly already:

Correct

255747730270

Wrong

+2557477302700747730270

Calculate charges

Every merchant has their own negotiated fee — call this first to show the full breakdown before submitting. It never creates a withdrawal or touches your balance.

json — POST /v1/merchant/withdrawals/quote
{
  "amount": "100000.00",
  "method": "MOBILE_MONEY",
  "destination_code": "MPESA",
  "destination_identifier": "255747730270"
}
json — 200 OK
{
  "success": true,
  "data": {
    "withdrawal_amount": "100000.00",
    "processor_charge": "300.00",
    "infinity_fee": "1500.00",
    "percentage_fee": "1000.00",
    "flat_fee": "500.00",
    "total_charges": "1800.00",
    "total_reserved_amount": "101800.00",
    "recipient_net_amount": "100000.00",
    "channel": "MOBILE_MONEY",
    "destination_code": "MPESA",
    "pricing_rule_id": "8f2c1a90-...",
    "pricing_rule_label": "Negotiated enterprise rate",
    "processor_fee_pass_through": true
  }
}

Submit a withdrawal

Charges are recalculated and frozen server-side at submission time — never trust a client-side quote. The request always comes back PENDING_ADMIN_APPROVAL.

json — POST /v1/merchant/withdrawals
{
  "method": "MOBILE_MONEY",
  "amount": "100000.00",
  "destination_code": "MPESA",
  "destination_name": "Grace Mwakalinga",
  "destination_phone": "255747730270"
}
json — 202 Accepted
{
  "success": true,
  "data": {
    "id": "c9d8e7f6-...",
    "merchant_id": "5c1f0b2a-3e21-4b9a-9c33-2f6a1d0e8b71",
    "method": "MOBILE_MONEY",
    "amount": "100000.00",
    "currency": "TZS",
    "destination_name": "Grace Mwakalinga",
    "destination_identifier": "255747730270",
    "destination_code": "MPESA",
    "status": "PENDING_ADMIN_APPROVAL",
    "requires_approval": true,
    "total_charges": "1800.00",
    "total_reserved_amount": "101800.00",
    "recipient_net_amount": "100000.00",
    "provider_reference": null,
    "transaction_reference": null,
    "initiated_at": "2026-08-14T09:00:00Z",
    "completed_at": null,
    "created_at": "2026-08-14T09:00:00Z",
    "updated_at": "2026-08-14T09:00:00Z"
  }
}

For a bank account payout, use the bank_name/bank_account_number/bank_account_name fields instead of destination_phone — the account number is never phone-normalized.

json — POST /v1/merchant/withdrawals (bank)
{
  "method": "BANK_ACCOUNT",
  "amount": "100000.00",
  "destination_code": "CRDB",
  "bank_name": "CRDB Bank",
  "bank_account_number": "0151234567890",
  "bank_account_name": "PAUL MASANJA"
}

Insufficient balance

Checked against the total reserved amount (withdrawal amount + all fees), not just the raw amount. If it's not enough, you get a 409 and nothing is created or reserved:

json — 409 Conflict
{
  "success": false,
  "error": {
    "code": "insufficient_balance",
    "message": "Insufficient balance: available TZS 45,000, requested TZS 80,000 (amount + fees)",
    "details": null
  }
}

Unverified merchant

Withdrawals are only available to merchants who have completed onboarding verification. A merchant that isn't yet active/verified gets the same 409 shape, before any balance check runs:

json — 409 Conflict
{
  "success": false,
  "error": {
    "code": "withdrawal_restricted",
    "message": "Withdrawals require a verified, active merchant account. Complete onboarding verification first.",
    "details": null
  }
}

Status lifecycle

StatusMeaning
PENDING_ADMIN_APPROVALEvery withdrawal starts here — no amount is ever auto-processed. Nothing is reserved, Selcom is never called.
INFO_REQUESTEDA Super Admin asked for more information before deciding. You'll see this via a notification.
PROCESSINGApproved. Balance reserved, payout sent to Selcom, awaiting confirmation.
SUCCESSFunds delivered. Terminal. (Sometimes called "COMPLETED" in prose — the API value is always SUCCESS.)
FAILEDSelcom declined the payout — the balance reservation was automatically reversed.
REJECTEDA Super Admin declined the request outright. Nothing was ever reserved, so nothing to reverse.
NEEDS_ADMIN_ATTENTIONAn anomaly needs a human look — rare, not a normal outcome.
NEEDS_RECONCILIATIONSelcom's response was ambiguous. Balance stays reserved pending manual resolution.
BLOCKED_IP_WHITELISTSelcom rejected the request because the backend's IP isn't whitelisted — an operator problem, not a payout failure. Balance stays reserved.
REVERSEDA previously-SUCCESS payout was reversed after the fact by the provider.

Super Admin approval

info

Every withdrawal is held for manual review

A withdrawal always comes back PENDING_ADMIN_APPROVAL with requires_approval: true and is not sent to Selcom until an Infinity Africa Super Admin approves it in the dashboard. Poll GET .../merchant/withdrawals or listen for disbursement.success/disbursement.failed to know the outcome.