$EX5 pre-sale is open — Seed tier · 500M fixed supply · 50% burn · KYC via EX5 ExchangeJOIN →
EX5 API · REST & WEBSOCKET

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

  1. Turn on two-factor authentication (2FA) in your EX5 account. API keys cannot be created without it.
  2. Open exchange.ex5.com/profile/api-keys and create a key. You confirm with a code from your authenticator app.
  3. 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

HeaderValue
X-Auth-ApikeyYour key id (kid).
X-Auth-NonceCurrent Unix time in milliseconds, as a decimal string. It must be within 5 seconds of the server clock, in either direction.
X-Auth-SignatureLowercase 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:

HMAC-SHA256
kid       = 0000aaaa1111bbbb
secret    = ffff0000eeee1111dddd2222cccc3333
nonce     = 1790530000000
payload   = nonce + kid = "17905300000000000aaaa1111bbbb"
signature = hex(HMAC-SHA256(key = secret, message = payload))
          = 958c259f2ce8e4d0453d1563cc5825bd63a984d3ae1b6892da770d58c2aaba15

Python

Python 3 · requests
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 None
Usage
balances = 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+)

JavaScript · fetch
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;
}
Usage
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

bash
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
curl -s "https://exchange.ex5.com/api/v2/barong/public/time"
# {"time":1790530000}   ← Unix seconds

Keep the machine synchronised with NTP.

Authentication errors

StatusError keyMeaning
422authz.invalid_api_key_headersOne of the three X-Auth headers is missing or empty.
401authz.unexistent_apikeyUnknown key id.
401authz.nonce_not_valid_timestampThe nonce is not a positive integer.
401authz.nonce_expiredThe nonce is more than 5 seconds away from the server time.
401authz.invalid_signatureThe signature does not match. Check the secret and that you signed nonce + kid.
401authz.apikey_not_activeThe key is disabled.
401authz.disabled_2fa2FA was turned off on the account; keys stop working until it is turned on again.
401authz.invalid_permissionThe endpoint is not available to your account.
401authz.missing_csrf_tokenA POST, PUT or DELETE request was sent without the X-Auth headers.
401authz.invalid_sessionNo 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.