Skip to main content
OpenFX trading is a quote-then-execute model. You request a quote (a rate locked in for 3 seconds by default), then execute a trade against that quote. Execution is atomic: it either succeeds at the quoted rate or it fails. The same flow covers every conversion: fiat to fiat, fiat to stablecoin, stablecoin to stablecoin.
Splitting quote and trade forces deterministic execution: the rate you saw is the rate that executes, or the trade fails — never a silent re-price. Your retry strategy follows from this. After QUOTE_EXPIRED or TRADE_EXECUTION_FAILED, request a new quote and retry the trade with a NEW idempotency key; replaying the same key returns the cached failure.

The trading flow

Endpoints

Quote lifecycle

Quotes are short-lived: 3 seconds by default. You can extend via the quoteForSeconds request parameter.
quoteForSeconds accepts a closed set of standard durations: 3, 15, 30, 45, 60 seconds.
The quote is binding only if you execute before expiresAt. Once expired, executing returns QUOTE_EXPIRED and you must request a new quote.

Retrieving a quote

You can fetch a previously-created quote by ID for 2 days from its createdAt. The endpoint reads from durable storage, not the create-call’s idempotency cache — so the quote stays retrievable long after the 30-minute idempotency TTL has expired. Within that 2-day window, retrieval is independent of expiresAt (the tradability window — 3 to 60 s for standard durations, longer where custom durations are enabled) and of whether the quote has been consumed by a trade. Past the 2-day window, GET /v3/fx/quotes/{id} returns 404 QUOTE_NOT_FOUND — the same as an unknown id. Persist any quote details you need for audit or reconciliation beyond that window.

When to use it

  • Verify before executing. When more than a few hundred milliseconds pass between quote and trade (a user-confirmation step, a multi-system handoff), call GET /v3/fx/quotes/{id} first to confirm status: ACTIVE before POST /v3/fx/trades. Cheap (~50 ms p99) defensive check; it does not lock or consume the quote.
  • Audit and reconciliation. Given a Trade.quoteId, follow the foreign key to reconstruct what was quoted vs what executed.
  • Late-binding UX flows. Hand off a quoteId between services or to a user-confirmation surface; the receiving side fetches without needing the full quote payload to ride along.
  • Cross-system handoffs. A backoffice or compliance pipeline can resolve a quote by ID without the original creator’s request context.
This endpoint never consumes the quote. Only a successful POST /v3/fx/trades consumes it (flips status to CONSUMED atomically). You can GET the same quote any number of times — the read is idempotent.

Quote status

GET /v3/fx/quotes/{id} includes a status field not present on the POST /v3/fx/quotes create response: Once a quote has been consumed by a trade, it shows CONSUMED even after expiresAt passes.

Example response

The actual response wraps this object in data; the Quote resource itself is:

Pre-execution verification pattern

This fragment runs inside your asynchronous checkout handler. It omits the surrounding handler plus the application-provided session, jwt, and UI helper definitions.

Latency and cache interaction

Target p99 is 50 ms (single-row read; warm-path served from the quote service’s in-memory cache). Suitable for inline verification steps without meaningfully widening your quote → trade window. The endpoint is independent of POST /v3/fx/quotes’s 30-minute idempotency cache. It reads from durable storage and works after the idempotency TTL expires. If you’ve lost the original create-call response and the 30-minute window has passed, this is the canonical path to recover the quote — as long as you’re within the 2-day retrieval window from createdAt (see Retrieving a quote). Expiry (expiresAt passing) and consumption (a successful trade) both change the derived status field, but the quote stays readable until the 2-day window elapses.

Trade lifecycle

Trades have a three-state public enum: [PENDING, EXECUTED, FAILED]. Execution runs asynchronously: a successful POST /v3/fx/trades returns a Trade in PENDING while balance movement completes in the background. The trade then resolves atomically to EXECUTED (success; funds moved between balances at the quoted rate) or FAILED (no funds moved). PENDING is transient — clients may observe it on the create response or on GET /v3/fx/trades/{id} during the brief execution window, and should treat any non-terminal read as “still in flight, poll or wait.” Status casing matches v2 (EXECUTED uppercase, no change). For the side-by-side of trade vs deposit vs withdrawal status enums, see Glossary → Status enums.
The trade response includes buyAmount / sellAmount (whichever side you supplied) and executedAmount — the final amount received in the counter-currency. To see updated balances after a trade, call GET /v3/fx/balances separately. v2 bundled balances in the trade response; v3 separates them.

Common errors

For the full status code + error.code catalog, see the Trade and Quote tabs in Errors. The most common cases and their recovery patterns:
Quotes default to 3 seconds. If you take longer than expiresAt to call POST /v3/fx/trades, execution returns QUOTE_EXPIRED. Recover by re-quoting and retrying with a NEW idempotency key on the trade. Extend the window up front via quoteForSeconds (standard durations: 3, 15, 30, 45, 60) when the user-confirmation step is slow.
The sell-side balance dropped between quote and trade — an intervening trade or withdrawal consumed funds the quote relied on. The response carries details: { requiredAmount, availableAmount, currency, side } so you can render an actionable message without re-fetching balances. Check GET /v3/fx/balances for availableBalance, re-quote against the new figure, and retry with a fresh idempotency key. (Trade-execute and withdrawal use distinct, domain-scoped codes; see the Withdrawal tab of Errors for the withdrawal-side equivalent.)
The currency pair isn’t on the platform, or isn’t enabled for your account. Verify against GET /v3/fx/pairs. For stablecoin pairs the chain matters only at withdrawal time; the trade itself is chain-agnostic.
Indeterminate state — the trade may have landed before the error surfaced. Call GET /v3/fx/trades/{id} with the same trade ID first. If the record exists with status: EXECUTED, treat the original call as successful. Otherwise re-quote and retry with a NEW idempotency key. See the GET-first recovery pattern.
Reusing a key with a different request body returns 422 IDEMPOTENCY_MISMATCH (details.originalRequestHash) — not a replay and not a fresh execution. Reusing a key with the same body returns the original cached response (Idempotency-Replayed: true). For a retry against a new quote, generate a NEW key. The 24-hour TTL on trade keys exists so legitimate retries of an in-flight call (network timeouts, client crashes) are safe; it is not a way to re-execute against a new quote.

v2 → v3 mapping

The trading flow is unchanged from v2; only the URLs and field names move. quoteForSeconds (unchanged name), the 3-second default quote validity, and the closed {3, 15, 30, 45, 60} bucket set all carry forward from v2.

What’s next

Trade Settlement

How funds flow in (deposits) and out (withdrawals).

Quickstart

Worked end-to-end: quote → trade → balance.

Idempotency

Why both /quotes and /trades require keys.

Amounts

String-encoded amounts with semantic prefixes.