Skip to main content
Treasury workloads don’t always end in a settlement. You hold balances in multiple currencies, rebalance between them as rates move, and only push funds out to a bank or wallet when you need to. v3 supports this with a clean separation: 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
If every conversion ends in a payout to a third party, see Cross-border payments instead — the trade-then-withdraw flow is the same calls but a different mental model. For a side-by-side comparison of all four patterns, see Integration patterns.

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
The response carries a server-computed quoteAmount at the quoted rate — the exact USD you’ll receive.
For larger conversions where 3 seconds isn’t enough to get human approval, ask for a longer quote window via quoteForSeconds (standard durations: 3, 15, 30, 45, 60).

Step 3 — Execute

cURL
Atomic: 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:
Assuming the quote returned 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, real signRequest 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:
per currency, per day.

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 by createdAt; cursor pagination is documented in Pagination. Walk forward (newest → oldest) with startingAfter, passing the opaque pagination.nextCursor back verbatim until it’s null:
JavaScript
For incremental “what’s new since the last reconciliation run”, reverse direction: persist 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

A trade carries exactly one of 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 == 0 per currency. Non-zero means a missing record or off-by-one.
  • Every withdrawal.withdrawalAccountId is present in GET /v3/fx/withdrawal-accounts.
  • No long-aged PENDING trades. PENDING is a transient state during async execution and resolves to EXECUTED or FAILED within seconds; rows that stay PENDING past a small reconciliation window indicate a stuck execution worth investigating.
  • PROCESSING withdrawals are aged within rail cut-offs (Settlement times).
Quotes are not directly listable in v3 — persist quote responses yourself when you POST so the audit trail is two-sided. At minimum, capture id, quoteAmount, buyCurrency, sellCurrency, and createdAt at quote time alongside the idempotency key. There is no standalone rate field on QuotequoteAmount 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

  1. 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 balances fetched after the last trade of the day, not at midnight sharp.
  2. In-flight withdrawals. PENDING and PROCESSING withdrawals earmark balance but haven’t left. Reconcile on totalBalance for cash-impact totals; on availableBalance for spendable totals. Pick one and stay consistent.
  3. Failed-after-fetch. A withdrawal can transition PENDING → FAILED after you fetched the list. Re-poll GET /v3/fx/withdrawals/<uuid> for any non-terminal row older than the rail cut-off.
  4. Pagination boundary. If pagination.nextCursor !== null and you stopped paginating early, you missed records. Page until nextCursor is null.

Common mistakes (reconciliation edition)

  • Treating availableBalance as the balance. It excludes funds earmarked against in-flight operations. For end-of-day, totalBalance is usually what reconciliation wants.
  • Reconciling on createdAt instead of completedAt. A withdrawal created on day N but COMPLETED on day N+1 belongs to day N+1’s outflows. Use completedAt for 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 totalBalance with availableBalance. availableBalancetotalBalance; the difference is earmarked against in-flight operations. Use availableBalance to size new operations; the held portion (totalBalance − availableBalance) is locked against in-flight withdrawals.
  • Treating stablecoin balances as chain-specific. Your USDC balance is one number that spans all supported networks — but withdrawals are pinned to one chain via withdrawalAccountId. 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 deposits event 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.