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

# Errors

> Errors return in a stable, machine-readable envelope with a code, type, human message, retry strategy, and per-code details.

Every v3 error response has the same shape, regardless of status code. Wire your client up once to switch on `error.code` and you're done.

<Note>
  **`type` is required on every v3 error.** Alongside `code` and `message`, the
  `type` field is now a **mandatory** part of the envelope: it is always present
  on every non-2xx response and is part of the OpenAPI `required` set. You can
  safely rely on `error.type` for coarse routing (auth vs validation vs
  <span class="ofx-enum">CONFLICT</span> vs rate-limited vs internal) without null-checks. Treat a response
  without `type` as a transport-level bug, not a normal API outcome.
</Note>

```json theme={"theme":{"light":"poimandres","dark":"poimandres"}}
{
  "error": {
    "code": "QUOTE_EXPIRED",
    "type": "CONFLICT",
    "message": "The quoted rate has expired. Request a new quote.",
    "retryStrategy": "FIX_AND_RESUBMIT",
    "details": {
      "quoteId": "qte_3FfGK34vwMvVFDedyb2nkf",
      "expiredAt": "2026-04-28T10:00:00.000Z"
    }
  }
}
```

<Warning>
  **The trace ID is in the `X-Trace-Id` response header, not the error body.**
  Every response (success and error) carries it. Log it alongside the request
  payload; support uses it as the first input on every triage. See [Metadata &
  Tracing](/v3/metadata-and-tracing).
</Warning>

## Why this shape

A bare `{ "status": "error", "message": "..." }` shape has three problems:

1. **No code.** You'd have to string-match `message`, which breaks the moment a copy edit lands.
2. **No trace ID.** Triage starts with "send us your logs."
3. **No structure.** "Insufficient balance" tells you what's wrong, but not by how much, in which currency.

The error envelope below fixes all three.

## Error envelope fields

| Field | Type | Required | Notes |
| - | - | - | - |
| `code` | string | yes | Stable, machine-readable. Format `<PRODUCT>_<ERROR>`. Never renamed or removed without a major version bump. |
| `type` | string | yes | Coarse category. Always present on every error response. One of: <span class="ofx-enum">VALIDATION\_ERROR</span>, <span class="ofx-enum">AUTHENTICATION\_ERROR</span>, <span class="ofx-enum">AUTHORIZATION\_ERROR</span>, <span class="ofx-enum">NOT\_FOUND</span>, <span class="ofx-enum">CONFLICT</span>, <span class="ofx-enum">RATE\_LIMITED</span>, <span class="ofx-enum">INTERNAL\_ERROR</span>. |
| `message` | string | yes | Human-readable. **May change** between versions. Switch on `code`, not `message`. |
| `retryStrategy` | string | yes | Machine-readable recovery hint, independent of `code` and `type`. Always present. One of: <span class="ofx-enum">RETRY\_SAME\_KEY</span>, <span class="ofx-enum">WAIT\_THEN\_RETRY\_SAME\_KEY</span>, <span class="ofx-enum">FIX\_AND\_RESUBMIT</span>, <span class="ofx-enum">CHECK\_STATE\_THEN\_RETRY</span>, <span class="ofx-enum">TERMINAL</span>. See [Triage matrix](#triage-matrix). |
| `details` | object | yes | Per-code, structured context. Shape varies by `code`. Always present (`{}` when there is no structured context for this code). |

Errors are returned as `application/json`; the envelope above is the complete contract.

<Note>
  **Error responses never carry a `metadata` field.** Client-supplied
  request-body `metadata` is echoed back only on **success** responses. On any
  non-2xx, the envelope is exactly `code`, `type`, `message`, `retryStrategy`,
  and `details`. Don't look for your request `metadata` on an error.
</Note>

## Triage matrix

`error.retryStrategy` is the direct, server-declared signal for how to react. Read it instead of pattern-matching on `error.code` or HTTP status. It's one of five values, and every error response carries one:

| `retryStrategy` | Meaning | Example codes |
| - | - | - |
| <span class="ofx-enum">RETRY\_SAME\_KEY</span> | Safe to retry immediately with the same `Idempotency-Key`. Reserved for future use (no current code emits this value, but you must still handle it). | *(none currently)* |
| <span class="ofx-enum">WAIT\_THEN\_RETRY\_SAME\_KEY</span> | Transient backpressure, then retry with the **same** key. <span class="ofx-enum">RATE\_LIMIT\_EXCEEDED</span> and <span class="ofx-enum">IDEMPOTENCY\_IN\_FLIGHT</span> carry `Retry-After` (seconds); wait that long. <span class="ofx-enum">IDEMPOTENCY\_UNAVAILABLE</span> carries no `Retry-After`; apply your own short backoff. | <span class="ofx-enum">RATE\_LIMIT\_EXCEEDED</span>, <span class="ofx-enum">IDEMPOTENCY\_IN\_FLIGHT</span>, <span class="ofx-enum">IDEMPOTENCY\_UNAVAILABLE</span> |
| <span class="ofx-enum">FIX\_AND\_RESUBMIT</span> | This exact request cannot succeed as submitted. Fix the request or wait out the blocking state, then resubmit with a **new** key. | <span class="ofx-enum">VALIDATION\_BODY\_FAILED</span>, <span class="ofx-enum">WITHDRAWAL\_INSUFFICIENT\_BALANCE</span>, <span class="ofx-enum">QUOTE\_EXPIRED</span>, <span class="ofx-enum">IDEMPOTENCY\_MISMATCH</span> |
| <span class="ofx-enum">CHECK\_STATE\_THEN\_RETRY</span> | Indeterminate write. `GET /v3/fx/{resource}/{id}` first (using `details.resourceId` when present) before deciding whether to retry. | <span class="ofx-enum">TRADE\_EXECUTION\_FAILED</span>, <span class="ofx-enum">WITHDRAWAL\_INITIATION\_FAILED</span>, <span class="ofx-enum">INTERNAL\_ERROR</span> |
| <span class="ofx-enum">TERMINAL</span> | Do not retry. Auth failures, `*_NOT_FOUND`, and account-level policy blocks. | <span class="ofx-enum">AUTH\_TOKEN\_EXPIRED</span>, <span class="ofx-enum">TRADE\_NOT\_FOUND</span>, <span class="ofx-enum">TRADE\_DISABLED</span>, <span class="ofx-enum">WITHDRAWAL\_DISABLED\_FOR\_ACCOUNT</span> |

Default-handle unknown future `retryStrategy` values as <span class="ofx-enum">TERMINAL</span>, the same open-vocabulary rule as `error.code` and `error.type` (see [Client error modeling](#client-error-modeling)).

The table below cross-references the same catalog by HTTP status, for clients that key off status first:

| HTTP status | `type` examples | Behavior bucket | Example codes |
| - | - | - | - |
| **400** schema | <span class="ofx-enum">VALIDATION\_ERROR</span> | Your JSON is malformed or has wrong field types. Fix and resubmit. | <span class="ofx-enum">VALIDATION\_BODY\_FAILED</span>, <span class="ofx-enum">VALIDATION\_QUERY\_FAILED</span>, <span class="ofx-enum">VALIDATION\_PATH\_FAILED</span>, <span class="ofx-enum">VALIDATION\_CONTENT\_TYPE</span> |
| **401** auth | <span class="ofx-enum">AUTHENTICATION\_ERROR</span> | Token missing, invalid, or expired. Mint a fresh JWT. | <span class="ofx-enum">AUTH\_TOKEN\_EXPIRED</span>, <span class="ofx-enum">AUTH\_API\_KEY\_INVALID</span> |
| **403** authz | <span class="ofx-enum">AUTHORIZATION\_ERROR</span> | Authenticated, but not allowed. Check your key's scope, IP allowlist, environment, and product entitlement. | <span class="ofx-enum">AUTH\_INSUFFICIENT\_SCOPE</span>, <span class="ofx-enum">AUTH\_FORBIDDEN\_IP</span>, <span class="ofx-enum">SANDBOX\_MODE\_REQUIRED</span>, <span class="ofx-enum">TRADE\_DISABLED</span>, <span class="ofx-enum">FEATURE\_NOT\_ENABLED</span> |
| **404** not found | <span class="ofx-enum">NOT\_FOUND</span> | Resource doesn't exist. Treat as terminal in your app. | <span class="ofx-enum">TRADE\_NOT\_FOUND</span>, <span class="ofx-enum">WITHDRAWAL\_NOT\_FOUND</span>, <span class="ofx-enum">DEPOSIT\_NOT\_FOUND</span> |
| **405** method | <span class="ofx-enum">VALIDATION\_ERROR</span> | Wrong HTTP method on this path. Check the API reference. | <span class="ofx-enum">METHOD\_NOT\_ALLOWED</span> |
| **409** state conflict | <span class="ofx-enum">CONFLICT</span> | Well-formed request blocked by current state. Resolve state, then retry as a new operation. | <span class="ofx-enum">WITHDRAWAL\_INSUFFICIENT\_BALANCE</span>, <span class="ofx-enum">TRADE\_INSUFFICIENT\_BALANCE</span>, <span class="ofx-enum">IDEMPOTENCY\_IN\_FLIGHT</span>, <span class="ofx-enum">QUOTE\_EXPIRED</span>, <span class="ofx-enum">QUOTE\_ALREADY\_CONSUMED</span>, <span class="ofx-enum">TRADE\_CREDIT\_LIMIT\_EXCEEDED</span> |
| **413** payload too large | <span class="ofx-enum">VALIDATION\_ERROR</span> | The request body exceeds the endpoint's payload limit. Reduce the body size and resubmit. | <span class="ofx-enum">VALIDATION\_PAYLOAD\_TOO\_LARGE</span> |
| **422** semantic | <span class="ofx-enum">VALIDATION\_ERROR</span>, <span class="ofx-enum">CONFLICT</span> | Well-formed input that violates a business rule. Fix the value and resubmit. | <span class="ofx-enum">WITHDRAWAL\_INVALID\_AMOUNT\_PRECISION</span>, <span class="ofx-enum">TRADE\_AMOUNT\_ABOVE\_LIMIT</span>, <span class="ofx-enum">IDEMPOTENCY\_MISMATCH</span> |
| **428** missing precondition | <span class="ofx-enum">VALIDATION\_ERROR</span> | A required precondition header is missing. Add it and retry with the SAME key. | <span class="ofx-enum">IDEMPOTENCY\_KEY\_MISSING</span> |
| **429** rate limited | <span class="ofx-enum">RATE\_LIMITED</span> | Wait `Retry-After` seconds, then retry with the same key. | <span class="ofx-enum">RATE\_LIMIT\_EXCEEDED</span> |
| **500** internal | <span class="ofx-enum">INTERNAL\_ERROR</span> | Server error. For write paths that echo `details.resourceId` (<span class="ofx-enum">TRADE\_EXECUTION\_FAILED</span>, <span class="ofx-enum">WITHDRAWAL\_INITIATION\_FAILED</span>), check state by ID before retrying. See [Retrying trade execution failed](#retrying-trade-execution-failed). | <span class="ofx-enum">INTERNAL\_ERROR</span>, <span class="ofx-enum">TRADE\_EXECUTION\_FAILED</span>, <span class="ofx-enum">WITHDRAWAL\_INITIATION\_FAILED</span> |
| **503** unavailable | <span class="ofx-enum">INTERNAL\_ERROR</span> | A required upstream dependency (the idempotency service) is temporarily unreachable. Wait, then retry with the same key. | <span class="ofx-enum">IDEMPOTENCY\_UNAVAILABLE</span> |

## Decision flow

Use this as a mental model for what to do when a non-2xx lands. Reading `error.retryStrategy` directly (see the [triage matrix](#triage-matrix)) gets you the same answer without maintaining a code-pattern list; the flow below is for clients that branch on `error.code` and `type` instead.

```mermaid theme={"theme":{"light":"poimandres","dark":"poimandres"}}
%%{init: {'theme':'base','themeVariables':{'fontFamily':'Inter, system-ui, sans-serif','fontSize':'13px'}}}%%
flowchart LR
  Start([API call returns non-2xx]) --> ReadCode{{Read error.code}}
  ReadCode --> Auth401(["401 AUTH_TOKEN_*<br/>AUTH_API_KEY_INVALID<br/>AUTH_API_KEY_MISSING?"])
  ReadCode --> Authz403(["403 AUTH_FORBIDDEN_IP<br/>AUTH_INSUFFICIENT_SCOPE<br/>SANDBOX_MODE_REQUIRED<br/>*_KEY_*_REQUIRED?"])
  ReadCode --> Schema400(["400 VALIDATION_BODY_FAILED<br/>VALIDATION_QUERY_FAILED<br/>VALIDATION_PATH_FAILED?"])
  ReadCode --> Semantic422(["422 *_INVALID_AMOUNT_PRECISION<br/>TRADE_AMOUNT_*_LIMIT<br/>IDEMPOTENCY_MISMATCH?"])
  ReadCode --> Precondition428(["428 IDEMPOTENCY_KEY_MISSING?"])
  ReadCode --> Idem(["IDEMPOTENCY_IN_FLIGHT?"])
  ReadCode --> Rate(["429 RATE_LIMIT_EXCEEDED?"])
  ReadCode --> NotFound(["404 *_NOT_FOUND?"])
  ReadCode --> StateConflict(["409 *_INSUFFICIENT_*<br/>*_LIMIT_REACHED<br/>QUOTE_EXPIRED<br/>*_ADDRESS_NOT_VERIFIED?"])
  ReadCode --> ExecFailed(["500 TRADE_EXECUTION_FAILED<br/>WITHDRAWAL_INITIATION_FAILED?"])
  ReadCode --> Internal(["500 INTERNAL_ERROR?"])

  Auth401 -->|yes| MintToken[/"Mint a fresh JWT.<br/>If it still fails, check API key validity in the dashboard."/]
  Authz403 -->|yes| CheckEntitlement[/"Check IP allowlist, key scope, environment header."/]
  Schema400 -->|yes| FixSchema[/"Fix the request shape.<br/>Resubmit with NEW idempotency key."/]
  Semantic422 -->|yes| FixSemantic[/"Fix the business-rule violation in details.<br/>Resubmit with NEW key.<br/>IDEMPOTENCY_MISMATCH = reused key with different body."/]
  Precondition428 -->|yes| AddHeader[/"Add the Idempotency-Key header.<br/>Retry with SAME key."/]
  Idem -->|yes| WaitRetry[/"Wait, then retry<br/>with the SAME key."/]
  Rate -->|yes| WaitRetry
  NotFound -->|yes| Terminal[/"Terminal.<br/>Treat as not found in your app."/]
  StateConflict -->|yes| ResolveState[/"Resolve underlying state, then retry as NEW operation with NEW key.<br/>For QUOTE_EXPIRED specifically: re-quote."/]
  ExecFailed -->|yes| CheckState[/"GET the resource by ID first.<br/>If it exists, the write succeeded.<br/>If 404, safe to retry with SAME key."/]
  Internal -->|yes| CheckState

  classDef ok stroke-width:1.5px
  classDef bad stroke-width:1.5px
  classDef info stroke-width:1.5px
  classDef hold stroke-width:1.5px

  class MintToken,CheckEntitlement,FixSchema,FixSemantic,AddHeader,ResolveState ok
  class WaitRetry hold
  class CheckState info
  class Terminal bad
  class Terminal terminal
```

When in doubt, read `error.retryStrategy` directly. Switch on the specific `error.code` for finer-grained handling, and fall back to the `type` column for categorization.

## HTTP status mapping

A duplicate JSON key in a request body returns `400` <span class="ofx-enum">VALIDATION\_BODY\_FAILED</span>. A rejected `X-Request-Id` returns `400` <span class="ofx-enum">VALIDATION\_HEADER\_FAILED</span>; its `details.issues` identifies the header. Response bodies use `Content-Type: application/json` without a charset parameter.

`type` is the coarse routing taxonomy. HTTP status is the wire-level signal. They line up like this:

| `type` | Status | Used for |
| - | - | - |
| <span class="ofx-enum">VALIDATION\_ERROR</span> | **400** | Schema / syntax violations: malformed JSON, wrong types, missing required fields |
| <span class="ofx-enum">VALIDATION\_ERROR</span> | **422** | Semantic violations: well-formed input that breaks a business rule, cross-field constraint, or precision rule |
| <span class="ofx-enum">VALIDATION\_ERROR</span> | **428** | Missing required precondition header (`Idempotency-Key`) |
| <span class="ofx-enum">VALIDATION\_ERROR</span> | **405** | HTTP method not allowed for this path |
| <span class="ofx-enum">VALIDATION\_ERROR</span> | **413** | Request body exceeds the endpoint's payload size limit |
| <span class="ofx-enum">AUTHENTICATION\_ERROR</span> | **401** | Token missing, invalid, or expired |
| <span class="ofx-enum">AUTHORIZATION\_ERROR</span> | **403** | Authenticated, but not allowed: wrong scope, wrong env, IP blocked, account disabled |
| <span class="ofx-enum">NOT\_FOUND</span> | **404** | Resource doesn't exist |
| <span class="ofx-enum">NOT\_FOUND</span> | **410** | Endpoint has been sunset (v1 after December 31, 2026). `Sunset` + `Link` headers carry the migration info |
| <span class="ofx-enum">CONFLICT</span> | **409** | State-driven conflict: well-formed request blocked by current system state (insufficient balance, expired quote, in-flight idempotency key, exposure limit reached) |
| <span class="ofx-enum">CONFLICT</span> | **422** | <span class="ofx-enum">IDEMPOTENCY\_MISMATCH</span>: same key reused with different parameters |
| <span class="ofx-enum">RATE\_LIMITED</span> | **429** | Per-org or per-key rate limit |
| <span class="ofx-enum">INTERNAL\_ERROR</span> | **500** | Server error. Read the affected resource state before retrying a write, or retry a read with backoff. Write-path codes may echo `details.resourceId` for the state check. |
| <span class="ofx-enum">INTERNAL\_ERROR</span> | **503** | A required upstream dependency (the idempotency service) is temporarily unreachable |

**<span class="ofx-enum">VALIDATION\_ERROR</span> spans five statuses, not one.** 400 and 422 carry most of the volume and are worth distinguishing: 400 means "your input didn't pass the schema." 422 means "your input is well-formed JSON of the right types, but it violates a business rule" (a withdrawal amount with too many decimals, a trade above your account limit, an idempotency-key reuse with mismatched body). Modern clients can build different UI flows for the two: 400 → "fix the field"; 422 → "tell the user about the rule." The remaining three are each single-purpose and don't need their own flow: 428 (missing `Idempotency-Key`), 405 (method not allowed), and 413 (payload too large).

A common confusion: **negative request-body amount → 400, not 422.** Request amount fields (e.g. `WithdrawalCreateRequest.withdrawalAmount`) are typed as `PositiveAmount` and the schema regex `^[0-9]+(\.[0-9]{1,8})?$` rejects the leading sign. So `POST /v3/fx/withdrawals` with `"withdrawalAmount": "-1.00"` returns `400` <span class="ofx-enum">VALIDATION\_BODY\_FAILED</span> (schema), not `422` <span class="ofx-enum">WITHDRAWAL\_INVALID\_AMOUNT\_PRECISION</span> (semantic). The precision codes (<span class="ofx-enum">WITHDRAWAL\_INVALID\_AMOUNT\_PRECISION</span>, etc.) are only reached by inputs that already pass the schema regex, i.e. positive numbers with too many fractional digits.

**Why two statuses for <span class="ofx-enum">CONFLICT</span>?** 409 is the state-driven conflict bucket (a fact about the system blocks the request). 422 is reserved for the one case where idempotency-key semantics are violated.

### 404 vs 403: when each fires

The boundary between 404 ("doesn't exist") and 403 ("not allowed") is precise in v3. Use this table when classifying a non-2xx that landed before your business logic ran:

| Scenario | Status | Code |
| - | - | - |
| The path doesn't exist in v3 at all | 404 | <span class="ofx-enum">ROUTE\_NOT\_FOUND</span> |
| The path exists, but the resource ID doesn't, or it belongs to another org (we don't disclose which) | 404 | `*_NOT_FOUND` (e.g. <span class="ofx-enum">TRADE\_NOT\_FOUND</span>, <span class="ofx-enum">QUOTE\_NOT\_FOUND</span>, <span class="ofx-enum">WITHDRAWAL\_NOT\_FOUND</span>, <span class="ofx-enum">WITHDRAWAL\_ADDRESS\_NOT\_FOUND</span>) |
| The path exists, but a specific resource is toggled off for your org | 403 | <span class="ofx-enum">TRADE\_CURRENCY\_NOT\_ENABLED\_FOR\_ACCOUNT</span>, <span class="ofx-enum">WITHDRAWAL\_ACCOUNT\_NOT\_LIVE</span> |
| The path exists, but the whole domain is disabled for your org | 403 | <span class="ofx-enum">TRADE\_DISABLED</span>, <span class="ofx-enum">WITHDRAWAL\_DISABLED\_FOR\_ACCOUNT</span> |
| The path exists, but your API key is missing the required scope | 403 | <span class="ofx-enum">AUTH\_INSUFFICIENT\_SCOPE</span> |

A 404 means the request never reached an entitlement check: it was rejected at routing or hidden for leakage prevention. A 403 means the request reached a handler that knows who you are and is telling you what to fix (or who to contact). Switch on the specific `error.code` rather than the status to decide the next action.

**Product-level entitlement uses `_DISABLED` codes.** A `_DISABLED` code means access has been revoked or blocked; this catalog defines <span class="ofx-enum">TRADE\_DISABLED</span> and <span class="ofx-enum">WITHDRAWAL\_DISABLED\_FOR\_ACCOUNT</span> (both 403). The second names the account scope in its suffix. Resource-level toggles keep their own codes (<span class="ofx-enum">TRADE\_CURRENCY\_NOT\_ENABLED\_FOR\_ACCOUNT</span>). Only codes in the generated catalog are part of the contract.

## Code stability guarantee

Once a code ships in v3, **it does not get renamed or removed without a major version bump.** New codes may be introduced in a minor version, documented in the changelog.

### Client error modeling

**Treat `error.code`, `error.type`, and `error.retryStrategy` as non-exhaustive (open) vocabularies.** All three can gain new members in a minor version, so model each as an open enum: switch on the value but **always keep a `default` branch**. When you hit a value you don't recognize:

* Handle an **unknown `error.type`** as <span class="ofx-enum">INTERNAL\_ERROR</span> (treat it as a server-side category you don't yet model).
* Bubble up an **unknown `error.code`** with its `X-Trace-Id` so a newly-added code never silently breaks your client.
* Handle an **unknown `error.retryStrategy`** as <span class="ofx-enum">TERMINAL</span> (the safest default: don't retry automatically).

This keeps your client forward-compatible: a code, type, or retry strategy added after you shipped degrades to a safe default instead of an unhandled branch.

**Every `details` object is typed and closed.** Each code maps to a dedicated schema through `x-openfx-error-catalog`; codes without additional context reference the shared closed `EmptyErrorDetails` schema. Adding an optional detail field is additive. Removing or renaming a field, or making an optional field required, is a breaking change.

## Error code catalog

The FX and Payments APIs define **107 distinct error codes**; a code shared by both appears once, or once per surface when the surfaces define it differently. The OpenAPI extension `x-openfx-error-catalog` is the source of truth for status, client action, lifecycle, and the closed `details` schema.

<div id="authentication-authorization" />

<div id="validation" />

<div id="idempotency" />

<div id="routing" />

<div id="rate-limiting" />

<div id="trade" />

<div id="quotes" />

<div id="deposits" />

<div id="withdrawal" />

<div id="withdrawal-accounts" />

<div id="exposures" />

<div id="platform" />

<div id="lifecycle" />

<div id="accounts" />

<div id="capabilities" />

<div id="payout-addresses" />

<div id="payout-quotes" />

<div id="payouts" />

<Tabs>
  <Tab title="Authentication">
    | Code | Status | Type | Trigger | Details | Client action |
    | - | -: | - | - | - | - |
    | <span class="ofx-enum">AUTH\_TOKEN\_MISSING</span> | 401 | <span class="ofx-enum">AUTHENTICATION\_ERROR</span> | The Authorization header is missing. | `{}` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">AUTH\_TOKEN\_INVALID</span> (FX) | 401 | <span class="ofx-enum">AUTHENTICATION\_ERROR</span> | The bearer token or request signature is malformed or fails verification. | `{ reason? }` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">AUTH\_TOKEN\_EXPIRED</span> (FX) | 401 | <span class="ofx-enum">AUTHENTICATION\_ERROR</span> | The bearer token expiry time is in the past. | `{ expiredAt? }` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">AUTH\_TOKEN\_INVALID\_CONFIG</span> (FX) | 401 | <span class="ofx-enum">AUTHENTICATION\_ERROR</span> | The bearer token claims or validity window violate the authentication contract. | `{ reason, maxSeconds? }` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">AUTH\_API\_KEY\_MISSING</span> | 401 | <span class="ofx-enum">AUTHENTICATION\_ERROR</span> | The bearer token does not reference an API key. | `{}` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">AUTH\_API\_KEY\_INVALID</span> | 401 | <span class="ofx-enum">AUTHENTICATION\_ERROR</span> | The referenced API key does not exist or is inactive. | `{}` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">AUTH\_API\_KEY\_SANDBOX\_REQUIRED</span> | 403 | <span class="ofx-enum">AUTHORIZATION\_ERROR</span> | Sandbox traffic uses a Live API key. | `{}` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">AUTH\_API\_KEY\_LIVE\_REQUIRED</span> | 403 | <span class="ofx-enum">AUTHORIZATION\_ERROR</span> | Live traffic uses a Sandbox API key. | `{}` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">SANDBOX\_MODE\_REQUIRED</span> | 403 | <span class="ofx-enum">AUTHORIZATION\_ERROR</span> | A request reaches the Sandbox environment with an `X-App-Mode` other than <span class="ofx-enum">SANDBOX</span>, for example <span class="ofx-enum">LIVE</span>. | `{}` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">AUTH\_FORBIDDEN\_IP</span> | 403 | <span class="ofx-enum">AUTHORIZATION\_ERROR</span> | The source IP address is outside the API key allowlist. | `{ ip? }` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">AUTH\_INSUFFICIENT\_SCOPE</span> | 403 | <span class="ofx-enum">AUTHORIZATION\_ERROR</span> | The API key lacks a scope required by the operation. | `{}` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">AUTH\_TOKEN\_EXPIRED</span> (Payments) | 401 | <span class="ofx-enum">AUTHENTICATION\_ERROR</span> | The bearer token expiry time is in the past. | `{}` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">AUTH\_TOKEN\_INVALID</span> (Payments) | 401 | <span class="ofx-enum">AUTHENTICATION\_ERROR</span> | The bearer token or request signature is malformed or fails verification. | `{}` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">AUTH\_TOKEN\_INVALID\_CONFIG</span> (Payments) | 401 | <span class="ofx-enum">AUTHENTICATION\_ERROR</span> | The bearer token claims or validity window violate the authentication contract. | `{ reason? }` | Do not retry automatically; correct access or surface the error. |
  </Tab>

  <Tab title="Validation">
    | Code | Status | Type | Trigger | Details | Client action |
    | - | -: | - | - | - | - |
    | <span class="ofx-enum">INVALID\_APP\_MODE</span> | 400 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The `X-App-Mode` header is present with a value other than <span class="ofx-enum">LIVE</span> or <span class="ofx-enum">SANDBOX</span>. Matching ignores case, and an empty value is also rejected. | `{ received, allowed }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">VALIDATION\_QUERY\_FAILED</span> | 400 | <span class="ofx-enum">VALIDATION\_ERROR</span> | One or more query parameters fail schema validation. | `{ issues, firstIssuePath? }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">VALIDATION\_PATH\_FAILED</span> | 400 | <span class="ofx-enum">VALIDATION\_ERROR</span> | One or more path parameters fail schema validation. | `{ issues, firstIssuePath? }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">VALIDATION\_BODY\_FAILED</span> | 400 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The request body fails schema validation. | `{ issues, firstIssuePath? }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">VALIDATION\_CONTENT\_TYPE</span> | 400 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The request's Content-Type is not application/json, or specifies an unsupported charset. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">VALIDATION\_PAYLOAD\_TOO\_LARGE</span> (FX) | 413 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The request body exceeds the maximum allowed payload size. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">METHOD\_NOT\_ALLOWED</span> | 405 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The HTTP method is not supported on the request path. | `{ method, allowed }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">VALIDATION\_HEADER\_FAILED</span> | 400 | <span class="ofx-enum">VALIDATION\_ERROR</span> | A supplied X-Request-Id header does not match the required format. | `{ issues, firstIssuePath }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">INVALID\_ORGANIZATION</span> | 400 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The organization identifier on the request is malformed. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">MISSING\_REQUEST\_CONTEXT</span> | 400 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The organization, user, or idempotency context every request carries was absent or unreadable. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">VALIDATION\_PAYLOAD\_TOO\_LARGE</span> (Payments) | 413 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The request body exceeds the maximum allowed payload size of 8192 bytes. | `{ reason, receivedBytes?, limitBytes }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">UNSUPPORTED\_MEDIA\_TYPE</span> | 415 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The request's Content-Type is not application/json. | `{}` | Correct the request or blocking state, then submit a new operation. |
  </Tab>

  <Tab title="Idempotency">
    | Code | Status | Type | Trigger | Details | Client action |
    | - | -: | - | - | - | - |
    | <span class="ofx-enum">IDEMPOTENCY\_KEY\_MISSING</span> (FX) | 428 | <span class="ofx-enum">VALIDATION\_ERROR</span> | A write request omits the Idempotency-Key header. | `{ endpoint }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">IDEMPOTENCY\_KEY\_INVALID</span> | 400 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The Idempotency-Key value does not match ^\[a-zA-Z0-9\_-]{1,255}\$. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">IDEMPOTENCY\_IN\_FLIGHT</span> | 409 | <span class="ofx-enum">CONFLICT</span> | Another request with the same idempotency key is still processing. | `{}` | Wait as directed, then retry with the same idempotency key. |
    | <span class="ofx-enum">IDEMPOTENCY\_MISMATCH</span> (FX) | 422 | <span class="ofx-enum">CONFLICT</span> | An idempotency key is reused with a different request body. | `{ originalRequestHash, firstSeenAt? }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">IDEMPOTENCY\_UNAVAILABLE</span> | 503 | <span class="ofx-enum">INTERNAL\_ERROR</span> | The idempotency service is temporarily unreachable, so the request's Idempotency-Key could not be checked. | `{}` | Wait as directed, then retry with the same idempotency key. |
    | <span class="ofx-enum">IDEMPOTENCY\_KEY\_MISSING</span> (Payments) | 428 | <span class="ofx-enum">VALIDATION\_ERROR</span> | A write request omits the Idempotency-Key header. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">IDEMPOTENCY\_MISMATCH</span> (Payments) | 422 | <span class="ofx-enum">CONFLICT</span> | An idempotency key is reused with a different request body. | `{ originalRequestHash }` | Correct the request or blocking state, then submit a new operation. |
  </Tab>

  <Tab title="Routing">
    | Code | Status | Type | Trigger | Details | Client action |
    | - | -: | - | - | - | - |
    | <span class="ofx-enum">ROUTE\_NOT\_FOUND</span> | 404 | <span class="ofx-enum">NOT\_FOUND</span> | The request path does not match a v3 route. | `{ path }` | Do not retry automatically; correct access or surface the error. |
  </Tab>

  <Tab title="Rate limiting">
    | Code | Status | Type | Trigger | Details | Client action |
    | - | -: | - | - | - | - |
    | <span class="ofx-enum">RATE\_LIMIT\_EXCEEDED</span> | 429 | <span class="ofx-enum">RATE\_LIMITED</span> | The organization exceeds its request limit. | `{}` | Wait as directed, then retry with the same idempotency key. |
  </Tab>

  <Tab title="Trading">
    | Code | Status | Type | Trigger | Details | Client action |
    | - | -: | - | - | - | - |
    | <span class="ofx-enum">TRADE\_NOT\_FOUND</span> | 404 | <span class="ofx-enum">NOT\_FOUND</span> | The trade identifier is unknown or inaccessible to the caller. | `{ tradeId }` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">TRADE\_NOT\_REVERSIBLE</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The trade is outside its reversal window or has already been reversed or settled. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">TRADE\_NOT\_SETTLEABLE</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The trade isn't in a settleable state (already settled, not yet executed, or reversed). | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">TRADE\_NOT\_MANUAL</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The trade is not a manual trade and cannot be settled through this endpoint. | `{}` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">TRADE\_ALREADY\_SETTLED</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The trade has already been settled. | `{}` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">TRADE\_EXECUTION\_FAILED</span> | 500 | <span class="ofx-enum">INTERNAL\_ERROR</span> | Trade execution fails after the write is accepted. | `{ resourceId, timeoutMs? }` | Read the resource state before deciding whether to retry. |
    | <span class="ofx-enum">TRADE\_DUPLICATE\_CLIENT\_REFERENCE</span> | 409 | <span class="ofx-enum">CONFLICT</span> | A trade already exists with the given clientReferenceId. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">TRADE\_DISABLED</span> | 403 | <span class="ofx-enum">AUTHORIZATION\_ERROR</span> | Trading is disabled for the organization. | `{}` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">TRADE\_AMOUNT\_ABOVE\_LIMIT</span> | 422 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The trade amount exceeds the organization's maximum. | `{ requestedAmount, currency, limitAmount }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">TRADE\_AMOUNT\_BELOW\_MINIMUM</span> | 422 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The trade amount is below the organization's minimum. | `{ requestedAmount, currency, minimumAmount }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">TRADE\_PAIR\_VOLUME\_LIMIT\_REACHED</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The currency pair reaches its global net-position limit. | `{ pair }` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">TRADE\_CREDIT\_LIMIT\_EXCEEDED</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The trade would exceed the organization's exposure limit. | `{ usedAmount, limitAmount, currency }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">TRADE\_INSUFFICIENT\_BALANCE</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The sell-side balance is insufficient at execution time. | `{ requiredAmount, availableAmount, currency, side }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">TRADE\_CURRENCY\_NOT\_SUPPORTED</span> | 400 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The requested currency is not supported. | `{ currency }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">TRADE\_CURRENCY\_NOT\_ENABLED\_FOR\_ACCOUNT</span> | 403 | <span class="ofx-enum">AUTHORIZATION\_ERROR</span> | The requested currency is not enabled for the organization. | `{ currency }` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">TRADE\_PAIR\_NOT\_SUPPORTED</span> | 400 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The requested currency pair is not supported. | `{ pair }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">TRADE\_PAIR\_NOT\_ENABLED\_FOR\_ACCOUNT</span> | 403 | <span class="ofx-enum">AUTHORIZATION\_ERROR</span> | The requested currency pair is not enabled for the organization. | `{ pair }` | Do not retry automatically; correct access or surface the error. |
  </Tab>

  <Tab title="Quotes">
    | Code | Status | Type | Trigger | Details | Client action |
    | - | -: | - | - | - | - |
    | <span class="ofx-enum">QUOTE\_INVALID\_REFERENCE\_UNIT</span> | 400 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The reference unit does not match either currency in the requested pair. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">QUOTE\_EXPIRED</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The quote expires before trade execution. | `{ quoteId, expiredAt }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">QUOTE\_ALREADY\_CONSUMED</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The quote was consumed by an earlier successful trade. | `{ quoteId, consumedAt }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">QUOTE\_NOT\_FOUND</span> | 404 | <span class="ofx-enum">NOT\_FOUND</span> | The quote identifier is unknown or inaccessible to the caller, or more than 2 days have passed since the quote's `createdAt`. | `{ quoteId }` | Do not retry automatically; correct access or surface the error. |
  </Tab>

  <Tab title="Deposits">
    | Code | Status | Type | Trigger | Details | Client action |
    | - | -: | - | - | - | - |
    | <span class="ofx-enum">DEPOSIT\_NOT\_FOUND</span> | 404 | <span class="ofx-enum">NOT\_FOUND</span> | The deposit identifier is unknown or inaccessible to the caller. | `{ depositId }` | Do not retry automatically; correct access or surface the error. |
  </Tab>

  <Tab title="Withdrawals">
    | Code | Status | Type | Trigger | Details | Client action |
    | - | -: | - | - | - | - |
    | <span class="ofx-enum">WITHDRAWAL\_NOT\_FOUND</span> | 404 | <span class="ofx-enum">NOT\_FOUND</span> | The withdrawal identifier is unknown or inaccessible to the caller. | `{}` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">WITHDRAWAL\_INITIATION\_FAILED</span> | 500 | <span class="ofx-enum">INTERNAL\_ERROR</span> | Withdrawal initiation fails after the write is accepted. | `{ resourceId, timeoutMs? }` | Read the resource state before deciding whether to retry. |
    | <span class="ofx-enum">WITHDRAWAL\_INVALID\_AMOUNT\_PRECISION</span> | 422 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The withdrawal amount exceeds the currency's decimal precision. | `{ maxDecimals, currency }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">WITHDRAWAL\_INSUFFICIENT\_BALANCE</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The available balance is lower than the withdrawal amount. | `{ requiredAmount, availableAmount, currency }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">WITHDRAWAL\_ADDRESS\_NOT\_VERIFIED</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The destination account is inactive or unverified. | `{ accountId }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">WITHDRAWAL\_ADDRESS\_CHECKSUM\_FAILED</span> | 422 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The destination address checksum is invalid for the selected network. | `{ address, network }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">WITHDRAWAL\_CURRENCY\_NOT\_SUPPORTED</span> | 400 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The requested currency is not supported for withdrawals. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">WITHDRAWAL\_DISABLED\_FOR\_ACCOUNT</span> | 403 | <span class="ofx-enum">AUTHORIZATION\_ERROR</span> | Withdrawals are currently disabled for the caller's account. | `{}` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">WITHDRAWAL\_PENDING\_DEPOSIT\_REQUIRED</span> | 409 | <span class="ofx-enum">CONFLICT</span> | An outstanding settlement deposit must be paid first. | `{ outstandingAmount, currency }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">WITHDRAWAL\_ADDRESS\_CURRENCY\_MISMATCH</span> | 422 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The request currency differs from the destination account currency. | `{ expected, received }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">WITHDRAWAL\_ADDRESS\_NOT\_FOUND</span> | 404 | <span class="ofx-enum">NOT\_FOUND</span> | The destination account identifier is unknown or inaccessible to the caller. | `{ accountId }` | Do not retry automatically; correct access or surface the error. |
  </Tab>

  <Tab title="Withdrawal accounts">
    | Code | Status | Type | Trigger | Details | Client action |
    | - | -: | - | - | - | - |
    | <span class="ofx-enum">WITHDRAWAL\_ACCOUNT\_NOT\_LIVE</span> | 403 | <span class="ofx-enum">AUTHORIZATION\_ERROR</span> | The destination withdrawal account is not active. | `{ accountId, status }` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">WITHDRAWAL\_ACCOUNT\_FIAT\_INVALID\_NETWORK</span> | 400 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The `network` filter was supplied together with `assetType=FIAT`, which has no network. | `{ network }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">WITHDRAWAL\_ACCOUNT\_CRYPTO\_INVALID\_RAIL</span> | 400 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The `rail` filter was supplied together with `assetType=CRYPTO`, which has no rail. | `{ rail }` | Correct the request or blocking state, then submit a new operation. |
  </Tab>

  <Tab title="Exposures">
    | Code | Status | Type | Trigger | Details | Client action |
    | - | -: | - | - | - | - |
    | <span class="ofx-enum">EXPOSURE\_NOT\_ENABLED</span> | 403 | <span class="ofx-enum">AUTHORIZATION\_ERROR</span> | No exposure limit is configured for the organization. | `{}` | Do not retry automatically; correct access or surface the error. |
  </Tab>

  <Tab title="Platform">
    | Code | Status | Type | Trigger | Details | Client action |
    | - | -: | - | - | - | - |
    | <span class="ofx-enum">FEATURE\_NOT\_ENABLED</span> | 403 | <span class="ofx-enum">AUTHORIZATION\_ERROR</span> | The caller's organization is not enabled for this feature. | `{}` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">INTERNAL\_ERROR</span> (FX) | 500 | <span class="ofx-enum">INTERNAL\_ERROR</span> | An unclassified server-side failure occurs. | `{ resourceId? }` | Read the resource state before deciding whether to retry. |
    | <span class="ofx-enum">INTERNAL\_ERROR</span> (Payments) | 500 | <span class="ofx-enum">INTERNAL\_ERROR</span> | An unclassified server-side failure occurs. | `{}` | Read the resource state before deciding whether to retry. |
  </Tab>

  <Tab title="Accounts">
    | Code | Status | Type | Trigger | Details | Client action |
    | - | -: | - | - | - | - |
    | <span class="ofx-enum">ACCOUNT\_ARCHIVED</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The account named by the request is archived, a one-way state. | `{ accountId }` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">ACCOUNT\_HOLDER\_MISMATCH</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The receiving-side account holder name check on the destination payout address returned a blocking result, or the account identifier is known bad. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">ACCOUNT\_NOT\_ACTIVE</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The account's standing with OpenFX is not ACTIVE. | `{ accountId, status }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">ACCOUNT\_NOT\_FOUND</span> | 404 | <span class="ofx-enum">NOT\_FOUND</span> | The account identifier is unknown or inaccessible to the caller. | `{}` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">AGREEMENTS\_NOT\_ACTIVE</span> | 409 | <span class="ofx-enum">CONFLICT</span> | An agreement the organization needs exists but is not active, expired, or revoked. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">AGREEMENTS\_PENDING</span> | 409 | <span class="ofx-enum">CONFLICT</span> | An agreement the organization must accept before this payout can settle is still awaiting acceptance. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">BENEFICIARY\_DETAILS\_MISSING</span> | 409 | <span class="ofx-enum">CONFLICT</span> | A detail the receiving institution requires for this payout is missing from the destination payout address. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">FINANCIAL\_ACCOUNT\_CURRENCY\_MISMATCH</span> | 422 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The currency of the named financial account is not the sendCurrency the request states. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">FINANCIAL\_ACCOUNT\_NOT\_FOUND</span> | 404 | <span class="ofx-enum">NOT\_FOUND</span> | The financial account identifier is unknown or inaccessible to the caller. | `{}` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">IDENTITY\_REQUIREMENT\_UNMET</span> | 422 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The request exercises a capability the account's supplied identity information does not support. | `{ issues, firstIssuePath? }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">KYC\_REQUIRED</span> | 409 | <span class="ofx-enum">CONFLICT</span> | A know-your-customer check the receiving side requires has not cleared. | `{}` | Correct the request or blocking state, then submit a new operation. |
  </Tab>

  <Tab title="Capabilities">
    | Code | Status | Type | Trigger | Details | Client action |
    | - | -: | - | - | - | - |
    | <span class="ofx-enum">ADDRESS\_SERVES\_NO\_CAPABILITY</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The account the payout address would belong to holds no capability able to receive at it, in any status. | `{ accountId, required }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">CAPABILITY\_DUPLICATE</span> | 422 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The account already holds a cell for a requested capability, or the request names the same capability twice. | `{ duplicates }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">CAPABILITY\_NOT\_ACTIVE</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The account named for this activity holds no ACTIVE capability covering what the request needs. | `{ accountId, required, capabilityId, status }` | Correct the request or blocking state, then submit a new operation. |
  </Tab>

  <Tab title="Payout addresses">
    | Code | Status | Type | Trigger | Details | Client action |
    | - | -: | - | - | - | - |
    | <span class="ofx-enum">BANK\_ACCOUNT\_NOT\_FOUND</span> | 404 | <span class="ofx-enum">NOT\_FOUND</span> | No usable account exists at the submitted identifiers. | `{ reason }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">PAYOUT\_ADDRESS\_ARCHIVED</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The destination payout address is archived, a one-way retirement. | `{}` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">PAYOUT\_ADDRESS\_CONFLICT</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The bank details in the request already back a payout address on the same account. | `{ payoutAddressId, status }` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">PAYOUT\_ADDRESS\_FIELD\_MISSING</span> | 400 | <span class="ofx-enum">VALIDATION\_ERROR</span> | A field required by the selected routingFamily is missing. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">PAYOUT\_ADDRESS\_NOT\_FOUND</span> | 404 | <span class="ofx-enum">NOT\_FOUND</span> | The payout address identifier is unknown or inaccessible to the caller. | `{}` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">PAYOUT\_ADDRESS\_REJECTED</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The destination payout address is REJECTED and cannot back a payout. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">PAYOUT\_ADDRESS\_VERIFICATION\_FAILED</span> | 409 | <span class="ofx-enum">CONFLICT</span> | A verification check on the destination payout address could not be completed. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">VERIFICATION\_EXECUTION\_FAILED</span> | 500 | <span class="ofx-enum">INTERNAL\_ERROR</span> | The verification could not be performed because of a technical failure on the platform's or the scheme's side. | `{}` | Wait as directed, then retry with the same idempotency key. |
  </Tab>

  <Tab title="Payout quotes">
    | Code | Status | Type | Trigger | Details | Client action |
    | - | -: | - | - | - | - |
    | <span class="ofx-enum">AMOUNT\_OUT\_OF\_RANGE</span> | 422 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The amount is outside the permitted range for payouts to this destination. | `{ issues, firstIssuePath?, amount?, minimum?, maximum?, currency? }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">CURRENCY\_NOT\_SUPPORTED</span> | 422 | <span class="ofx-enum">VALIDATION\_ERROR</span> | No rail can deliver the requested receiveCurrency to this destination country. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">QUOTE\_ALREADY\_CONSUMED</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The quote is already bound to another payout. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">QUOTE\_EXPIRED</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The applied quote has expired. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">QUOTE\_NOT\_FOUND</span> | 404 | <span class="ofx-enum">NOT\_FOUND</span> | The quote identifier is unknown or inaccessible to the caller. | `{}` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">QUOTE\_RAIL\_UNAVAILABLE</span> | 422 | <span class="ofx-enum">VALIDATION\_ERROR</span> | No settlement rail serves the requested receiveCurrency to this payout address. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">QUOTE\_REJECTED</span> | 422 | <span class="ofx-enum">VALIDATION\_ERROR</span> | Pricing declined the request. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">QUOTE\_SEND\_CURRENCY\_UNAVAILABLE</span> | 422 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The sendCurrency of the named financial account cannot fund the requested receiveCurrency. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">QUOTE\_TEMPORARILY\_UNAVAILABLE</span> | 409 | <span class="ofx-enum">CONFLICT</span> | Quoting is temporarily halted for reasons independent of the request. | `{}` | Wait as directed, then retry with the same idempotency key. |
    | <span class="ofx-enum">SENDER\_ACCOUNT\_REQUIRED</span> | 422 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The payout needs a sender account named on sender and none was supplied. | `{ issues, firstIssuePath? }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">SENDER\_NOT\_SUPPORTED</span> | 422 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The payout names a sender, and a payout in this receiveCurrency cannot carry one. | `{ issues, firstIssuePath? }` | Correct the request or blocking state, then submit a new operation. |
  </Tab>

  <Tab title="Payouts">
    | Code | Status | Type | Trigger | Details | Client action |
    | - | -: | - | - | - | - |
    | <span class="ofx-enum">PAYOUT\_CANCELLATION\_NOT\_ALLOWED</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The payout isn't in REQUIRES\_CONFIRMATION. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">PAYOUT\_CONFIRMATION\_NOT\_ALLOWED</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The payout isn't in REQUIRES\_CONFIRMATION. | `{}` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">PAYOUT\_CURRENCY\_NOT\_ENABLED</span> | 403 | <span class="ofx-enum">AUTHORIZATION\_ERROR</span> | The organization isn't enabled for payouts in the requested receiveCurrency. | `{ receiveCurrency }` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">PAYOUT\_INSUFFICIENT\_FUNDS</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The financial account named by financialAccountId cannot cover this payout's debit. | `{ financialAccountId }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">PAYOUT\_NOT\_ENABLED</span> | 409 | <span class="ofx-enum">CONFLICT</span> | The organization has no active provider configuration for this destination and receiveCurrency. | `{}` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">PAYOUT\_NOT\_FOUND</span> | 404 | <span class="ofx-enum">NOT\_FOUND</span> | The payout identifier is unknown or inaccessible to the caller. | `{}` | Do not retry automatically; correct access or surface the error. |
    | <span class="ofx-enum">PAYOUT\_TEMPORARILY\_UNAVAILABLE</span> | 409 | <span class="ofx-enum">CONFLICT</span> | Payout processing is temporarily unavailable because of a provider outage or a platform pause. | `{}` | Wait as directed, then retry with the same idempotency key. |
    | <span class="ofx-enum">PURPOSE\_INVALID</span> | 400 | <span class="ofx-enum">VALIDATION\_ERROR</span> | The payout's purpose is missing where its receiveCurrency requires one, refused for that receiveCurrency, or given where the receiveCurrency takes none. | `{ issues?, firstIssuePath? }` | Correct the request or blocking state, then submit a new operation. |
    | <span class="ofx-enum">RECEIVER\_MEMO\_INVALID</span> | 400 | <span class="ofx-enum">VALIDATION\_ERROR</span> | receiverMemo violates the character set or length the bound rail's own grammar allows. | `{}` | Correct the request or blocking state, then submit a new operation. |
  </Tab>
</Tabs>

## Handling errors: the catch-and-branch pattern

Handle known codes that need bespoke behavior first (quote refresh, structured balance details), then fall back to `error.retryStrategy` for everything else. It sorts any current or future code into the right bucket without a client-side code list to maintain. Always capture `X-Trace-Id` before parsing the body, so you have it even when the body is malformed.

```javascript theme={"theme":{"light":"poimandres","dark":"poimandres"}}
// Sign on every attempt. Do not capture one. JWTs have a 60s TTL and the
// X-Request-Signature is bound to the JWT's nonce, so a captured pair goes
// stale across any wait that approaches the TTL (next attempt fails with
// `401 AUTH_TOKEN_EXPIRED` or `401 AUTH_TOKEN_INVALID`). Request bodies are
// also consumed by the first fetch(), so retrying the same Request object
// throws `TypeError: Body has already been read`.
//
// The caller passes `method`, `body`, an `extraHeaders` object for things like
// `Idempotency-Key` that must stay stable across attempts, and `signRequest`.
// Each recursive retry signs fresh and rebuilds the `init`. See Rate limiting →
// Handling rate limits pattern (canonical reference):
// https://docs.openfx.com/v3/rate-limiting#handling-rate-limits-pattern
async function call(
  url,
  method,
  body,
  extraHeaders,
  signRequest,
  attempt = 0,
  maxAttempts = 5,
) {
  const signed = signRequest({ method, url, body });
  const res = await fetch(url, {
    method,
    headers: { ...signed.headers, ...extraHeaders },
    body: signed.body,
  });
  const traceId = res.headers.get("x-trace-id"); // Capture FIRST: always present
  console.log({ url, trace_id: traceId, status: res.status });

  if (res.ok) return res.json();

  const { error } = await res.json();

  switch (error.code) {
    // Bespoke: quote stale, get a fresh one instead of a blind retry
    case "QUOTE_EXPIRED": {
      return refreshQuoteAndRetry(url, method, body, extraHeaders, signRequest);
    }

    // Bespoke: surface the structured balance shortfall to the user
    case "WITHDRAWAL_INSUFFICIENT_BALANCE": {
      const { requiredAmount, availableAmount, currency } = error.details;
      throw new Error(
        `${error.code}: required ${requiredAmount} ${currency}, available ${availableAmount} ${currency} (trace ${traceId})`,
      );
    }
  }

  // No code-specific override matched above. Fall back to the
  // server-declared retryStrategy. This is where every future code lands
  // automatically, with no client-side code list to maintain.
  switch (error.retryStrategy) {
    // Retry immediately with the same Idempotency-Key.
    case "RETRY_SAME_KEY": {
      if (attempt + 1 >= maxAttempts) {
        throw new Error(
          `Retry budget exhausted: ${error.code} (trace ${traceId})`,
        );
      }
      return call(
        url,
        method,
        body,
        extraHeaders,
        signRequest,
        attempt + 1,
      ); // next attempt: fresh JWT + signature
    }

    // Transient backpressure. See the Triage matrix above for which codes
    // carry Retry-After. Falls back to a fixed short wait when it's absent.
    case "WAIT_THEN_RETRY_SAME_KEY": {
      const retryAfterHeader = res.headers.get("retry-after");
      const retryAfter =
        retryAfterHeader === null ? NaN : Number(retryAfterHeader);
      const waitMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 5000; // 5s fallback when Retry-After is absent
      if (attempt + 1 >= maxAttempts) {
        throw new Error(
          `Retry budget exhausted: ${error.code} (trace ${traceId})`,
        );
      }
      await sleep(waitMs);
      return call(
        url,
        method,
        body,
        extraHeaders,
        signRequest,
        attempt + 1,
      ); // next attempt: fresh JWT + signature
    }

    // FIX_AND_RESUBMIT, CHECK_STATE_THEN_RETRY, TERMINAL, and any future
    // unrecognized value: bubble up. The trace ID is always present for support.
    case "FIX_AND_RESUBMIT":
    case "CHECK_STATE_THEN_RETRY":
    case "TERMINAL":
    default:
      throw new Error(
        `API error: ${error.code} (${error.retryStrategy}) — ${error.message} (trace ${traceId})`,
      );
  }
}

// Usage:
//   const idempotencyKey = randomUUID();  // stable per logical operation
//   const body = JSON.stringify(payload);
//   await call(
//     'https://api.openfx.com/v3/fx/trades',
//     'POST',
//     body,
//     { 'Idempotency-Key': idempotencyKey },     // stable across attempts; merged after signed headers
//     signRequest,
//   );
```

<Tip>
  **Don't retry blindly on 5xx.** When `error.retryStrategy` is
  <span class="ofx-enum">CHECK\_STATE\_THEN\_RETRY</span> (e.g. <span class="ofx-enum">TRADE\_EXECUTION\_FAILED</span>,
  <span class="ofx-enum">WITHDRAWAL\_INITIATION\_FAILED</span>, <span class="ofx-enum">INTERNAL\_ERROR</span>) on a write endpoint, GET the
  resource by `details.resourceId` before deciding to retry. See [the rule
  below](#retrying-trade-execution-failed). When in doubt, fetch the resource by
  ID before retrying. See [Idempotency → Crash
  recovery](/v3/idempotency#crash-recovery).
</Tip>

<div id="retrying-trade-execution-failed" />

### Retrying trade execution failed

On <span class="ofx-enum">TRADE\_EXECUTION\_FAILED</span> (500), the original request **may** have EXECUTED before the failure surfaced. There is exactly one safe procedure:

1. **Call `GET /v3/fx/trades/{id}` with the resource ID returned by the original POST.** If the trade exists, it EXECUTED; if you get `404` <span class="ofx-enum">TRADE\_NOT\_FOUND</span>, it didn't.
2. **If the trade exists:** treat the operation as successful. Do not retry.
3. **If the trade doesn't exist:** retry the original `POST /v3/fx/trades` with the **same** `Idempotency-Key`. The 24-hour TTL means the server still knows the key, so a duplicate cannot land. If the original quote has since expired, request a fresh quote first and use a new idempotency key for the new trade attempt.

The same rule applies to <span class="ofx-enum">WITHDRAWAL\_INITIATION\_FAILED</span> (call `GET /v3/fx/withdrawals/{id}` first) and to any other 5xx on a write endpoint.

## Common mistakes

* **Switching on `message` instead of `code`.** `message` is human-readable and may be rephrased between releases. Only `code` is stable. The trade-up: lock your switch statement to codes the day you ship.
* **Forgetting to read `X-Trace-Id` until something breaks.** Capture it on every response (success too) and store it in the same log line as the request. Asking support to find a request without a trace ID adds hours to triage.
* **Assuming HTTP status maps cleanly to `error.type`.** Most do, but <span class="ofx-enum">IDEMPOTENCY\_MISMATCH</span> returns **422** with `type: "CONFLICT"`. Always branch on `error.code` for behavior; use `type` only for categorization.
* **Rebuilding a code-pattern list to decide retry behavior.** `error.retryStrategy` already carries this. Read it directly instead of maintaining your own mapping of codes to retry buckets. It also covers codes added after you shipped.
* **Treating <span class="ofx-enum">WITHDRAWAL\_INSUFFICIENT\_BALANCE</span> as retryable.** It's a 409 (well-formed request, state-driven failure). Retrying without resolving the underlying balance hits the same error every time.
* **Retrying a 500 trade failure with a fresh `Idempotency-Key`.** Risks executing the trade twice. See [Retrying trade execution failed](#retrying-trade-execution-failed) above for the correct procedure.

## What's next

<CardGroup cols={2}>
  <Card title="Idempotency" icon="https://public-assets.openfx.com/docs/icons/idempotency.svg" href="/v3/idempotency#how-it-works">
    Understand same-key, different-body collisions and the in-flight rule.
  </Card>

  <Card title="Rate limiting" icon="https://public-assets.openfx.com/docs/icons/rate-limiting.svg" href="/v3/rate-limiting#handling-rate-limits-pattern">
    The back-off pattern for <span class="ofx-enum">RATE\_LIMIT\_EXCEEDED</span>.
  </Card>

  <Card title="Metadata & tracing" icon="https://public-assets.openfx.com/docs/icons/metadata-tracing.svg" href="/v3/metadata-and-tracing#filing-a-support-ticket">
    Capture `X-Trace-Id` on every response for fast triage.
  </Card>
</CardGroup>
