> ## Documentation Index
> Fetch the complete documentation index at: https://api-docs-v3.openfx.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Trade

> Atomic FX trades: discover supported pairs, quote a rate, execute the trade against the quote.

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.

<Note title="Why this matters">
  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.
</Note>

## The trading flow

```mermaid theme={null}
%%{init: {'theme':'base','themeVariables':{'fontFamily':'Inter, system-ui, sans-serif','fontSize':'13px','actorBkg':'#ffffff','actorBorder':'#299f68','actorTextColor':'#114330','actorLineColor':'#a7d6bb','signalColor':'#114330','signalTextColor':'#114330','noteBkgColor':'#d7f4e1','noteBorderColor':'#299f68','noteTextColor':'#114330','labelBoxBkgColor':'#114330','labelBoxBorderColor':'#114330','labelTextColor':'#ffffff','activationBkgColor':'#d7f4e1','activationBorderColor':'#299f68'}}}%%
sequenceDiagram
  participant Client
  participant OpenFX as 🟢 OpenFX

  Client->>+OpenFX: GET /v3/fx/pairs
  OpenFX-->>-Client: List of tradable pairs

  Client->>+OpenFX: POST /v3/fx/quotes<br/>(Idempotency-Key)
  OpenFX-->>-Client: Quote<br/>{ id, buyAmount/sellAmount, quoteAmount, expiresAt }

  Note over Client,OpenFX: Quote is valid for 3s by default

  Client->>+OpenFX: POST /v3/fx/trades<br/>(quoteId · NEW Idempotency-Key)
  OpenFX-->>-Client: Trade (status: EXECUTED)

  Client->>+OpenFX: GET /v3/fx/balances
  OpenFX-->>-Client: Updated balances
```

## Endpoints

| Path                                                                | Description                                           | Idempotency-Key      |
| ------------------------------------------------------------------- | ----------------------------------------------------- | -------------------- |
| [`GET /v3/fx/pairs`](/v3/api-reference/trade/list-currency-pairs)   | List all tradable currency pairs with min/max amounts | —                    |
| [`POST /v3/fx/quotes`](/v3/api-reference/trade/create-quote)        | Lock in an exchange rate for a few seconds            | Required, 30-min TTL |
| [`GET /v3/fx/quotes/{id}`](/v3/api-reference/trade/get-quote-by-id) | Look up a single quote (status)                       | —                    |
| [`POST /v3/fx/trades`](/v3/api-reference/trade/execute-trade)       | Execute against a quote                               | Required, 24-hr TTL  |
| [`GET /v3/fx/trades`](/v3/api-reference/trade/list-trades)          | Paginated list of trades, newest first                | —                    |
| [`GET /v3/fx/trades/{id}`](/v3/api-reference/trade/get-trade)       | Look up a single trade                                | —                    |

## Quote lifecycle

Quotes are **short-lived**: 3 seconds by default. You can extend via the `quoteForSeconds` request parameter.

<Note>
  **`quoteForSeconds` accepts a closed set of standard durations:** `3`, `15`,
  `30`, `45`, `60` seconds.
</Note>

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.

<Note>
  **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.
</Note>

### Quote status

`GET /v3/fx/quotes/{id}` includes a `status` field not present on the `POST /v3/fx/quotes` create response:

| Status     | Meaning                                   | Trade execution                                              |
| ---------- | ----------------------------------------- | ------------------------------------------------------------ |
| `ACTIVE`   | Unconsumed and within the validity window | Will succeed (subject to balance + compliance)               |
| `EXPIRED`  | Unconsumed and past `expiresAt`           | Returns `409 QUOTE_EXPIRED` — re-quote and retry             |
| `CONSUMED` | A trade has executed against this quote   | Returns `409 QUOTE_ALREADY_CONSUMED` — quotes are single-use |

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:

```json theme={null}
{
  "object": "quote",
  "id": "qte_3FfGK34vwMvVFDedyb2nkf",
  "buyCurrency": "USDC",
  "sellCurrency": "USD",
  "sellAmount": "1000.00",
  "quoteAmount": "999.800000",
  "settlementWindow": "T0",
  "expiresAt": "2026-05-22T10:15:35.250Z",
  "createdAt": "2026-05-22T10:15:32.250Z",
  "status": "ACTIVE"
}
```

### 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.

```js theme={null}
// Fetched at quote-creation time, stored alongside the cart / session
const quoteId = session.quoteId;

const quote = await fetch(`https://api.openfx.com/v3/fx/quotes/${quoteId}`, {
  headers: { Authorization: `Bearer ${jwt}` },
}).then((r) => r.json());

if (quote.data.status === "CONSUMED") {
  // Another tab / device already traded this quote — surface a friendly error
  return showAlreadyTradedError();
}
if (quote.data.status === "EXPIRED") {
  // Re-quote and prompt the user to confirm again at the new rate
  return reQuoteAndConfirm();
}

// Safe to execute
await executeTrade(quoteId);
```

### 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](#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

```mermaid theme={null}
%%{init: {'theme':'base','themeVariables':{'fontFamily':'Inter, system-ui, sans-serif','fontSize':'13px','lineColor':'#299f68','primaryColor':'#fef3c7','primaryTextColor':'#78350f','primaryBorderColor':'#d97706'}}}%%
stateDiagram-v2
  direction LR
  [*] --> PENDING: POST /v3/fx/trades
  PENDING --> EXECUTED: Atomic execution succeeds
  PENDING --> FAILED: Execution fails
  EXECUTED --> [*]
  FAILED --> [*]

  classDef pendingStyle fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e
  classDef successStyle fill:#d7f4e1,stroke:#299f68,stroke-width:2px,color:#114330
  classDef failedStyle fill:#fee2e2,stroke:#dc2626,stroke-width:2px,color:#7f1d1d

  class PENDING pendingStyle
  class EXECUTED successStyle
  class FAILED failedStyle
```

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](/v3/references/glossary#status-enums).

<Note>
  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`](/v3/api-reference/trade-settlement/list-balances) separately.
  v2 bundled balances in the trade response; v3 separates them.
</Note>

## Common errors

For the full status code + `error.code` catalog, see the [Trade](/v3/errors#error-code-catalog) and [Quote](/v3/errors#error-code-catalog) tabs in [Errors](/v3/errors). The most common cases and their recovery patterns:

<AccordionGroup>
  <Accordion title="Expired quote (`QUOTE_EXPIRED`)">
    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.
  </Accordion>

  <Accordion title="Insufficient balance (`TRADE_INSUFFICIENT_BALANCE` on the sell side)">
    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`](/v3/api-reference/trade-settlement/list-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](/v3/errors#error-code-catalog) tab of Errors for the withdrawal-side equivalent.)
  </Accordion>

  <Accordion title="Pair not tradable (`TRADE_PAIR_NOT_SUPPORTED` / `TRADE_PAIR_NOT_ENABLED_FOR_ACCOUNT`)">
    The currency pair isn't on the platform, or isn't enabled for your account. Verify against [`GET /v3/fx/pairs`](/v3/api-reference/trade/list-currency-pairs). For stablecoin pairs the chain matters only at withdrawal time; the trade itself is chain-agnostic.
  </Accordion>

  <Accordion title="Execution failed mid-flight (`TRADE_EXECUTION_FAILED`)">
    Indeterminate state — the trade may have landed before the error surfaced. Call [`GET /v3/fx/trades/{id}`](/v3/api-reference/trade/get-trade) 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](/v3/errors#retrying-trade-execution-failed).
  </Accordion>

  <Accordion title="Idempotency key replay (`IDEMPOTENCY_MISMATCH`)">
    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.
  </Accordion>
</AccordionGroup>

## 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.

| v3 endpoint              | v2 endpoint                                                                                                                  |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `GET /v3/fx/pairs`       | [`GET /v2/brokerage/{orgId}/available_markets`](/v2/api-reference/market-data/get-available-markets)                         |
| `POST /v3/fx/quotes`     | [`POST /v2/brokerage/{orgId}/generate_quote`](/v2/api-reference/trade/generate-quote)                                        |
| `GET /v3/fx/quotes/{id}` | *(new in v3)* — surfaces a GET on the existing Quote resource. Useful for audit, reconciliation, pre-execution verification. |
| `POST /v3/fx/trades`     | [`POST /v2/brokerage/{orgId}/trade`](/v2/api-reference/trade/execute-trade)                                                  |
| `GET /v3/fx/trades/{id}` | [`GET /v2/brokerage/{orgId}/trade/{id}`](/v2/api-reference/trade/get-trade-by-id)                                            |
| `GET /v3/fx/trades`      | [`GET /v2/brokerage/{orgId}/trades`](/v2/api-reference/trade/list-trades)                                                    |

## What's next

<CardGroup cols={2}>
  <Card title="Trade Settlement" icon="wallet" href="/v3/trade-settlement">
    How funds flow in (deposits) and out (withdrawals).
  </Card>

  <Card title="Quickstart" icon="rocket" href="/v3/quickstart">
    Worked end-to-end: quote → trade → balance.
  </Card>

  <Card title="Idempotency" icon="repeat" href="/v3/idempotency">
    Why both /quotes and /trades require keys.
  </Card>

  <Card title="Amounts" icon="dollar-sign" href="/v3/amounts">
    String-encoded amounts with semantic prefixes.
  </Card>
</CardGroup>
