/pairs → /quotes → /trades), but the shape of the integration changes: you’ll want longer quote windows, careful key generation per user request, and a clean separation between “show price” and “execute”.
When to reach for this
- A neobank or broker offering in-app currency conversion
- A platform letting merchants accept payment in their preferred currency
- Any UX where a human sees a rate, clicks “confirm”, and you execute on their behalf
Setup
1
Cache the pair list
GET /v3/fx/pairs returns the full set of tradable currency pairs with minTradeAmount and maxTradeAmount. The list is stable on the order of hours, not seconds — cache it on your server and refresh periodically. Don’t call /pairs on every page load.2
Build a request signer that mints JWT + X-Request-Signature on demand
JWTs are 60s, single-use; every request also needs a body-bound
X-Request-Signature header. Wrap the Authentication helper into a small server-side function: signRequest({method, url, body}) per outbound request. Don’t share JWTs or signatures across user sessions.3
Decide quote validity per UX step
The default
quoteForSeconds is 3 — too short for a human to read and confirm. Bump to 15, 30, 45, or 60 depending on how much friction sits between “rate shown” and “user clicks confirm”. Standard durations: 3, 15, 30, 45, 60. See Trading.4
(Optional) Verify withdrawal accounts
If your product also settles trades to a bank or wallet (e.g. payouts to a user’s external account), set those up via Verified accounts. For a pure conversion product that keeps funds on your platform’s omnibus, you don’t need any verified withdrawal accounts.
The flow
Step 1 — Show rates from the cached pair list
Render available pairs from your cache. Min/max bounds matter: a UX that lets a user request belowminTradeAmount will hit TRADE_AMOUNT_BELOW_MINIMUM at execution, after they’ve already seen a quote.
The actual response wraps the object below in data; the PairList shape itself is:
Step 2 — Quote with a UX-appropriate window
Anchor whichever side the user actually typed into your UI. For “sell £1,000” UX →sellAmount: "1000.00". For “buy €500” UX → buyAmount: "500.00". Don’t compute one from the other client-side; let the server lock the rate against the value the user committed to.
cURL
3, 15, 30, 45, 60; if your UX needs a window beyond 60 seconds, contact your account team to enable a custom duration.
The rate is binding only until expiresAt. After that, executing returns QUOTE_EXPIRED and you need to re-quote.
Step 3 — Execute on confirmation
The execution call carries a newIdempotency-Key. If your user clicks “confirm” twice in 200ms, the same key on a retry returns the original trade — not a duplicate execution.
JavaScript
The “click twice” problem. A user double-clicking “confirm” within the
quote’s validity window will land two requests with the same Idempotency-Key —
the second returns the cached first result, not a second trade. Without
idempotency they’d both execute, and you’d owe your user a refund. This is the
core reason
Idempotency-Key is required on POST /v3/fx/trades.Step 4 — Record the trade in your ledger
The trade response is your source of truth for what the user got. Persistid, buyAmount, sellAmount, executedAmount, and createdAt against your internal user/order ID. Reconcile against GET /v3/fx/trades periodically.
Catch-up (newer than last seen) — backward iteration. Persist the pagination.prevCursor value from your last reconciliation run; on the next run, pass it as endingBefore to fetch only what’s appeared since.
cURL
pagination.nextCursor is null.
JavaScript
Putting it together
A minimal Express-style controller:JavaScript
Common mistakes
- Using
quoteForSeconds: 3for a human UX. The default is for machine-to-machine flows. Bump to 15 or 30 for any UI that involves reading a rate. - Generating Idempotency-Keys on the server during execution retries. If the client retries, server-side regeneration creates a brand-new key and a duplicate trade. Key generation must be tied to the client’s logical execution attempt — usually a
client_request_idfrom the request body. - Treating quotes as cacheable. Each quote is single-execution and expires within seconds. Don’t store quotes across user sessions; re-quote per request.
- Calling
/v3/fx/pairson every page render. The list updates on a much slower cadence than your traffic. Cache server-side and refresh on a schedule.
What’s next
Trade
Quote lifecycle and trade state machine.
Idempotency
Why
/trades requires a key and how the 24-hour TTL works.Authentication
JWT minter samples in JS / TS / Python / Go.
Errors
The full trading-error catalog + retry triage matrix.