GaiaExGaiaEx
API

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 GET endpoints, parameters are sent as query strings. For POST, PATCH, and DELETE endpoints, parameters are sent as a JSON request body with Content-Type: application/json.

Endpoint Security Type

Each endpoint has a security type that determines how you interact with it.

Security TypeDescription
NONEEndpoint can be accessed freely. No authentication required.
SIGNEDEndpoint requires HMAC-SHA256 signature via API key headers.
SESSIONEndpoint 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:

  1. Mobile app → Settings → API Keys → Create. Pick read + trade. Copy the api_key and api_secret (the secret is shown only once).
  2. Put them in a config.json next to your script.
  3. Sign each request with HMAC-SHA256 over timestamp + METHOD + path + body and send three headers: X-GAIAEX-APIKEY, X-GAIAEX-TIMESTAMP, X-GAIAEX-SIGNATURE.
  4. Call GET /user/{address}/balance. If it returns your balance, you're trading-ready — POST /order to 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:

HeaderDescription
X-GAIAEX-APIKEYYour public API key (32-character hex string)
X-GAIAEX-TIMESTAMPCurrent Unix timestamp in milliseconds
X-GAIAEX-SIGNATUREHMAC-SHA256 hex digest of the message

Signature construction:

message = timestamp + method + path + body
signature = HMAC-SHA256(api_secret, message)
  • timestamp — the value of X-GAIAEX-TIMESTAMP (string)
  • method — HTTP method in uppercase (GET, POST, DELETE, PATCH)
  • path — request path without the /v1/trade prefix 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.

StepValue
1. Timestamp (ms)1712345678000
2. HTTP MethodGET
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 digest8bb72b649cea0ef7e170cf82d7e7e902279cf8b4fbf73b7248c1eb00a62ddc42

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.

StepValue
1. Timestamp (ms)1712345678000
2. MethodPOST
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. Message1712345678000POST/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 digestc3e85abeacfbb9ef64cfb7163b31d622e1a9744c128be6249e8347479c899158

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

  1. Including /v1/trade in the path — The path in the signature must NOT include the base URL prefix. Use /order, not /v1/trade/order.
  2. Including query string in path — For GET /user/0x.../fills?limit=50, the signing path is /user/0x.../fills without the ?limit=50.
  3. Clock skew — The server rejects timestamps >5s from server time. Call GET /time to measure offset.
  4. 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:

  1. Fetch GET /user/{address}/openOrders — does the order already exist?
  2. Fetch GET /user/{address}/historicalOrders — was it filled or canceled?
  3. 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-TIMESTAMP header.
  • The server rejects requests where the timestamp is more than 5 seconds away from the server time.
  • Use GET /time to 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:

LimitWindowDefault
BurstPer second10 requests/s
RollingPer minute600 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:

ZoneRateApplies To
General30 requests/sAll REST endpoints
Trading10 requests/sOrder placement, cancellation, modification

Session / Handshake

LimitWindow
Per IP10 requests/min
Per address5 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 HeaderDescription
Retry-AfterSeconds 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

CodeDescription
200Request succeeded.
400Bad request — malformed parameters or validation failure.
401Unauthorized — missing or invalid authentication.
403Forbidden — API key lacks required permissions, or address mismatch.
404Endpoint not found, or resource does not exist.
409Conflict — duplicate order or idempotency violation.
422Unprocessable entity — valid JSON but semantic validation failed.
429Rate limit exceeded. Check Retry-After header.
500Internal server error. Retry with exponential backoff.
502Upstream exchange temporarily unreachable. See retry guidance below.
503Service 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:

  1. Query your open orders via GET /user/{address}/openOrders or the User Data WebSocket stream to verify whether the order was executed.
  2. Only re-submit the order if you have confirmed it was not placed.
  3. 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:

PermissionAllows
readAccount data, balances, positions, order history, market data
tradePlace, 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:

  • Authorization
  • Content-Type
  • X-Requested-With
  • X-GAIAEX-APIKEY
  • X-GAIAEX-TIMESTAMP
  • X-GAIAEX-SIGNATURE