API referenceCross-currency payouts

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_currency must differ from currency.
  • Your API key needs cross-currency payouts enabled (a per-key setting on the API keys page — enabling it requires the payout_fx permission), 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:

MethodAnchor fieldMeaningRate-movement guard
Destinationamount_minorThe 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_minorYou 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; neither400 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 exactly funding_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_inclusiveDebited from CAD walletBeneficiary 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_staleno 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:

  1. Call GET /v1/rates to 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).
  2. Handle 422 fx_rate_stale gracefully — queue the payout and retry when a fresh rate is published. Because nothing moved, retrying with the same Idempotency-Key is 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

POST/v1/payouts
Destination method — pay ₦50,000, fund from CAD
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"
  }'
Source method — send C$15 from CAD, deliver naira
# 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

CodeHTTPCause
ambiguous_amount400Both amount_minor and funding_amount_minor were sent — pick one method
amount_required400Neither amount_minor nor funding_amount_minor was sent
guard_field_wrong_method400A 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_mismatch400The optional amount_basis label doesn’t match the amount field you sent (or was sent on a same-currency payout)
invalid_funding_currency400funding_currency missing/blank or equal to currency
fx_payout_disabled403Cross-currency payouts aren’t enabled for your account (or are paused / globally off)
fx_payout_terms_not_accepted403An account owner hasn’t accepted the cross-currency payout terms
fx_payout_source_not_allowed422funding_currency isn’t an enabled funding currency for your account
fx_payout_tier_invalid422The currency pair isn’t eligible for cross-currency funding
fx_rate_unavailable422No active rate for the currency pair right now
fx_rate_stale422The rate for the pair is being refreshed — no funds were moved; retry in a moment
min_receive_not_met422(Source) The derived beneficiary amount fell below min_receive_minor — the rate may have moved
funding_below_fee422(Source, inclusive) funding_amount_minor is too small to cover the payout fee
max_debit_exceeded422(Destination) The required funding-wallet debit would exceed max_debit_minor
fx_payout_unavailable503Temporarily 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).