Error Responses
This page documents all error codes, response formats, and recommended handling strategies for the GX Exchange API.
HTTP Status Codes
| Status | Error | Description | Action |
|---|---|---|---|
200 | — | Success | Process response body |
400 | bad_request | Malformed request, missing fields, or invalid parameters | Fix request format |
401 | unauthorized | Invalid signature, expired timestamp, or missing authentication | Verify signature and clock sync |
403 | forbidden | Agent wallet not authorized for this account | Check agent authorization |
404 | not_found | Unknown endpoint or resource | Verify URL path |
429 | rate_limit_exceeded | Too many requests | Back off per Retry-After header |
500 | internal_error | Server-side error | Retry with exponential backoff |
503 | service_unavailable | Temporary maintenance or overload | Retry after short delay |
Error Response Format
All HTTP errors follow this structure:
{
"error": "error_key",
"message": "Human-readable explanation of what went wrong.",
"details": { }
}| Field | Type | Description |
|---|---|---|
error | string | Machine-readable error code |
message | string | Human-readable description |
details | Object? | Optional additional context |
Exchange Endpoint Errors
The POST /exchange endpoint returns errors within the response body rather than using HTTP status codes for business logic errors:
Success
{
"status": "ok",
"response": {
"type": "order",
"data": { "statuses": [{ "resting": { "oid": 12345 } }] }
}
}Business Logic Error
{ "status": "err", "response": "Insufficient margin" }Per-Order Errors (Batch)
When submitting multiple orders, each order can succeed or fail independently:
{
"status": "ok",
"response": {
"type": "order",
"data": {
"statuses": [
{ "resting": { "oid": 12345 } },
{ "error": "Price out of range" }
]
}
}
}Common Exchange Errors
| Error Message | Cause | Resolution |
|---|---|---|
Insufficient margin | Not enough collateral for the order | Reduce order size or deposit more funds |
Order not found | Cancel/modify target does not exist or is already filled | Verify the order ID is correct and still active |
Invalid signature | EIP-712 signature verification failed | Check signing domain (GXExchange), chain ID (42069), and key |
Price out of range | Limit price too far from the oracle price | Use a price closer to the current market |
Reduce only violated | Reduce-only order would increase position size | Check position direction before placing |
Self-trade prevented | Order would match against your own resting order | Cancel the existing order first |
Max open orders exceeded | Too many open orders for this market | Cancel some existing orders |
Account not activated | Address has not been activated on GX Exchange | Complete the activation process first |
Agent not authorized | Agent wallet has not been approved by this address | Submit approveAgent from the main wallet |
Nonce too old | The nonce timestamp is outside the validity window | Synchronize your system clock via NTP |
Validation Errors (400)
Missing Required Field
{
"error": "bad_request",
"message": "Missing required field: pair",
"details": { "field": "pair" }
}Invalid Order Size
{
"error": "bad_request",
"message": "Order size 0.00001 is below minimum 0.0001 for BTC-USDC",
"details": { "field": "size", "min": "0.0001", "provided": "0.00001" }
}Invalid Info Type
{
"error": "bad_request",
"message": "Unknown info type: invalidType"
}Authentication Errors (401)
Invalid Signature
{
"error": "unauthorized",
"message": "Invalid signature. Verify your EIP-712 computation.",
"details": { "hint": "Check domain name=GXExchange, chainId=42069, and verifyingContract=0x0" }
}Expired Timestamp (HMAC)
{
"error": "unauthorized",
"message": "Timestamp is outside the 30-second validity window.",
"details": { "server_time": 1700000030, "provided": 1700000000 }
}Rate Limit Errors (429)
{
"error": "rate_limit_exceeded",
"message": "Rate limit exceeded. Retry after 0.3 seconds.",
"retry_after": 0.3
}The Retry-After header is also set on the HTTP response.
Error Handling Best Practices
Retry Strategy
| Error Type | Retry? | Strategy |
|---|---|---|
400 Bad Request | No | Fix the request; retrying will produce the same error |
401 Unauthorized | No | Check signature logic or clock sync |
403 Forbidden | No | Verify agent authorization |
404 Not Found | No | Check endpoint URL |
429 Rate Limited | Yes | Wait for retry_after seconds |
500 Internal Error | Yes | Exponential backoff: 1s, 2s, 4s, 8s, max 30s |
503 Service Unavailable | Yes | Wait 5-10 seconds, check status page |
TypeScript Error Handler
async function apiRequest(url: string, body: any, maxRetries = 3): Promise<any> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (response.ok) {
return response.json();
}
if (response.status === 429) {
const retryAfter = parseFloat(response.headers.get("Retry-After") || "1");
await new Promise(r => setTimeout(r, retryAfter * 1000));
continue;
}
if (response.status >= 500) {
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
await new Promise(r => setTimeout(r, delay));
continue;
}
// Non-retryable error
const error = await response.json();
throw new Error(`API Error ${response.status}: ${error.message}`);
}
throw new Error("Max retries exceeded");
}Python Error Handler
import time
import requests
def api_request(url: str, body: dict, max_retries: int = 3) -> dict:
for attempt in range(max_retries):
resp = requests.post(url, json=body)
if resp.ok:
return resp.json()
if resp.status_code == 429:
retry_after = float(resp.headers.get("Retry-After", "1"))
time.sleep(retry_after)
continue
if resp.status_code >= 500:
delay = min(2 ** attempt, 30)
time.sleep(delay)
continue
# Non-retryable
error = resp.json()
raise Exception(f"API Error {resp.status_code}: {error.get('message')}")
raise Exception("Max retries exceeded")