Errors
The API uses standard HTTP status codes and returns JSON error responses with structured error codes.
HTTP Status Codes
| Code | Meaning |
|---|---|
| 200 | Success |
| 400 | Bad Request — Invalid parameters |
| 401 | Unauthorized — Missing or invalid auth |
| 403 | Forbidden — Insufficient scope or IP blocked |
| 404 | Not Found — Resource doesn't exist |
| 409 | Conflict — Duplicate or state conflict |
| 429 | Too Many Requests — Rate limit exceeded |
| 500 | Internal Server Error |
| 503 | Service Unavailable — Platform is paused |
Error Response Format
All error responses return a JSON object with an error field:
{
"error": "descriptive error message"
}
Some errors include an additional code field for programmatic handling:
{
"error": "missing or invalid authentication",
"code": "UNAUTHORIZED"
}
Global Error Code Catalog
These error codes can be used for programmatic error handling across all endpoints:
| Code | HTTP Status | Description |
|---|---|---|
UNAUTHORIZED | 401 | Missing or invalid authentication credentials |
API_KEY_UNAUTHORIZED | 401 | API key not found, revoked, or expired |
FORBIDDEN | 403 | Authenticated but not permitted to access resource |
INSUFFICIENT_SCOPE | 403 | API key lacks the required scope for the endpoint |
RATE_LIMITED | 429 | Request rate limit exceeded |
VALIDATION_ERROR | 400 | Request body or parameters failed validation |
NOT_FOUND | 404 | Requested resource does not exist |
INTERNAL_ERROR | 500 | Unexpected server error |
Example Response Bodies
UNAUTHORIZED (401)
{
"error": "missing or invalid authentication",
"code": "UNAUTHORIZED"
}
API_KEY_UNAUTHORIZED (401)
{
"error": "API key is revoked or expired",
"code": "API_KEY_UNAUTHORIZED"
}
FORBIDDEN (403)
{
"error": "access denied",
"code": "FORBIDDEN"
}
INSUFFICIENT_SCOPE (403)
{
"error": "API key lacks scope: trade:orders",
"code": "INSUFFICIENT_SCOPE"
}
RATE_LIMITED (429)
{
"error": "rate limit exceeded",
"code": "RATE_LIMITED"
}
VALIDATION_ERROR (400)
{
"error": "invalid side: must be BUY or SELL",
"code": "VALIDATION_ERROR"
}
NOT_FOUND (404)
{
"error": "order not found",
"code": "NOT_FOUND"
}
INTERNAL_ERROR (500)
{
"error": "internal server error",
"code": "INTERNAL_ERROR"
}
Common Errors by Category
Authentication
| Error | HTTP | Cause |
|---|---|---|
missing required API key headers | 401 | One or more HMAC headers missing |
invalid timestamp | 401 | Timestamp not a valid Unix epoch |
timestamp outside acceptable window | 401 | Clock skew > 30 seconds |
invalid API key | 401 | Key not found or revoked |
API key is revoked or expired | 401 | Key was revoked by admin |
invalid passphrase | 401 | Passphrase doesn't match |
invalid signature | 401 | HMAC signature verification failed |
Orders
| Error | HTTP | Cause |
|---|---|---|
platform is paused | 503 | Trading is temporarily suspended |
market not active | 400 | Market is not in active status |
invalid side | 400 | Side must be BUY or SELL |
invalid maker_amount | 400 | Non-positive or non-numeric amount |
order already expired | 400 | Expiration timestamp is in the past |
FOK order could not be fully filled | 400 | No complete match available (FOK) |
FAK order had no immediate fills | 400 | No matching orders on the book (FAK) |
post-only order would cross the book | 400 | Order would take liquidity (post-only) |
order cannot be cancelled | 409 | Order already filled/cancelled/expired |
order not found | 404 | Order hash doesn't exist or wrong user |
Time-in-Force
| Error | HTTP | Cause |
|---|---|---|
invalid time_in_force | 400 | TIF value not GTC/GTD/FOK/FAK |
GTD orders require an expiration time | 400 | Missing expiration for GTD |
Rate Limits
| Error | HTTP | Cause |
|---|---|---|
rate limit exceeded | 429 | Too many requests per window |
Batch Operations
| Error | HTTP | Cause |
|---|---|---|
too many orders (max 50) | 400 | Batch exceeds 50 orders |
no orders provided | 400 | Empty orders array |
no order hashes provided | 400 | Empty cancel hashes array |
Handling Errors
import requests
import time
resp = requests.post(f"{BASE_URL}/orders", json=order, headers=headers)
if resp.status_code == 429:
# Rate limited — wait and retry
reset_time = int(resp.headers.get("X-RateLimit-Reset", 0))
wait_seconds = max(1, reset_time - int(time.time()))
time.sleep(wait_seconds)
# Retry...
elif resp.status_code == 401:
error = resp.json()
code = error.get("code", "")
if code == "API_KEY_UNAUTHORIZED":
print("API key revoked — regenerate key")
else:
print("Auth error — check credentials")
elif resp.status_code == 503:
# Platform paused — stop trading
print("Platform is paused, stopping")
elif resp.status_code >= 400:
error = resp.json().get("error", "unknown error")
print(f"Error {resp.status_code}: {error}")
const resp = await fetch(`${BASE_URL}/orders`, {
method: 'POST',
headers: { ...authHeaders, 'Content-Type': 'application/json' },
body: JSON.stringify(order),
});
if (resp.status === 429) {
const resetTime = parseInt(resp.headers.get('X-RateLimit-Reset') ?? '0');
const waitMs = Math.max(1000, (resetTime - Math.floor(Date.now() / 1000)) * 1000);
await new Promise((r) => setTimeout(r, waitMs));
// Retry...
} else if (resp.status === 401) {
const { code } = await resp.json();
if (code === 'API_KEY_UNAUTHORIZED') {
console.error('API key revoked — regenerate key');
}
} else if (!resp.ok) {
const { error } = await resp.json();
console.error(`Error ${resp.status}: ${error}`);
}
Idempotency
Order placement supports idempotency via the EIP-712 order hash. Submitting the same signed order twice returns the existing order instead of creating a duplicate.