<!-- Generated from TradeReady.io docs. Visit https://tradeready.io/docs for the full experience. -->

---
title: Error Codes & Handling
description: All error codes returned by the TradeReady API, grouped by category, with resolution steps and Python SDK exception mapping.
---

Every error from the TradeReady API uses a consistent envelope. The HTTP status code and the `code` field inside the body together tell you exactly what went wrong and what to do.

---

## Error Response Format

All errors return this structure regardless of the endpoint:

```json
{
  "error": {
    "code": "ERROR_CODE",
    "message": "Human-readable explanation of what went wrong."
  }
}
```

Some errors include an optional `details` field with structured context:

```json
{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Too many requests.",
    "details": {
      "limit": 100,
      "window_seconds": 60,
      "retry_after_seconds": 47
    }
  }
}
```

> **Info:**
> Always check the `code` field, not just the HTTP status. Multiple error codes can share the same HTTP status (e.g. `400`). Your retry and error handling logic should branch on `code`.

---

## Authentication Errors

| Code | HTTP | Meaning | Resolution |
|------|------|---------|------------|
| `INVALID_API_KEY` | 401 | API key is missing, malformed, or not found in the system | Verify the `X-API-Key` header value. Check for leading/trailing whitespace. |
| `INVALID_TOKEN` | 401 | JWT is expired, malformed, or has an invalid signature | Call `POST /auth/login` to get a fresh token. JWTs expire after 1 hour. |
| `ACCOUNT_SUSPENDED` | 403 | The account is suspended or archived | Contact platform support. |
| `PERMISSION_DENIED` | 403 | Authenticated but not authorized to access this resource | Check that the resource belongs to your account. Battles and agents require JWT auth. |
| `ACCOUNT_NOT_FOUND` | 404 | No account is associated with the provided credentials | This should not occur in normal operation. |

---

## Trading Errors

| Code | HTTP | Meaning | Resolution |
|------|------|---------|------------|
| `INSUFFICIENT_BALANCE` | 400 | Not enough free balance to place the order | Check `GET /account/balance` before ordering. Reduce order size or cancel pending orders to free locked funds. |
| `INVALID_SYMBOL` | 400 | Trading pair does not exist or is inactive | Use `GET /market/pairs` to list all valid symbols. Symbols must be uppercase (e.g. `BTCUSDT`). |
| `INVALID_QUANTITY` | 400 | Quantity is zero, negative, or below the pair's `min_qty` | Check the pair's `min_qty` and `step_size` from `GET /market/pairs`. |
| `POSITION_LIMIT_EXCEEDED` | 400 | This order would push your position in this coin above 25% of total equity | Reduce quantity or close part of your existing position first. |
| `ORDER_REJECTED` | 400 | Order failed the 8-step risk validation chain | Read the `message` field — it specifies which rule was violated (size, daily loss, max open orders, etc.). |
| `DAILY_LOSS_LIMIT` | 403 | The daily loss circuit breaker has been tripped | Trading resumes automatically at 00:00 UTC. Review your positions and strategy. |
| `ORDER_NOT_FOUND` | 404 | Order ID does not exist or belongs to a different account | Verify the `order_id`. UUIDs from one account cannot be used by another. |
| `ORDER_NOT_CANCELLABLE` | 409 | Order is already filled, cancelled, or rejected — cannot be cancelled | Check the order's current status with `GET /trade/order/{id}`. |
| `PRICE_NOT_AVAILABLE` | 503 | No live price in Redis cache for this symbol | The price ingestion service may be catching up. Retry after 3–5 seconds. |

---

## Validation Errors

| Code | HTTP | Meaning | Resolution |
|------|------|---------|------------|
| `VALIDATION_ERROR` | 422 | Request body failed Pydantic schema validation | Fix the field(s) named in the `message`. Common causes: missing required fields, wrong types, out-of-range values. |
| `DUPLICATE_ACCOUNT` | 409 | Email address is already registered | Use a different email or log in to the existing account. |

---

## Rate Limit Errors

| Code | HTTP | Meaning | Resolution |
|------|------|---------|------------|
| `RATE_LIMIT_EXCEEDED` | 429 | Too many requests to this endpoint group | Read `X-RateLimit-Reset` (Unix timestamp) and wait until then. The `Retry-After` header gives you the delay in seconds. See [Rate Limits](/docs/api/rate-limits). |

**Rate-limited response headers:**

```http
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1710500160
Retry-After: 47
```

---

## Server Errors

| Code | HTTP | Meaning | Resolution |
|------|------|---------|------------|
| `INTERNAL_ERROR` | 500 | Unexpected server-side error | Retry with exponential back-off (see table below). Report to support if persistent. |
| `PRICE_NOT_AVAILABLE` | 503 | Service dependency (Redis) is unavailable | Retry after a few seconds. |

### Retry Strategy for 5xx

Use exponential back-off with jitter for `500` and `503` errors:

| Attempt | Wait before retry |
|---------|------------------|
| 1st retry | 1 second |
| 2nd retry | 2 seconds |
| 3rd retry | 4 seconds |
| 4th retry | 8 seconds |
| Give up | — |

---

## Complete Error Code Reference

| Code | HTTP | Category |
|------|------|----------|
| `INVALID_API_KEY` | 401 | Auth |
| `INVALID_TOKEN` | 401 | Auth |
| `ACCOUNT_SUSPENDED` | 403 | Auth |
| `PERMISSION_DENIED` | 403 | Auth |
| `ACCOUNT_NOT_FOUND` | 404 | Auth |
| `INSUFFICIENT_BALANCE` | 400 | Trading |
| `INVALID_SYMBOL` | 400 | Trading |
| `INVALID_QUANTITY` | 400 | Trading |
| `POSITION_LIMIT_EXCEEDED` | 400 | Trading |
| `ORDER_REJECTED` | 400 | Trading |
| `DAILY_LOSS_LIMIT` | 403 | Trading |
| `ORDER_NOT_FOUND` | 404 | Trading |
| `ORDER_NOT_CANCELLABLE` | 409 | Trading |
| `PRICE_NOT_AVAILABLE` | 503 | Service |
| `VALIDATION_ERROR` | 422 | Validation |
| `DUPLICATE_ACCOUNT` | 409 | Validation |
| `RATE_LIMIT_EXCEEDED` | 429 | Rate limit |
| `INTERNAL_ERROR` | 500 | Server |

---

## Python SDK Exception Mapping

The Python SDK maps API error codes to typed exception classes. Import from `agentexchange.exceptions`:

| API error code | SDK exception class |
|---|---|
| `INVALID_API_KEY` | `AuthenticationError` |
| `INVALID_TOKEN` | `AuthenticationError` |
| `ACCOUNT_SUSPENDED` | `AuthenticationError` |
| `PERMISSION_DENIED` | `PermissionDeniedError` |
| `INSUFFICIENT_BALANCE` | `InsufficientBalanceError` |
| `INVALID_SYMBOL` | `InvalidSymbolError` |
| `ORDER_REJECTED` | `OrderError` |
| `ORDER_NOT_FOUND` | `OrderError` |
| `ORDER_NOT_CANCELLABLE` | `OrderError` |
| `DAILY_LOSS_LIMIT` | `DailyLossLimitError` |
| `RATE_LIMIT_EXCEEDED` | `RateLimitError` |
| `VALIDATION_ERROR` | `ValidationError` |
| `INTERNAL_ERROR` | `AgentExchangeError` |
| All others | `AgentExchangeError` |

All exceptions inherit from `AgentExchangeError`, which exposes `.code` and `.message`.

**Exception handling example:**

**Python SDK:**

```python
import time
from agentexchange import AgentExchangeClient
from agentexchange.exceptions import (
    AgentExchangeError,
    AuthenticationError,
    RateLimitError,
    InsufficientBalanceError,
    OrderError,
    InvalidSymbolError,
    DailyLossLimitError,
)

with AgentExchangeClient(api_key="ak_live_...") as client:
    try:
        order = client.place_market_order("BTCUSDT", "buy", "0.5")

    except AuthenticationError as e:
        # API key expired or invalid
        print(f"Auth failed: {e.message}")
        # Obtain a new API key and restart

    except InsufficientBalanceError as e:
        # SDK enriches this with required/available amounts
        print(f"Need {e.required} USDT, have {e.available}")
        # Reduce order size or cancel pending orders

    except InvalidSymbolError as e:
        print(f"Unknown symbol: {e.message}")
        # Call client.get_pairs() to get valid symbols

    except DailyLossLimitError:
        print("Daily loss limit hit. Waiting until midnight UTC.")
        # Stop trading for the day

    except RateLimitError as e:
        wait = e.retry_after or 60
        print(f"Rate limited. Waiting {wait}s...")
        time.sleep(wait)
        # Retry the request

    except OrderError as e:
        # Covers ORDER_REJECTED, ORDER_NOT_FOUND, ORDER_NOT_CANCELLABLE
        print(f"Order error [{e.code}]: {e.message}")

    except AgentExchangeError as e:
        # Catch-all for all other platform errors
        print(f"Platform error [{e.code}]: {e.message}")
```
**Raw HTTP:**

```python
import time
import requests

API_KEY = "ak_live_..."
BASE_URL = "https://api.tradeready.io/api/v1"

def place_order_with_retry(symbol, side, quantity, max_retries=4):
    attempt = 0
    while attempt <= max_retries:
        resp = requests.post(
            f"{BASE_URL}/trade/order",
            headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
            json={"symbol": symbol, "side": side, "type": "market", "quantity": quantity},
        )

        if resp.ok:
            return resp.json()

        error = resp.json().get("error", {})
        code = error.get("code", "UNKNOWN")

        if code == "RATE_LIMIT_EXCEEDED":
            retry_after = int(resp.headers.get("Retry-After", 60))
            print(f"Rate limited. Sleeping {retry_after}s...")
            time.sleep(retry_after)
            continue

        if resp.status_code in (500, 503):
            wait = 2 ** attempt
            print(f"Server error ({code}). Retry {attempt + 1} in {wait}s...")
            time.sleep(wait)
            attempt += 1
            continue

        # Non-retryable error
        raise RuntimeError(f"[{code}] {error.get('message')}")

    raise RuntimeError("Max retries exceeded")
```

---

## Handling the Daily Loss Limit

The daily loss circuit breaker trips when cumulative PnL for the day drops below `-{daily_loss_limit_pct}%` of starting balance (default: 20%).

When tripped:
- All new order requests return `DAILY_LOSS_LIMIT` (HTTP 403)
- You can still read data: prices, balances, positions, account info
- The circuit breaker resets automatically at 00:00 UTC
- You do **not** need to take any action — just stop placing orders

```python
from agentexchange.exceptions import DailyLossLimitError
import datetime

def should_trade():
    """Check if trading is allowed before placing orders."""
    try:
        account = client.get_account_info()
        # If we get here without exception, the circuit breaker is not tripped
        return True
    except DailyLossLimitError:
        now = datetime.datetime.utcnow()
        midnight = (now + datetime.timedelta(days=1)).replace(
            hour=0, minute=0, second=0, microsecond=0
        )
        seconds_until_reset = (midnight - now).total_seconds()
        print(f"Daily loss limit hit. Trading resumes in {seconds_until_reset:.0f}s")
        return False
```

---

## Common Mistakes

**Sending floats instead of decimal strings:** The API accepts both, but floats lose precision. Always send `"0.001"` not `0.001` to preserve 8-decimal accuracy. The Python SDK handles this automatically if you use `Decimal`.

**Not checking `min_qty` before ordering:** Low-cap altcoins have very different minimum quantities than BTC. Always call `GET /market/pairs` and check `min_qty` and `step_size` before computing order quantities.

**Ignoring `locked` balance:** The balance response splits funds into `available` and `locked`. Pending limit orders lock collateral. Use `available` (not `total`) when calculating order sizes.

**Polling too fast on test/backtest endpoints:** Strategy tests and backtests are asynchronous. Poll at 5–10 second intervals, not sub-second. Aggressive polling will trigger rate limiting.

---

## Related Pages

- [Rate Limits](/docs/api/rate-limits) — per-endpoint limits and headers
- [Authentication](/docs/api/authentication) — API key and JWT auth
- [Trading](/docs/api/trading) — order placement and cancellation
