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.
Withdrawals vs. Disbursements
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.Every withdrawal needs Super Admin approval — no exceptions
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
/v1/merchant/withdrawals/quoteCalculate charges before submitting — no withdrawal is created, no funds are reserved.
dashboard/v1/merchant/withdrawalsSubmit a withdrawal request. Always PENDING_ADMIN_APPROVAL.
dashboard, Idempotency-Key required/v1/merchant/withdrawalsList your own withdrawals.
dashboard/v1/disbursements/selcom-pesaPayout to a Selcom Pesa wallet — direct API-key integration.
dashboard or API key, Idempotency-Key required/v1/disbursements/mobile-moneyPayout to a mobile money number — direct API-key integration.
dashboard or API key, Idempotency-Key required/v1/disbursements/bank-accountPayout to a bank account (bank_name required) — direct API-key integration.
dashboard or API key, Idempotency-Key required/v1/disbursementsList disbursements (merchant_id required as a query param).
dashboard or API key/v1/disbursements/{id}Get a disbursement.
dashboard or API keyApproval 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
255747730270Wrong
+2557477302700747730270Calculate 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.
{
"amount": "100000.00",
"method": "MOBILE_MONEY",
"destination_code": "MPESA",
"destination_identifier": "255747730270"
}{
"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.
{
"method": "MOBILE_MONEY",
"amount": "100000.00",
"destination_code": "MPESA",
"destination_name": "Grace Mwakalinga",
"destination_phone": "255747730270"
}{
"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.
{
"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:
{
"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:
{
"success": false,
"error": {
"code": "withdrawal_restricted",
"message": "Withdrawals require a verified, active merchant account. Complete onboarding verification first.",
"details": null
}
}Status lifecycle
| Status | Meaning |
|---|---|
| PENDING_ADMIN_APPROVAL | Every withdrawal starts here — no amount is ever auto-processed. Nothing is reserved, Selcom is never called. |
| INFO_REQUESTED | A Super Admin asked for more information before deciding. You'll see this via a notification. |
| PROCESSING | Approved. Balance reserved, payout sent to Selcom, awaiting confirmation. |
| SUCCESS | Funds delivered. Terminal. (Sometimes called "COMPLETED" in prose — the API value is always SUCCESS.) |
| FAILED | Selcom declined the payout — the balance reservation was automatically reversed. |
| REJECTED | A Super Admin declined the request outright. Nothing was ever reserved, so nothing to reverse. |
| NEEDS_ADMIN_ATTENTION | An anomaly needs a human look — rare, not a normal outcome. |
| NEEDS_RECONCILIATION | Selcom's response was ambiguous. Balance stays reserved pending manual resolution. |
| BLOCKED_IP_WHITELIST | Selcom rejected the request because the backend's IP isn't whitelisted — an operator problem, not a payout failure. Balance stays reserved. |
| REVERSED | A previously-SUCCESS payout was reversed after the fact by the provider. |
Super Admin approval
Every withdrawal is held for manual review
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.