Authentication — HMAC
Header, query and signature contract for every protected REST call.
Every protected REST request needs three elements:
X-API-KEYheader — your public API key.timestampquery parameter — millisecond Unix epoch.signaturequery parameter — hex-encoded HMAC-SHA256.
Wire contract
HEADER: X-API-KEY: <apiKey>
QUERY: ?<other-params>×tamp=<ms-epoch>&signature=<hex-hmac-sha256>Computing the signature
- Take all query parameters — include
timestamp, excludesignatureitself. - Sort them alphabetically by key — same algorithm as
URLSearchParams.sort(). - URL-encode each value with the standard form encoder, then join:
key1=val1&key2=val2&.... - Run
HMAC_SHA256(secretKey, queryString)and take the hex digest. - Append it as the
signaturequery parameter.
Rules to remember
- Timestamp drift — must be within ±5000 ms of the server clock. NTP sync is mandatory.
- Replay protection — the same
(apiKey, signature)pair can be used once within 60 s. Reusing it returns401. - Body is not signed — POST/PATCH bodies are not part of the signature; integrity is delegated to TLS. HTTPS is mandatory.
- Body content type — POST/PATCH requests must send
Content-Type: application/json.
Worked example — single parameter
secretKey = "abc123secretkey..."
timestamp = 1714123456789
URL = GET /v2/futures/balance?timestamp=1714123456789
queryString = "timestamp=1714123456789"
signature = HMAC_SHA256("abc123secretkey...", "timestamp=1714123456789")
= "a3f1d2e8b4c5..."
Final URL: GET /v2/futures/balance?timestamp=1714123456789&signature=a3f1d2e8b4c5...
Header: X-API-KEY: zd_84444a6e...Worked example — multiple parameters
URL: GET /v2/futures/myTrades?symbol=BTCUSDT&fromId=1234×tamp=1714123456789
Sorted: fromId=1234&symbol=BTCUSDT×tamp=1714123456789
signature: HMAC_SHA256(secret, "fromId=1234&symbol=BTCUSDT×tamp=1714123456789")POST example
The body is sent separately; the signature is computed from the query string only.
POST /v2/orders?timestamp=1714123456789&signature=... HTTP/1.1
X-API-KEY: zd_...
Content-Type: application/json
{"symbol":"BTCUSDT","side":"BUY","type":"LIMIT","quantity":"0.001","price":"30000","clientOrderId":"<uuid>"}IP whitelist
If the API key was created with an IP whitelist, the backend verifies the
originating IP. Behind a reverse proxy (nginx, Cloudflare) the operator can
set TRUST_PROXY so the last trusted hop in X-Forwarded-For is honoured;
otherwise the raw socket IP is used. The check is spoof-resistant — but if your
IP is not on the list, the request returns 403.
Auth error responses
{ "ok": false, "error": "Invalid API key" } // 401
{ "ok": false, "error": "API key expired" } // 401
{ "ok": false, "error": "Invalid or expired timestamp" } // 401 — drift > 5 s
{ "ok": false, "error": "Missing signature" } // 401
{ "ok": false, "error": "Invalid signature" } // 401 — HMAC mismatch
{ "ok": false, "error": "Signature replay detected" } // 401 — second use within 60 s
{ "ok": false, "error": "IP not whitelisted for this API key" } // 403A clock drift of even a few seconds is the single most common reason
authentication fails in production. Run chrony or systemd-timesyncd
on every client host.