Skip to main content
The cross-border pattern is the most common v3 flow: you hold one currency, your beneficiary expects another, and you need the converted funds to land in their bank account on the same banking day. v3 splits this into three calls — quote, trade, withdraw — over the same /v3/fx/ surface. The worked example below sends USD → MXN and settles to a Mexican bank via SPEI; the same shape covers any pair × any fiat rail.

When to reach for this

  • Paying suppliers, contractors, or payroll across borders
  • Funding a beneficiary in their local currency on the same day
  • Settling remittance flows where price needs to be confirmed before execution
If you instead want to convert and hold (without sending out), see Treasury management. If you want to settle on-chain rather than to a bank, see Stablecoin on/off ramp. For a side-by-side comparison of all four patterns, see Integration patterns.

Setup

Before the first cross-border payment, do these once per beneficiary:
1

Verify the beneficiary's withdrawal account

Add the recipient’s bank account in the OpenFX dashboard. The rail (SPEI, SEPA, Fed Wire, etc.) is bound to the withdrawal account at creation time. See Verified accounts for the full setup walkthrough.
2

Read it back via the API

Call GET /v3/fx/withdrawal-accounts and store the withdrawalAccountId. That UUID is the only thing your withdrawal call needs — the rail and currency are inferred from the withdrawal account.
3

(Recommended) Subscribe to webhooks

Subscribe to the withdrawals event so you don’t have to poll fiat rails through banking hours — the terminal state lives in data.status (COMPLETED or FAILED). See Webhooks setup.
Funding side: make sure you have a USD (or source-currency) balance. Either wire USD into your OpenFX account (Deposit lifecycle) or convert from an existing balance first.

The flow

Step 1 — Quote the FX

POST /v3/fx/quotes returns a binding rate good for ~3 seconds (extendable via quoteForSeconds — standard durations 3, 15, 30, 45, 60). For cross-border, you usually owe the beneficiary a fixed destination amount — use buyAmount to anchor the destination side so the math comes out exact at settlement.
cURL
Keep quote.id for the next step. quoteAmount carries the server-computed USD cost at the quoted rate. Both amounts come back as strings — see Amounts.
If your UI lets the user enter the source amount instead (e.g. “send $1,000 USD”), anchor the source side: send sellAmount: "1000.00" (and drop buyAmount). The server computes destination-side MXN into buyAmount. The trade and withdrawal steps don’t change.

Step 2 — Execute the trade

POST /v3/fx/trades locks the rate in before expiresAt. Use a new Idempotency-Key — it’s a different logical operation than the quote.
cURL
A successful trade returns status: "EXECUTED". Your USD balance has shifted into MXN. If you see QUOTE_EXPIRED, re-quote (Step 1) with a fresh key and try again — see the Trade error recovery rules.

Step 3 — Settle to the beneficiary

POST /v3/fx/withdrawals initiates settlement to the verified withdrawal account. You don’t pick the rail; SPEI is bound to the MXN withdrawal account.
cURL
For safety, prefer withdrawalAmount: trade.buyAmount (the exact MXN credited by the trade) over reusing the user-input number. The two are equal when the quote was anchored to the destination side, but reading from the trade response insulates you from any future rounding or fee changes. The response comes back with status: "PENDING". SPEI is 24/7 so MXN typically lands within minutes; other rails take longer. See Settlement times for the full cut-off matrix.

Step 4 — Confirm completion

Prefer the webhook over polling: a withdrawals event with data.status: "COMPLETED" arrives when funds have left OpenFX. If you must poll, use adaptive backoff — fixed 5-second polling burns rate-limit budget for nothing on a wire that may take hours.
The terminal state is COMPLETED (with a populated completedAt) or FAILED. See Withdrawal lifecycle.

Putting it together

In Live, persist each idempotency key before issuing the request — see the crash-recovery pattern. The function above generates keys inline for brevity; a crash between the trade and the withdrawal call would otherwise leave you unable to safely retry.

Common mistakes

  • Reusing an Idempotency-Key across quote + trade + withdrawal. Each is a distinct logical operation. Generate a fresh UUID for each call or you’ll trip 422 IDEMPOTENCY_MISMATCH.
  • Quoting in the source currency when the beneficiary owes a fixed destination amount. If the invoice says “MXN 50,000”, anchor the destination: buy: "MXN", buyAmount: "50000.00". Anchoring the USD side (via sellAmount) leaves you to absorb the rounding gap on conversion.
  • Submitting after a fiat rail’s daily cut-off. Watch Settlement times — a 5:15 PM ET SWIFT submission won’t dispatch until the next banking day.
  • Polling fiat withdrawals every 5 seconds. Use webhooks or adaptive backoff. Fixed-interval polling on a SEPA wire just rate-limits you.

What’s next

Treasury management

Hold multi-currency balances, convert on demand without settling out.

Stablecoin on/off ramp

Fiat ↔ stablecoin with on-chain delivery.

Settlement times

Per-rail submission cut-offs.

Withdrawal lifecycle

pending → processing → completed.