Authentication
Private endpoints are authenticated with an API key. Every request carries your key id, a millisecond nonce and an HMAC-SHA256 signature in three headers.
Create an API key
- Turn on two-factor authentication (2FA) in your EX5 account. API keys cannot be created without it.
- Open exchange.ex5.com/profile/api-keys and create a key. You confirm with a code from your authenticator app.
- Copy the secret immediately: it is shown only once, in the create response. The key id (
kid, 16 hex characters) stays visible in the list.
Keys use the HS256 algorithm. You can disable, re-enable or delete a key at any time; each change asks for a 2FA code.
Treat the secret like a password. A key can call every API endpoint your account may use: there are no per-key permission scopes and no IP allow-list. Withdrawals and internal transfers additionally require a current 2FA code in each request. Turning 2FA off disables all of your keys. Delete keys you no longer use.
Sign a request
| Header | Value |
|---|---|
X-Auth-Apikey | Your key id (kid). |
X-Auth-Nonce | Current Unix time in milliseconds, as a decimal string. It must be within 5 seconds of the server clock, in either direction. |
X-Auth-Signature | Lowercase hex HMAC-SHA256 of the string nonce + kid (the two header values concatenated), keyed with your secret. |
All three headers are required together. The signature covers only the nonce and the key id — not the method, path, query or body — so the same signing function works for every endpoint. Send increasing nonces, and send request bodies as JSON.
Worked example
These credentials are fake; use them to test your implementation:
kid = 0000aaaa1111bbbb
secret = ffff0000eeee1111dddd2222cccc3333
nonce = 1790530000000
payload = nonce + kid = "17905300000000000aaaa1111bbbb"
signature = hex(HMAC-SHA256(key = secret, message = payload))
= 958c259f2ce8e4d0453d1563cc5825bd63a984d3ae1b6892da770d58c2aaba15Python
import hashlib
import hmac
import time
import requests
API_KEY = "your-api-key-id" # "kid", shown in the API keys list
API_SECRET = "your-api-secret" # shown only once, when the key is created
BASE = "https://exchange.ex5.com"
def ex5_request(method, path, body=None, params=None):
nonce = str(int(time.time() * 1000)) # Unix time in milliseconds
signature = hmac.new(API_SECRET.encode(), (nonce + API_KEY).encode(), hashlib.sha256).hexdigest()
headers = {
"X-Auth-Apikey": API_KEY,
"X-Auth-Nonce": nonce,
"X-Auth-Signature": signature,
}
r = requests.request(method, BASE + path, params=params, json=body, headers=headers, timeout=10)
r.raise_for_status()
return r.json() if r.content else Nonebalances = ex5_request("GET", "/api/v2/peatio/account/balances")
order = ex5_request("POST", "/api/v2/finex/market/orders",
body={"market": "btcusdt", "side": "buy", "type": "limit", "amount": "0.01", "price": "84000"})JavaScript (Node.js 18+)
import { createHmac } from "node:crypto";
const API_KEY = "your-api-key-id"; // "kid", shown in the API keys list
const API_SECRET = "your-api-secret"; // shown only once, when the key is created
const BASE = "https://exchange.ex5.com";
export async function ex5Request(method, path, body, params) {
const nonce = String(Date.now()); // Unix time in milliseconds
const signature = createHmac("sha256", API_SECRET).update(nonce + API_KEY).digest("hex");
const url = new URL(BASE + path);
for (const [k, v] of Object.entries(params ?? {})) url.searchParams.set(k, String(v));
const res = await fetch(url, {
method,
headers: {
"X-Auth-Apikey": API_KEY,
"X-Auth-Nonce": nonce,
"X-Auth-Signature": signature,
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
if (!res.ok) throw new Error(`HTTP ${res.status}: ${text}`);
return text ? JSON.parse(text) : null;
}const balances = await ex5Request("GET", "/api/v2/peatio/account/balances");
const order = await ex5Request("POST", "/api/v2/finex/market/orders",
{ market: "btcusdt", side: "buy", type: "limit", amount: "0.01", price: "84000" });curl and OpenSSL
KID="your-api-key-id"
SECRET="your-api-secret"
NONCE=$(date +%s%3N) # Unix time in milliseconds (GNU date; on macOS use: python3 -c 'import time; print(int(time.time()*1000))')
SIG=$(printf '%s' "$NONCE$KID" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $NF}')
curl -s "https://exchange.ex5.com/api/v2/peatio/account/balances" \
-H "X-Auth-Apikey: $KID" \
-H "X-Auth-Nonce: $NONCE" \
-H "X-Auth-Signature: $SIG"Check your clock
A nonce more than 5 seconds away from the server time is rejected with authz.nonce_expired. Compare your clock with the server:
curl -s "https://exchange.ex5.com/api/v2/barong/public/time"
# {"time":1790530000} ← Unix secondsKeep the machine synchronised with NTP.
Authentication errors
| Status | Error key | Meaning |
|---|---|---|
422 | authz.invalid_api_key_headers | One of the three X-Auth headers is missing or empty. |
401 | authz.unexistent_apikey | Unknown key id. |
401 | authz.nonce_not_valid_timestamp | The nonce is not a positive integer. |
401 | authz.nonce_expired | The nonce is more than 5 seconds away from the server time. |
401 | authz.invalid_signature | The signature does not match. Check the secret and that you signed nonce + kid. |
401 | authz.apikey_not_active | The key is disabled. |
401 | authz.disabled_2fa | 2FA was turned off on the account; keys stop working until it is turned on again. |
401 | authz.invalid_permission | The endpoint is not available to your account. |
401 | authz.missing_csrf_token | A POST, PUT or DELETE request was sent without the X-Auth headers. |
401 | authz.invalid_session | No credentials were sent, or the account is not active. |
Endpoints that need a higher account level answer 403 (for example account.withdraw.not_permitted); see account levels.