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

# Error reference

> Complete catalog of v2 error responses — the canonical envelope, status codes that fire on every endpoint, idempotency-specific codes, and per-endpoint extras.

v2 returns errors with HTTP status codes in the 4xx–5xx range and a JSON body that describes what went wrong. This page is the single source of truth for every error shape v2 emits, the conditions that produce it, and how to recover.

Use this page to:

* Look up the exact message string for a given HTTP status
* Wire up retry logic for idempotency-aware POST endpoints
* See which endpoints carry extra `400` cases beyond the common set
* Plan ahead for the v3 status code changes

## Response shape

Most v2 errors use a bare two-field envelope:

```json theme={null}
{
  "status": "error",
  "message": "Human-readable description"
}
```

Idempotency errors extend that envelope with a nested `error` object carrying a machine-readable `code` and a `details` string:

```json theme={null}
{
  "status": "error",
  "message": "Idempotency key is required for this endpoint",
  "error": {
    "code": "IDEMPOTENCY_KEY_REQUIRED",
    "details": "Please provide an Idempotency-Key header with a unique value (UUID recommended)"
  }
}
```

<Note>
  **v2 has a hybrid error shape.** Most errors use the bare `{ status, message }` form, while idempotency errors add the `error.code` nested object. Branch on `response.status` first, then check `data.error?.code` when you need to disambiguate idempotency cases. v3 normalises this so every error carries a stable `code` — see [Notable v2 → v3 changes](#v3-status-code-changes) below.
</Note>

`message` is human-readable and is **not** guaranteed stable across releases — a copy edit can change it. For idempotency errors, switch on `error.code`. For everything else in v2, switch on HTTP status code.

## Common errors (every endpoint)

These five status codes can fire on any v2 endpoint. The message strings below are the exact strings the OpenAPI spec documents.

| Status | Message                                                                         | Trigger                                                                                                                                                                                                           |
| ------ | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `Query validation failed`                                                       | Malformed query parameters or request body — field type wrong, required field missing, value out of range                                                                                                         |
| 401    | `The authorization key provided is either invalid or expired, please try again` | JWT missing, malformed, or past its `exp`. Mint a fresh JWT — see [Authentication](/v2/authentication)                                                                                                            |
| 409    | `Request with this idempotency key is currently processing`                     | A previous request with the same `Idempotency-Key` is still in flight. Wait `Retry-After` seconds and retry with the **same** key                                                                                 |
| 422    | `Idempotency key reused with different parameters`                              | Same `Idempotency-Key`, different request body. Bug in your retry code — generate a new key for the new operation                                                                                                 |
| 500    | `An internal error has occurred`                                                | Server-side failure. Safe to retry **read** endpoints with backoff. For **write** endpoints, see [Idempotency errors](#idempotency-errors) below — call GET on the resource first to see whether the write landed |

<Note>
  The 409/422 rows above only apply to endpoints that participate in idempotency. In v2 that includes both POST endpoints (where `Idempotency-Key` is required) and GETs (where the header is honoured if you send it). All v2 endpoints can return the 409 or 422 idempotency conflict — the OpenAPI spec documents both responses on every operation.
</Note>

## Idempotency errors

These codes come back inside the nested `error.code` field. They are the only v2 errors with a stable machine-readable code today.

| Code (in `error.code`)            | HTTP | Trigger                                                                         | Retry policy                                                    |
| --------------------------------- | ---- | ------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| `IDEMPOTENCY_KEY_REQUIRED`        | 400  | `Idempotency-Key` header missing on a POST/PUT/PATCH/DELETE                     | Don't retry. Add the header and resubmit                        |
| `INVALID_IDEMPOTENCY_KEY`         | 400  | Header value is malformed (wrong format, length, or character set)              | Don't retry. Regenerate the key (UUID v4 recommended)           |
| `IDEMPOTENCY_IN_FLIGHT`           | 409  | Same key still processing a previous request                                    | Backoff `Retry-After` seconds, then retry with the **same** key |
| `IDEMPOTENCY_MISMATCH`            | 422  | Same key reused with a different request body                                   | Don't retry. Generate a new key for the new operation           |
| `IDEMPOTENCY_SERVICE_UNAVAILABLE` | 503  | Idempotency store (Valkey) is down. v2 fails closed to prevent duplicate writes | Exponential backoff, retry with the **same** key                |

See [Migration v1 → v2 — Step 5: Handle new error codes](/v2/migration-v1-to-v2#step-5-handle-new-error-codes) for a worked client-side handler that branches on these codes.

<Tip>
  **Same-key-on-retry is the rule.** When you retry a write after a transient failure (network drop, 409, 503), keep the original `Idempotency-Key`. A new key on the same logical operation defeats the protection and can land the same trade or withdrawal twice. The keys are valid for 24h (`/trade`) to 7 days (`/withdrawal`, `/fiat_withdrawal`) — plenty of headroom for retry loops.
</Tip>

## Endpoint-specific errors

Most endpoints emit only the [common five](#common-errors-every-endpoint). The list below covers the endpoints whose OpenAPI catalog adds messages beyond that set.

### POST /v2/brokerage/{orgId}/withdrawal (stablecoin)

Three additional 400 cases on top of the common set. All three are state- or input-driven and should be surfaced to the user rather than retried.

| Status | Message                                               | Trigger                                                                                                                                                               |
| ------ | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `Checksum verification failed for withdrawal address` | Destination wallet address fails its on-chain checksum. The address is syntactically a string but not valid on the network                                            |
| 400    | `Insufficient balance for this withdraw.`             | Organization balance is below the requested withdrawal amount (note: the trailing period and `withdraw` typo are preserved from the spec — match this string exactly) |
| 400    | `Withdrawal address is not active or verified`        | Destination wallet exists but is not yet approved for outbound transfers                                                                                              |

See [Initiate stablecoin withdrawal](/v2/api-reference/withdrawals/initiate-stablecoin-withdrawal).

### POST /v2/brokerage/{orgId}/fiat\_withdrawal

Three additional 400 cases — analogous to the stablecoin case but applied to bank accounts.

| Status | Message                                               | Trigger                                                                        |
| ------ | ----------------------------------------------------- | ------------------------------------------------------------------------------ |
| 400    | `Checksum verification failed for withdrawal address` | Destination account fails verification (e.g. malformed routing/SWIFT data)     |
| 400    | `Insufficient balance for this withdraw.`             | Organization fiat balance is below the requested withdrawal amount             |
| 400    | `Withdrawal account is not active or verified`        | Destination bank account exists but is not yet approved for outbound transfers |

See [Initiate fiat withdrawal](/v2/api-reference/withdrawals/initiate-fiat-withdrawal).

### POST /v2/brokerage/{orgId}/generate\_quote

The 500 case carries a quote-specific message instead of the generic one.

| Status | Message                                                | Trigger                                                                                                                      |
| ------ | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- |
| 500    | `Error while creating a quote, please try again later` | Pricing engine or upstream rate provider unavailable. Retry with a **new** idempotency key — quote keys cache for 30 minutes |

See [Generate quote](/v2/api-reference/trade/generate-quote).

### POST /v2/brokerage/{orgId}/trade/reverse

Adds a 404 and a business-logic 409 on top of the common five.

| Status | Message                        | Trigger                                                              |
| ------ | ------------------------------ | -------------------------------------------------------------------- |
| 404    | `Trade not found`              | `tradeId` or `quoteId` doesn't resolve to a record on this org       |
| 409    | `The trade cannot be reversed` | The trade is outside its reversal window or already reversed/settled |

See [Reverse a trade](/v2/api-reference/trade/reverse-trade).

### POST /v2/brokerage/{orgId}/trade/reverse-quote

Adds a 404 and a quote-specific 500 on top of the common five.

| Status | Message                                        | Trigger                                                                                              |
| ------ | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| 404    | `Trade not found`                              | `tradeId` doesn't resolve to a record on this org                                                    |
| 500    | `Something went wrong, please try again later` | Quote-engine failure. Retry with a **new** idempotency key — reverse-quote keys cache for 30 minutes |

See [Generate a reverse quote](/v2/api-reference/trade/generate-reverse-quote).

### POST /v2/brokerage/{orgId}/trade/settle

Adds a 404 and a business-logic 409 on top of the common five.

| Status | Message                       | Trigger                                                                         |
| ------ | ----------------------------- | ------------------------------------------------------------------------------- |
| 404    | `Trade not found`             | `tradeId` doesn't resolve to a record on this org                               |
| 409    | `The trade cannot be settled` | The trade isn't in a settleable state (already settled, not yet executed, etc.) |

See [Settle a trade](/v2/api-reference/trade/settle-trade).

## v3 status code changes

v3 reshapes errors into a single uniform envelope with a stable `error.code` on every response, and lines several status codes up with the bucket they semantically belong to. Highlights:

| Change                    | v2                                                               | v3                                                           |
| ------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------ |
| Missing idempotency key   | `400 IDEMPOTENCY_KEY_REQUIRED`                                   | `428 IDEMPOTENCY_KEY_MISSING`                                |
| Resource not found        | `400` (lookup misses surface as `Query validation failed`)       | `404 *_NOT_FOUND`                                            |
| Insufficient balance      | `400`                                                            | `409 WITHDRAWAL_INSUFFICIENT_BALANCE`                        |
| Idempotency service down  | `503 IDEMPOTENCY_SERVICE_UNAVAILABLE`                            | `503 IDEMPOTENCY_UNAVAILABLE` (renamed)                      |
| Address checksum mismatch | `400 Checksum verification failed for withdrawal address`        | `422 WITHDRAWAL_ADDRESS_CHECKSUM_FAILED`                     |
| Address not verified      | `400 Withdrawal address is not active or verified`               | `409 WITHDRAWAL_ADDRESS_NOT_VERIFIED`                        |
| Error envelope            | `{ status, message }` (with nested `error` only for idempotency) | `{ error: { code, type, message, details } }` on every error |

See [Migration from v2 → HTTP status changes](/v3/migration-from-v2) for the full delta and a worked migration plan.

## What's next

* **Building retry logic?** Read [Migration v1 → v2 — Step 3: Handle retry logic](/v2/migration-v1-to-v2#step-3-handle-retry-logic).
* **Want stable error codes today?** v3's [error catalog](/v3/errors) ships a uniform `{ code, type, message, details }` envelope on every response.
* **Hit an error not listed here?** It's almost certainly a transient `500 An internal error has occurred`. Capture the response body and timestamp, then contact support.
