Skip to main content

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​

authTypeNameDescriptionRequired headers
noneNo authenticationPublic API, passed through directly-
app_keySimple authenticationX-AppKey compared against the API's authorized app listX-AppKey
app_secretSignature authenticationAppKey + timestamp + nonce + HMAC signature, tamper-proof and replay-proofX-AppKey, X-Timestamp, X-Nonce, X-Sign, optional X-Algorithm
oauth2OAuth2 client credentialsExchanges AppKey/AppSecret for a short-lived access_token and calls with a Bearer tokenAuthorization: Bearer <access_token>
jwtEnterprise IdP JWTThe enterprise IdP issues the token; the platform only verifies it and identifies the app by subject bindingAuthorization: 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_secret signature authentication. If the API uses oauth2, no signature is needed: exchange the AppKey/AppSecret for a token instead. See OAuth2 client credentials authentication. If it uses jwt, the token is issued by the enterprise IdP; see Enterprise IdP JWT authentication.

Signature authentication logic​

Request headers​

HeaderRequiredDescription
X-AppKeyYesThe app's AppKey
X-TimestampYesMillisecond timestamp (13 digits)
X-NonceYesRandom string, recommended as the hex of 16 strongly-random bytes (32 characters). Must not repeat for the same AppKey within the validity window
X-SignYesSignature result (hex, case-insensitive)
X-AlgorithmNoSM3 (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):

ComponentRequirement
HTTP_METHODUppercase method name, such as GET / POST
API_PATHExactly 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 / NonceExactly the values of the corresponding headers (no trimming, no case conversion)
Raw query stringThe query string actually sent, without ?, in its raw undecoded encoded form. Empty string when there is no query
Request body bytesThe raw body bytes actually sent (UTF-8). Empty bytes for GET / HEAD or when there is no body
SeparatorThe 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)
Note

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

ItemValue
AppSecretsk-test-1234567890abcdef
METHOD / PATHGET / /open/api/demo
AppKeyak_1234567890
Timestamp / Nonce1730000000000 / abc123def456
Payload digestf4051d239b766c4111e92979aa31af0b35def053646e347bc41e8b73cfd080bc
X-Signfaf82457ec6890c750f15cd23127401a07ec51c56bac3b1b957e4ddc434831ed

Vector V2: POST, with a query string and a Chinese-text request body

ItemValue
AppSecretsk-test-1234567890abcdef
METHOD / PATHPOST / /open/api/demo
AppKeyak_1234567890
Timestamp / Nonce1730000002222 / n0nce-b
Query string / request bodypage=1&size=20 / {"name":"张三","n":1}
Payload digest71f7b849ada63301458054f9b5f748b99e3df834778ce7d9cb0fbe02f7ebeb42
X-Sign93ebde8a1023eaf8a60034521415c171bb15a20a1d148ad75480cec15134f280

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​

RuleDescription
Timestamp windowTimestamps 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 formatMilliseconds (13 digits). Second-level timestamps or non-numeric values are judged as "expired"
nonceMust be unique per request; the gateway performs SETNX on AppKey + SM3(nonce), and a duplicate is treated as a replay
Retry adviceOn retry you must regenerate X-Timestamp and X-Nonce and sign again

Configuration and limits​

ItemConfiguration / limitDescription
Default algorithmapigo.hmac.auth (default SM3)Used when the X-Algorithm header is absent
Clock skew toleranceapigo.hmac.auth.timestamp-skew-millis (default 60000)Maximum milliseconds a client timestamp may be ahead
Body signing limitapigo.auth.max-body-bytes (default 1MB)Over-limit requests return 40013 directly
Supported algorithmsSM3, HmacSHA256HmacSHA1 and others have been retired
Path restrictionEncoded path separators (%2F, %5C) are not allowed in the raw pathRejected 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: HmacSHA256 header and sign with an SHA256-based HMAC; the payload digest still uses SM3.

Common errors & troubleshooting​

Error codes​

codeMeaningCommon causes
40008X-AppKey cannot be emptyX-AppKey is missing
40009Authentication parameters cannot be emptyAny of X-AppKey / X-Timestamp / X-Nonce / X-Sign is empty
40010Timestamp expired / nonce invalidTimestamp outside the window, a second-level timestamp; or a duplicate nonce (replay)
40013Signature verification failedInconsistent sign string construction, algorithm mismatch, wrong AppSecret, body over the limit
40002App has no permission for the APIThe API is not authorized to this app
40018IP not in the authorized rangeThe app has a credential IP whitelist and the source IP is not on it
40019App disabledThe app has been disabled; credential status is checked before signature verification, so a valid signature is still rejected
40001API not found or offlinePath mismatch; or the path contains %2F / %5C
40023Daily call quota exceededThe daily quota is exhausted
40017Request contains illegal characters (WAF blocked)Returns HTTP 400 (other authentication errors return HTTP 200 + body.code)

40013 checklist (ordered by frequency)​

  1. The query string is not included in the signature, or the query used for signing differs from the one sent (most common). %20 vs +, %2F vs /, parameter order, and whether ? is present must all match.
  2. The request body is re-serialized by the HTTP library. The signature must cover "the exact bytes finally sent": in Python use data=body_bytes instead of json=; in Node pass the same string; in Java/Go serialize to a byte array first and use it for both signing and sending.
  3. The payload digest concatenation is missing the newline: there must be one 0x0A byte between query and body, even when both are empty (for GET without a body the digest is SM3(0x0A)).
  4. Algorithm mismatch: signing with HMAC-SHA256 but not sending X-Algorithm: HmacSHA256.
  5. Using SM3(AppSecret + sign string) by mistake: the correct algorithm is HMAC-SM3(AppSecret, sign string).
  6. Path mismatch: trailing slash or case differences.
  7. Clock skew: unsynchronized server/client time causes 40010.
  8. 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​

  1. 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
  2. Use HTTPS: signatures guarantee integrity and replay protection only, not confidentiality
  3. Use strong randomness for nonces; never replace them with timestamps or auto-increment sequences
  4. Do not reuse (Timestamp, Nonce): regenerate and re-sign on retry
  5. Restrict call origins with IP whitelists
  6. Rotate the AppSecret periodically, following the principle of least privilege
  7. 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