Log in to see your API keys
| API Key | Label | Last Used | |
|---|---|---|---|
Required Headers
| Header | Description |
|---|---|
x-api-key | Your client API key. |
x-timestamp | Current time as a millisecond Unix timestamp, sent as a string (e.g. 1755123456789). |
x-nonce | A random, single-use string. A UUID v4 is recommended. |
x-signature | The 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 calculatex-signature.
Building the Signature
1. Build the string to sign
Join five pieces with \n (newline):
METHOD
PATH
TIMESTAMP
NONCE
PAYLOAD
METHOD— HTTP method, uppercasePATH— request path only, no domain or query string (e.g./wallet/transactions)TIMESTAMP/NONCE— same values as the corresponding headersPAYLOAD— 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=valuepairs 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'sjson.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
| Symptom | Likely cause |
|---|---|
| Every request fails | PATH doesn't match server expectations — check version-prefix handling, and that it excludes domain/query string. |
GET works, POST doesn't | Wrong payload canonicalization used for the method. |
| Fails only on some payloads | JSON isn't compact, or keys aren't recursively sorted at every level. |
x-timestamp rejected | Clock skew — sync your system clock (e.g. via NTP). |
x-nonce rejected | Nonce 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.
