Skip to main content

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

  1. Run your signing code with the test vectors and check that you get exactly the same original and signature.
  2. Print the original string for the failing request and compare it with what is actually sent on the wire: path, query, body and validate-timestamp must match character by character.

Error messages​

ResponseMeaningCheck
HTTP 403 {"code":"403","msg":"invalid signature"}The server built a different string to sign than you didItems 1 – 11 below
HTTP 403 ... The request time <t> exceeds the specified time <now> limit by <n> secondvalidate-timestamp is outside the recvwindowItem 10
HTTP 400 appkey <key> not foundThe API key does not existKey copied correctly; sandbox and production keys are different
HTTP 400 Absence of [appKey] in client request is prohibitedHeader validate-appkey missingHeader names and spelling
HTTP 400 Absence of [validate-timestamp] in client request is prohibitedHeader validate-timestamp missingHeader names and spelling
HTTP 400 with a JSON parse message (for example Expected BEGIN_OBJECT but was STRING)Header validate-signversion was sentItem 1
HTTP 400 For input string: "..."validate-timestamp or validate-recvwindow is not a numberSend plain integers
HTTP 403 The client source IP address is not configuredThe API key is bound to IP addresses and the request came from another IPIP whitelist of the API key
HTTP 403 builder code illegalThe API key is not correctly configuredContact support
HTTP 200 {"result":{"openapiDocs":...}}The path does not existItem 9
HTTP 403 with an HTML page instead of JSONBlocked 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 %2B in 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 ) )