Skip to main content

Obtain Signature

⚠️ Futures and spot sign differently

This page describes the futures API (f-api). The spot API (s-api) uses a different string to sign — see Spot: Signature generation. Reusing spot signing code for futures (or the other way round) is the most common cause of invalid signature.

Futures (this page)Spot
String to signX#path[#query][#body]X#METHOD#path[#query][#body]
X (header part)validate-appkey=..&validate-timestamp=.. (2 items)validate-algorithms=..&validate-appkey=..&validate-recvwindow=..&validate-timestamp=.. (4 items)
HTTP method in the stringNoYes (upper case)
Required headersappkey, timestamp, signaturealgorithms, appkey, recvwindow, timestamp, signature

The string to sign does not include the host, so both production domains use the same signature.

1. Request headers​

HeaderRequiredDescription
validate-appkeyYesYour API key
validate-timestampYesUnix time in milliseconds (13 digits) when the request is sent
validate-signatureYesThe signature computed below
validate-recvwindowNoValidity window in milliseconds. Default 5000; larger values are treated as 60000. Not part of the string to sign
validate-algorithmsNoSignature algorithm, default HmacSHA256 (recommended: leave the header out). If you send it, sign with that algorithm; an unrecognized name is treated as HmacSHA256. Not part of the string to sign

The request is rejected if the difference between validate-timestamp and server time is larger than the recvwindow.

⛔ Do not send validate-signversion

This header is not supported. Requests that carry it fail with invalid signature or HTTP 400.

2. Build the string to sign​

original = X + "#" + path [+ "#" + query] [+ "#" + body]
X = "validate-appkey=" + appkey + "&validate-timestamp=" + timestamp
  • X — exactly these two items in this order, with the header names written without any prefix. Do not add validate-algorithms or validate-recvwindow here even if you send them.
  • path — the request path exactly as sent, starting with /az/future/..., without host and without ?. Example: /az/future/trade/v1/order/create.
  • 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. This applies to both application/json and application/x-www-form-urlencoded bodies. 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#path
query onlyX#path#query
body onlyX#path#body
query and bodyX#path#query#body

3. Compute the signature​

signature = hex( HMAC-SHA256( key = secretKey, message = original ) )

HMAC-SHA256 is used unless you send validate-algorithms with another supported algorithm. Use 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

GET with query — GET /az/future/user/v1/balance/detail?coin=usdt

original : validate-appkey=3976eb88-76d0-4f6e-a6b2-a57980770085&validate-timestamp=1641446237201#/az/future/user/v1/balance/detail#coin=usdt
signature : 4b9bb7601ae3dd2b1134ee5f8458715c2616fa2a9c0b96fc5899bbe4a5d4e255

POST with JSON body — POST /az/future/trade/v1/order/create

body : {"symbol":"btc_usdt","orderSide":"BUY","orderType":"LIMIT","positionSide":"LONG","origQty":"0.001","price":"70000","timeInForce":"GTC"}
original : validate-appkey=3976eb88-76d0-4f6e-a6b2-a57980770085&validate-timestamp=1641446237201#/az/future/trade/v1/order/create#{"symbol":"btc_usdt","orderSide":"BUY","orderType":"LIMIT","positionSide":"LONG","origQty":"0.001","price":"70000","timeInForce":"GTC"}
signature : 419cf02089398befb70e74e34f7039f40a73baad9421730dab9fa5cc6f8d78af

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://f-api.az-qa2.xyz); sandbox and production API keys are different. To run against production, set AZ_HOST=https://f-api.azverse.xyz and adjust price / origQty 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://f-api.az-qa2.xyz") # sandbox; production: https://f-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)

# Futures: header part has ONLY appkey + timestamp, and there is NO method segment
original = f"validate-appkey={API_KEY}&validate-timestamp={timestamp}#{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-appkey": API_KEY,
"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/future/user/v1/balance/detail", params={"coin": "usdt"}))

# POST with JSON body: place a post-only limit order
print(signed_request("POST", "/az/future/trade/v1/order/create", payload={
"symbol": "btc_usdt",
"orderSide": "BUY",
"orderType": "LIMIT",
"positionSide": "LONG",
"origQty": "0.001", # pass decimals as strings to keep their exact text
"price": "70000",
"timeInForce": "GTX", # post-only: never fills immediately
}))

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://f-api.az-qa2.xyz'; // sandbox; production: https://f-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)

// Futures: header part has ONLY appkey + timestamp, and there is NO method segment
let original = `validate-appkey=${API_KEY}&validate-timestamp=${timestamp}#${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-appkey': API_KEY,
'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/future/user/v1/balance/detail', { coin: 'usdt' }));

// POST with JSON body: place a post-only limit order
console.log(await signedRequest('POST', '/az/future/trade/v1/order/create', null, {
symbol: 'btc_usdt',
orderSide: 'BUY',
orderType: 'LIMIT',
positionSide: 'LONG',
origQty: '0.001', // pass decimals as strings to keep their exact text
price: '70000',
timeInForce: 'GTX', // post-only: never fills immediately
}));

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://f-api.az-qa2.xyz"); // sandbox; production: https://f-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)

// Futures: header part has ONLY appkey + timestamp, and there is NO method segment
StringBuilder original = new StringBuilder("validate-appkey=" + API_KEY + "&validate-timestamp=" + timestamp + "#" + 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-appkey", API_KEY)
.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/future/user/v1/balance/detail", Map.of("coin", "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\",\"orderSide\":\"BUY\",\"orderType\":\"LIMIT\",\"positionSide\":\"LONG\","
+ "\"origQty\":\"0.001\",\"price\":\"70000\",\"timeInForce\":\"GTX\"}";
System.out.println(signedRequest("POST", "/az/future/trade/v1/order/create", 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://f-api.az-qa2.xyz}" # sandbox; production: https://f-api.azverse.xyz
PATH_="/az/future/trade/v1/order/create"
BODY='{"symbol":"btc_usdt","orderSide":"BUY","orderType":"LIMIT","positionSide":"LONG","origQty":"0.001","price":"70000","timeInForce":"GTX"}'
TS=$(( $(date +%s) * 1000 )) # milliseconds (13 digits)

# Futures: header part has ONLY appkey + timestamp, and there is NO method segment
ORIGINAL="validate-appkey=${AZ_API_KEY}&validate-timestamp=${TS}#${PATH_}#${BODY}"
SIGNATURE=$(printf '%s' "$ORIGINAL" | openssl dgst -sha256 -hmac "$AZ_SECRET_KEY" | awk '{print $NF}')
echo "original : $ORIGINAL"

curl -sS -X POST "${HOST}${PATH_}" \
-H 'Content-Type: application/json' \
-H "validate-appkey: ${AZ_API_KEY}" \
-H "validate-timestamp: ${TS}" \
-H "validate-signature: ${SIGNATURE}" \
--data-raw "$BODY"
echo

6. Still getting invalid signature?​

Print your original string and compare it segment by segment with the rules above, then go through Common Signature Errors.