Common Signature Errors
This page lists the mistakes that most often cause signature failures on the futures API, with the wrong and the correct way for each. The signing rules themselves are in Obtain Signature.
💡 Fastest way to find the problem
- Run your signing code with the test vectors and check that you get exactly the same
originalandsignature.- Print the
originalstring for the failing request and compare it with what is actually sent on the wire: path, query, body andvalidate-timestampmust match character by character.
Error messages
| Response | Meaning | Check |
|---|---|---|
HTTP 403 {"code":"403","msg":"invalid signature"} | The server built a different string to sign than you did | Items 1 – 11 below |
HTTP 403 ... The request time <t> exceeds the specified time <now> limit by <n> second | validate-timestamp is outside the recvwindow | Item 10 |
HTTP 400 appkey <key> not found | The API key does not exist | Key copied correctly; sandbox and production keys are different |
HTTP 400 Absence of [appKey] in client request is prohibited | Header validate-appkey missing | Header names and spelling |
HTTP 400 Absence of [validate-timestamp] in client request is prohibited | Header validate-timestamp missing | Header names and spelling |
HTTP 400 with a JSON parse message (for example Expected BEGIN_OBJECT but was STRING) | Header validate-signversion was sent | Item 1 |
HTTP 400 For input string: "..." | validate-timestamp or validate-recvwindow is not a number | Send plain integers |
HTTP 403 The client source IP address is not configured | The API key is bound to IP addresses and the request came from another IP | IP whitelist of the API key |
HTTP 403 builder code illegal | The API key is not correctly configured | Contact support |
HTTP 200 {"result":{"openapiDocs":...}} | The path does not exist | Item 9 |
| HTTP 403 with an HTML page instead of JSON | Blocked before reaching the API (CDN / firewall) | Send a User-Agent header; do not call through a proxy |
1. Sending the validate-signversion header
validate-signversion is not supported. Requests that carry it fail with invalid signature or with HTTP 400, however they are signed.
❌ validate-signversion: 2
✅ no validate-signversion header; sign the body exactly as sent (see Obtain Signature)
2. The HTTP library re-serializes the body
You sign one JSON string, but the library sends a different one. Python requests with json= is the most common case: it serializes again with spaces after , and :.
# ❌ signed '{"symbol":"btc_usdt","price":"70000"}' but sent '{"symbol": "btc_usdt", "price": "70000"}'
body = json.dumps(payload, separators=(",", ":"))
sign(... + "#" + body)
requests.post(url, json=payload, headers=headers)
# ✅ send the exact string that was signed
requests.post(url, data=body.encode("utf-8"), headers=headers)
The same applies to any client that takes an object and serializes it itself (for example a Java RestTemplate or WebClient given a POJO or Map): build the JSON string once and pass that string as the body.
3. Signing something other than the body that is sent
❌ signing str(payload) → {'symbol': 'btc_usdt', 'price': None} (Python repr, not JSON)
❌ sorting keys for the signature only → signed {"orderSide":..,"symbol":..}, sent {"symbol":..,"orderSide":..}
❌ building the body twice → two serializations can differ in spacing, key order or number format
✅ serialize once → sign that string → send that string
The body is not sorted by the server. Any key order works, as long as the signed string and the sent string are identical.
4. Numbers, precision and decimal places
The body and query are signed as text, so the number must be written in exactly the same way in the signed string and in the request.
❌ signed "price":82000 sent "price":82000.0 (int vs float formatting)
❌ signed "origQty":0.3 sent "origQty":0.30000000000000004 (0.1 + 0.2 in floating point)
❌ signed "price":"82000.00" sent "price":"82000" (rounded/formatted for one side only)
❌ query: signed price=82000 sent ?price=82000.0
✅ pass prices and quantities as strings, e.g. "price":"82000.00", "origQty":"0.001",
and use that one string everywhere
Strings also keep the exact decimal places you intend and avoid floating-point rounding before the order reaches the exchange.
5. Form body (application/x-www-form-urlencoded)
A form body is signed exactly as sent: not sorted, and not URL-decoded.
sent body : clientOrderId=a%3Ab&symbol=btc_usdt&orderSide=BUY
❌ signed : orderSide=BUY&symbol=btc_usdt&clientOrderId=a:b (sorted and decoded)
✅ signed : clientOrderId=a%3Ab&symbol=btc_usdt&orderSide=BUY (identical to the body)
6. Empty body
If you send a body, it is part of the string to sign, even when it is {}.
sent body : {}
❌ original : validate-appkey=..&validate-timestamp=..#/az/future/...
✅ original : validate-appkey=..&validate-timestamp=..#/az/future/...#{}
7. Using the spot signing rules
Futures has no method segment and only two items in the header part.
❌ ...#POST#/az/future/trade/v1/order/create#{...}
❌ validate-algorithms=HmacSHA256&validate-appkey=..&validate-recvwindow=5000&validate-timestamp=..#/az/future/...
✅ validate-appkey=..&validate-timestamp=..#/az/future/trade/v1/order/create#{...}
8. Query string problems
The query segment is built from the decoded values, sorted by key in ASCII order.
request URL : ...?symbol=btc_usdt&Limit=10&endTime=1700000000000
❌ symbol=btc_usdt&Limit=10&endTime=1700000000000 (not sorted)
❌ endTime=1700000000000&Limit=10&symbol=btc_usdt (case-insensitive sort)
✅ Limit=10&endTime=1700000000000&symbol=btc_usdt (ASCII: upper case before lower case)
request URL : ...?symbols=btc_usdt%2Ceth_usdt
❌ symbols=btc_usdt%2Ceth_usdt (encoded value)
✅ symbols=btc_usdt,eth_usdt (decoded value)
- A literal
+in the URL is decoded as a space. Encode+as%2Bin the URL and sign the+. - If a key appears more than once, only the first value is used — avoid repeated keys.
- Always send
key=value; a key without=(?flag) cannot be signed reliably. - No query parameters → no query segment and no extra
#.
9. Path problems
The path in the string to sign must be the path that is actually requested.
❌ https://f-api.azverse.xyz/az/future/trade/v1/order/create (host included)
❌ /future/trade/v1/order/create (missing /az prefix — the request does not reach the API)
❌ signed /az/future/.../create requested /az/future/.../create/ (trailing slash differs)
✅ /az/future/trade/v1/order/create
10. Timestamp problems
❌ validate-timestamp: 1791541985 (seconds, 10 digits) → 403 The request time ... exceeds ...
❌ generating the timestamp twice: one value in the header, another in the signed string → 403 invalid signature
❌ local clock off by more than the recvwindow
✅ one 13-digit millisecond timestamp, used in both the header and the string to sign;
keep the system clock synchronized (NTP); optionally raise validate-recvwindow (values above 60000 count as 60000)
11. Key and output format
❌ Base64 output → output must be hex (64 characters for HmacSHA256)
❌ secret key with a trailing space or newline (copied from a file or console)
❌ secret key hex-decoded into bytes → use the secret key string as-is (UTF-8)
❌ sandbox key used on production, or the secret of another API key
❌ validate-algorithms: HmacSHA512 sent, but HMAC-SHA256 computed → the header and the algorithm must match
✅ hex( HMAC-SHA256( secretKey, original ) )