Signature authentication guide
Applies to: open APIs with
authType = app_secret(AppSecret signature authentication) Signature algorithm: HMAC-SM3 (Chinese national cryptographic standard) by default, HmacSHA256 optional The self-test vectors in this document match the platform gateway implementation and can be used directly to validate your signature implementation.
Authentication overview
| authType | Name | Description | Required headers |
|---|---|---|---|
none | No authentication | Public API, passed through directly | - |
app_key | Simple authentication | X-AppKey compared against the API's authorized app list | X-AppKey |
app_secret | Signature authentication | AppKey + timestamp + nonce + HMAC signature, tamper-proof and replay-proof | X-AppKey, X-Timestamp, X-Nonce, X-Sign, optional X-Algorithm |
oauth2 | OAuth2 client credentials | Exchanges AppKey/AppSecret for a short-lived access_token and calls with a Bearer token | Authorization: Bearer <access_token> |
jwt | Enterprise IdP JWT | The enterprise IdP issues the token; the platform only verifies it and identifies the app by subject binding | Authorization: Bearer <enterprise_token> |
AppKey / AppSecret are obtained after creating an app on the platform and authorizing APIs to it. Keep the AppSecret safe.
This document only covers
app_secretsignature authentication. If the API usesoauth2, no signature is needed: exchange the AppKey/AppSecret for a token instead. See OAuth2 client credentials authentication. If it usesjwt, the token is issued by the enterprise IdP; see Enterprise IdP JWT authentication.
Signature authentication logic
Request headers
| Header | Required | Description |
|---|---|---|
X-AppKey | Yes | The app's AppKey |
X-Timestamp | Yes | Millisecond timestamp (13 digits) |
X-Nonce | Yes | Random string, recommended as the hex of 16 strongly-random bytes (32 characters). Must not repeat for the same AppKey within the validity window |
X-Sign | Yes | Signature result (hex, case-insensitive) |
X-Algorithm | No | SM3 (default) or HmacSHA256; any other value is rejected |
Sign string construction (core)
Sign string = HTTP_METHOD + "_" + API_PATH + "_" + AppKey + "_" + Timestamp + "_" + Nonce + "_" + payload digest
Payload digest = SM3( raw query string ‖ 0x0A (newline byte) ‖ request body bytes )
Every component must match the actual sent content byte-for-byte, otherwise signature verification will inevitably fail (40013):
| Component | Requirement |
|---|---|
HTTP_METHOD | Uppercase method name, such as GET / POST |
API_PATH | Exactly the API path configured on the platform, trailing slash significant; the gateway signs the decoded path (encoded %2F / %5C are rejected, other percent-encoding is decoded first) |
AppKey / Timestamp / Nonce | Exactly the values of the corresponding headers (no trimming, no case conversion) |
| Raw query string | The query string actually sent, without ?, in its raw undecoded encoded form. Empty string when there is no query |
| Request body bytes | The raw body bytes actually sent (UTF-8). Empty bytes for GET / HEAD or when there is no body |
| Separator | The 6 segments are joined with underscores _; query and body are separated by one newline byte 0x0A |
Computing X-Sign
Default: X-Sign = HMAC-SM3(key = AppSecret, message = sign string)
Optional: add the header X-Algorithm: HmacSHA256
X-Sign = HMAC-SHA256(key = AppSecret, message = sign string)
This is not SM3(AppSecret + sign string). Bare SM3 prefix concatenation produces a completely different result from HMAC-SM3 and provides no MAC security.
Self-test vectors
Vector V1: GET, no query, no body
| Item | Value |
|---|---|
| AppSecret | sk-test-1234567890abcdef |
| METHOD / PATH | GET / /open/api/demo |
| AppKey | ak_1234567890 |
| Timestamp / Nonce | 1730000000000 / abc123def456 |
| Payload digest | f4051d239b766c4111e92979aa31af0b35def053646e347bc41e8b73cfd080bc |
| X-Sign | faf82457ec6890c750f15cd23127401a07ec51c56bac3b1b957e4ddc434831ed |
Vector V2: POST, with a query string and a Chinese-text request body
| Item | Value |
|---|---|
| AppSecret | sk-test-1234567890abcdef |
| METHOD / PATH | POST / /open/api/demo |
| AppKey | ak_1234567890 |
| Timestamp / Nonce | 1730000002222 / n0nce-b |
| Query string / request body | page=1&size=20 / {"name":"张三","n":1} |
| Payload digest | 71f7b849ada63301458054f9b5f748b99e3df834778ce7d9cb0fbe02f7ebeb42 |
| X-Sign | 93ebde8a1023eaf8a60034521415c171bb15a20a1d148ad75480cec15134f280 |
Validate your signature function against the V1/V2 vectors before integration; proceed to joint debugging only when the outputs match.
Gateway verification flow
Failures return JSON (HTTP status code 200, business code in body.code):
{ "code": 40013, "msg": "Signature verification failed. Please check the authentication parameters." }
Timestamp and nonce rules
| Rule | Description |
|---|---|
| Timestamp window | Timestamps up to 10 minutes in the past are allowed; up to 60 seconds of clock skew ahead is allowed (apigo.hmac.auth.timestamp-skew-millis configurable) |
| Timestamp format | Milliseconds (13 digits). Second-level timestamps or non-numeric values are judged as "expired" |
| nonce | Must be unique per request; the gateway performs SETNX on AppKey + SM3(nonce), and a duplicate is treated as a replay |
| Retry advice | On retry you must regenerate X-Timestamp and X-Nonce and sign again |
Configuration and limits
| Item | Configuration / limit | Description |
|---|---|---|
| Default algorithm | apigo.hmac.auth (default SM3) | Used when the X-Algorithm header is absent |
| Clock skew tolerance | apigo.hmac.auth.timestamp-skew-millis (default 60000) | Maximum milliseconds a client timestamp may be ahead |
| Body signing limit | apigo.auth.max-body-bytes (default 1MB) | Over-limit requests return 40013 directly |
| Supported algorithms | SM3, HmacSHA256 | HmacSHA1 and others have been retired |
| Path restriction | Encoded path separators (%2F, %5C) are not allowed in the raw path | Rejected as "API not found" (40001) |
Calling examples
Script version (OpenSSL 3.x + curl)
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="https://api.example.com"
API_PATH="/open/api/demo"
APP_KEY="ak_1234567890"
APP_SECRET="sk-test-1234567890abcdef"
METHOD="POST"
RAW_QUERY="page=1&size=20" # The raw query string actually sent; leave empty if none
BODY='{"name":"张三","n":1}' # The request body actually sent; leave empty for GET/HEAD
TS="$(python3 -c 'import time; print(int(time.time()*1000))')"
NONCE="$(openssl rand -hex 16)" # 16 strongly-random bytes, hex
hex_of() { printf '%s' "$1" | xxd -p | tr -d '\n'; }
# Payload digest = SM3(raw query ‖ 0x0A ‖ request body bytes)
DIGEST="$(printf '%s\n%s' "$RAW_QUERY" "$BODY" | openssl dgst -sm3 -binary | xxd -p | tr -d '\n')"
# Sign string = METHOD_PATH_AppKey_Timestamp_Nonce_payload digest
SIGN_STRING="${METHOD}_${API_PATH}_${APP_KEY}_${TS}_${NONCE}_${DIGEST}"
# X-Sign = HMAC-SM3(key=AppSecret, message=sign string)
SIGN="$(printf '%s' "$SIGN_STRING" | openssl mac -macopt digest:SM3 -macopt hexkey:"$(hex_of "$APP_SECRET")" HMAC | tr 'A-F' 'a-f')"
URL="${BASE_URL}${API_PATH}"
[ -n "$RAW_QUERY" ] && URL="${URL}?${RAW_QUERY}"
curl -sS -X "$METHOD" "$URL" \
-H "Content-Type: application/json" \
-H "X-AppKey: ${APP_KEY}" \
-H "X-Timestamp: ${TS}" \
-H "X-Nonce: ${NONCE}" \
-H "X-Sign: ${SIGN}" \
--data-binary "${BODY}"
Java (BouncyCastle)
Dependency: org.bouncycastle:bcprov-jdk18on.
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.util.encoders.Hex;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
public class ApiSignDemo {
/** Generate X-Sign: HMAC-SM3(AppSecret as key, sign string as message) */
public static String sign(String method, String path, String rawQuery, String body,
String appKey, String appSecret, String timestamp, String nonce) {
String data = method + "_" + path + "_" + appKey + "_" + timestamp + "_" + nonce + "_"
+ Hex.toHexString(payloadDigest(rawQuery, body));
return Hex.toHexString(hmacSm3(appSecret.getBytes(StandardCharsets.UTF_8),
data.getBytes(StandardCharsets.UTF_8)));
}
/** Payload digest = SM3(raw query string ‖ 0x0A ‖ request body bytes) */
private static byte[] payloadDigest(String rawQuery, String body) {
SM3Digest digest = new SM3Digest();
byte[] queryBytes = rawQuery == null ? new byte[0] : rawQuery.getBytes(StandardCharsets.UTF_8);
digest.update(queryBytes, 0, queryBytes.length);
digest.update((byte) 0x0A);
byte[] bodyBytes = body == null ? new byte[0] : body.getBytes(StandardCharsets.UTF_8);
digest.update(bodyBytes, 0, bodyBytes.length);
byte[] out = new byte[digest.getDigestSize()];
digest.doFinal(out, 0);
return out;
}
private static byte[] hmacSm3(byte[] key, byte[] message) {
HMac mac = new HMac(new SM3Digest());
mac.init(new KeyParameter(key));
mac.update(message, 0, message.length);
byte[] out = new byte[mac.getMacSize()];
mac.doFinal(out, 0);
return out;
}
/** 16 strongly-random-byte nonce (hex) */
private static String randomNonce() {
byte[] bytes = new byte[16];
new SecureRandom().nextBytes(bytes);
return Hex.toHexString(bytes);
}
}
Header assembly:
HttpRequest request = HttpRequest.newBuilder(URI.create(url))
.header("X-AppKey", APP_KEY)
.header("X-Timestamp", timestamp)
.header("X-Nonce", nonce)
.header("X-Sign", sign)
.header("Content-Type", "application/json")
// When using HmacSHA256, add: .header("X-Algorithm", "HmacSHA256")
.method(method, HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
.build();
Python (gmssl + requests)
Dependency: pip install gmssl requests.
import hashlib, hmac, time, uuid
from urllib.parse import quote, urlencode
import requests
from gmssl import sm3
def sm3_hex(data: bytes) -> str:
return sm3.sm3_hash(list(data))
def hmac_sm3_hex(key: bytes, message: bytes) -> str:
"""HMAC-SM3: the SM3 block size is 64 bytes; compress the key first if it is too long."""
if len(key) > 64:
key = bytes.fromhex(sm3_hex(key))
key = key.ljust(64, b"\x00")
ipad_key = bytes(b ^ 0x36 for b in key)
opad_key = bytes(b ^ 0x5C for b in key)
inner = bytes.fromhex(sm3_hex(ipad_key + message))
return sm3_hex(opad_key + inner)
def payload_digest(raw_query: str, body_bytes: bytes) -> str:
"""Payload digest = SM3(raw query string || 0x0A || request body bytes); both sides are UTF-8 bytes."""
query_bytes = (raw_query or "").encode("utf-8")
return sm3_hex(query_bytes + b"\n" + (body_bytes or b""))
def sign(method, path, raw_query, body_bytes, app_key, app_secret, timestamp, nonce):
data = f"{method}_{path}_{app_key}_{timestamp}_{nonce}_{payload_digest(raw_query, body_bytes)}"
return hmac_sm3_hex(app_secret.encode("utf-8"), data.encode("utf-8"))
def call_api(base_url, path, app_key, app_secret, method="GET",
query_params=None, body=None):
# 1) Build the raw query string "as it will be sent": signing and sending share the same string
raw_query = urlencode(query_params, quote_via=quote) if query_params else ""
# 2) Normalize the body to UTF-8 bytes: signing and sending must use the exact same bytes
body_bytes = body.encode("utf-8") if isinstance(body, str) else (body or b"")
timestamp = str(int(time.time() * 1000))
nonce = uuid.uuid4().hex
sign_value = sign(method, path, raw_query, body_bytes, app_key, app_secret, timestamp, nonce)
headers = {
"X-AppKey": app_key,
"X-Timestamp": timestamp,
"X-Nonce": nonce,
"X-Sign": sign_value,
# When using HmacSHA256, add: 'X-Algorithm': 'HmacSHA256'
}
if body_bytes:
headers["Content-Type"] = "application/json"
url = base_url + path + (f"?{raw_query}" if raw_query else "")
# data=body_bytes sends the exact signed bytes, avoiding body re-serialization by the HTTP library
return requests.request(method, url, headers=headers, data=body_bytes or None, timeout=10)
Go (gmsm)
Dependency: github.com/emmansun/gmsm/sm3.
package main
import (
"crypto/hmac"
"encoding/hex"
"fmt"
"strings"
"github.com/emmansun/gmsm/sm3"
)
const (
appKey = "ak_1234567890"
appSecret = "sk-test-1234567890abcdef"
)
// sign generates X-Sign: HMAC-SM3(AppSecret as key, sign string as message)
func sign(method, path, rawQuery, body, timestamp, nonce string) string {
data := strings.Join([]string{method, path, appKey, timestamp, nonce, payloadDigest(rawQuery, []byte(body))}, "_")
mac := hmac.New(sm3.New, []byte(appSecret))
mac.Write([]byte(data))
return hex.EncodeToString(mac.Sum(nil))
}
// payloadDigest = SM3(raw query string ‖ 0x0A ‖ request body bytes)
func payloadDigest(rawQuery string, body []byte) string {
h := sm3.New()
h.Write([]byte(rawQuery))
h.Write([]byte{'\n'})
h.Write(body)
return hex.EncodeToString(h.Sum(nil))
}
func main() {
fmt.Println(sign("POST", "/open/api/demo", "page=1&size=20",
`{"name":"张三","n":1}`, "1730000002222", "n0nce-b"))
}
Node.js / browser (sm-crypto)
// npm install sm-crypto
const sm3 = require('sm-crypto').sm3
const APP_KEY = 'ak_1234567890'
const APP_SECRET = 'sk-test-1234567890abcdef'
/** UTF-8 byte array (sm-crypto's hmac mode requires the key as a byte array) */
function utf8Bytes(text) {
return Array.from(new TextEncoder().encode(text || ''))
}
/** Payload digest = SM3(raw query string ‖ 0x0A ‖ request body) */
function payloadDigest(rawQuery, bodyText) {
return sm3([...utf8Bytes(rawQuery), 0x0A, ...utf8Bytes(bodyText)])
}
/** X-Sign = HMAC-SM3(AppSecret as key, sign string as message) */
function sign(method, path, rawQuery, bodyText, timestamp, nonce) {
const data = `${method}_${path}_${APP_KEY}_${timestamp}_${nonce}_${payloadDigest(rawQuery, bodyText)}`
return sm3(data, { key: utf8Bytes(APP_SECRET), mode: 'hmac' })
}
/** 16 strongly-random-byte nonce (hex) */
function createNonce() {
const bytes = new Uint8Array(16)
crypto.getRandomValues(bytes)
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('')
}
HmacSHA256 variant: the request must carry the
X-Algorithm: HmacSHA256header and sign with an SHA256-based HMAC; the payload digest still uses SM3.
Common errors & troubleshooting
Error codes
| code | Meaning | Common causes |
|---|---|---|
40008 | X-AppKey cannot be empty | X-AppKey is missing |
40009 | Authentication parameters cannot be empty | Any of X-AppKey / X-Timestamp / X-Nonce / X-Sign is empty |
40010 | Timestamp expired / nonce invalid | Timestamp outside the window, a second-level timestamp; or a duplicate nonce (replay) |
40013 | Signature verification failed | Inconsistent sign string construction, algorithm mismatch, wrong AppSecret, body over the limit |
40002 | App has no permission for the API | The API is not authorized to this app |
40018 | IP not in the authorized range | The app has a credential IP whitelist and the source IP is not on it |
40019 | App disabled | The app has been disabled; credential status is checked before signature verification, so a valid signature is still rejected |
40001 | API not found or offline | Path mismatch; or the path contains %2F / %5C |
40023 | Daily call quota exceeded | The daily quota is exhausted |
40017 | Request contains illegal characters (WAF blocked) | Returns HTTP 400 (other authentication errors return HTTP 200 + body.code) |
40013 checklist (ordered by frequency)
- The query string is not included in the signature, or the query used for signing differs from the one sent (most common).
%20vs+,%2Fvs/, parameter order, and whether?is present must all match. - The request body is re-serialized by the HTTP library. The signature must cover "the exact bytes finally sent": in Python use
data=body_bytesinstead ofjson=; in Node pass the same string; in Java/Go serialize to a byte array first and use it for both signing and sending. - The payload digest concatenation is missing the newline: there must be one
0x0Abyte between query and body, even when both are empty (forGETwithout a body the digest isSM3(0x0A)). - Algorithm mismatch: signing with HMAC-SHA256 but not sending
X-Algorithm: HmacSHA256. - Using
SM3(AppSecret + sign string)by mistake: the correct algorithm isHMAC-SM3(AppSecret, sign string). - Path mismatch: trailing slash or case differences.
- Clock skew: unsynchronized server/client time causes 40010.
- Incorrect AppSecret: using another app's key, or copying it with extra whitespace.
Quick self-checks
- Run self-test vectors V1/V2 first to confirm the algorithm implementation is correct;
- Use "API details → Request parameters → Online signing" to generate a signature and compare it with your own implementation;
- Use "API details → Online testing" to make a real call and quickly distinguish signature problems from link problems.
Security recommendations
- Keep the AppSecret on the server side only; never expose it in browsers, app packages, logs, or frontend code. The "Online signing" tool on the API details page computes the HMAC locally in the browser and never uploads it, so it is safe for debugging
- Use HTTPS: signatures guarantee integrity and replay protection only, not confidentiality
- Use strong randomness for nonces; never replace them with timestamps or auto-increment sequences
- Do not reuse
(Timestamp, Nonce): regenerate and re-sign on retry - Restrict call origins with IP whitelists
- Rotate the AppSecret periodically, following the principle of least privilege
- Do not blindly retry after a verification failure: apart from clock skew, 40013 is almost always a code issue
Appendix: signature cheat sheet
① Prepare: AppKey / AppSecret (obtained after creating an app and authorizing APIs)
② Generate: Timestamp = current millisecond timestamp (13 digits)
Nonce = hex of 16 strongly-random bytes (just needs to be unique)
③ Digest: PayloadDigest = SM3( rawQuery ‖ 0x0A ‖ bodyBytes )
④ Sign string: SignString = METHOD_API-PATH_AppKey_Timestamp_Nonce_PayloadDigest
⑤ Sign: X-Sign = HMAC-SM3(AppSecret, SignString) // default
HMAC-SHA256(AppSecret, SignString) // requires X-Algorithm: HmacSHA256
⑥ Headers: X-AppKey / X-Timestamp / X-Nonce / X-Sign [ / X-Algorithm ]
⑦ Self-test vectors: see above