Skip to main content

Error Handling

Every API response uses standard HTTP status codes with structured JSON error bodies.

Error Response Format

All errors return a consistent JSON structure:
{
  "error": "INSUFFICIENT_BALANCE",
  "message": "Account balance $12.50 insufficient for order cost $25.00",
  "details": {
    "available": "12.50",
    "required": "25.00"
  }
}
FieldTypeDescription
errorstringMachine-readable error code
messagestringHuman-readable description
detailsobjectOptional additional context

HTTP Status Codes

CodeMeaningRetry?Action
200SuccessProcess response
400Bad requestNoFix request payload
401Invalid or missing API keyNoCheck X-API-Key header
403Insufficient permissionsNoCheck key scopes
404Resource not foundNoVerify market ID or order ID
409Conflict (duplicate idempotency key)NoUse original response
422Validation errorNoFix input fields
429Rate limitedYesWait for Retry-After header
500Internal server errorYesRetry with backoff
502Upstream errorYesRetry with backoff
503Service unavailableYesRetry with backoff

Common Error Codes

Trading Errors

Error CodeHTTPDescription
INSUFFICIENT_BALANCE400Not enough funds for order
INVALID_QUANTITY422Quantity must be positive integer
INVALID_PRICE422Price must be between 0 and 1
MARKET_NOT_FOUND404Unknown market_id
MARKET_CLOSED400Market is resolved or inactive
ORDER_NOT_FOUND404Unknown order_id
CANNOT_CANCEL400Order already filled or cancelled
SLIPPAGE_EXCEEDED400Price moved beyond max_slippage_bps

Authentication Errors

Error CodeHTTPDescription
MISSING_API_KEY401No X-API-Key header
INVALID_API_KEY401Key doesn’t exist or is revoked
KEY_EXPIRED401API key has expired
INSUFFICIENT_SCOPE403Key lacks required permission

Rate Limit Errors

Error CodeHTTPDescription
RATE_LIMITED429Too many requests

Retry Strategy

import time
import requests

def api_call_with_retry(method, url, max_retries=3, **kwargs):
    """Make API call with exponential backoff on retryable errors."""
    for attempt in range(max_retries + 1):
        try:
            resp = requests.request(method, url, **kwargs)

            if resp.status_code == 429:
                # Rate limited — use server-provided wait time
                wait = int(resp.headers.get("Retry-After", 2 ** attempt))
                print(f"Rate limited, waiting {wait}s...")
                time.sleep(wait)
                continue

            if resp.status_code >= 500:
                # Server error — retry with backoff
                wait = 2 ** attempt
                print(f"Server error {resp.status_code}, retry in {wait}s...")
                time.sleep(wait)
                continue

            # Success or client error (no retry)
            resp.raise_for_status()
            return resp.json()

        except requests.exceptions.ConnectionError:
            wait = 2 ** attempt
            print(f"Connection error, retry in {wait}s...")
            time.sleep(wait)

    raise Exception(f"Max retries ({max_retries}) exceeded for {url}")

WebSocket Error Handling

WebSocket connections use custom close codes:
Close CodeMeaningAction
1000Normal closeReconnect if desired
1001Server going awayReconnect after 1s
4001Authentication failedGet new token, reconnect
4002Subscription limit exceededReduce subscriptions
import asyncio
import aiohttp

async def resilient_ws(url, token, market_ids):
    """WebSocket connection with automatic reconnection."""
    backoff = 1
    while True:
        try:
            async with aiohttp.ClientSession() as session:
                async with session.ws_connect(f"{url}?token={token}") as ws:
                    backoff = 1  # Reset on successful connect

                    await ws.send_json({
                        "type": "subscribe",
                        "channel": "prices",
                        "market_ids": market_ids,
                    })

                    async for msg in ws:
                        if msg.type == aiohttp.WSMsgType.TEXT:
                            handle_message(msg.data)
                        elif msg.type == aiohttp.WSMsgType.CLOSED:
                            break
                        elif msg.type == aiohttp.WSMsgType.ERROR:
                            break

        except Exception as e:
            print(f"WS error: {e}")

        wait = min(backoff, 30)
        print(f"Reconnecting in {wait}s...")
        await asyncio.sleep(wait)
        backoff *= 2

Best Practices

Always check status codes

Never assume a 2xx response. Parse the status code and handle each category appropriately.

Use Retry-After header

On 429 responses, the Retry-After header tells you exactly how long to wait. Don’t guess.

Don't retry 4xx errors

Client errors (400-422) indicate a problem with your request. Fix the payload instead of retrying.

Log error details

Always log the full error response body for debugging — the details field often contains actionable info.