Before you start
Idempotency-Keyis required. A retried payout without one is a double payment. With one, a replay returns the original response verbatim and no second transfer happens. See Idempotency.- Amounts are integer minor units. UGX, RWF, XAF and XOF are zero-decimal. See Money and minor units.
- Check the corridor first. Capability is per
(country, currency, direction, method), never per country. Nigeria takes bank payouts but no mobile money, Rwanda mobile money but no bank, Tanzania only wallet-to-wallet.GET /v1/reference/corridorsis the matrix to render your UI against; a route that does not exist is a422here, not a surprise at settlement.
Fees come out of your float, on top of the amount
The provider’s fee is charged on top ofamount_minor, and it comes out of your float. A UGX 10,000 bank payout debits the wallet roughly UGX 15,400 at current rates — bank fees are far higher than mobile money, around UGX 5,400 against UGX 1,000 on a comparable mobile money transfer.
fees_minor on the response carries the provider’s own figure. Your float is debited both the amount and the fee when the payout settles.
Budget for amount + fee, not the amount alone. Before reserving anything, the gateway asks the provider what this exact transfer will cost and holds that full total, refusing the payout if it does not fit. Your float therefore can never go negative.
The flow
1
Quote the payout (optional)
POST /v1/payouts/quote is the confirm-screen endpoint: “you send 50,000 UGX, they receive 1,340 KES, fee 500” — priced by the real provider, with no money moved and nothing recorded as a transaction.It is optional. POST /v1/payouts re-quotes internally, so you never have to call this first; it exists purely so a UI can show a rate before the user taps send. Quoting is free and works even when the amount is unaffordable.Quote a cross-currency payout
total_amount_minor is what leaves your float. Show that figure.The response carries our quote_id plus the human-meaningful fields. The provider’s own quotation token never leaves the gateway — holding it would let a caller go straight to the provider, bypassing our limits, idempotency and transaction records.2
Send the payout
Same-currency mobile money payout
201 with a gateway reference. Persist it.On a bank destination, destination.msisdn is the recipient’s phone number. It is not needed to reach the account, but the provider requires a contact number on bank payouts and rejects an empty one — a bank payout without it is refused as validation_failed naming destination.msisdn.destination.account_name is advisory only. The gateway re-resolves the account name at payout time, every time, because a stale “yes, this is Jane Doe” can outlive the account being closed, renamed or reassigned.3
Wait for the real answer
The webhook is the primary signal.
GET /v1/payouts/{reference} is the fallback that a “sending your money…” screen polls, and what a reconciliation job sweeps for payouts that never produced a webhook. It is a read of the gateway’s own ledger — cheap, and safe to poll.A
404 means “not found for this tenant”. Another tenant’s reference looks exactly like one that never existed, so this endpoint cannot be used to probe whether a reference belongs to somebody else.Cross-currency payouts
Whencurrency differs from destination_currency, amount_type is required. It decides whether the sender pays a fixed amount or the recipient receives one — a materially different product, so there is no silent default.
destination_currency defaults to currency for same-currency payouts, where amount_type is not needed.
destination.country is required on a mobile money payout and is not inherited from the request’s top-level country. A payout pushes money to whoever answers that number, and once it lands it cannot be recalled, so the field deciding which country’s subscriber is paid must be stated explicitly. Collections are deliberately more lenient here.Withdrawing a whole balance
Setdeduct_fee_from_amount: true and pass the balance as amount_minor. The fee is taken out of it rather than added on top, so the total lands exactly on the balance and nothing is stranded. A UGX 10,000 mobile money payout then delivers 9,000 with a 1,000 fee, and the full 10,000 leaves your float.
The same flag works on /quote, so a confirm screen can show the reduced destination_amount_minor the recipient will actually receive.
Minimums are per corridor and per method
Some rails impose a floor the others do not. A Uganda bank payout is refused below UGX 10,000, while mobile money on the same corridor is accepted from UGX 500. An amount under the floor isamount_below_minimum, and error.details names the minimum_minor that applies to the method you sent. Read it there rather than hardcoding the figure — floors are provider-set and change without a release.
Refusals are safe
A payout is refused before any provider call, with no money moved, when the amount is over your float, outside a corridor’s min/max window, over your velocity cap, or over the test ceiling in test mode. A422 is therefore always safe: nothing was sent.
On an insufficient-float refusal, error.details carries the figures you need to explain it:
Insufficient float
suggested_method and suggested_total_minor are present, so an unaffordable bank payout can point you at mobile money rather than leaving you to guess.
A complete Node example
payout.js
Correlate on metadata, not on the reference
Put your own withdrawal id inmetadata. It is stored on the transaction and echoed back on every webhook delivery as data.metadata.
Our reference is opaque. It looks like APT-LCS-oRUZWq8fyn9DttNb3SN3B — the shape is APT-{prefix}-{nanoid} — but it carries no field you are meant to parse.
Errors worth handling
Full list and response shape: Errors.
Next steps
Collect a payment
The money-in side.
Test your integration
Test keys, the amount ceiling, and the Postman collection.