Cross-currency payouts
Fund a payout from a different wallet currency in a single POST /v1/payouts call — e.g. pay an NGN beneficiary from your CAD wallet. Add funding_currency to the request and we convert from your funding wallet at the prevailing rate, then pay the beneficiary. Every other request field is unchanged.
New to payouts? Start with the Payouts overview for the common fields, customer attribution, and management endpoints shared across every currency.
Prerequisites
funding_currencymust differ fromcurrency.- Your API key needs cross-currency payouts enabled (a per-key setting on the API keys page — enabling it requires the
payout_fxpermission), and your account must be enabled for cross-currency payouts (request access under Settings → FX payouts; an admin reviews and approves). All other request fields are unchanged. - The rate applied is our prevailing system rate at execution; there’s no separate quote step. Read the current rate first with
GET /v1/rates— it’s indicative (the binding rate is the one active when the payout is processed; see After-hours & rate freshness below).
Two ways to specify the amount
Choose exactly one — the field you send selects the method:
| Method | Anchor field | Meaning | Rate-movement guard |
|---|---|---|---|
| Destination | amount_minor | The beneficiary receives exactly amount_minor; the fee + converted amount are debited from your funding wallet. | Optional max_debit_minor — caps what may leave your funding wallet. |
| Source (“fixed-send”) | funding_amount_minor | You name what leaves your funding wallet; we derive the beneficiary amount at the rate. | Optional min_receive_minor — floors what the beneficiary receives. |
Send both amount fields → 400 ambiguous_amount; neither → 400 amount_required. An optional amount_basis label ("source" / "destination") can be sent for self-documentation — it must match the amount field you sent or you get 400 amount_basis_mismatch. Each guard is valid only with its own method (max_debit_minor with amount_minor; min_receive_minor / fee_inclusive with funding_amount_minor) — otherwise 400 guard_field_wrong_method.
If a cross-currency payout fails (validation or provider), the converted funds stay in your destination wallet — they’re never lost. You can retry the payout from that wallet, or convert back.
The response is the usual payout object plus an fx block:
{
"object": "payout",
"id": "po_...",
"currency": "NGN",
"amount_minor": "5000000",
"status": "paid",
"fx": {
"funding_currency": "CAD",
"funding_wallet_id": "wal_...",
"source_debit_minor": "4810",
"fee_source_minor": "10",
"rate": "1050.5",
"converted_minor": "5012000",
"conversion_id": "fxc_..."
}
}amount_minor (top level) is what the beneficiary receives — for the source method this is the derived amount; converted_minor is what we credited to your NGN wallet to fund the payout (the beneficiary amount + fee); source_debit_minor is what left your funding wallet; fee_source_minor is the payout fee expressed in your funding currency (the destination-currency fee stays on the top-level fee_minor + tax_minor); rate is the prevailing rate applied.
Choose the fee placement (fee_inclusive)
The source method lets you decide where the fee sits, via fee_inclusive:
false(default, on-top) — the beneficiary receives the full converted value of what you send; the fee is added on top, so your wallet is debited a little more. Use when you control the recipient amount.true(inclusive) — your wallet is debited exactlyfunding_amount_minor; the fee is carved out and the beneficiary receives the converted remainder. Use when you control what leaves your wallet (e.g. sweeping a balance).
Same send, side by side — you send C$15.00 at rate ₦1,000/C$1 with a ₦50 payout fee:
fee_inclusive | Debited from CAD wallet | Beneficiary receives |
|---|---|---|
false (on-top) | C$15.05 | ₦15,000.00 |
true (inclusive) | C$15.00 | ₦14,950.00 |
Either way, fx.fee_source_minor in the response tells you the fee actually charged, in the currency you sent.
After-hours & rate freshness
Rates are published on a cycle. Outside publishing hours a rate can age past its freshness window, and a send then returns 422 fx_rate_stale — no funds are moved; retry once a fresh rate is published. This is fail-safe behaviour, not an integration error.
If you send around the clock:
- Call
GET /v1/ratesto price the send — the rate is indicative (there’s no staleness signal on that endpoint today, so a returned rate may still be past its window at execution). - Handle
422 fx_rate_stalegracefully — queue the payout and retry when a fresh rate is published. Because nothing moved, retrying with the sameIdempotency-Keyis safe.
min_receive_minor (source) and max_debit_minor (destination) are your real protection against the rate moving between pricing and execution — set them.
POSTCross-currency payout
curl https://api.swappr.me/api/v1/payouts \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"amount_minor": "5000000",
"currency": "NGN",
"funding_currency": "CAD",
"max_debit_minor": "5000",
"recipient": {
"account_number": "0690000032",
"bank_code": "044"
},
"merchant_reference": "PAYROLL_042"
}'# funding_amount_minor = what leaves your CAD wallet (C$15.00).
# fee_inclusive defaults to false (fee on top). min_receive_minor floors
# the beneficiary amount against rate movement.
curl https://api.swappr.me/api/v1/payouts \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"funding_amount_minor": "1500",
"funding_currency": "CAD",
"currency": "NGN",
"min_receive_minor": "1450000",
"recipient": {
"account_number": "0690000032",
"bank_code": "044"
},
"merchant_reference": "REMIT_991"
}'Cross-currency errors
| Code | HTTP | Cause |
|---|---|---|
ambiguous_amount | 400 | Both amount_minor and funding_amount_minor were sent — pick one method |
amount_required | 400 | Neither amount_minor nor funding_amount_minor was sent |
guard_field_wrong_method | 400 | A guard field was used with the wrong method (e.g. min_receive_minor without funding_amount_minor, or max_debit_minor without amount_minor) |
amount_basis_mismatch | 400 | The optional amount_basis label doesn’t match the amount field you sent (or was sent on a same-currency payout) |
invalid_funding_currency | 400 | funding_currency missing/blank or equal to currency |
fx_payout_disabled | 403 | Cross-currency payouts aren’t enabled for your account (or are paused / globally off) |
fx_payout_terms_not_accepted | 403 | An account owner hasn’t accepted the cross-currency payout terms |
fx_payout_source_not_allowed | 422 | funding_currency isn’t an enabled funding currency for your account |
fx_payout_tier_invalid | 422 | The currency pair isn’t eligible for cross-currency funding |
fx_rate_unavailable | 422 | No active rate for the currency pair right now |
fx_rate_stale | 422 | The rate for the pair is being refreshed — no funds were moved; retry in a moment |
min_receive_not_met | 422 | (Source) The derived beneficiary amount fell below min_receive_minor — the rate may have moved |
funding_below_fee | 422 | (Source, inclusive) funding_amount_minor is too small to cover the payout fee |
max_debit_exceeded | 422 | (Destination) The required funding-wallet debit would exceed max_debit_minor |
fx_payout_unavailable | 503 | Temporarily unable to fund payouts in the destination currency — retry shortly |
A key without cross-currency payouts enabled that sends funding_currency gets 403 (permission_denied).