All errors follow a consistent JSON structure:
{
"name": "BadRequest",
"message": "Validation failed: 'pair' is required",
"code": 400,
"className": "bad-request",
"errors": [
{
"field": "pair",
"message": "'pair' is required"
}
]
}
| Field | Type | Description |
|---|
name | string | Error class name |
message | string | Human-readable description |
code | integer | HTTP status code |
className | string | Kebab-case error identifier |
errors | array | Optional: field-level validation errors |
HTTP Status Codes
Success
| Code | Meaning | When |
|---|
200 | OK | GET, PATCH, DELETE succeeded |
201 | Created | POST created a new resource |
Client Errors
| Code | Name | Meaning |
|---|
400 | BadRequest | Invalid parameters or validation failure |
401 | NotAuthenticated | Missing or invalid authentication |
403 | Forbidden | Authenticated but lacking permission |
404 | NotFound | Resource doesn’t exist |
405 | MethodNotAllowed | HTTP method not supported for this endpoint |
408 | Timeout | Request took too long |
409 | Conflict | Resource already exists or state conflict |
422 | Unprocessable | Request understood but semantically invalid |
429 | TooManyRequests | Rate limit exceeded |
Server Errors
| Code | Name | Meaning |
|---|
500 | GeneralError | Internal server error |
502 | BadGateway | Upstream service (exchange) unreachable |
503 | Unavailable | Service temporarily unavailable |
Common Error Scenarios
{
"name": "NotAuthenticated",
"message": "jwt expired",
"code": 401,
"className": "not-authenticated"
}
Fix: Re-authenticate with POST /authentication to get a fresh token.
{
"name": "Forbidden",
"message": "You do not have permission to access this resource",
"code": 403,
"className": "forbidden"
}
Fix: You can only access your own resources. Check the resource ID.
{
"name": "BadRequest",
"message": "Validation failed",
"code": 400,
"className": "bad-request",
"errors": [
{"field": "pair", "message": "'pair' is required"},
{"field": "timeframe", "message": "must be one of: 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w"}
]
}
Fix: Check the errors array for specific field issues.
{
"name": "TooManyRequests",
"message": "Rate limit exceeded. Try again in 30 seconds.",
"code": 429,
"className": "too-many-requests"
}
Fix: Implement exponential backoff. Check Retry-After header.
Rate Limits
Limits by Plan
| Plan | Requests/min | Burst | WebSocket connections |
|---|
| Free | 60 | 10 | 1 |
| Pro | 300 | 50 | 5 |
| Enterprise | 1000 | 100 | 25 |
Every response includes rate limit information:
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 287
X-RateLimit-Reset: 1705316460
Retry-After: 30
| Header | Description |
|---|
X-RateLimit-Limit | Max requests per window |
X-RateLimit-Remaining | Remaining requests in current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
Retry-After | Seconds to wait (only on 429 responses) |
Endpoint-Specific Limits
Some endpoints have stricter limits:
| Endpoint | Limit | Reason |
|---|
POST /authentication | 10/min | Brute-force protection |
POST /auth-management | 5/min | Password reset abuse prevention |
POST /strategies/backtest | 5/min | Compute-intensive |
POST /strategies/hyperopt | 3/min | Very compute-intensive |
POST /strategies/ai | 10/min | AI generation cost |
Handling Errors in Code
async function apiRequest(url, options) {
const response = await fetch(url, options);
if (!response.ok) {
const error = await response.json();
if (error.code === 401) {
// Token expired — refresh and retry
const newToken = await refreshAuth();
options.headers['Authorization'] = `Bearer ${newToken}`;
return fetch(url, options);
}
if (error.code === 429) {
// Rate limited — wait and retry
const retryAfter = response.headers.get('Retry-After') || 30;
await new Promise(r => setTimeout(r, retryAfter * 1000));
return fetch(url, options);
}
throw new Error(`${error.name}: ${error.message}`);
}
return response.json();
}
import time
import requests
def api_request(url, headers, max_retries=3):
for attempt in range(max_retries):
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.json()
if response.status_code == 401:
headers['Authorization'] = f'Bearer {refresh_auth()}'
continue
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 30))
time.sleep(retry_after)
continue
error = response.json()
raise Exception(f"{error['name']}: {error['message']}")
raise Exception("Max retries exceeded")
Retry Strategy
For production integrations, implement exponential backoff:
async function withRetry(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (error.code === 429 || error.code >= 500) {
const delay = Math.pow(2, i) * 1000 + Math.random() * 1000;
await new Promise(r => setTimeout(r, delay));
continue;
}
throw error; // Don't retry client errors (4xx except 429)
}
}
throw new Error('Max retries exceeded');
}
Never retry 400, 403, or 404 errors — they won’t resolve by retrying. Only retry 429 (rate limit) and 5xx (server errors).