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

# Errors and refusals

> Read the one error body every refusal uses, know which refusals to retry and when, and find every code with what to do about it.

When TetraFi refuses a request, the answer is always the same JSON object, whatever the status and whichever call you made. One parser reads every refusal on this page.

This includes `400`, `401`, `403`, `404`, `413` and `415` responses. A body-size refusal is JSON too, never a separate text format. Use the [error-code reference](https://tetrafi.mintlify.site/reference/error-codes.md) for the complete declared list of codes and source reasons; the tables below group common refusals by what you are doing.

The examples come from one script, [`samples/http/errors.sh`](https://github.com/tetrafi/docs/blob/main/samples/http/errors.sh). It provokes three refusals and places no order.

> [!NOTE]
>
> The API may change until launch. Every change is listed in the [Changelog](https://tetrafi.mintlify.site/changelog.md).

## Read a refusal

```json theme={null}
{
  "error": "QUOTE_EXPIRED",
  "message": "This quote has expired, so nothing was sent. Request a new price.",
  "requestId": "5ee4d511-5ef8-4e47-9999-5b144ec4fcbc",
  "timestamp": 1792443399,
  "docUrl": "https://tetrafi.mintlify.app/reference/error-codes#QUOTE_EXPIRED",
  "next": { "kind": "requote", "needs": [] }
}
```

| Field | What it is | What to do with it |
| - | - | - |
| `error` | The stable code. | Match on it. It never changes for the same refusal. |
| `message` | A sentence for people. | Show or log it. Never match on it: the words can change. |
| `requestId` | The same value as the `X-Request-Id` response header. | Log it, and quote it when you contact support. |
| `timestamp` | When the refusal was made, in Unix seconds. | Log it. |
| `docUrl` | This code's entry in the [error-code reference](https://tetrafi.mintlify.site/reference/error-codes.md). | Link it in your logs or show it to a developer. |
| `next` | The way forward, when there is one. | See [what next tells you](#what-next-tells-you). |
| `details` | The code's detail, when there is any. | Read the keys you know. Keys are camelCase and more can be added. |

`details.value` names the value that was refused, such as an unknown asset id. `details.reason` carries a token that says why, for example on `REQUOTE_REFUSED`. `details.retryAfter` says how long to wait.

Here the script asks for a quote on an asset TetraFi does not list, prints the refusal and acts on its code:

```bash cURL theme={null}
# Every refusal is one JSON body: {error, message, requestId, timestamp, docUrl, next?, details?}.
# Match on `error`, never on `message`. Keep `requestId` for support; docUrl links the code's entry.
if refusal=$(tetrafi POST /api/v1/quotes \
  '{"sell":{"asset":"NOT_AN_ASSET"},"buy":{"asset":"USDT"},"sellAmount":"10"}' 2>/dev/null); then
  echo '{"error":"EXPECTED_A_REFUSAL"}' >&2
  exit 1
fi
jq -c '{error, requestId, docUrl, next, details}' <<<"$refusal" >&2
case $(jq -r '.error' <<<"$refusal") in
  UNKNOWN_ASSET | UNKNOWN_CHAIN | UNKNOWN_VENUE)
    echo "use values from GET /api/v1/assets: $(jq -c '.details' <<<"$refusal")" >&2 ;;
  *)
    echo "refused: $(jq -r '.message' <<<"$refusal") (requestId $(jq -r '.requestId' <<<"$refusal"))" >&2 ;;
esac
```

```typescript TypeScript SDK theme={null}
/** Run a call that should be refused, and read the refusal as a value. */
async function refusal(call: () => Promise<unknown>) {
  try {
    await call();
    return { refused: false as const };
  } catch (error) {
    if (!isTetrafiApiError(error)) throw error; // No HTTP answer at all is a different problem: rethrow it.
    return { refused: true as const, status: error.status, code: error.code, message: error.message,
      next: error.next, requestId: error.requestId };
  }
}
```

```json Output theme={null}
{"error":"UNKNOWN_ASSET","requestId":"5ee4d511-…","next":null,"details":{"value":"NOT_AN_ASSET"}}
use values from GET /api/v1/assets: {"value":"NOT_AN_ASSET"}
```

## What next tells you

A refusal that you can recover from in a known way carries `next`. Its `kind` is the same set of steps a quote's own `next` uses:

| `next.kind` | What to do |
| - | - |
| `requote` | Send `POST /api/v1/quotes` with `{"requote": "<quote id>"}`. `next.needs` lists what the new quote needs from you, such as a trading wallet to name; an empty list means requote as it is. See [Requotes and expiry](https://tetrafi.mintlify.site/trade/requotes-and-expiry.md). |
| `fund` | The paying account holds too little. Fund it, then get a new price. |
| `preflight`, `sign`, `transact` | Your own wallet would act on-chain (Advanced), which is not open yet. Order a Basic quote instead. |
| `order` | The quote can be ordered as it is. |

More kinds can be added. Treat a `kind` you do not recognise as a step you cannot take automatically: show `message` and stop.

## Retries

Whether to retry depends on the code, and for a code you do not know, on the status class.

| Answer | Retry? |
| - | - |
| `429` | Yes, after the wait it names. |
| `503` | Yes, after `Retry-After` when it is set, otherwise after a second or two, with backoff. |
| `500`, `502`, `504` | Yes, with backoff. For an order, only with the same `Idempotency-Key` (below). |
| `409` | Only where the table says so, such as `ORDER_IN_PROGRESS`. Otherwise the state has to change first. |
| Any other `4xx` | No. Fix the request, or act on `next`, then send it again. |

### Waits

A refusal that names a wait carries it twice:

* the `Retry-After` header, in whole seconds;
* `details.retryAfter`, an ISO 8601 duration such as `PT3S`. It can carry a fraction, such as `PT1.5S`.

Wait at least that long. Sending sooner is refused again.

### The quote rate limit

You can send one live quote request every 3 seconds per API key. A requote counts as a quote request. A request answered entirely from cached prices does not count. A faster request answers `429 QUOTE_RATE_LIMITED`.

The script sends two quote requests at once, so one of them is refused. It waits `details.retryAfter`, then sends the refused one again:

```bash cURL theme={null}
# One live quote request every 3 seconds. Send two at once: one is answered, the other
# answers 429 QUOTE_RATE_LIMITED with details.retryAfter (ISO 8601, such as PT3S) and the
# same wait in whole seconds in the Retry-After header. Wait that long, then retry once.
# Each signature is accepted once, so the two requests differ in amount.
quote_body() {
  jq -c -n --arg sell "$sell" --arg buy "$buy" --arg amount "$1" \
    '{sell: {asset: $sell}, buy: {asset: $buy}, sellAmount: $amount}'
}
tetrafi POST /api/v1/quotes "$(quote_body 10)" >"$tmp/first" 2>/dev/null & first=$!
tetrafi POST /api/v1/quotes "$(quote_body 11)" >"$tmp/second" 2>/dev/null & second=$!
wait "$first" && first_ok=true || first_ok=false
wait "$second" && second_ok=true || second_ok=false

if [[ $first_ok == true ]]; then limited=$(<"$tmp/second") amount=11; else limited=$(<"$tmp/first") amount=10; fi
jq -c '{error, requestId, retryAfter: .details.retryAfter}' <<<"$limited" >&2
if [[ $(jq -r '.error' <<<"$limited") == QUOTE_RATE_LIMITED ]]; then
  # PT3S is 3 seconds; a fraction such as PT1.5S rounds up.
  wait_seconds=$(jq -r '.details.retryAfter | capture("^PT(?<s>[0-9.]+)S$").s | tonumber | ceil' <<<"$limited")
  sleep "$wait_seconds"
  retried=$(tetrafi POST /api/v1/quotes "$(quote_body "$amount")")
  echo "retried after ${wait_seconds}s: $(jq '.quotes | length' <<<"$retried") quotes" >&2
fi
```

```json Output theme={null}
{"error":"QUOTE_RATE_LIMITED","requestId":"522b8ba5-…","retryAfter":"PT3S"}
retried after 3s: 2 quotes
```

The two requests carry different amounts on purpose. A signed write is accepted once, so two identical quote requests signed in the same second would read as a replay.

### Orders

`POST /api/v1/orders` is safe to retry only with the same `Idempotency-Key` and the same body: the retry returns the first answer and never places a second order. When you do not know whether an order was placed (a timeout, a dropped connection, a `5xx`), never send it again with a new key. Retry with the same key, or [read the order by its quote](https://tetrafi.mintlify.site/trade/idempotency.md#recover-an-order-by-its-quote) first. [Idempotency](https://tetrafi.mintlify.site/trade/idempotency.md) has the full rules.

## Codes you do not know

New codes can be added at any time. Act on the codes you know, and treat any other by its status class, as the [compatibility policy](https://tetrafi.mintlify.site/compatibility.md#what-your-client-should-do) says: retry a `429` or `503` after `Retry-After`, fix the request on any other `4xx`, and retry a `5xx` with backoff. Show `message` and log `requestId` either way.

## The wrong workspace

An API key is bound to its workspace, so it may leave `X-Workspace-Id` out. When it sends one, it must be the key's own: any other workspace answers `403 WORKSPACE_SCOPE_MISMATCH`, and a value that is not a workspace id answers `400 INVALID_WORKSPACE_ID`. A signed-in member's session must always send the header, or it is refused with `400 MISSING_WORKSPACE`.

A few paths name a workspace, such as `GET /api/v1/workspaces/{workspaceId}/webhooks`. A path workspace that is not yours answers `404 WORKSPACE_NOT_FOUND`, exactly as one that does not exist.

```bash cURL theme={null}
# An API key implies its workspace, so it may leave X-Workspace-Id out. When it sends one,
# it must be the key's own: any other workspace answers 403 WORKSPACE_SCOPE_MISMATCH.
other_workspace=$(openssl rand -hex 16 | sed -E 's/(.{8})(.{4})(.{4})(.{4})(.{12})/\1-\2-\3-\4-\5/')
if refusal=$(TETRAFI_WORKSPACE_ID=$other_workspace tetrafi GET /api/v1/assets 2>/dev/null); then
  echo '{"error":"EXPECTED_A_REFUSAL"}' >&2
  exit 1
fi
jq -c '{error, message, requestId}' <<<"$refusal" >&2
```

## A workspace that cannot trade yet

A company, broker or liquidity provider workspace can request quotes, place orders and connect to counterparties once its company details are on file: legal name, country of incorporation, registration number and registered address. Until then, those requests answer `403 CAPABILITY_BLOCKED`. Each entry in `details.capabilityBlockers` names one thing to fix:

```json Refusal theme={null}
{
  "error": "CAPABILITY_BLOCKED",
  "message": "Add your company details (legal name, country of incorporation, registration number and registered address) in the TetraFi app under Company profile to start trading",
  "details": {
    "capabilityBlockers": [
      {
        "code": "BASE_PROFILE_INCOMPLETE",
        "unlockStep": "company_details",
        "currentlyDue": ["legalName", "jurisdiction", "registrationNumber", "registeredAddress"],
        "message": "Add your company details (…) in the TetraFi app under Company profile to start trading"
      }
    ]
  }
}
```

| What is missing | Blocker `code` | What to do |
| - | - | - |
| Company details. `currentlyDue` lists the missing ones. | `BASE_PROFILE_INCOMPLETE` | Add them in the TetraFi app under **Company profile**, then send the same request again. |
| In a direct user's own workspace, the user's identity verification. | `KYC_NOT_VERIFIED` | Complete identity verification in the TetraFi app. |

Both are done in the app, not through the API. When TetraFi cannot read the company details just now, the request answers `503 CAPABILITY_UNAVAILABLE` with the blocker `BASE_PROFILE_UNREADABLE`: send it again shortly. Treat a blocker code you do not recognise by its `message` and `unlockStep`.

To check before you quote, read `GET /api/v1/workspaces/{workspaceId}/capabilities`: each capability, such as `request_quotes` or `submit_order`, says whether it is `allowed`, with the same `blockers` when it is not.

## Refusals by area

Each table lists the codes you can get from that part of the API with an API key. Unless a row says otherwise, nothing was placed or sent.

### Authentication and workspace

These can answer any request. [Authentication](https://tetrafi.mintlify.site/authentication.md) explains keys, permissions and signing.

| Status and code | When it happens | What to do |
| - | - | - |
| 400 `MISSING_WORKSPACE` | A session, or a key bound to no workspace, sent no `X-Workspace-Id`. | Send the workspace id, or use a key created in the workspace. |
| 400 `INVALID_WORKSPACE_ID` | `X-Workspace-Id` is not a workspace id, or is sent more than once. | Send it once, as the workspace id. |
| 401 `AUTH_REQUIRED` | The request did not authenticate. `details.reason` says why (below). | Act on `details.reason`. |
| 403 `WORKSPACE_SCOPE_MISMATCH` | `X-Workspace-Id` names a different workspace from the key's. | Send the key's own workspace. |
| 404 `WORKSPACE_NOT_FOUND` | A path names a workspace that does not exist or is not yours. Both read the same. | Use your own workspace id in the path. |
| 403 `WORKSPACE_NOT_ACTIVE` | The workspace is suspended or deactivated. | Contact support. |
| 403 `CAPABILITY_BLOCKED` | The workspace cannot do this yet, such as trading before its company details are on file. `details.capabilityBlockers` names each cause. | Fix each blocker in the app ([A workspace that cannot trade yet](#a-workspace-that-cannot-trade-yet)). |
| 503 `CAPABILITY_UNAVAILABLE` | TetraFi could not read whether the workspace can do this. | Send it again shortly. |
| 403 `IP_NOT_ALLOWED` | The key has an IP allowlist and the request came from elsewhere. | Send from an allowed address, or change the key's allowlist. |
| 403 `INSUFFICIENT_SCOPE` | The key lacks the permission this call needs. | Create a key with that permission ([Permissions](https://tetrafi.mintlify.site/authentication.md#permissions)). |
| 403 `INSUFFICIENT_PERMISSION` | You, the key's owner, no longer hold the permission in the workspace. A key never does more than you can. | Ask a workspace admin for the permission. |
| 403 `VENUE_TRADE_SCOPE_REQUIRED` | The call trades on a venue account and the key lacks the venue trading scope. | Create a key with it ([Trading on your venue accounts](https://tetrafi.mintlify.site/authentication.md#trading-on-your-venue-accounts)). |
| 403 `VENUE_TRADE_KEY_DISABLED` | A call requiring venue trading finds it switched off for the key. A valid new quote request instead answers `200` with no venue-account quote cards. | Check `GET /api/v1/me` → `organisationWallets.venueTradeEnabled` before searching; ask a workspace owner or admin to switch it on. |
| 403 `VENUE_TRADE_SIGNATURE_REQUIRED` | The call trades on a venue account and the request is not signed. | Sign the request. |
| 403 `VENUE_TRADE_KEY_CHANGED` | The key's venue trading settings changed while the request ran. | Send it again. |
| 413 `REQUEST_BODY_TOO_LARGE` | The body is larger than TetraFi accepts. | Send a smaller body. |
| 415 `UNSUPPORTED_CONTENT_TYPE` | A body was sent without `Content-Type: application/json`. | Send that header. |
| 429 `RATE_LIMITED` | The key's own request budget, or the limit for your address, is used up. | Wait `Retry-After` seconds. |
| 503 `API_KEY_UNAVAILABLE`, `AUTHORIZATION_UNAVAILABLE`, `KEY_AUTHORITY_UNAVAILABLE`, `VENUE_TRADE_KEY_UNAVAILABLE` | TetraFi could not check the key or the workspace just now. | Send it again after a second or two. |
| 408 `REQUEST_TIMEOUT` | The request took too long to complete. | Retry with backoff. For an order, use the same `Idempotency-Key`. |
| 500 `INTERNAL_ERROR` | Something went wrong on TetraFi's side. | Retry with backoff, and quote `requestId` to support if it persists. |
| 503 `SERVICE_UNAVAILABLE` | The service is briefly unavailable. | Retry with backoff. |

Every authentication failure is the one code `401 AUTH_REQUIRED`. Its `details.reason` names the cause:

| What went wrong | `details.reason` | What to do |
| - | - | - |
| No API key was sent. | `MISSING_CREDENTIALS` | Send `X-API-Key`. |
| The key is unknown, or it was revoked. | `INVALID_API_KEY` | Use a current key. |
| The key is past its expiration. | `API_KEY_EXPIRED` | Create a new key. |
| The key needs signing and `X-Timestamp` or `X-Signature` is missing. | `SIGNATURE_REQUIRED` | [Sign the request](https://tetrafi.mintlify.site/authentication.md#sign-requests). |
| The signature is malformed or does not match. | `SIGNATURE_INVALID` | Sign the exact method, path with query, and body bytes you send, with the hex-decoded secret. |
| `X-Timestamp` is more than 300 seconds from TetraFi's clock. | `TIMESTAMP_OUT_OF_RANGE` | Sync your clock and sign again. |
| This exact signed write (`POST`, `PUT`, `PATCH` or `DELETE`) was already accepted. | `SIGNATURE_REPLAYED` | Sign each write afresh. Two identical writes in the same second need different bodies. A repeated signed read is accepted. |

More reasons may be added: treat one you do not recognise as "fix the credentials".

### Quote request

`POST /api/v1/quotes` for a new search, and `GET /api/v1/quotes/requests/{quoteRequestId}` to read a quote set again. [How search works](https://tetrafi.mintlify.site/search/how-it-works.md) covers the request.

| Status and code | When it happens | What to do |
| - | - | - |
| 400 `INVALID_REQUEST` | A field is unknown, missing, malformed or out of range. `message` names it. | Fix that field. |
| 400 `UNKNOWN_ASSET` | An asset id is not in the catalogue. `details.value` is the id. | Use an `assetId` from `GET /api/v1/assets`. |
| 400 `UNKNOWN_CHAIN` | A chain id is not one TetraFi knows. `details.value` is the chain. | Use a chain from `GET /api/v1/assets`. |
| 400 `UNKNOWN_VENUE` | A venue id in `filters.venues` is not one TetraFi knows. `details.value` is the id. | Use an id from `sources[].venue.id`. |
| 400 `INVALID_AMOUNT` | `sellAmount` or `buyAmount` is not a positive decimal string. | Send it as a string, such as `"10"`. |
| 400 `ASSET_NOT_ON_CHAIN` | The pinned chain has no deployment of that side's asset. | Pin a chain the asset is deployed on, or leave the side open. |
| 400 `SAME_ASSET_NEEDS_CHAIN` | Both sides are the same asset and no chain tells them apart. | Pin a chain on at least one side ([Chains and routes](https://tetrafi.mintlify.site/search/chains-and-routes.md)). |
| 400 `CHAIN_PIN_NEEDS_ON_CHAIN` | A side is pinned to a chain, but `filters.locations` allows only `offChain`. | Remove the pin, or allow `singleChain` or `crossChain`. |
| 400 `WALLET_NOT_ON_CHAIN` | A wallet in `source.tradingWallets` cannot pay on the chain you keyed it under. | Choose a candidate listed for that chain. |
| 403 `SOURCE_NOT_OWNED` | A wallet or account in `source` or `deliverTo` does not exist, or you cannot use it. | Choose a candidate from the answer's `sourceOptions` ([Wallets and accounts](https://tetrafi.mintlify.site/trade/wallets-and-accounts.md#when-tetrafi-refuses-a-source)). |
| 403 `API_KEY_ORGANISATION_WALLET_NOT_ALLOWLISTED` | An API key's `deliverTo` names an address outside the workspace's organisation wallets. The quote request is refused, before an order is placed. | Use an organisation-wallet destination the key may use. Manage delivery destinations in the app, not through a per-key address allowlist ([Where the output goes](https://tetrafi.mintlify.site/trade/wallets-and-accounts.md#where-the-output-goes)). |
| 409 `PERSONAL_TARGET_NOT_ALLOWED` | An API key named a personal wallet. | Use an organisation-scope wallet. |
| 404 `QUOTE_REQUEST_NOT_FOUND` | No quote set with this id is readable by you. | Check the id. |
| 429 `QUOTE_RATE_LIMITED` | More than one live quote request in 3 seconds. | Wait `details.retryAfter`, then send it again. |
| 503 `QUOTE_REQUEST_NOT_READY` | The quote set was returned moments ago and is still being recorded. | Read it again after about a second. |
| 503 `QUOTE_CATALOGUE_UNAVAILABLE`, `QUOTE_ID_UNAVAILABLE`, `QUOTE_REQUEST_UNAVAILABLE` | Something the search needs could not be read just now. | Send it again after a second or two. |

A venue that cannot quote is not a refusal: the request succeeds and lists that venue in `sources` with its outcome ([below](#source-outcomes)).

### Requote

`POST /api/v1/quotes` with `requote`. [Requotes and expiry](https://tetrafi.mintlify.site/trade/requotes-and-expiry.md) covers the request.

| Status and code | When it happens | What to do |
| - | - | - |
| 400 `REQUOTE_FIELDS_CONFLICT` | The body has a field that cannot go with `requote`, such as `sellAmount` or `filters`. | Send only `requote`, `source`, `deliverTo`, `corridor` and `priceProtectionBps`, or search again. |
| 400 `CORRIDOR_NOT_OFFERED` | The original quote was not offered on that corridor. | Use a corridor from the quote's `corridors`. |
| 400 `WALLET_NOT_ON_CHAIN` | The trading wallet you named cannot pay on that chain. | Name a trading wallet on that chain in `source.tradingWallets`. |
| 403 `SOURCE_NOT_OWNED` | A wallet or account in `source` or `deliverTo` is not one you can use. | Name a wallet or account from `sourceOptions`. |
| 403 `ACCOUNT_NOT_AVAILABLE` | That venue account is not the one this quote was priced on. | Leave out `source.venueAccountId`, or search again. |
| 403 `VENUE_NOT_AVAILABLE_TO_CREDENTIAL` | This key cannot ask this venue for a firm quote. | Use a key with **Read quotes** and **Submit orders**, or a member session. |
| 404 `QUOTE_NOT_FOUND` | No quote with this id is available to you. | Check the id. Requote only quotes your own key requested. |
| 409 `PERSONAL_TARGET_NOT_ALLOWED` | An API key named a personal wallet. | Bind an organisation-scope wallet. |
| 409 `REQUOTE_REFUSED` | The venue declined. `details.reason` carries a token, such as `venueNotListed`. | Show `message`. The original quote holds until its expiry. Treat a reason you do not recognise as "other". |
| 409 `REQUOTE_NEEDS_WALLET` | The venue quotes only once you choose a wallet. | Requote with `source.tradingWallets`. |
| 410 `REQUOTE_WINDOW_CLOSED` | The quote is past its `retainUntil`, 10 minutes after it expired. `next` is `search`. | Search again. |
| 429 `QUOTE_RATE_LIMITED` | More than one live quote request in 3 seconds. | Wait `details.retryAfter`, then send it again. |
| 429 `VENUE_FIRM_QUOTE_LIMITED` | The workspace reached the venue's firm-quote limit. | Wait `Retry-After`. The quote's `firmAskAvailableAt` says when, where known. |
| 429 `BUDGET_EXHAUSTED` | The venue's quote budget is used up for now. | Wait `Retry-After` when it is set, otherwise try later. |
| 503 `QUOTE_STORE_UNAVAILABLE` | Held quotes cannot be read just now. | Try again shortly. |
| 504 `REQUOTE_TIMED_OUT` | The venue did not answer in time. | Try again, or order the original while it holds. |

### Orders

`POST /api/v1/orders` and the order reads. [Orders](https://tetrafi.mintlify.site/trade/orders.md) covers the request.

| Status and code | When it happens | What to do |
| - | - | - |
| 400 `INVALID_CURSOR` | The order list's `cursor` is not one this key got for these filters. | Start again without `cursor`, or send the `nextCursor` you were given. |
| 409 `ORDER_NOT_CANCELLABLE` | The order was already sent, its funding or delivery moved, or it ended. `details.status` is its status. | Read the order; it can no longer be cancelled. |
| 400 `INVALID_REQUEST` | A field is unknown or malformed, or `priceProtectionBps` was sent on a Basic quote. `message` names it. | Fix that field. |
| 400 `SIGNATURE_NOT_ACCEPTED` | A signature was sent on a quote that takes none. | Leave out `signature`. |
| 400 `MIN_RECEIVE_REQUIRED` | This venue executes within your limit and the quote is not firm. | Send `minReceive` ([Orders](https://tetrafi.mintlify.site/trade/orders.md#venue-quotes-that-execute-within-your-limit)). |
| 400 `MIN_RECEIVE_INVALID` | `minReceive` is not a positive amount. | Send it as a positive decimal string. |
| 403 `ADVANCED_MODE_DISABLED` | The quote executes on-chain from your own wallet (Advanced), which is not open yet. | Order a Basic quote instead. |
| 404 `QUOTE_NOT_FOUND` | No quote with this id is available to you. | Check the id. |
| 404 `ORDER_NOT_FOUND` | No order with this id is readable by you. | Check the id, or [read it by its quote](https://tetrafi.mintlify.site/trade/idempotency.md#recover-an-order-by-its-quote). |
| 409 `QUOTE_NOT_EXECUTABLE` | The quote is a price for comparison, not executable. `next` is `requote`. | Requote it to get an executable price. |
| 409 `QUOTE_NOT_HELD` | The quote is no longer held. `next` is `requote`. | Search again. |
| 409 `VENUE_QUOTE_OUTSIDE_BOUNDS` | The venue's held firm price is below your `minReceive`. | Lower `minReceive` or get a new price. |
| 409 `VENUE_QUOTE_NOT_PRICED` | The venue did not give a firm price for the order. | Get a new price. |
| 409 `FUNDING_SHORTFALL` | The venue account holds less than the trade needs. `next` is `fund`. | Fund the account, then get a new price. |
| 409 `PILOT_TRADE_CAP_EXCEEDED` | The trade is above the venue's current per-trade limit. `next` is `requote`. | Get a price for a smaller amount. |
| 409 `EXECUTION_STAGE_DISABLED` | The venue is not open for trading. | Choose another venue's quote. |
| 409 `PRICE_PROTECTION_CHANGED` | `priceProtectionBps` is looser than the quote's. | Requote with the new protection. |
| 409 `QUOTE_PRICE_MOVED` | The price moved beyond your price protection. | Request the price again. |
| 409 `PAY_WALLET_UNAVAILABLE` | The wallet the quote pays from is no longer available to you. | Get new prices. |
| 422 `PAY_WALLET_SHORT` | The paying wallet holds less than the quote pays. | Top up that wallet, or requote with another, then order again. |
| 410 `QUOTE_EXPIRED` | The quote's `expiresAt` has passed. `next` is `requote`. | [Requote it](https://tetrafi.mintlify.site/trade/requotes-and-expiry.md#refresh-an-expired-quote). |
| 429 `VENUE_FIRM_QUOTE_LIMITED` | The workspace reached the desk's firm-quote limit. | Wait `Retry-After`, then get a new price. |
| 503 `QUOTE_STORE_UNAVAILABLE` | Held quotes cannot be read just now. | Send the same request with the same key after a second. |
| 503 `VENUE_POLICY_UNAVAILABLE` | How the venue executes could not be read just now. | Send the same request with the same key shortly. |

### Idempotency

The `Idempotency-Key` on `POST /api/v1/orders`, and `GET /api/v1/orders/by-quote/{quoteId}`. [Idempotency](https://tetrafi.mintlify.site/trade/idempotency.md) has the full rules.

| Status and code | When it happens | What to do |
| - | - | - |
| 400 `IDEMPOTENCY_KEY_REQUIRED` | The order has no `Idempotency-Key`. | Send one, generated per order and stored before you send. |
| 400 `IDEMPOTENCY_KEY_INVALID` | The header is not exactly one value of 1 to 255 bytes without surrounding spaces. | Send one value, such as 32 hexadecimal characters. |
| 403 `IDEMPOTENCY_AUTHORITY_REQUIRED` | A replay, but the key no longer holds the permission the first order needed. | Restore the permission, then replay with the same key. |
| 409 `IDEMPOTENCY_CONFLICT` | The key was already used for a different body. Nothing was placed. | Fix your code. Do not send this body with a new key until you have read the order by its quote. |
| 409 `ORDER_IN_PROGRESS` | An order with this key is still being placed. | Wait a few seconds, then send the same body with the same key. |
| 409 `QUOTE_ALREADY_ORDERED` | Another order, sent with a different key, already used this quote. It is not a failure. | Read it by its quote. Do not order again. |
| 503 `IDEMPOTENCY_UNAVAILABLE` | The key could not be checked, so the request did not run. | Send the same body with the same key. |
| 503 `QUOTE_ORDER_UNAVAILABLE` | Reading the order by its quote failed. | Do not place another order until a read succeeds. |

### Prices and assets

`GET /api/v1/prices`, `GET /api/v1/prices/stream` and `GET /api/v1/assets`. [Prices](https://tetrafi.mintlify.site/search/prices.md) covers the request.

| Status and code | When it happens | What to do |
| - | - | - |
| 400 `INVALID_REQUEST` | A parameter is unknown or malformed. `message` names it. | Fix that parameter. |
| 400 `UNKNOWN_ASSET`, `UNKNOWN_CHAIN`, `UNKNOWN_VENUE` | A value is not in the catalogue. | Use values from `GET /api/v1/assets`. |
| 400 `INVALID_AMOUNT` | `amount` or a size is not a positive decimal. | Send positive decimals. |
| 400 `SAME_ASSET_NEEDS_CHAIN` | `pay` and `receive` are the same asset with no chain pinned. | Add `pay.chain` or `receive.chain`. |
| 403 `INSUFFICIENT_SCOPE` | The key lacks the `prices_read` scope, which **Read quotes** grants. | Use a key with **Read quotes**. |
| 429 `PRICES_RATE_LIMIT` | Too many price reads. | Wait `Retry-After` seconds. |
| 429 `PRICES_STREAM_LIMIT` | You already hold 4 price streams. | Close one first. |
| 503 `PRICES_ELIGIBILITY_WARMING` | Your workspace's prices are still loading. | Retry after `Retry-After`, about 1 second. |
| 503 `PRICES_ELIGIBILITY_UNAVAILABLE`, `PRICES_POLICIES_UNAVAILABLE`, `PRICES_IDENTITY_UNAVAILABLE` | Something prices need could not be read just now. | Retry after a second or two. |

## Source outcomes

A quote request that succeeds lists every venue it considered in `sources`, each with a `status` and, when it did not quote, a `reason`. These are outcomes, not refusals: the request worked, and the venue's line says why it gave no quote.

| `status` | What happened |
| - | - |
| `answered` | The venue quoted. Its quotes are in `quotes`. |
| `filteredOut` | Your filters left it out, or the venue cannot serve this request. It was not asked. `detail` names the token. |
| `unsupported` | It does not serve this request, such as this pair or a buy-exact amount. |
| `unavailable` | It could not quote now. `reason` and `detail` say why, such as `advancedModeDisabled` for an Advanced venue while Advanced execution is not open. |
| `restricted` | A permission keeps it from you. `reason` names it. |
| `needsWallet` | It quotes only once you name a trading wallet. |
| `walletRefused` | The wallet it would use cannot trade there. |
| `timedOut` | It did not answer before the collection deadline. |
| `refused` | The venue declined. |

Some reasons you may see:

| `reason` | What to do |
| - | - |
| `pairNotServed`, `buyAmountUnsupported` | The venue does not trade this pair, or cannot quote a fixed buy amount. Choose another pair or send `sellAmount` instead of `buyAmount`. Quotes and prices use the same tokens. A venue you trade through your own account can show `buyAmountUnsupported` on a `filteredOut` row, in `detail`. |
| `venueTradeScopeRequired` | Use a key with venue trading ([Trading on your venue accounts](https://tetrafi.mintlify.site/authentication.md#trading-on-your-venue-accounts)). |
| `notConnected` | Connect your account at that venue ([Wallets and accounts](https://tetrafi.mintlify.site/trade/wallets-and-accounts.md#connect-a-venue-account)). |
| `apiKeyOrganisationWalletScopeRequired` | The key has no organisation wallet granted for this venue. Ask a workspace owner to grant the key an organisation wallet ([Give a key organisation wallets](https://tetrafi.mintlify.site/trade/wallets-and-accounts.md#give-a-key-organisation-wallets)). |
| `onchainAssetVariantFiltered` | Your `nativeOnly` filter left out the only variant it trades ([Native only](https://tetrafi.mintlify.site/search/native-only.md)). |
| `onchainAccountDefaultWalletRequired`, `onchainWalletNotUsable` | No usable wallet was found for that venue. Name one from `sourceOptions` in `source.tradingWallets`, or ask an owner to set a default organisation wallet. With an API key, the saved wallet must also be granted to the key ([Give a key organisation wallets](https://tetrafi.mintlify.site/trade/wallets-and-accounts.md#give-a-key-organisation-wallets)). |

Both lists are open. New statuses and reasons appear without notice. Reasons are camelCase tokens. Treat a status or reason you do not know as a source you cannot use: show its `detail` if you show anything, and carry on with the quotes you have.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.