authentication

Log in to see your API keys
API KeyLabelLast Used

Required Headers

HeaderDescription
x-api-keyYour client API key.
x-timestampCurrent time as a millisecond Unix timestamp, sent as a string (e.g. 1755123456789).
x-nonceA random, single-use string. A UUID v4 is recommended.
x-signatureThe HMAC-SHA256 signature of this request, hex-encoded (lowercase).
🔑

Your API secret is never sent in any request. It is only used locally, on your side, to calculate x-signature.

Building the Signature

1. Build the string to sign

Join five pieces with \n (newline):

METHOD
PATH
TIMESTAMP
NONCE
PAYLOAD
  • METHOD — HTTP method, uppercase
  • PATH — request path only, no domain or query string (e.g. /wallet/transactions)
  • TIMESTAMP / NONCE — same values as the corresponding headers
  • PAYLOAD — see below; depends on the HTTP method

2. Build the payload

GET requests — canonicalized query string:

  • Sort query keys lexicographically (A→Z)
  • Join as key=value pairs with &
  • No URL-encoding at this step — use raw values
  • No query parameters → empty string
{ limit: 20, direction: "in" }  →  direction=in&limit=20

POST/PUT/PATCH/DELETE requests — canonicalized JSON body:

  • Recursively sort object keys at every nesting level (array order preserved; recurse into each element)
  • Serialize compactly — no whitespace after : or ,
  • No body, or empty object {} → empty string
{ b: 1, a: { d: 2, c: 3 } }  →  {"a":{"c":3,"d":2},"b":1}
⚠️

Whitespace is the most common cause of signature mismatches. Many JSON libraries add a space after : and , by default (e.g. Python's json.dumps). Your serialization must be compact — see the code examples below.

3. Compute the signature

x-signature = hex(HMAC-SHA256(api_secret, string_to_sign))

Code Examples

Node.js

const crypto = require('crypto');

function sortObjectKeys(obj) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (Array.isArray(obj)) return obj.map(sortObjectKeys);
  return Object.keys(obj)
    .sort()
    .reduce((acc, key) => {
      acc[key] = sortObjectKeys(obj[key]);
      return acc;
    }, {});
}

function canonicalizeQuery(query) {
  if (!query || Object.keys(query).length === 0) return '';
  return Object.keys(query)
    .sort()
    .map((k) => `${k}=${query[k]}`)
    .join('&');
}

function canonicalizeBody(body) {
  if (body === undefined || body === null) return '';
  if (typeof body === 'object' && Object.keys(body).length === 0) return '';
  return JSON.stringify(sortObjectKeys(body));
}

function buildStringToSign({ method, path, timestamp, nonce, query, body }) {
  const upperMethod = String(method).toUpperCase();
  const payload = upperMethod === 'GET' ? canonicalizeQuery(query) : canonicalizeBody(body);
  return [upperMethod, path, timestamp, nonce, payload].join('\n');
}

function sign(secret, stringToSign) {
  return crypto.createHmac('sha256', secret).update(stringToSign, 'utf8').digest('hex');
}

// --- Example: signing a GET request ---
const apiKey = 'YOUR_API_KEY';
const apiSecret = 'YOUR_API_SECRET';

const timestamp = Date.now().toString();
const nonce = crypto.randomUUID();
const method = 'GET';
const path = '/wallet/transactions';
const query = { direction: 'in', page: 1, limit: 20 };

const stringToSign = buildStringToSign({ method, path, timestamp, nonce, query });
const signature = sign(apiSecret, stringToSign);

const headers = {
  'x-api-key': apiKey,
  'x-timestamp': timestamp,
  'x-nonce': nonce,
  'x-signature': signature
};

Python

import json
import time
import uuid
import hmac
import hashlib


def sort_keys(value):
    if isinstance(value, dict):
        return {k: sort_keys(value[k]) for k in sorted(value.keys())}
    if isinstance(value, list):
        return [sort_keys(item) for item in value]
    return value


def canonicalize_query(query):
    if not query:
        return ''
    return '&'.join(f'{k}={query[k]}' for k in sorted(query.keys()))


def canonicalize_body(body):
    if body is None:
        return ''
    if isinstance(body, dict) and len(body) == 0:
        return ''
    # separators=(',', ':') removes the default whitespace json.dumps
    # inserts after ',' and ':' -- required to match the canonical form.
    return json.dumps(sort_keys(body), separators=(',', ':'), ensure_ascii=False)


def build_string_to_sign(method, path, timestamp, nonce, query=None, body=None):
    upper_method = method.upper()
    payload = canonicalize_query(query) if upper_method == 'GET' else canonicalize_body(body)
    return '\n'.join([upper_method, path, timestamp, nonce, payload])


def sign(secret, string_to_sign):
    return hmac.new(
        secret.encode('utf-8'),
        string_to_sign.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()


# --- Example: signing a GET request ---
api_key = 'YOUR_API_KEY'
api_secret = 'YOUR_API_SECRET'

timestamp = str(int(time.time() * 1000))
nonce = str(uuid.uuid4())
method = 'GET'
path = '/wallet/transactions'
query = {'direction': 'in', 'page': 1, 'limit': 20}

string_to_sign = build_string_to_sign(method, path, timestamp, nonce, query=query)
signature = sign(api_secret, string_to_sign)

headers = {
    'x-api-key': api_key,
    'x-timestamp': timestamp,
    'x-nonce': nonce,
    'x-signature': signature,
}

Java

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.InvalidKeyException;
import java.util.*;

public class HmacSigner {

    public static String canonicalizeQuery(Map<String, String> query) {
        if (query == null || query.isEmpty()) return "";
        TreeMap<String, String> sorted = new TreeMap<>(query);
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> e : sorted.entrySet()) {
            if (sb.length() > 0) sb.append('&');
            sb.append(e.getKey()).append('=').append(e.getValue());
        }
        return sb.toString();
    }

    public static String canonicalizeBody(Object body) {
        if (body == null) return "";
        if (body instanceof Map && ((Map<?, ?>) body).isEmpty()) return "";
        return toCanonicalJson(body);
    }

    // Minimal illustrative JSON serializer: sorted keys, no whitespace.
    // For production, prefer a JSON library configured for the same compact,
    // sorted-key output rather than hand-rolling escaping logic.
    @SuppressWarnings("unchecked")
    private static String toCanonicalJson(Object value) {
        if (value == null) return "null";
        if (value instanceof Map) {
            TreeMap<String, Object> sorted = new TreeMap<>((Map<String, Object>) value);
            StringBuilder sb = new StringBuilder("{");
            boolean first = true;
            for (Map.Entry<String, Object> e : sorted.entrySet()) {
                if (!first) sb.append(',');
                first = false;
                sb.append('"').append(e.getKey()).append("\":").append(toCanonicalJson(e.getValue()));
            }
            return sb.append('}').toString();
        }
        if (value instanceof List) {
            List<?> list = (List<?>) value;
            StringBuilder sb = new StringBuilder("[");
            for (int i = 0; i < list.size(); i++) {
                if (i > 0) sb.append(',');
                sb.append(toCanonicalJson(list.get(i)));
            }
            return sb.append(']').toString();
        }
        if (value instanceof String) return "\"" + ((String) value).replace("\"", "\\\"") + "\"";
        return value.toString(); // Number / Boolean
    }

    public static String buildStringToSign(String method, String path, String timestamp,
                                            String nonce, Map<String, String> query, Object body) {
        String upperMethod = method.toUpperCase();
        String payload = "GET".equals(upperMethod) ? canonicalizeQuery(query) : canonicalizeBody(body);
        return String.join("\n", upperMethod, path, timestamp, nonce, payload);
    }

    public static String sign(String secret, String stringToSign)
            throws NoSuchAlgorithmException, InvalidKeyException {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] rawHmac = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
        StringBuilder hex = new StringBuilder();
        for (byte b : rawHmac) hex.append(String.format("%02x", b));
        return hex.toString();
    }

    public static void main(String[] args) throws Exception {
        String apiKey = "YOUR_API_KEY";
        String apiSecret = "YOUR_API_SECRET";

        String timestamp = String.valueOf(System.currentTimeMillis());
        String nonce = UUID.randomUUID().toString();
        String method = "GET";
        String path = "/wallet/transactions";

        Map<String, String> query = new LinkedHashMap<>();
        query.put("direction", "in");
        query.put("page", "1");
        query.put("limit", "20");

        String stringToSign = buildStringToSign(method, path, timestamp, nonce, query, null);
        String signature = sign(apiSecret, stringToSign);

        Map<String, String> headers = new LinkedHashMap<>();
        headers.put("x-api-key", apiKey);
        headers.put("x-timestamp", timestamp);
        headers.put("x-nonce", nonce);
        headers.put("x-signature", signature);
    }
}

Troubleshooting

SymptomLikely cause
Every request failsPATH doesn't match server expectations — check version-prefix handling, and that it excludes domain/query string.
GET works, POST doesn'tWrong payload canonicalization used for the method.
Fails only on some payloadsJSON isn't compact, or keys aren't recursively sorted at every level.
x-timestamp rejectedClock skew — sync your system clock (e.g. via NTP).
x-nonce rejectedNonce reused — always generate a fresh value per request.

Security Notes

  • Never expose your API secret in client-side code, mobile app binaries, or public repositories.
  • Treat signature generation as sensitive logic — anyone who can compute a valid signature can act on your behalf.
  • Rotate your API key/secret pair immediately if you suspect either has been exposed.
Credentials
LoadingLoading…
Response
Click Try It! to start a request and see the response here!

Did this page help you?