POST /api/v1/disburse
Send money to a mobile money wallet. Live mode only.
Building a full payout flow? Follow the Integration Guide.
Endpoint
POST https://api.rohopay.com/api/v1/disburse
Headers
| Header | Required | Value |
|---|---|---|
Authorization | ✅ | Bearer {live_api_key} |
Content-Type | ✅ | application/json |
Idempotency-Key | ✅ | Stable per withdrawal — see Idempotency |
Disbursements require a live API key and a funded live wallet. Requests with test_ keys return 400 DISBURSEMENT_BLOCKED.
Request Body
{
"phone": "256700123456",
"amount": 25000,
"currency": "UGX",
"description": "Agent payout – July",
"callback_url": "https://your-app.com/webhooks/rohopay"
}
| Field | Type | Required | Description |
|---|---|---|---|
phone | string | ✅ | 256XXXXXXXXX — country code, no 0, no + |
amount | integer | ✅ | Whole currency units (no ×100). Debited from your live wallet |
currency | string | ✅ | UGX, KES, TZS, or RWF |
description | string | — | Note attached to the payout |
callback_url | string | — | Public http(s) URL that receives the webhook |
How the fee works
Your live wallet is debited exactly amount. The transfer fee is taken out
of what is sent to the recipient — it is not charged on top of your debit.
| Field | Meaning |
|---|---|
wallet_debit | What was taken from your wallet (amount) |
recipient_amount | What the recipient's phone receives (amount minus the transfer fee) |
Both figures are fixed when the payout is created and returned in this
response — show recipient_amount to your user. If the recipient must receive
an exact figure, add the fee to amount before calling.
Response (200 OK)
{
"success": true,
"data": {
"transaction_id": "01j5m6n7p8q9r0s1t2u3vwxy",
"reference": "ref_9f8e7d6c5b4a3210",
"status": "pending",
"amount": 25000,
"currency": "UGX",
"commission": 0,
"provider": "rohopay",
"wallet_debit": 25000,
"recipient_amount": 24100
},
"message": "Payment request initiated"
}
Check data.status, not just the HTTP code. If the provider rejects the
payout immediately, you still get HTTP 200, but status is "failed" and an
error_reason explains why. The reserved funds have already been returned to
your wallet, and no webhook follows — refund your own user right away.
{
"success": true,
"data": {
"status": "failed",
"reference": "ref_…",
"error_reason": "We couldn't reach our payment partner. Your wallet has been refunded — please try again shortly."
}
}
A pending payout resolves via the webhook
(withdraw.successful / withdraw.failed). On failure your wallet is refunded.
One payout at a time
While a payout on your wallet is pending, another disburse returns
409 INFLIGHT_TRANSACTION. Send payouts sequentially and wait for each
webhook (or poll) before the next.
Error Responses
| HTTP | Code | Description |
|---|---|---|
| 400 | MISSING_IDEMPOTENCY_KEY | Idempotency-Key header missing |
| 400 | INVALID_PHONE | Phone is not a supported international number |
| 400 | AMOUNT_TOO_LOW / AMOUNT_TOO_HIGH | Outside the allowed range |
| 400 | DISBURSEMENT_BLOCKED | Test key used — payouts are live only |
| 401 | INVALID_API_KEY | Missing, unknown, or inactive API key |
| 402 | INSUFFICIENT_BALANCE | Live wallet balance too low |
| 409 | INFLIGHT_TRANSACTION | A payout on this wallet is already pending |
| 409 | CONCURRENCY_CONFLICT | Wallet changed mid-request — retry as a new attempt |
| 429 | DAILY_LIMIT_EXCEEDED | Daily disbursement limit reached (resets 00:00 UTC) |
| 429 | RATE_LIMITED | Too many requests |
| 503 | DISBURSEMENT_TEMPORARILY_UNAVAILABLE | No provider can take this payout right now |
| 502 | PROVIDER_ERROR | The payout could not be initiated |
