General Info & Authentication
Base URL, HMAC-SHA256 authentication, rate limits, HTTP status codes, and request formatting for the GaiaEx API.
General API Information
- The base endpoint is:
https://openapi.gaiaex.com/v1/trade - All endpoints return either a JSON object or array.
- All time and timestamp related fields are in milliseconds (Unix epoch).
- Data is returned in ascending order unless otherwise specified.
- For
GETendpoints, parameters are sent as query strings. ForPOST,PATCH, andDELETEendpoints, parameters are sent as a JSON request body withContent-Type: application/json.
Endpoint Security Type
Each endpoint has a security type that determines how you interact with it.
| Security Type | Description |
|---|---|
| NONE | Endpoint can be accessed freely. No authentication required. |
| SIGNED | Endpoint requires HMAC-SHA256 signature via API key headers. |
| SESSION | Endpoint requires a valid session token (Bearer auth). |
- Most trading and account endpoints accept both SIGNED (API key) and SESSION (Bearer token) authentication.
- Market data endpoints are public (NONE).
Authentication
FROM ZERO TO FIRST TRADE
If you already have USDC deposited in the GaiaEx mobile app, you're four steps away from trading via API:
- Mobile app → Settings → API Keys → Create. Pick
read + trade. Copy theapi_keyandapi_secret(the secret is shown only once). - Put them in a
config.jsonnext to your script. - Sign each request with HMAC-SHA256 over
timestamp + METHOD + path + bodyand send three headers:X-GAIAEX-APIKEY,X-GAIAEX-TIMESTAMP,X-GAIAEX-SIGNATURE. - Call
GET /user/{address}/balance. If it returns your balance, you're trading-ready —POST /orderto place.
That's the whole contract. The rest of this page is the precise definition of step 3.
GaiaEx supports two authentication methods:
Method 1: API Key + HMAC-SHA256 (Recommended for programmatic trading)
What HMAC-SHA256 is, in one paragraph. Your API secret never leaves your machine. For each request, you concatenate four pieces of text into one message, run HMAC-SHA256 over it with your secret as the key, and attach the resulting hex digest as a header. The server does the exact same computation and compares digests. If they match, the request is yours; if any byte differs (wrong timestamp, wrong path, tampered body), the digests don't match and the server rejects. You never send the secret over the network.
What EIP-712 is, and why you don't need it for API trading. EIP-712 is a structured-data wallet signature used for on-chain actions (deposits, withdrawals, swaps, handshake). It requires a wallet private key and, in GaiaEx, is only produced by the mobile app's embedded wallet. API-key integrations never produce EIP-712 signatures — HMAC-SHA256 is the only signing you need. If a doc page mentions EIP-712, it's describing an in-app flow, not something you'll implement in your bot.
API keys are created in the GaiaEx mobile app (Settings > API Keys) and can be managed programmatically via the /api-keys endpoints. Each request must include three custom headers:
| Header | Description |
|---|---|
X-GAIAEX-APIKEY | Your public API key (32-character hex string) |
X-GAIAEX-TIMESTAMP | Current Unix timestamp in milliseconds |
X-GAIAEX-SIGNATURE | HMAC-SHA256 hex digest of the message |
Signature construction:
message = timestamp + method + path + body
signature = HMAC-SHA256(api_secret, message)timestamp— the value ofX-GAIAEX-TIMESTAMP(string)method— HTTP method in uppercase (GET,POST,DELETE,PATCH)path— request path without the/v1/tradeprefix and without query string (e.g.,/user/0xABC.../balance, not/v1/trade/user/0xABC.../balance)body— JSON request body as string (empty string""for GET/DELETE)
Example (Python):
import hmac
import hashlib
import time
import requests
import json
api_key = "your_api_key_here"
api_secret = "your_api_secret_here"
timestamp = str(int(time.time() * 1000))
method = "GET"
path = "/user/0xYourAddress/balance"
body = ""
message = timestamp + method + path + body
signature = hmac.new(
api_secret.encode(), message.encode(), hashlib.sha256
).hexdigest()
headers = {
"X-GAIAEX-APIKEY": api_key,
"X-GAIAEX-TIMESTAMP": timestamp,
"X-GAIAEX-SIGNATURE": signature,
"Content-Type": "application/json",
}
resp = requests.get(f"https://openapi.gaiaex.com/v1/trade{path}", headers=headers)
print(resp.json())Example (curl):
TIMESTAMP=$(date +%s000)
METHOD="GET"
PATH="/user/0xYourAddress/balance"
BODY=""
MESSAGE="${TIMESTAMP}${METHOD}${PATH}${BODY}"
SIGNATURE=$(echo -n "$MESSAGE" | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')
curl -H "X-GAIAEX-APIKEY: $API_KEY" \
-H "X-GAIAEX-TIMESTAMP: $TIMESTAMP" \
-H "X-GAIAEX-SIGNATURE: $SIGNATURE" \
-H "Content-Type: application/json" \
"https://openapi.gaiaex.com/v1/trade${PATH}"Method 2: Bearer Session Token (Web app / mobile only)
Session tokens are issued by the GaiaEx mobile app after the user completes an in-app wallet handshake. They are not obtainable from the public REST API and are not intended for programmatic integrations — use API keys instead.
Scope of the Public API
API keys can be used to trade, read account data (balances, positions, fills, order history, funding), manage orders and leverage, and query read-only wallet data (portfolio, swap quotes). They cannot:
- Deposit or withdraw funds
- Execute on-chain transfers or swaps
- Register or authenticate passkeys
- Complete the account handshake
- Create, modify, or delete API keys
These actions require the user's embedded-wallet signature and, in several cases, a passkey step-up — both performed in the GaiaEx mobile app. Users fund and defund accounts from the app; API integrations operate within the deposited balance.
HMAC Signing Walkthrough — Real Values You Can Verify
This section uses concrete values at every step. If you plug the same secret, timestamp, method, path, and body into your own code, you must get the same signature shown here. If you don't, your implementation has a bug — most likely in how you serialize the body or strip the path prefix.
Example 1: GET Balance (no body)
Goal: query perpetual account balance for address 0xA6E3c04eF78427b5B53F43CDBA881d7E15B0bccD.
| Step | Value |
|---|---|
| 1. Timestamp (ms) | 1712345678000 |
| 2. HTTP Method | GET |
| 3. Path (no /v1/trade prefix) | /user/0xA6E3c04eF78427b5B53F43CDBA881d7E15B0bccD/balance |
| 4. Body | "" (empty string for GET) |
| 5. Message (concatenation) | 1712345678000GET/user/0xA6E3c04eF78427b5B53F43CDBA881d7E15B0bccD/balance |
| 6. API Secret (example) | my_secret_key_example_32chars_xx |
| 7. HMAC-SHA256 hex digest | 8bb72b649cea0ef7e170cf82d7e7e902279cf8b4fbf73b7248c1eb00a62ddc42 |
Example response (HTTP 200):
{
"address": "0xA6E3c04eF78427b5B53F43CDBA881d7E15B0bccD",
"account_value": "1523.47",
"available_margin": "892.10",
"margin_used": "631.37",
"leverage_used": "2.4",
"unrealized_pnl": "18.92",
"timestamp": 1712345679123
}import hmac, hashlib, time, json, requests
# Load from config.json (recommended)
with open("config.json") as f:
cfg = json.load(f)
api_key = cfg["api_key"]
api_secret = cfg["api_secret"]
address = cfg["user_address"]
# Step 1: Generate timestamp
timestamp = str(int(time.time() * 1000))
# Step 2-4: Build message components
method = "GET"
path = f"/user/{address}/balance"
body = "" # Empty for GET
# Step 5: Concatenate message
message = timestamp + method + path + body
print(f"Message: {message}")
# Step 6-7: Compute HMAC-SHA256
signature = hmac.new(
api_secret.encode("utf-8"),
message.encode("utf-8"),
hashlib.sha256,
).hexdigest()
print(f"Signature: {signature}")
# Step 8: Send request
headers = {
"X-GAIAEX-APIKEY": api_key,
"X-GAIAEX-TIMESTAMP": timestamp,
"X-GAIAEX-SIGNATURE": signature,
"Content-Type": "application/json",
}
resp = requests.get(
f"https://openapi.gaiaex.com/v1/trade{path}",
headers=headers,
)
print(f"Status: {resp.status_code}")
print(f"Response: {resp.json()}")Example 2: POST Place Order (with body)
Goal: place a limit buy order for 0.1 ETH at $3,500.
THE BODY YOU SIGN MUST BE THE BYTE-FOR-BYTE BODY YOU SEND
The signature is computed over the exact JSON string you put on the wire. If you re-serialize the body (e.g., different whitespace, key order, or number formatting) between signing and sending, the server will reject the request. Sign once, send that exact string.
| Step | Value |
|---|---|
| 1. Timestamp (ms) | 1712345678000 |
| 2. Method | POST |
| 3. Path | /order |
| 4. Body (exact JSON string) | {"user_address": "0xA6E3c04eF78427b5B53F43CDBA881d7E15B0bccD", "symbol": "ETH", "is_buy": true, "size": "0.1", "price": "3500.00", "order_type": "limit"} |
| 5. Message | 1712345678000POST/order{"user_address": "0xA6E3c04eF78427b5B53F43CDBA881d7E15B0bccD", "symbol": "ETH", "is_buy": true, "size": "0.1", "price": "3500.00", "order_type": "limit"} |
| 6. API Secret (example) | my_secret_key_example_32chars_xx |
| 7. HMAC-SHA256 hex digest | c3e85abeacfbb9ef64cfb7163b31d622e1a9744c128be6249e8347479c899158 |
Example response (HTTP 200):
{
"status": "ok",
"order_id": 41298374,
"client_order_id": "bot-a1b2c3",
"symbol": "ETH",
"is_buy": true,
"size": "0.1",
"price": "3500.00",
"order_type": "limit",
"state": "resting",
"filled": "0",
"avg_fill_price": null,
"timestamp": 1712345679456
}Common rejection (HTTP 401, signature mismatch):
{"detail": "Invalid signature"}If you see this, the server computed a different digest than yours. Check, in order: (1) the body you signed equals the body you sent; (2) path has no /v1/trade prefix; (3) timestamp is in milliseconds (not seconds) and within 5s of the server clock.
import hmac, hashlib, time, json, requests
with open("config.json") as f:
cfg = json.load(f)
api_key = cfg["api_key"]
api_secret = cfg["api_secret"]
address = cfg["user_address"]
timestamp = str(int(time.time() * 1000))
method = "POST"
path = "/order"
# Build JSON body — NOTE: order of keys matters for reproducibility
body_dict = {
"user_address": address,
"symbol": "ETH",
"is_buy": True,
"size": "0.1",
"price": "3500.00",
"order_type": "limit",
}
body = json.dumps(body_dict)
# Sign: timestamp + method + path + body
message = timestamp + method + path + body
signature = hmac.new(
api_secret.encode(), message.encode(), hashlib.sha256
).hexdigest()
headers = {
"X-GAIAEX-APIKEY": api_key,
"X-GAIAEX-TIMESTAMP": timestamp,
"X-GAIAEX-SIGNATURE": signature,
"Content-Type": "application/json",
}
resp = requests.post(
f"https://openapi.gaiaex.com/v1/trade{path}",
headers=headers,
data=body,
)
print(resp.json())COMMON SIGNING MISTAKES
- Including /v1/trade in the path — The path in the signature must NOT include the base URL prefix. Use
/order, not/v1/trade/order. - Including query string in path — For
GET /user/0x.../fills?limit=50, the signing path is/user/0x.../fillswithout the?limit=50. - Clock skew — The server rejects timestamps >5s from server time. Call
GET /timeto measure offset. - Body mismatch — The body string used for signing MUST be identical to the body sent in the HTTP request. Don't re-serialize.
Timeouts, Retries, Idempotency
Follow these rules to keep a production bot safe from the most common duplicate-order failure mode.
Recommended HTTP timeout
Set your client timeout to 20 seconds for /order, /order/modify, and /order/cancel. Typical responses are under 2 seconds, but allow headroom for network spikes.
Never blind-retry a timeout
IF A CALL TIMES OUT, THE ORDER MAY HAVE PLACED
A client-side HTTP timeout does not mean the order failed. The server may have accepted and placed the order. Before placing a replacement, reconcile:
- Fetch
GET /user/{address}/openOrders— does the order already exist? - Fetch
GET /user/{address}/historicalOrders— was it filled or canceled? - Only if neither shows the order should you place a replacement.
Idempotency via client_order_id
Pass a unique ASCII string (max 64 chars) as client_order_id on POST /order. The server treats duplicate client_order_id values from the same address within a 10-minute window as idempotent — it returns the existing order instead of creating a second one. Use this as belt-and-suspenders on every order:
import uuid
order = gaiaex_post("/order", {
"user_address": ADDRESS,
"symbol": "BTC",
"is_buy": True,
"size": "0.001",
"price": "79000",
"order_type": "limit",
"client_order_id": f"bot-{uuid.uuid4()}", # safe to retry
})Modify replaces the order
A successful POST /order/modify cancels the old order and creates a new one. The response returns a NEW orderId; the old id is no longer valid. Always use orderId from the modify response for subsequent cancel or modify calls. oldOrderId is echoed back for convenience.
Exponential backoff for transient errors
HTTP 429 (rate limited) and 502 (upstream unavailable) are transient — retry with jittered exponential backoff (e.g., 1s, 2s, 4s, up to 30s). HTTP 400 and 401 are not retryable without fixing the request.
Timing Security
- All SIGNED requests must include a
X-GAIAEX-TIMESTAMPheader. - The server rejects requests where the timestamp is more than 5 seconds away from the server time.
- Use
GET /timeto synchronize clocks if needed.
TIP
Network latency can cause clock drift. If you receive timestamp-related rejections, call GET /time to measure the offset between your local clock and the server.
Rate Limits
API Key Rate Limits (Trading Actions)
Endpoints that place, cancel, or modify orders are subject to per-API-key rate limits:
| Limit | Window | Default |
|---|---|---|
| Burst | Per second | 10 requests/s |
| Rolling | Per minute | 600 requests/min |
Rate-limited endpoints include: /order, /order/cancel, /order/cancel-all, /order/modify, /order/tpsl, /position/close, /leverage, /spot/order, /spot/order/cancel, /spot/order/cancel-all.
IP-Level Rate Limits
The API gateway enforces IP-based rate limits in addition to per-API-key limits:
| Zone | Rate | Applies To |
|---|---|---|
| General | 30 requests/s | All REST endpoints |
| Trading | 10 requests/s | Order placement, cancellation, modification |
Session / Handshake
| Limit | Window |
|---|---|
| Per IP | 10 requests/min |
| Per address | 5 requests/min |
HTTP 429 Response
When a rate limit is exceeded, the server returns HTTP 429. The response includes a Retry-After header indicating the number of seconds to wait before retrying.
{
"detail": "Rate limit exceeded. Try again in 5s.",
"retry_after": 5
}| Response Header | Description |
|---|---|
Retry-After | Seconds to wait before retrying (present on all 429 responses) |
Ban Escalation
Clients that repeatedly violate rate limits or fail to back off after receiving 429 responses may be temporarily IP-banned. Ban durations escalate for repeat offenders.
USE WEBSOCKET STREAMS
For real-time data (order book, positions, balances), use WebSocket streams instead of polling REST endpoints. This reduces rate limit pressure and provides lower-latency updates. See the Servers & URLs page for connection details.
HTTP Return Codes
| Code | Description |
|---|---|
200 | Request succeeded. |
400 | Bad request — malformed parameters or validation failure. |
401 | Unauthorized — missing or invalid authentication. |
403 | Forbidden — API key lacks required permissions, or address mismatch. |
404 | Endpoint not found, or resource does not exist. |
409 | Conflict — duplicate order or idempotency violation. |
422 | Unprocessable entity — valid JSON but semantic validation failed. |
429 | Rate limit exceeded. Check Retry-After header. |
500 | Internal server error. Retry with exponential backoff. |
502 | Upstream exchange temporarily unreachable. See retry guidance below. |
503 | Service temporarily unavailable. See retry guidance below. |
Handling 502 / 503 on Order Endpoints
UNKNOWN EXECUTION STATUS
If you receive a 502 or 503 response on an order-related endpoint (/order, /order/cancel, /order/modify, etc.), do NOT assume the order failed. The request may have been processed by the exchange before the timeout occurred.
Correct handling:
- Query your open orders via
GET /user/{address}/openOrdersor the User Data WebSocket stream to verify whether the order was executed. - Only re-submit the order if you have confirmed it was not placed.
- Retry with exponential backoff:
1s → 2s → 4s → 8s(max 30s).
For non-order endpoints (market data, account queries), a 502 or 503 is always safe to retry immediately with backoff.
Error Response Format
All error responses follow a consistent structure:
{
"detail": "Human-readable error message"
}For validation errors (422):
{
"detail": [
{
"loc": ["body", "field_name"],
"msg": "field required",
"type": "value_error.missing"
}
]
}API Key Permissions
Each API key has a set of permissions that control which endpoints it can access:
| Permission | Allows |
|---|---|
read | Account data, balances, positions, order history, market data |
trade | Place, cancel, modify orders; set leverage; close positions |
By default, newly created API keys have read permission only. Add trade permission to enable order placement.
IP Whitelist
API keys can optionally be restricted to specific IP addresses. When an IP whitelist is configured, requests from non-whitelisted IPs will be rejected with HTTP 403.
CORS
The API supports Cross-Origin Resource Sharing (CORS) with the following allowed headers:
AuthorizationContent-TypeX-Requested-WithX-GAIAEX-APIKEYX-GAIAEX-TIMESTAMPX-GAIAEX-SIGNATURE