Common Signature Errors
This page lists the mistakes that most often cause signature failures on the spot API, with the wrong and the correct way for each. The signing rules themselves are in Signature generation.
💡 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: method, path, query, body and allvalidate-*header values must match character by character.
Error codes
Authentication errors are returned with HTTP 200 and "rc":1; the reason is in mc, for example {"rc":1,"mc":"AUTH_103","ma":[],"result":null}.
| mc | Meaning | Check |
|---|---|---|
| AUTH_001 | Header validate-appkey missing | Header names and spelling |
| AUTH_002 | Header validate-timestamp missing | Header names and spelling |
| AUTH_003 | Header validate-recvwindow missing | Item 9 |
| AUTH_004 | validate-recvwindow is not between 5000 and 60000 | Item 9 |
| AUTH_005 | Header validate-algorithms missing | Item 9 |
| AUTH_006 | validate-algorithms is not a supported algorithm name | Item 9 |
| AUTH_007 | Header validate-signature missing | Header names and spelling |
| AUTH_101 | API key does not exist | Key copied correctly; sandbox and production keys are different |
| AUTH_102 | API key is not activated | API key status |
| AUTH_103 | Signature error — the server built a different string to sign than you did | Items 1 – 10 below |
| AUTH_104 | Request from an IP that is not bound to the API key | IP whitelist of the API key |
| AUTH_105 | validate-timestamp is outside the recvwindow | Item 8 |
| AUTH_106 | The API key does not have the permission for this endpoint | API key permissions (for example TRADE, TRANSFER) |
For input string: "..." | validate-timestamp or validate-recvwindow is not a number | Send plain integers |
An HTTP 403 with an HTML page instead of JSON means the request was blocked before reaching the API (CDN / firewall): send a User-Agent header and do not call through a proxy.
1. Using the futures signing rules
Spot needs the method segment and four items in the header part.
❌ validate-appkey=..&validate-timestamp=..#POST#/az/spot/order#{...} (futures-style header part)
❌ validate-algorithms=..&validate-appkey=..&validate-recvwindow=..&validate-timestamp=..#/az/spot/order#{...} (no method)
❌ ...#post#/az/spot/order#{...} (method in lower case)
✅ validate-algorithms=HmacSHA256&validate-appkey=..&validate-recvwindow=5000&validate-timestamp=..#POST#/az/spot/order#{...}
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 {"side":..,"symbol":..}, sent {"symbol":..,"side":..}
❌ 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 "quantity":0.3 sent "quantity":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", "quantity":"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. Empty body
If you send a body, it is part of the string to sign, even when it is {}.
sent body : {}
❌ original : ...#POST#/az/spot/...
✅ original : ...#POST#/az/spot/...#{}
6. Query string problems
The query segment is built from the decoded values, sorted by key in ASCII order, and placed in its own # segment.
request URL : /az/spot/balance?currency=usdt
❌ ...#GET#/az/spot/balance?currency=usdt (query glued to the path)
✅ ...#GET#/az/spot/balance#currency=usdt
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
#.
7. Path problems
The path in the string to sign must be the path that is actually requested.
❌ https://s-api.azverse.xyz/az/spot/order (host included)
❌ signed /az/spot/order requested /az/spot/order/ (trailing slash differs)
✅ /az/spot/order
8. Timestamp problems
❌ validate-timestamp: 1791541985 (seconds, 10 digits) → AUTH_105
❌ generating the timestamp twice: one value in the header, another in the signed string → AUTH_103
❌ local clock off by more than the recvwindow → AUTH_105
✅ 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 (max 60000)
9. Required headers and their values
❌ validate-recvwindow missing → AUTH_003
❌ validate-recvwindow: 3000 or 70000 → AUTH_004 (must be 5000 – 60000)
❌ validate-algorithms missing → AUTH_005
❌ validate-algorithms: SHA256 → AUTH_006 (use HmacSHA256)
❌ header says HmacSHA512, code computes HMAC-SHA256 → AUTH_103
✅ send all five headers, and put exactly the same values into the string to sign
10. 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
✅ hex( HMAC-SHA256( secretKey, original ) )