跳到主要内容

签名生成

⚠️ 合约与现货的签名方式不同

本页说明的是合约 API(f-api)。现货 API(s-api)使用不同的待签名串,参见现货:签名生成。把现货的签名代码直接用于合约(或反过来),是导致 invalid signature 最常见的原因。

合约(本页)现货
待签名串X#path[#query][#body]X#METHOD#path[#query][#body]
X(请求头部分)validate-appkey=..&validate-timestamp=..(2 项)validate-algorithms=..&validate-appkey=..&validate-recvwindow=..&validate-timestamp=..(4 项)
待签名串中是否包含 HTTP 方法否是(大写)
必填请求头appkey、timestamp、signaturealgorithms、appkey、recvwindow、timestamp、signature

待签名串不包含 host,因此两个生产域名使用相同的签名。

1. 请求头​

请求头必填说明
validate-appkey是你的 API key
validate-timestamp是发送请求时的 Unix 时间戳,单位毫秒(13 位)
validate-signature是按下文计算出的签名
validate-recvwindow否有效时间窗口,单位毫秒。默认 5000;大于 60000 的值按 60000 处理。不参与签名
validate-algorithms否签名算法,默认 HmacSHA256(推荐:不发送该请求头)。如果发送,须使用该算法签名;无法识别的名称按 HmacSHA256 处理。不参与签名

validate-timestamp 与服务器时间之差大于 recvwindow 时,请求会被拒绝。

⛔ 不要发送 validate-signversion

不支持该请求头。携带它的请求会返回 invalid signature 或 HTTP 400。

2. 构造待签名串​

original = X + "#" + path [+ "#" + query] [+ "#" + body]
X = "validate-appkey=" + appkey + "&validate-timestamp=" + timestamp
  • X —— 严格为这两项、按此顺序,请求头名称不加任何前缀。即使你发送了 validate-algorithms 或 validate-recvwindow,也不要把它们加进来。
  • path —— 与实际发送完全一致的请求路径,以 /az/future/... 开头,不含 host,也不含 ?。例如:/az/future/trade/v1/order/create。
  • query —— 仅当 URL 带查询参数时才有:按参数名的 ASCII 顺序排序(大写字母排在小写字母之前),以 key=value 形式用 & 连接。使用未编码的值(服务器对解码后的值签名;线上传输的 a%2Cb 按 a,b 签名)。同一个参数名出现多次时,只取第一个值。参数一律以 key=value 形式发送(不要有不带 = 的参数名)。
  • body —— 仅当请求有请求体时才有:与实际发送完全一致的请求体字符串,逐字节相同。不排序、不重新格式化、不做 URL 解码。application/json 和 application/x-www-form-urlencoded 两种请求体都适用此规则。空对象 {} 也算请求体,必须拼接上。
  • 不存在的段连同它前面的 # 一起省略——待签名串永远不能以 # 结尾。
请求包含待签名串
无 query、无 bodyX#path
仅 queryX#path#query
仅 bodyX#path#body
query 和 bodyX#path#query#body

3. 计算签名​

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

除非你通过 validate-algorithms 指定了其他受支持的算法,否则使用 HMAC-SHA256。secretKey 按原字符串(UTF-8)直接使用。输出小写或大写的 hex(不是 Base64),放入 validate-signature 请求头。

4. 测试向量​

用以下数据检验你的实现。你的代码必须得到完全相同的 original 和 signature。

appKey : 3976eb88-76d0-4f6e-a6b2-a57980770085
secretKey : bc6630d0231fda5cd98794f52c4998659beda290
timestamp : 1641446237201

带 query 的 GET —— 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

带 JSON 请求体的 POST —— 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. 代码示例​

每个示例都会查询余额(带 query 的 GET)并下一笔限价单(带 JSON 请求体的 POST)。请先设置环境变量 AZ_API_KEY 和 AZ_SECRET_KEY。每个示例都会打印 original,便于与上文规则对照。

⚠️ 示例会真实下单

示例默认使用沙盒环境(https://f-api.az-qa2.xyz);沙盒与生产的 API key 不同。如需在生产环境运行,请设置 AZ_HOST=https://f-api.azverse.xyz,并先调整 price / origQty。订单为只做 maker(GTX),不会立即成交;运行后请将其撤销。

所有示例的关键原则:请求体只序列化一次,对这个字符串签名,并发送这同一个字符串。

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. 仍然返回 invalid signature?​

打印你的 original 字符串,与上文规则逐段对照,然后逐项排查签名常见错误。