General Info & Authentication
Base URL, HMAC-SHA256 authentication, rate limits, HTTP status codes, and request formatting for the GaiaEx API.
SKILL.mdDrop-in Claude Code skill with HMAC-SHA256 signing, /fapi/v1 & /fapi/v2 schemas, and secret-redaction guardrails.
AGENTS.mdDrop-in repository AGENTS.md for OpenAI Codex CLI — tick-size rules, rate-limit budgets, and self-custody boundaries.
AGY.mdGoogle Antigravity (agy) & Gemini agent spec with verified async Python WebSocket + REST execution templates.
llms.txtSingle-URL machine-readable index of all GaiaEx documentation pages, OpenAPI 3.0 schema, and Postman collection.
General API Information
- The base endpoint is:
https://openapi.gaiaex.com - GaiaEx implements the GaiaEx Unified Futures standard under
/fapi/v1/*and/fapi/v2/*. - 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
GETandDELETEendpoints, parameters are sent as query strings. ForPOSTandPUTendpoints, parameters can be sent as query strings orapplication/x-www-form-urlencodedrequest bodies. - Your trading account is automatically resolved server-side from your
X-MBX-APIKEYheader — standard quantitative SDKs (ccxt, Python, TypeScript, Rust) and bots work as a 1:1 drop-in.
Endpoint Security Type
Each endpoint has a security type that determines how you interact with it.
| Security Type | Description |
|---|---|
| NONE | Public market data endpoint. Can be accessed freely with no authentication required. |
| TRADE / USER_DATA (SIGNED) | Endpoint requires the X-MBX-APIKEY header, timestamp, and an HMAC-SHA256 signature parameter. |
| USER_STREAM | Listen-key lifecycle endpoints (/fapi/v1/listenKey) requiring the X-MBX-APIKEY header. |
| SESSION | Mobile/web app endpoints requiring a valid session token (Authorization: Bearer <token>). |
- All trading and account endpoints under
/fapi/v1/*and/fapi/v2/*accept SIGNED (X-MBX-APIKEY+HMAC-SHA256) authentication. - Market data endpoints (
/fapi/v1/ping,/fapi/v1/time,/fapi/v1/exchangeInfo,/fapi/v1/depth,/fapi/v1/klines,/fapi/v1/ticker/24hr) are public (NONE).
Authentication
FROM ZERO TO FIRST TRADE
If you already have USDC or USDT 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 environment variables next to your script (
GAIAEX_API_KEYandGAIAEX_API_SECRET— never a project file; see Security Best Practices). - Sign each request's query string (or form body) with
HMAC-SHA256(api_secret, query_string), append&signature=<hex>, and send theX-MBX-APIKEYheader. - Call
GET /fapi/v2/balance. If it returns your balances, you're trading-ready — callPOST /fapi/v1/orderto place an order.
That's the whole contract. The rest of this page is the precise definition of step 3.
GaiaEx supports two authentication methods:
Method 1: 1:1 GaiaEx 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 build a query string (or form body) that includes timestamp=<ms> (and optional recvWindow=<ms>, default 5000), compute HMAC-SHA256(api_secret, query_string_or_form_body), and append the resulting lowercase hex digest as signature=<hex>. You send your public key in the X-MBX-APIKEY HTTP header. The server resolves your wallet address automatically from the API key, recomputes the HMAC digest, and verifies they match.
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). Existing keys can be listed programmatically via the GET /api-keys endpoint, but creating, updating, and revoking keys all require a passkey step-up and are app-only — they cannot be performed headlessly via API key alone (see the API Keys page). Each signed request requires:
| Component | Location | Description |
|---|---|---|
X-MBX-APIKEY | HTTP Header | Your public API key (32-character hex string) |
timestamp | Query / form param | Current Unix timestamp in milliseconds (e.g., 1712345678000) |
recvWindow | Query / form param (optional) | Validity window in milliseconds (default 5000, max 60000) |
signature | Query / form param | Lowercase hex HMAC-SHA256(api_secret, query_string_or_form_body) |
Signature construction:
payload = query_string_or_form_body # includes timestamp=<ms> and optional recvWindow=<ms>, WITHOUT &signature=...
signature = HMAC-SHA256(api_secret, payload)query_string_or_form_body— all request parameters formatted askey=value&key2=value2...×tamp=1712345678000in the exact order sent on the wire.- Append
&signature=<hex>as the final parameter.
Example (Python):
import hmac
import hashlib
import time
import os
import requests
api_key = os.environ["GAIAEX_API_KEY"]
api_secret = os.environ["GAIAEX_API_SECRET"]
timestamp = int(time.time() * 1000)
query_string = f"timestamp={timestamp}&recvWindow=5000"
signature = hmac.new(
api_secret.encode("utf-8"),
query_string.encode("utf-8"),
hashlib.sha256,
).hexdigest()
headers = {
"X-MBX-APIKEY": api_key,
}
resp = requests.get(
f"https://openapi.gaiaex.com/fapi/v2/balance?{query_string}&signature={signature}",
headers=headers,
)
print(resp.json())Example (curl):
TIMESTAMP=$(date +%s000)
QUERY="timestamp=${TIMESTAMP}&recvWindow=5000"
SIGNATURE=$(echo -n "$QUERY" | openssl dgst -sha256 -hmac "$GAIAEX_API_SECRET" | awk '{print $2}')
curl -s -H "X-MBX-APIKEY: $GAIAEX_API_KEY" \
"https://openapi.gaiaex.com/fapi/v2/balance?${QUERY}&signature=${SIGNATURE}"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 and query string into your own code, you must get the exact same signature shown here. If you don't, your implementation has a bug — most likely in parameter ordering or URL encoding.
Example 1: GET Futures Balance (GET /fapi/v2/balance)
Goal: query your USDⓈ-M Futures account balances (wallet address is resolved automatically from X-MBX-APIKEY).
| Step | Value |
|---|---|
| 1. Endpoint | GET /fapi/v2/balance |
| 2. Timestamp (ms) | 1712345678000 |
| 3. RecvWindow (ms) | 5000 |
| 4. Query string to sign | timestamp=1712345678000&recvWindow=5000 |
| 5. API Secret (example) | my_secret_key_example_32chars_xx |
| 6. HMAC-SHA256 hex digest | 70e828ec6d529dbbe924f63c84d33fa273a88c23f5674e62740e7f20efb6ab6d |
Example response (HTTP 200):
[
{
"accountAlias": "gaiaex-usdc",
"asset": "USDC",
"balance": "1523.47",
"crossWalletBalance": "1523.47",
"crossUnPnl": "18.92",
"availableBalance": "892.10",
"updateTime": 1712345679123
},
{
"accountAlias": "gaiaex-usdt",
"asset": "USDT",
"balance": "500.00",
"crossWalletBalance": "500.00",
"crossUnPnl": "0.00",
"availableBalance": "500.00",
"updateTime": 1712345679123
}
]import hmac, hashlib, time, os, requests
# Load from environment variables (recommended)
api_key = os.environ["GAIAEX_API_KEY"]
api_secret = os.environ["GAIAEX_API_SECRET"]
# Step 1-4: Build query string with timestamp
timestamp = int(time.time() * 1000)
query_string = f"timestamp={timestamp}&recvWindow=5000"
print(f"Query to sign: {query_string}")
# Step 5-6: Compute HMAC-SHA256
signature = hmac.new(
api_secret.encode("utf-8"),
query_string.encode("utf-8"),
hashlib.sha256,
).hexdigest()
print(f"Signature: {signature}")
# Step 7: Send request with X-MBX-APIKEY
resp = requests.get(
f"https://openapi.gaiaex.com/fapi/v2/balance?{query_string}&signature={signature}",
headers={"X-MBX-APIKEY": api_key},
)
print(f"Status: {resp.status_code}")
print(f"Response: {resp.json()}")Example 2: POST Place Order (POST /fapi/v1/order)
Goal: place a limit buy order for 0.1 ETHUSDC at $3,500.00.
THE PAYLOAD YOU SIGN MUST MATCH THE WIRE ORDER
The signature is computed over the exact query string (or form-encoded body) you send, excluding &signature=.... Do not reorder parameters or alter number formatting between signing and sending.
| Step | Value |
|---|---|
| 1. Endpoint | POST /fapi/v1/order |
| 2. Timestamp (ms) | 1712345678000 |
| 3. Query / form payload | symbol=ETHUSDC&side=BUY&type=LIMIT&timeInForce=GTC&quantity=0.1&price=3500.00×tamp=1712345678000&recvWindow=5000 |
| 4. API Secret (example) | my_secret_key_example_32chars_xx |
| 5. HMAC-SHA256 hex digest | 0dbdfdba87e6d08bd8f2e5c1a6f131e4863e39eb1c11425cf98cb9eb036af18d |
Example response (HTTP 200):
{
"orderId": 41298374,
"symbol": "ETHUSDC",
"status": "NEW",
"clientOrderId": "bot-a1b2c3",
"price": "3500.00",
"avgPrice": "0.00",
"origQty": "0.1",
"executedQty": "0.0",
"cumQuote": "0.00",
"timeInForce": "GTC",
"type": "LIMIT",
"reduceOnly": false,
"side": "BUY",
"PositionSide": "BOTH",
"updateTime": 1712345679456
}Common rejection (HTTP 401, signature mismatch):
{"code": -1022, "msg": "Signature for this request is not valid."}If you see this, the server computed a different digest than yours. Check, in order: (1) the parameter string you signed matches the exact parameter order sent on the wire; (2) &signature=... was not included in the signed input; (3) timestamp is in milliseconds (not seconds) and within recvWindow of the server clock.
import hmac, hashlib, time, os, urllib.parse, requests
api_key = os.environ["GAIAEX_API_KEY"]
api_secret = os.environ["GAIAEX_API_SECRET"]
params = {
"symbol": "ETHUSDC",
"side": "BUY",
"type": "LIMIT",
"timeInForce": "GTC",
"quantity": "0.1",
"price": "3500.00",
"timestamp": int(time.time() * 1000),
"recvWindow": 5000,
}
query_string = urllib.parse.urlencode(params)
signature = hmac.new(
api_secret.encode("utf-8"),
query_string.encode("utf-8"),
hashlib.sha256,
).hexdigest()
resp = requests.post(
f"https://openapi.gaiaex.com/fapi/v1/order?{query_string}&signature={signature}",
headers={"X-MBX-APIKEY": api_key},
)
print(resp.json())COMMON SIGNING MISTAKES
- Parameter reordering — Sign the exact URL-encoded query string you send on the wire; do not let your HTTP client re-sort parameters after signing.
- Signing the full URL or path — For
/fapi/v1&/fapi/v2, sign ONLY the query string / form body (e.g.,symbol=ETHUSDC&...×tamp=...), NOT the URL path. - Clock skew — The server rejects requests outside
recvWindow(default 5000ms). CallGET /fapi/v1/timeto measure clock offset. - Using seconds instead of milliseconds —
timestampmust be 13-digit Unix epoch milliseconds.
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 POST /fapi/v1/order, PUT /fapi/v1/order, and DELETE /fapi/v1/order. 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 /fapi/v1/openOrders— does the order already exist? - Fetch
GET /fapi/v1/allOrders— was it filled or canceled? - Only if neither shows the order should you place a replacement.
Idempotency via newClientOrderId
Pass a unique ASCII string (max 64 chars) as newClientOrderId in POST /fapi/v1/order. The server treats duplicate client order IDs from the same account 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 = fapi_request("POST", "/fapi/v1/order", {
"symbol": "BTCUSDC",
"side": "BUY",
"type": "LIMIT",
"timeInForce": "GTC",
"quantity": "0.001",
"price": "79000",
"newClientOrderId": f"bot-{uuid.uuid4().hex[:24]}", # safe to retry
})Modify order (PUT /fapi/v1/order)
A successful PUT /fapi/v1/order modifies the existing order on the book and returns the active orderId in the response. Always read orderId from the modify response for any subsequent cancel or modify calls.
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
timestampparameter (Unix epoch in milliseconds). - An optional
recvWindowparameter (default5000ms, maximum60000ms) specifies how many milliseconds aftertimestampthe request remains valid. - The server rejects requests where the timestamp is outside the
recvWindowtolerance. - Use
GET /fapi/v1/timeto synchronize clocks if needed.
TIP
Network latency can cause clock drift. If you receive timestamp-related rejections, call GET /fapi/v1/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: POST /fapi/v1/order, DELETE /fapi/v1/order, PUT /fapi/v1/order, DELETE /fapi/v1/allOpenOrders, POST /fapi/v1/leverage, POST /fapi/v1/marginType.
RATE LIMIT VALUES ARE SUBJECT TO CHANGE
The values above reflect current nginx-level configuration. They are not part of the versioned API contract and may be adjusted without notice. Always respect Retry-After headers on 429 responses rather than hard-coding these numbers into your bot logic.
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 |
|---|---|
X-MBX-USED-WEIGHT-1M | Cumulative request weight used by your IP in the current 1-minute window |
X-MBX-USED-WEIGHT | Alias of X-MBX-USED-WEIGHT-1M for GaiaEx SDK compatibility |
X-MBX-ORDER-COUNT-1M | Cumulative order mutations submitted in the current 1-minute window |
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 / signature. |
403 | Forbidden — API key lacks required permissions or IP restriction violated. |
404 | Endpoint not found, or resource does not exist. |
409 | Conflict — duplicate order or idempotency violation. |
422 | Unprocessable entity — valid syntax 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 (POST /fapi/v1/order, DELETE /fapi/v1/order, PUT /fapi/v1/order), 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 /fapi/v1/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
On /fapi/v1/* and /fapi/v2/* endpoints, errors return a GaiaEx-compatible JSON object with code and msg (or detail):
{
"code": -1121,
"msg": "Invalid symbol."
}For schema validation errors (422):
{
"detail": [
{
"loc": ["query", "symbol"],
"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; change margin type |
By default, newly created API keys have read permission only. Add trade permission to enable order placement.
PERMISSIONS ARE NOT AUTOMATICALLY ASSIGNED
The server does not assign a default permission set on your behalf. You must explicitly select the desired permissions when creating a key in the app (read + trade). The recommended configuration for trading bots is read + trade.
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:
X-MBX-APIKEYAuthorizationContent-TypeX-Requested-With