Skip to main content
A payout is the money-out entry point: what your app calls to pay a person. A seller payout, a withdrawal to a user’s mobile money wallet, a payroll run, a refund you are settling manually. You name an amount and a destination, and the gateway debits your float and credits them. One call. Providers that need a quotation-then-execute handshake do it internally, so that two-call shape never leaks to you — which is also why you never have to race a quote’s expiry window.
Payouts are asynchronous and irreversible. POST /v1/payouts returns 201 with status: "pending", which means the transfer was accepted, not that it landed. The webhook, or GET /v1/payouts/{reference}, tells you when it actually arrived.

Before you start

  • Idempotency-Key is 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/corridors is the matrix to render your UI against; a route that does not exist is a 422 here, not a surprise at settlement.

Fees come out of your float, on top of the amount

The provider’s fee is charged on top of amount_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.
expires_at is absolute and provider-reported. Do not assume a fixed TTL, and do not hold a quote across a slow confirm screen. Re-quote instead, or just send the payout and let the gateway price it.
2

Send the payout

Same-currency mobile money payout
Returns 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 payout reported failed has had its float returned. Do not re-send blindly — resolve why it failed first.
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

When currency 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

Set deduct_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.
The reduction happens before the minimum is checked. A UGX 10,000 withdraw-all by bank nets 4,600 and is refused against bank’s own UGX 10,000 floor. By mobile money the same 10,000 nets 9,000 and succeeds. error.details.suggested_method points at the rail that fits.

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 is amount_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. A 422 is therefore always safe: nothing was sent. On an insufficient-float refusal, error.details carries the figures you need to explain it:
Insufficient float
When a cheaper rail would fit, 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 in metadata. 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.
Never parse the reference to recover your own identifiers. Match on data.metadata instead, which must serialize to at most 4096 bytes.

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.