API Reference
Disbursements API
Send money out of your InfinityPay 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 InfinityPay 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 InfinityPay 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. InfinityPay 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 InfinityPay Super Admin approves it in the dashboard. Poll GET .../merchant/withdrawals or listen for disbursement.success/disbursement.failed to know the outcome.