Signature generation
⚠️ Spot and futures sign differently
This page describes the spot API (
s-api). The futures API (f-api) uses a different string to sign — see Futures: Obtain Signature. Reusing futures signing code for spot (or the other way round) is the most common cause ofAUTH_103.
| Spot (this page) | Futures | |
|---|---|---|
| String to sign | X#METHOD#path[#query][#body] | X#path[#query][#body] |
| X (header part) | validate-algorithms=..&validate-appkey=..&validate-recvwindow=..&validate-timestamp=.. (4 items) | validate-appkey=..&validate-timestamp=.. (2 items) |
| HTTP method in the string | Yes (upper case) | No |
| Required headers | algorithms, appkey, recvwindow, timestamp, signature | appkey, timestamp, signature |
The string to sign does not include the host, so both production domains use the same signature.
1. Request headers
All five headers are required.
| Header | Description |
|---|---|
| validate-algorithms | Signature algorithm: HmacSHA256 (recommended), HmacSHA1, HmacSHA224, HmacSHA384, HmacSHA512, HmacMD5 |
| validate-appkey | Your API key |
| validate-recvwindow | Validity window in milliseconds, from 5000 to 60000 |
| validate-timestamp | Unix time in milliseconds (13 digits) when the request is sent |
| validate-signature | The signature computed below |
The request is rejected (AUTH_105) if the difference between validate-timestamp and server time is not smaller than validate-recvwindow.
2. Build the string to sign
original = X + "#" + METHOD + "#" + path [+ "#" + query] [+ "#" + body]
X = "validate-algorithms=" + algorithms + "&validate-appkey=" + appkey
+ "&validate-recvwindow=" + recvwindow + "&validate-timestamp=" + timestamp
- X — exactly these four items in this order, with the same values as the headers you send.
- METHOD — the HTTP method in upper case:
GET,POST,DELETE,PUT. - path — the request path exactly as sent, starting with
/az/spot/..., without host and without?. Example:/az/spot/order. - query — only if the URL has query parameters: sort the parameters by key in ASCII order (upper case before lower case) and join as
key=valuewith&. Use the un-encoded values (the server signs the decoded value;a%2Cbon the wire is signed asa,b). If a key appears more than once, only the first value counts. Always send parameters askey=value(no key without=). - body — only if the request has a body: the body string exactly as sent, byte for byte. Nothing is sorted, re-formatted or URL-decoded. An empty object
{}is a body and must be appended. - Segments that are absent are left out together with their
#— never end the string with#.
| Request has | String to sign |
|---|---|
| no query, no body | X#METHOD#path |
| query only | X#METHOD#path#query |
| body only | X#METHOD#path#body |
| query and body | X#METHOD#path#query#body |
3. Compute the signature
signature = hex( HMAC-<algorithm>( key = secretKey, message = original ) )
Use the algorithm named in validate-algorithms and the secret key string as-is (UTF-8). Output lower-case or upper-case hex (not Base64). Put it in the validate-signature header.
4. Test vectors
Use these values to check your implementation. Your code must produce exactly the same original and signature.
appKey : 3976eb88-76d0-4f6e-a6b2-a57980770085
secretKey : bc6630d0231fda5cd98794f52c4998659beda290
timestamp : 1641446237201
recvwindow : 5000
algorithms : HmacSHA256
GET with query — GET /az/spot/balance?currency=usdt
original : validate-algorithms=HmacSHA256&validate-appkey=3976eb88-76d0-4f6e-a6b2-a57980770085&validate-recvwindow=5000&validate-timestamp=1641446237201#GET#/az/spot/balance#currency=usdt
signature : 7242659cfd6867b7b64dea781c40af20aba45082e9194caeb0c55ae96dbd8f1c
POST with JSON body — POST /az/spot/order
body : {"symbol":"btc_usdt","side":"BUY","type":"LIMIT","timeInForce":"GTC","bizType":"SPOT","price":"70000","quantity":"0.001"}
original : validate-algorithms=HmacSHA256&validate-appkey=3976eb88-76d0-4f6e-a6b2-a57980770085&validate-recvwindow=5000&validate-timestamp=1641446237201#POST#/az/spot/order#{"symbol":"btc_usdt","side":"BUY","type":"LIMIT","timeInForce":"GTC","bizType":"SPOT","price":"70000","quantity":"0.001"}
signature : 7def8177a2f43efd55b539f6c7366d09cd7ec976728317249e64685284258713
5. Code examples
Each example queries the balance (GET with query) and places a limit order (POST with JSON body). Set the environment variables AZ_API_KEY and AZ_SECRET_KEY first. Every example prints original so you can compare it with the rules above.
⚠️ The examples place a real order
They default to the sandbox (
https://s-api.az-qa2.xyz); sandbox and production API keys are different. To run against production, setAZ_HOST=https://s-api.azverse.xyzand adjustprice/quantityfirst. The order is post-only (GTX), so it never fills immediately; cancel it afterwards.
The key rule in all of them: serialize the body once, sign that string, and send that same string.
Python
import hashlib
import hmac
import json
import os
import time
from urllib.parse import urlencode
import requests
HOST = os.getenv("AZ_HOST", "https://s-api.az-qa2.xyz") # sandbox; production: https://s-api.azverse.xyz
API_KEY = os.environ["AZ_API_KEY"]
SECRET_KEY = os.environ["AZ_SECRET_KEY"]
def signed_request(method, path, params=None, payload=None):
timestamp = str(int(time.time() * 1000)) # milliseconds (13 digits)
recv_window = "5000" # 5000 ~ 60000
# Spot: header part has 4 items in this fixed order, followed by #METHOD#path
original = (f"validate-algorithms=HmacSHA256&validate-appkey={API_KEY}"
f"&validate-recvwindow={recv_window}&validate-timestamp={timestamp}"
f"#{method.upper()}#{path}")
query = ""
if params:
items = sorted(params.items()) # sort by key (ASCII order)
original += "#" + "&".join(f"{k}={v}" for k, v in items) # un-encoded values
query = "?" + urlencode(items) # URL-encoded on the wire
body = None
if payload is not None:
body = json.dumps(payload, separators=(",", ":")) # serialize ONCE
original += "#" + body # sign exactly the string you send
signature = hmac.new(SECRET_KEY.encode(), original.encode(), hashlib.sha256).hexdigest()
print("original :", original)
headers = {
"Content-Type": "application/json",
"validate-algorithms": "HmacSHA256",
"validate-appkey": API_KEY,
"validate-recvwindow": recv_window,
"validate-timestamp": timestamp, # the SAME timestamp used in `original`
"validate-signature": signature,
}
# send the signed string itself (as UTF-8 bytes), NOT json=payload (requests would re-serialize it)
data = None if body is None else body.encode("utf-8")
resp = requests.request(method, HOST + path + query, headers=headers, data=data)
return resp.json()
# GET with query parameters
print(signed_request("GET", "/az/spot/balance", params={"currency": "usdt"}))
# POST with JSON body: place a post-only limit order
print(signed_request("POST", "/az/spot/order", payload={
"symbol": "btc_usdt",
"side": "BUY",
"type": "LIMIT",
"timeInForce": "GTX", # post-only: never fills immediately
"bizType": "SPOT",
"price": "70000", # pass decimals as strings to keep their exact text
"quantity": "0.001",
}))
JavaScript (Node.js 18+)
// Node.js 18+ (built-in fetch and crypto). Save as sign.mjs and run: node sign.mjs
import crypto from 'node:crypto';
const HOST = process.env.AZ_HOST || 'https://s-api.az-qa2.xyz'; // sandbox; production: https://s-api.azverse.xyz
const API_KEY = process.env.AZ_API_KEY;
const SECRET_KEY = process.env.AZ_SECRET_KEY;
async function signedRequest(method, path, params, payload) {
const timestamp = String(Date.now()); // milliseconds (13 digits)
const recvWindow = '5000'; // 5000 ~ 60000
// Spot: header part has 4 items in this fixed order, followed by #METHOD#path
let original = `validate-algorithms=HmacSHA256&validate-appkey=${API_KEY}`
+ `&validate-recvwindow=${recvWindow}&validate-timestamp=${timestamp}`
+ `#${method.toUpperCase()}#${path}`;
let query = '';
if (params && Object.keys(params).length) {
const keys = Object.keys(params).sort(); // sort by key (ASCII order)
original += '#' + keys.map((k) => `${k}=${params[k]}`).join('&'); // un-encoded values
query = '?' + keys.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`).join('&');
}
let body;
if (payload !== undefined) {
body = JSON.stringify(payload); // serialize ONCE
original += '#' + body; // sign exactly the string you send
}
const signature = crypto.createHmac('sha256', SECRET_KEY).update(original).digest('hex');
console.log('original :', original);
const resp = await fetch(HOST + path + query, {
method,
headers: {
'Content-Type': 'application/json',
'validate-algorithms': 'HmacSHA256',
'validate-appkey': API_KEY,
'validate-recvwindow': recvWindow,
'validate-timestamp': timestamp, // the SAME timestamp used in `original`
'validate-signature': signature,
},
body, // the signed string itself
});
return resp.json();
}
// GET with query parameters
console.log(await signedRequest('GET', '/az/spot/balance', { currency: 'usdt' }));
// POST with JSON body: place a post-only limit order
console.log(await signedRequest('POST', '/az/spot/order', null, {
symbol: 'btc_usdt',
side: 'BUY',
type: 'LIMIT',
timeInForce: 'GTX', // post-only: never fills immediately
bizType: 'SPOT',
price: '70000', // pass decimals as strings to keep their exact text
quantity: '0.001',
}));
Java (11+)
// Java 11+ (no third-party dependency). Save as Sign.java and run: java Sign.java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class Sign {
static final String HOST = System.getenv().getOrDefault("AZ_HOST", "https://s-api.az-qa2.xyz"); // sandbox; production: https://s-api.azverse.xyz
static final String API_KEY = System.getenv("AZ_API_KEY");
static final String SECRET_KEY = System.getenv("AZ_SECRET_KEY");
static final HttpClient HTTP = HttpClient.newHttpClient();
static String hmacSha256Hex(String secret, String data) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
StringBuilder hex = new StringBuilder();
for (byte b : mac.doFinal(data.getBytes(StandardCharsets.UTF_8))) {
hex.append(String.format("%02x", b));
}
return hex.toString();
}
static String signedRequest(String method, String path, Map<String, String> params, String body) throws Exception {
String timestamp = String.valueOf(System.currentTimeMillis()); // milliseconds (13 digits)
String recvWindow = "5000"; // 5000 ~ 60000
// Spot: header part has 4 items in this fixed order, followed by #METHOD#path
StringBuilder original = new StringBuilder("validate-algorithms=HmacSHA256&validate-appkey=" + API_KEY
+ "&validate-recvwindow=" + recvWindow + "&validate-timestamp=" + timestamp
+ "#" + method.toUpperCase() + "#" + path);
String query = "";
if (params != null && !params.isEmpty()) {
TreeMap<String, String> sorted = new TreeMap<>(params); // sort by key (ASCII order)
original.append("#").append(sorted.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue()) // un-encoded values
.collect(Collectors.joining("&")));
query = "?" + sorted.entrySet().stream()
.map(e -> URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8) + "="
+ URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8).replace("+", "%20"))
.collect(Collectors.joining("&"));
}
if (body != null) {
original.append("#").append(body); // sign exactly the string you send
}
String signature = hmacSha256Hex(SECRET_KEY, original.toString());
System.out.println("original : " + original);
HttpRequest request = HttpRequest.newBuilder(URI.create(HOST + path + query))
.header("Content-Type", "application/json")
.header("validate-algorithms", "HmacSHA256")
.header("validate-appkey", API_KEY)
.header("validate-recvwindow", recvWindow)
.header("validate-timestamp", timestamp) // the SAME timestamp used in `original`
.header("validate-signature", signature)
.method(method, body == null ? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
.build();
return HTTP.send(request, HttpResponse.BodyHandlers.ofString()).body();
}
public static void main(String[] args) throws Exception {
// GET with query parameters
System.out.println(signedRequest("GET", "/az/spot/balance", Map.of("currency", "usdt"), null));
// POST with JSON body: place a post-only limit order (GTX never fills immediately).
// Build the JSON string ONCE (by hand or with your JSON library) and use it for both signing and sending.
String body = "{\"symbol\":\"btc_usdt\",\"side\":\"BUY\",\"type\":\"LIMIT\",\"timeInForce\":\"GTX\","
+ "\"bizType\":\"SPOT\",\"price\":\"70000\",\"quantity\":\"0.001\"}";
System.out.println(signedRequest("POST", "/az/spot/order", null, body));
}
}
curl + openssl (bash)
#!/usr/bin/env bash
# Requires: bash, openssl, curl. Export AZ_API_KEY / AZ_SECRET_KEY first.
HOST="${AZ_HOST:-https://s-api.az-qa2.xyz}" # sandbox; production: https://s-api.azverse.xyz
METHOD="POST"
PATH_="/az/spot/order"
BODY='{"symbol":"btc_usdt","side":"BUY","type":"LIMIT","timeInForce":"GTX","bizType":"SPOT","price":"70000","quantity":"0.001"}'
RECV=5000 # 5000 ~ 60000
TS=$(( $(date +%s) * 1000 )) # milliseconds (13 digits)
# Spot: header part has 4 items in this fixed order, followed by #METHOD#path
ORIGINAL="validate-algorithms=HmacSHA256&validate-appkey=${AZ_API_KEY}&validate-recvwindow=${RECV}&validate-timestamp=${TS}#${METHOD}#${PATH_}#${BODY}"
SIGNATURE=$(printf '%s' "$ORIGINAL" | openssl dgst -sha256 -hmac "$AZ_SECRET_KEY" | awk '{print $NF}')
echo "original : $ORIGINAL"
curl -sS -X "$METHOD" "${HOST}${PATH_}" \
-H 'Content-Type: application/json' \
-H 'validate-algorithms: HmacSHA256' \
-H "validate-appkey: ${AZ_API_KEY}" \
-H "validate-recvwindow: ${RECV}" \
-H "validate-timestamp: ${TS}" \
-H "validate-signature: ${SIGNATURE}" \
--data-raw "$BODY"
echo
6. Still getting AUTH_103?
Print your original string and compare it segment by segment with the rules above, then go through Common Signature Errors.