# Errors, limits and versioning

> Error format, rate limits, headers and the versioning policy.

Source: https://docs.botradix.com/api-errors/

## Error format

Every error from `/api/dex/v1` has the same JSON shape and an HTTP status that matches it:

```json
{
  "success": false,
  "error": {
    "schema_version": "botradix-dex-error-v1",
    "code": "SCOPE_NOT_ALLOWED",
    "message": "…",
    "retryable": false,
    "details": {},
    "request_id": "dxr_…"
  },
  "request_id": "dxr_…",
  "api_version": "v1",
  "api_schema_revision": "botradix-dex-api-v1.1"
}
```

Use `error.code` in your logic (messages can change) and keep `request_id` when you contact support. `retryable: true` means the same request may succeed later.

## Common errors

| HTTP | Code | Meaning |
|---|---|---|
| 400 | (validation codes) | A field is missing, malformed or out of range. `prepare` validates the body before the key |
| 400 | `IDEMPOTENCY_KEY_REQUIRED` | `/orders/prepare` needs a valid idempotency key |
| 401 | `API_KEY_REQUIRED` | No API key was sent |
| 401 | `API_KEY_INVALID` | The key is malformed or wrong |
| 401 | `API_KEY_INACTIVE` | The key is revoked or expired |
| 403 | `SCOPE_NOT_ALLOWED` | The key lacks the scope this endpoint or action needs |
| 403 | `IP_NOT_ALLOWED` | The request IP is not in the key's allowlist |
| 403 | `delegated_executor_binding_required` | Delegated execution needs an executor bound to the key |
| 409 | `IDEMPOTENCY_KEY_CONFLICT` | The idempotency key was used with a different payload in the last 24 hours |
| 409 | `DUPLICATE_CLIENT_ORDER_ID` | The `client_order_id` was used with a different payload |
| 409 | `active_api_key_limit_reached` | The wallet already has 10 active keys |
| 429 | `RATE_LIMITED` | A per-key limit was exceeded |
| 503 | `REALTIME_STREAM_ASYNC_WORKER_REQUIRED` | The SSE stream is not enabled on this service |

## Version 2.2 codes

| Code | Meaning | What to do |
|---|---|---|
| `COMMITTED_EXECUTOR_REQUIRED` | On a market with V2 risk rules, opening, increasing or inverting through the delegated route is executed committed to a price observed after your order, and only a protocol executor can do it. Your executor is not one | Use the BotRadix executor bound to your key |
| `INVERT_NEEDS_AGENT` | On a market with V2 risk rules, the inversion cannot be sent as a direct wallet transaction | Use your trading key (or the delegated route with a protocol executor) |
| `INTENT_CREATED_AT_BEFORE_RECEPTION` | The order's commitment time is earlier than its reception by the service (2 s margin); nothing is sent | Prepare it again |
| `WALLET_MARGIN_NEEDS_AGENT` | A market of your cross account has its session closed: the isolated margin comes from your wallet, signed by your wallet or co-signed by your wallet together with your trading key. "Margin +" needs the trading key | Enable the trading key and retry |
| `WALLET_MARGIN_NEEDS_WALLET_SIGNATURE` | Margin from your wallet needs your wallet's signature for this operation; a trading session cannot bring it | Repeat the operation and sign it with your wallet (or use your trading key, with your wallet's co-signature) |
| `WALLET_COSIGN_REQUIRED` | Your wallet's co-signature for margin from your wallet is missing | Confirm again and sign with your wallet |
| `WALLET_COSIGN_SIGNATURE_INVALID` | The co-signature does not match this wallet or this operation; nothing moved | Prepare it again; if it repeats, write to [support@botradix.com](mailto:support@botradix.com) |
| `WALLET_COSIGN_INVALID` | The prepared co-signature does not match the operation; nothing moved | Prepare it again |
| `WALLET_MARGIN_BALANCE_REQUIRED` | With that session closed, your wallet lacks enough USDC for the margin (and, for an open, the largest fees and, for a limit or stop order, the bond). `details.wallet_margin_usdc` gives the amount | Add USDC to your wallet or wait for the session to reopen |
| `LIQUIDITY_CAPACITY` (also on `/orders/execute`) | The vault refused the risk: its share for this market and direction is full, or an order that joins a position with a backed take profit would push it over (the preview does not always anticipate this). Since v2.2 the delegated route answers it with HTTP 400 instead of a `503` with `VAULT_CAPACITY`; `details.max_open_usdc` gives the room when it can be estimated | Try the opposite side, a smaller size or later |

When inverting next to an opposite-side net position with the same key, the response may return that net position's id: there is no `DexPositionOpened`, but a `DexPositionChanged` of the net position.

## Why a prepared action is not executable

When `prepare` succeeds but `execution.ready` is `false`, `execution.reason` explains it:

| Reason | Meaning |
|---|---|
| `withdraw_margin_insufficient` | The withdrawal would leave too little free margin. Withdraw less or close positions |
| `oracle_unavailable` | A current market price is not available on chain right now; try again shortly |
| `funding_unavailable` / `funding_stale` | The market's funding data must be updated before new positions can open |
| `market_session_closed` | The market is outside its trading session |
| `price_out_of_bounds` | The price moved beyond your allowed slippage; prepare again |
| `price_tick_rejected` | The price does not match the market tick size |
| `contract_limits_rejected` | The order exceeds a contract limit (size, leverage) or has expired |
| `vault_capacity_unavailable` | Not enough liquidity capacity for this position right now |
| `market_risk_paused` | New positions are paused on this market |
| `market_not_bound` / `dex_not_operational` | The market is not enabled for trading yet |
| `invalid_take_profit` / `invalid_stop_loss` | TP or SL is on the wrong side of the current price |
| `intent_expired` | The preparation expired; prepare again |
| `simulation_unavailable` | The pre-trade simulation could not run; prepare again in a moment |
| `contract_preflight_failed` | The contract would reject the action under current conditions |

## Rate limits

Each API key has its own limits, counted in 60-second windows:

| Limit | Default |
|---|---|
| All requests | 3,600 per minute |
| `/orders/prepare` | 900 per minute and 40 per second |
| Account reads (`/account`, `/positions…`, `/orders…`, `/collateral`, `/transactions`, `/trades`, `/orders/execute`) | 3,000 per minute |
| `/candles` | 900 per minute |

Per-key `quotas` can be set when a key is created or rotated, up to these caps: requests 30,000, orders 12,000, burst 1,000, account reads 20,000 and historical reads 12,000 per minute.

On top of the per-key limits, the network edge allows up to 100 requests per second per IP address (bursts of 200) and 60 per second per API key (bursts of 120). Edge rejections are plain `429` responses without the JSON body.

Responses include `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (Unix seconds when the window resets) for the general per-key bucket. `429` responses carry no `Retry-After`: back off exponentially with jitter (for example 1, 2, 4, 8 seconds).

## Retries

- Retry on `429`, on `5xx` and when `error.retryable` is `true`, with exponential backoff.
- Reuse the same idempotency key, with the same body, when you retry a `prepare` or an `execute`, so a lost response does not create a second preparation or a second order.
- Never blindly retry a broadcast transaction: check `/transactions` or `/native/executions/{tx_hash}` first.

## Response headers

| Header | Value |
|---|---|
| `X-BotRadix-API-Version` | `v1` |
| `X-BotRadix-API-Schema` | `botradix-dex-api-v1.1` |
| `X-Request-ID` | Your request ID, or one generated by the server (`dxr_…`) |
| `Cache-Control` | `no-store` |

## Versioning policy

- `/api/dex/v1` is the stable integration contract. New fields and endpoints can be added to v1 without notice; your client should ignore fields it does not know.
- Changes that would break existing integrations are published under a new version.
- `GET /meta` returns the current schema revision and capabilities.
- Unversioned endpoints used by the web terminal (`/api/dex/…` without `v1`) can change at any time.
