ZetariumZetariumDex

Authentication — HMAC

Header, query and signature contract for every protected REST call.

Every protected REST request needs three elements:

  1. X-API-KEY header — your public API key.
  2. timestamp query parameter — millisecond Unix epoch.
  3. signature query parameter — hex-encoded HMAC-SHA256.

Wire contract

HEADER:    X-API-KEY: <apiKey>
QUERY:     ?<other-params>&timestamp=<ms-epoch>&signature=<hex-hmac-sha256>

Computing the signature

  1. Take all query parameters — include timestamp, exclude signature itself.
  2. Sort them alphabetically by key — same algorithm as URLSearchParams.sort().
  3. URL-encode each value with the standard form encoder, then join: key1=val1&key2=val2&....
  4. Run HMAC_SHA256(secretKey, queryString) and take the hex digest.
  5. Append it as the signature query 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 returns 401.
  • 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&timestamp=1714123456789

Sorted:    fromId=1234&symbol=BTCUSDT&timestamp=1714123456789
signature: HMAC_SHA256(secret, "fromId=1234&symbol=BTCUSDT&timestamp=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" }   // 403

A 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.

On this page