Skip to main content

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 of AUTH_103.

Spot (this page)Futures
String to signX#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 stringYes (upper case)No
Required headersalgorithms, appkey, recvwindow, timestamp, signatureappkey, 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.

HeaderDescription
validate-algorithmsSignature algorithm: HmacSHA256 (recommended), HmacSHA1, HmacSHA224, HmacSHA384, HmacSHA512, HmacMD5
validate-appkeyYour API key
validate-recvwindowValidity window in milliseconds, from 5000 to 60000
validate-timestampUnix time in milliseconds (13 digits) when the request is sent
validate-signatureThe 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=value with &. Use the un-encoded values (the server signs the decoded value; a%2Cb on the wire is signed as a,b). If a key appears more than once, only the first value counts. Always send parameters as key=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 hasString to sign
no query, no bodyX#METHOD#path
query onlyX#METHOD#path#query
body onlyX#METHOD#path#body
query and bodyX#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, set AZ_HOST=https://s-api.azverse.xyz and adjust price / quantity first. 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.