GET /v3/fx/balances for the read side, the quote-then-trade flow for conversion, and POST /v3/fx/withdrawals only when you want funds to leave the platform.
When to reach for this
- A corporate treasurer rebalancing USD / EUR / GBP exposure
- A platform holding customer balances in multiple currencies on a single account
- Same-day intra-portfolio FX without an external settlement leg
Setup
1
Get funded
Wire your starting currency into OpenFX. Detection is automatic via
GET /v3/fx/deposits — see
Deposit lifecycle.2
Verify withdrawal accounts (only if you need them)
A pure treasury flow doesn’t require any verified withdrawal accounts —
you can hold balances and convert indefinitely without ever calling
/v3/fx/withdrawals. Add accounts only when you know you’ll need to pay
out. See Verified accounts.3
(Recommended) Subscribe to deposit webhooks
For an automated funding pipeline, subscribe to the
deposits event so your
treasury system reconciles incoming wires without polling — the webhook’s
data is the Deposit object, so the terminal state lives in data.status
(COMPLETED). See Webhooks setup.The flow
Step 1 — Read current positions
GET /v3/fx/balances returns a Balance per currency: availableBalance and totalBalance. Funds held against pending operations (a withdrawal that hasn’t dispatched yet, an in-flight trade) are the difference totalBalance − availableBalance, which is not broken out as a separate field in v3; only availableBalance is usable for new operations.
cURL
Step 2 — Quote the rebalance
Same quote-then-execute model as every other trade. For treasury workflows you’ll usually anchor the sell side (the position you’re trimming) —sellAmount lets you say “convert exactly this much of the over-weight currency”:
cURL
quoteAmount at the quoted rate — the exact USD you’ll receive.
Step 3 — Execute
cURL
status: "EXECUTED" means EUR has decreased and USD has increased by the exact executedAmount from the trade (the counter-currency amount received — here, USD, since the quote anchored the EUR sellAmount). If status: "FAILED", no balance has moved — see the TRADE_EXECUTION_FAILED recovery rule before retrying.
Step 4 — Verify the new position
In v3 the trade response does not bundle balances. Re-read them separately:quoteAmount: "540000.00" (the computed USD counter-leg for the 500,000 EUR anchored on sellAmount), the −500,000 EUR move corresponds to +540,000 USD in the new balances:
Step 5 (optional) — Withdraw
Treasury balances can sit on OpenFX as long as you want them to. Withdraw only when the funds need to be on a counterparty’s books — for vendor payments, payroll, or reserves at a bank. The call is the same shape regardless of currency:cURL
Putting it together
The read-balances → quote → trade → re-read-balances flow, realsignRequest and all — plus the optional withdraw from Step 5:
Reconciliation: tying deposits → trades → withdrawals
End-of-day reconciliation answers: “for each dollar that moved, can I trace it?” The minimum reconciliation walks three lists (/deposits, /trades, /withdrawals) plus current /balances and proves the equation:
Step 1 — fetch the day’s activity
For each list endpoint, page from newest backward to the start of the day. v3 lists are newest-first bycreatedAt; cursor pagination is documented in Pagination. Walk forward (newest → oldest) with startingAfter, passing the opaque pagination.nextCursor back verbatim until it’s null:
JavaScript
pagination.prevCursor after each run and pass it as endingBefore next time. The same loop pattern applies — stop when pagination.prevCursor === null (you’ve reached the newest record).
Materialize each into a flat ledger keyed by currency.
Step 2 — build the per-currency ledger
buyAmount/sellAmount — whichever side its originating quote was anchored on — with executedAmount holding the other side; fall back to executedAmount for whichever of the two is absent, or trade_buys/trade_sells will silently omit every trade anchored on the opposite side. Feed the string amounts verbatim into a decimal library (Decimal.js, Python decimal.Decimal, Java BigDecimal). Never coerce to float. See Amounts.
Step 3 — pair the foreign keys
A trade (readable,
tde_-prefixed) with quoteId (readable, qte_-prefixed) lets you reconstruct: “at this timestamp, we locked rate X via quote Y and EXECUTED trade Z — A on the anchored side (buyAmount or sellAmount, whichever the quote locked) and E on the other (executedAmount).” That’s the full story.
Step 4 — sanity checks
For each ledger row:delta == 0per currency. Non-zero means a missing record or off-by-one.- Every
withdrawal.withdrawalAccountIdis present inGET /v3/fx/withdrawal-accounts. - No long-aged
PENDINGtrades.PENDINGis a transient state during async execution and resolves toEXECUTEDorFAILEDwithin seconds; rows that stayPENDINGpast a small reconciliation window indicate a stuck execution worth investigating. PROCESSINGwithdrawals are aged within rail cut-offs (Settlement times).
id, quoteAmount, buyCurrency, sellCurrency, and createdAt at quote time alongside the idempotency key. There is no standalone rate field on Quote — quoteAmount is the counter-leg amount computed at the locked rate. Without persisting it you cannot reconstruct trade history later — Trade.quoteId only links back to a quote you can no longer fetch.
Step 5 — handle non-zero deltas
- Intra-day timing. A trade that executes at 23:59:58 and a balance snapshot at 00:00:00 can disagree by exactly one trade. Reconcile against
balancesfetched after the last trade of the day, not at midnight sharp. - In-flight withdrawals.
PENDINGandPROCESSINGwithdrawals earmark balance but haven’t left. Reconcile ontotalBalancefor cash-impact totals; onavailableBalancefor spendable totals. Pick one and stay consistent. - Failed-after-fetch. A withdrawal can transition
PENDING → FAILEDafter you fetched the list. Re-pollGET /v3/fx/withdrawals/<uuid>for any non-terminal row older than the rail cut-off. - Pagination boundary. If
pagination.nextCursor !== nulland you stopped paginating early, you missed records. Page untilnextCursorisnull.
Common mistakes (reconciliation edition)
- Treating
availableBalanceas the balance. It excludes funds earmarked against in-flight operations. For end-of-day,totalBalanceis usually what reconciliation wants. - Reconciling on
createdAtinstead ofcompletedAt. A withdrawal created on day N but COMPLETED on day N+1 belongs to day N+1’s outflows. UsecompletedAtfor cash-impact. - Assuming fees on the resource. v3 does not deduct or surface deposit/withdrawal fees on the resource — there is no fee field, so no fee term enters this reconciliation. Account for any rail/bank fees out of band.
Common mistakes
- Confusing
totalBalancewithavailableBalance.availableBalance≤totalBalance; the difference is earmarked against in-flight operations. UseavailableBalanceto size new operations; the held portion (totalBalance − availableBalance) is locked against in-flight withdrawals. - Treating stablecoin balances as chain-specific. Your
USDCbalance is one number that spans all supported networks — but withdrawals are pinned to one chain viawithdrawalAccountId. See Supported networks. - Expecting trade responses to carry balances. v2 did; v3 doesn’t. Read balances separately after a trade.
- Skipping the deposit webhook. Polling for incoming wires every minute works for development; in Live, subscribe to the
depositsevent so the treasury system reacts the moment funds clear.
What’s next
Cross-border payments
The same calls, ending in a payout to a third party.
Stablecoin on/off ramp
Convert fiat ↔ stablecoin and deliver on-chain.
Amounts
String-encoded amounts with semantic prefixes.
Pagination
Cursor-based reconciliation walks.