Payouts
A payout (disbursement) pushes money to a mobile wallet — salaries, winnings, supplier payments, withdrawals. No customer approval is needed, so payouts usually settle faster than collections.
Create a payout
POST /v1/payoutsRequires Authorization and Idempotency-Key.
Request
| Field | Type | Required | Notes |
|---|---|---|---|
amount | integer | yes | Minor units. Must be positive. |
currency | string | yes | ISO 4217. XAF at launch. |
msisdn | string | yes | Recipient's number, international format, digits only. |
description | string | yes | Shown in your dashboard and reports. |
externalId | string | no | Your reference (payroll line, withdrawal id). |
curl -X POST https://api-dev.moni-wave.com/v1/payouts \
-H "Authorization: Bearer sk_test_…" \
-H "Idempotency-Key: 8c2f4e1b-9a3d-4c7e-b1f5-6d8a2e0c4b93" \
-H "Content-Type: application/json" \
-d '{
"amount": 25000,
"currency": "XAF",
"msisdn": "237690000001",
"description": "Payout #88 — August winnings",
"externalId": "payout-88"
}'Response — 201 Created
{
"id": "pay_019fff7a2c184be9a0713fd8c2e5b410",
"status": "accepted",
"amount": 25000,
"currency": "XAF",
"msisdn": "237690000001",
"description": "Payout #88 — August winnings",
"externalId": "payout-88",
"failureReasonCode": null,
"createdAt": "2026-08-14T09:41:12.118Z"
}Payout ids are prefixed pay_. As with collections, 201 means accepted, not paid — wait for completed.
Idempotency matters more here than anywhere else. A duplicated payout sends real money twice, and unlike a duplicated charge you cannot simply refund it — the recipient already has it. Always send an Idempotency-Key, and reuse it when retrying.
Fee and net — a payout costs MORE than its amount
For a collection the fee is deducted. For a payout it is added: the recipient gets amount, and your balance is debited net, which is amount + fee.
{ "amount": 25000, "currency": "XAF", "fee": 250, "net": 25250, "status": "completed" }This is why you cannot pay out your whole balance. Holding 9 800 XAF, the largest payout that fits is 9 703 (9 703 + 97 = 9 800); asking for 9 800 needs 9 898 and is refused with payout_exceeds_balance. The dashboard shows this figure as the maximum payout — over the API, compute it or read the error, which states your spendable balance.
Both fields are null until the payout is completed, and both are read from the posted ledger entry rather than recalculated, so a past payout always reports what it really cost.
Retrieve a payout
GET /v1/payouts/{id}Same shape with the current status. Returns 404 for unknown ids and for other merchants' payouts alike.
Your balance
A payout must be covered by your own spendable balance in that currency, checked when you call the API. If it is not, the call is rejected outright with payout_exceeds_balance and no payout is created — nothing reaches the operator.
Spendable means your posted balance minus the payouts and refunds you have already requested that have not settled yet, and the check includes the payout's fee, not just its amount. Two consequences worth knowing before you script against this:
- A payout you requested a second ago is already holding its amount, even though your posted balance has not moved yet. The ledger only posts on completion, so this is what stops a second payout spending the same money.
- Balances are per currency and one cannot fund another.
{ "error": { "code": "payout_exceeds_balance", "message": "Payout of 10100 XAF (including fee) exceeds the spendable balance of 10000 XAF." } }This is distinct from the float check below: your balance is about your money, float is about ours.
Float
Payouts are funded from MoniWave' prefunded balance (float) at the operator. We check float at the moment of submission — not when you call the API, since the balance moves constantly.
If float is insufficient, the payout fails immediately with reason insufficient_float rather than queueing:
{ "id": "pay_019fff…", "status": "failed", "...": "..." }This is our problem, not yours — it means our operator float needs topping up. Contact support; retry once resolved. It is deliberately a fast failure rather than a silent wait, so you always know where a payout stands.
Statuses
| Status | Meaning |
|---|---|
accepted | Recorded, not yet with the operator. |
submitted | With the operator, in flight. |
completed | Recipient has the money. |
failed | Not sent. Check failureReasonCode. |
expired | No outcome within the time limit. |
Errors
Same validation codes as collections: invalid_amount, invalid_currency, invalid_msisdn, description_required, plus payout_exceeds_balance (see Your balance) and the shared authentication and idempotency codes.
Failure reasons (in the failureReasonCode field of a failed payout, not an error envelope): invalid_subscriber, insufficient_float. See errors.md.
Fees
Payout fees are charged in addition to the amount: a 25 000 XAF payout with a 1% fee debits 25 250 XAF from your balance — the recipient always receives exactly the amount you specified.
Testing
Use the magic MSISDNs — they behave the same for payouts as for collections, so you can rehearse declines, delays, and rejections before moving real money.
Bulk payouts (CSV and batch API) arrive in a later phase; today, submit payouts individually, each with its own idempotency key.