# Errors

Every error, on every path, has the same JSON body and the header `qg-zid`:

```json
{
  "name": "INPUT_INVALID",
  "message": "sellAmount must be a positive integer string",
  "data": {
    "zid": "0x7d1e0c55a9b2f4e83c61d0aa",
    "details": [ { "field": "sellAmount", "reason": "not an integer" } ]
  }
}
```

- `name` is the stable code; `message` is text for people and can change.
- `data.zid` identifies the request. `data.details[]` appears only with `INPUT_INVALID` and `ORIGIN_NOT_ALLOWED`. Other context is in `data` (`reason`, `token`, `chainId`, `minSellAmount`, `status`, `tradeHash`).
- The API answers JSON only, 404s included.

## 400

| Name | Where | Extra `data` | What to do |
|---|---|---|---|
| `INPUT_INVALID` | any | `details[]`, `reason` | Fix the field named in `details` |
| `HTTPS_REQUIRED` | any, plain HTTP | - | Use `https://`. The key sent over plain HTTP is revoked: create a new one |
| `API_VERSION_MISSING` | keyed | - | Send `qg-version: v1` |
| `CHAIN_NOT_SUPPORTED` | chain-bound | `chainId` | Use a chain from `/swap/chains` or `/gasless/chains` |
| `TOKEN_NOT_SUPPORTED` | price, quote, submit | `token`, `reason` (`transfer_tax`) | Use a token from `/tokens` |
| `TOKEN_PAIR_NOT_SUPPORTED` | price, quote, submit | `reason` (`no_anchor`, `wrap_unwrap`, `native_not_supported`) | Put an anchor on one side; no wrap or unwrap; no native coin on gasless |
| `RECIPIENT_NOT_SUPPORTED` | price, quote | - | Leave `recipient` out or set it to `taker` |
| `SELL_AMOUNT_TOO_SMALL` | price, quote, submit | `minSellAmount` | Sell at least `minSellAmount` |
| `INSUFFICIENT_BALANCE` | gasless quote with taker (the gasless price answers 200 with `issues.balance` instead) | `token`, `actual`, `expected` | The taker needs more of the sell token |
| `SWAP_VALIDATION_FAILED` | swap quote simulation | `reason` (`slippage`, `adapter_not_allowed`), `revert` | Quote again; raise `slippageBps` for volatile pairs |
| `UNABLE_TO_CALCULATE_GAS_FEE` | gasless price and quote | - | Retry shortly |
| `SMART_WALLET_NOT_SUPPORTED` | gasless with taker | - | Use the swap endpoints for contract wallets |
| `INTEGRATOR_FEE_NOT_SUPPORTED` | price, quote | - | Remove the `swapFee*` parameters |
| `EXACT_OUT_NOT_SUPPORTED` | price, quote | - | Send `sellAmount` instead of `buyAmount` |
| `IDEMPOTENCY_KEY_REUSED` | submit | - | Use a new `Idempotency-Key` for a different body |
| `INSUFFICIENT_BALANCE_OR_ALLOWANCE` | submit | `taker`, `sellToken`, `sellAmount`, `minBalanceOrAllowance` | Top up the balance or approve Permit2 |
| `INVALID_APPROVAL` | submit | - | Send `approval` as `null` or leave it out |
| `INVALID_SIGNATURE` | submit | - | Sign `trade.eip712` again with `eth_signTypedData_v4` |
| `INVALID_SIGNER` | submit | - | The signer must be the taker (`witness.recipient`) |
| `META_TRANSACTION_EXPIRY_TOO_SOON` | submit | - | Quote and sign again |
| `META_TRANSACTION_INVALID` | submit | `reason` (`price_moved`, `simulation_failed`, `gas_too_high`, ...), `revert` | Send the typed data unchanged; on `price_moved`, quote and sign again |
| `PENDING_TRADES_ALREADY_EXIST` | submit | - | Wait for the taker's pending trade on this chain |
| `TRADE_ALREADY_SUBMITTED` | submit | `reason` (`permit_nonce_used`) | Another app sent this trade, or the Permit2 nonce is used: quote again |

## 401, 403, 404, 405

| HTTP | Name | Where | Extra `data` | What to do |
|---|---|---|---|---|
| 401 | `API_KEY_MISSING` | keyed | - | Send `qg-api-key` (message: "No API key found in request") |
| 401 | `API_KEY_INVALID` | keyed | - | The key is unknown, malformed or revoked: use an active key |
| 403 | `APP_NOT_APPROVED` | keyed | `status` (`pending`, `suspended`) | Wait for approval, or see the app in the dashboard |
| 403 | `PRODUCT_NOT_ENABLED` | keyed | `reason` (`product_not_enabled`, `chain_not_enabled`) | Enable the product or chain in the app settings |
| 403 | `ORIGIN_NOT_ALLOWED` | keyed | `details` | Call from a server; a proxy must strip `Origin` and `Sec-Fetch-*` |
| 403 | `TAKER_NOT_AUTHORIZED_FOR_TRADE` | taker-bound | `reason` (`denied`, `gasless_blocked`) | The taker cannot trade through this endpoint |
| 404 | `NOT_FOUND` | any | - | Check the path |
| 404 | `META_TRANSACTION_STATUS_NOT_FOUND` | status | - | Use the `tradeHash` and `chainId` of a trade your app submitted |
| 405 | `METHOD_NOT_ALLOWED` | any | - | Use the method in the `Allow` header |

## 413, 414, 429

| HTTP | Name | Where | Extra `data` | What to do |
|---|---|---|---|---|
| 413 | `PAYLOAD_TOO_LARGE` | submit | - | The body limit is 256 KB |
| 414 | `URI_TOO_LONG` | any | - | The URL limit is 8,192 characters |
| 429 | `RATE_LIMITED` | keyed, and the gasless per-taker hourly limit | `reason` | Wait `Retry-After` seconds (10 for the app limit) |
| 429 | `QUOTA_EXCEEDED` | daily request cap, gasless submits per day | `reason` | Wait until 00:00 UTC (`Retry-After`) |
| 429 | `GAS_BUDGET_EXCEEDED` | gasless | `reason` (`app_daily`, `chain_daily`, `free_tier_daily`) | The sponsored-gas budget is used up for the day |

## 500, 503

| HTTP | Name | Where | Extra `data` | What to do |
|---|---|---|---|---|
| 500 | `INTERNAL_SERVER_ERROR` | any | - | Retry; keep the `zid` |
| 500 | `UNCATEGORIZED` | any | - | Retry; keep the `zid` |
| 503 | `SETTLEMENT_PAUSED` | chain-bound | `chainId` | The settlement contract of the chain is paused; `GET /status` shows when it is back |
| 503 | `SWAP_NOT_ENABLED` | swap | `chainId`, `reason` (`switched_off`) | Swap is switched off on this chain for now |
| 503 | `GASLESS_NOT_ENABLED` | gasless submit | `reason` (`gasless_not_enabled`), `chainId` | Gasless is not switched on for this chain yet; use the swap endpoints |
| 503 | `RELAYER_UNAVAILABLE` | gasless submit | `reason` (`relayer_low_balance`, `relayer_busy`, `relayer_stalled`, `gas_price_above_cap`, `relayer_timeout`, `relayer_unreachable`); `tradeHash` with `relayer_timeout` and `relayer_unreachable` | Retry later; `Retry-After: 3` when busy or timed out. With `tradeHash`, first read `GET /gasless/status/{tradeHash}`: the trade may have been accepted (`404` means it was not) |
| 503 | `RPC_UNAVAILABLE` | price, quote, submit, status | - | The chain's RPC operators did not agree or answer; retry |

## Retrying

- `429 RATE_LIMITED`: retry after `Retry-After`.
- `500` and `503`: retry with a growing delay.
- `400`, `401`, `403`, `404`: change the request first; the same request gets the same answer.
- A gasless submit can be repeated: the same trade from the same app returns the earlier answer ([Idempotency](idempotency.md)). Do not quote and sign a new trade while an earlier one may still be accepted: after a timeout, read its status first.
