llms.txt API v1 · 2026-11-02
Get started快速开始

Authentication签名认证

Request headers, HMAC-SHA256 signing, the test vector and IP whitelisting.请求头、HMAC-SHA256 签名、测试向量与 IP 白名单。

Headers请求头

Header请求头Required必填Description说明
X-Merchant-Coderequired必填Your merchantCode. On Wallet API calls it tells you which of our integrations is calling.商户编码 merchantCode。钱包接口里用来区分是平台哪一套接入在调用。
X-Request-Idrequired必填Unique id of this HTTP request, 1–64 printable ASCII characters. A retry uses a new id. Quote it when you contact support.本次 HTTP 请求的唯一编号,1–64 个可见 ASCII 字符。重试也要换新的。找客服排查时提供它。
X-Timestamprequired必填Send time, Unix epoch milliseconds, e.g.发送时刻,Unix 毫秒时间戳,如 1792000000000
X-Signaturerequired必填HMAC-SHA256 signature,HMAC-SHA256 签名,见 签名
X-Api-Versionoptional可选Protocol version, 1. Absent means 1.协议版本,当前 1;不带视为 1。
X-Test-Runoptional可选Only on Wallet API calls made by our conformance test (API conformance test). Not signed. You may tag such traffic, but you must process it exactly like real traffic.只出现在平台对接测试(接口对接测试)发出的钱包请求里,不参与签名。商户可以据此给测试流量打标,但处理逻辑必须和正式请求完全一样。

Signing签名

The same algorithm (HMAC-SHA256) is used in both directions. The signature covers the method, path, merchant code, timestamp, request id and the exact body bytes.

两个方向用同一个算法(HMAC-SHA256)。签名覆盖请求方法、路径、商户编码、时间戳、请求编号和正文的原始字节。

stringToSign = METHOD + "\n" + PATH + "\n" + X-Merchant-Code + "\n" + X-Timestamp + "\n" + X-Request-Id + "\n" + hex(SHA256(rawBody))
X-Signature  = hex(HMAC_SHA256(secretKey, stringToSign))        // lowercase hex, 64 chars
Part部分Value取值
METHODUppercase: POST or GET.大写:POST 或 GET。
PATHThe request path without host and query string, exactly as sent (case-sensitive). Platform API: /merchant/v1/games/launch. Wallet API: the path part of callbackUrl plus the action, e.g. /live/debit.请求路径,不含域名和查询串,与实际发送的完全一致(区分大小写)。平台接口如 /merchant/v1/games/launch;钱包接口是 callbackUrl 的路径部分加动作,如 /live/debit。
X-Merchant-Code · X-Timestamp · X-Request-IdByte-for-byte the values sent in those headers.与请求头里的值逐字节相同。
rawBodyThe exact bytes of the request body. For GET or an empty body, SHA-256 of the empty string:请求正文的原始字节。GET 或空正文取空串的 SHA-256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Must必须Serialize the JSON once, sign those bytes, send those bytes. On the receiving side, verify the signature on the raw bytes before parsing JSON. Compare signatures in constant time.JSON 只序列化一次,签这份字节、发这份字节。接收方先用原始字节验签,再解析 JSON。签名比较用常量时间比较。
Must必须Reject a request when |now − X-Timestamp| > 300 seconds with TIMESTAMP_EXPIRED. Keep server clocks synchronised (NTP).|当前时间 − X-Timestamp| > 300 秒 的请求拒绝,返回 TIMESTAMP_EXPIRED。服务器要用 NTP 对时。

We additionally reject a repeated X-Request-Id from the same merchant within 11 minutes (DUPLICATE_REQUEST; the ±300 s window plus one minute). Doing the same on your Wallet API is recommended but not required — wallet idempotency relies on txnId.

平台还会拒绝同一商户 11 分钟内重复的 X-Request-Id(DUPLICATE_REQUEST;±300 秒窗口再加 1 分钟)。商户钱包接口建议也这样做,但不强制——钱包的幂等靠 txnId。

Test vector测试向量

secretKey       = test-secret-key-0001
METHOD          = POST
PATH            = /wallet/debit
X-Merchant-Code = M0001
X-Timestamp     = 1792000000000
X-Request-Id    = req-0001
rawBody         = {"txnId":"T7K2M9QXH4ZP8WNA","amount":"10.00"}          (45 bytes, no trailing newline)

hex(SHA256(rawBody)) = b8d0525520debc2d5e2c62e83d2303a4cd29ed0c18c4139e98196c76c14fc2b8
X-Signature          = e5273ee9fb4c05408af2f8b32b26b4b98bc891fb77e5ffe5b46f9c112ebe6589
# bash + openssl
sign() { # secretKey METHOD path merchantCode timestamp requestId body
  local hash; hash=$(printf '%s' "$7" | openssl dgst -sha256 | awk '{print $NF}')
  printf '%s\n%s\n%s\n%s\n%s\n%s' "$2" "$3" "$4" "$5" "$6" "$hash" \
    | openssl dgst -sha256 -hmac "$1" | awk '{print $NF}'
}
import hashlib, hmac

def sign(secret_key, method, path, merchant_code, timestamp, request_id, raw_body: bytes) -> str:
    body_hash = hashlib.sha256(raw_body).hexdigest()
    s = "\n".join([method, path, merchant_code, timestamp, request_id, body_hash])
    return hmac.new(secret_key.encode(), s.encode(), hashlib.sha256).hexdigest()

# verify: hmac.compare_digest(sign(...), received_signature)
const crypto = require('crypto');

function sign(secretKey, method, path, merchantCode, timestamp, requestId, rawBody) {
  const bodyHash = crypto.createHash('sha256').update(rawBody).digest('hex');
  const s = [method, path, merchantCode, timestamp, requestId, bodyHash].join('\n');
  return crypto.createHmac('sha256', secretKey).update(s).digest('hex');
}

// verify: crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received)) (equal lengths first)
func Sign(secretKey, method, path, merchantCode, timestamp, requestID string, rawBody []byte) string {
	bodyHash := sha256.Sum256(rawBody)
	s := strings.Join([]string{method, path, merchantCode, timestamp, requestID, hex.EncodeToString(bodyHash[:])}, "\n")
	mac := hmac.New(sha256.New, []byte(secretKey))
	mac.Write([]byte(s))
	return hex.EncodeToString(mac.Sum(nil))
}

// verify: hmac.Equal([]byte(Sign(...)), []byte(received))
static String sign(String secretKey, String method, String path, String merchantCode,
                   String timestamp, String requestId, byte[] rawBody) throws Exception {
    String bodyHash = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(rawBody));
    String s = String.join("\n", method, path, merchantCode, timestamp, requestId, bodyHash);
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
    return HexFormat.of().formatHex(mac.doFinal(s.getBytes(StandardCharsets.UTF_8))); // Java 17+
}

// verify: MessageDigest.isEqual(expected.getBytes(), received.getBytes())
function sign(string $secretKey, string $method, string $path, string $merchantCode,
              string $timestamp, string $requestId, string $rawBody): string {
    $s = implode("\n", [$method, $path, $merchantCode, $timestamp, $requestId, hash('sha256', $rawBody)]);
    return hash_hmac('sha256', $s, $secretKey);
}

// verify: hash_equals(sign(...), $receivedSignature)
static string Sign(string secretKey, string method, string path, string merchantCode,
                   string timestamp, string requestId, byte[] rawBody)
{
    var bodyHash = Convert.ToHexString(SHA256.HashData(rawBody)).ToLowerInvariant();
    var s = string.Join("\n", method, path, merchantCode, timestamp, requestId, bodyHash);
    using var mac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey));
    return Convert.ToHexString(mac.ComputeHash(Encoding.UTF8.GetBytes(s))).ToLowerInvariant();
}

// verify: CryptographicOperations.FixedTimeEquals(expectedBytes, receivedBytes)
require 'openssl'

def sign(secret_key, method, path, merchant_code, timestamp, request_id, raw_body)
  body_hash = OpenSSL::Digest::SHA256.hexdigest(raw_body)
  s = [method, path, merchant_code, timestamp, request_id, body_hash].join("\n")
  OpenSSL::HMAC.hexdigest('SHA256', secret_key, s)
end

Every sample reproduces the test vector above (checked by running them). Complete files with verification: Signing toolkits.每段示例都能算出上面的测试向量(实跑核过)。带验签的完整文件见 签名工具包。

IP whitelistIP 白名单

Both directions can be restricted by source IP. The whitelist is a second barrier; the signature (Signing) is the first.

两个方向都可以按来源 IP 限制。白名单是第二道门,第一道是签名(签名)。

Direction方向Whitelist白名单How the source IP is determined来源 IP 怎么认
You → our Platform API商户 → 平台接口 Your server IPs / CIDRs, configured by us. Empty = not restricted.商户服务器的 IP / 网段,由平台配置。为空 = 不限制。 From the TCP connection at our edge. We ignore any IP header you send (X-Forwarded-For, X-Real-IP, True-Client-IP, CF-Connecting-IP, Forwarded …); setting them has no effect. Call us directly — not through a proxy or CDN whose address is not whitelisted.只看连到平台入口的 TCP 连接地址。请求里自带的任何 IP 头一律忽略(X-Forwarded-For、X-Real-IP、True-Client-IP、CF-Connecting-IP、Forwarded 等),写了也没用。请直接调用,不要经过地址不在白名单里的代理或 CDN。
We → your Wallet API平台 → 商户钱包接口 Our egress IPs (we give you the full list; all of them are in use).平台出口 IP(平台给完整列表,每个都会用到)。 Your decision — see the rule below.由商户实现,规则见下。
Must必须 On your Wallet API, check the whitelist against the TCP source address. Read X-Forwarded-For only if the connection comes from your own load balancer, and then take the address your load balancer appended (the right-most entry), never the left-most one — the left-most part is written by the client and can be forged. 商户钱包接口的白名单必须按 TCP 连接的来源地址判断。只有连接来自商户自己的负载均衡时才读 X-Forwarded-For,而且取的是自己负载均衡追加的那一段(最右边),绝不取最左边——最左边是客户端自己写的,可以伪造。
Security安全Never put secretKey in a browser, mobile app, log, ticket or e-mail. If it leaks, ask us to rotate it.secretKey 不许出现在浏览器、App、日志、工单、邮件里。泄露了请立即联系平台更换。

Test cases (every Platform API endpoint)测试用例(各平台接口通用)

The cases below apply to every signed endpoint and are sent to games/list. The platform checks each request in a fixed order (see Platform API overview), so each case yields exactly the result code shown. The platform runs the same cases, with the same IDs, before every release.

以下用例适用于所有需签名的接口,统一以 games/list 为载体发送。平台按固定顺序检查每个请求(见 平台接口概览),因此每条用例得到的结果码是确定的。平台每次发布前以相同的编号执行同一套用例。

A-01 Normal正常 Correct signature签名正确 Expected期望 OK
Request signed per the specification按规范签名的请求
A-02 Rejection拒绝 Wrong signature签名错误 Expected期望 INVALID_SIGNATURE ×2
All-zero signature; last character of a valid signature changed全零签名;有效签名的末位字符改错
A-03 Rejection拒绝 Body changed after signing签名后改动正文 Expected期望 INVALID_SIGNATURE
Signed, then a space appended to the body签名后在正文末尾追加一个空格
A-04 Rejection拒绝 Missing signing header缺少签名请求头 Expected期望 INVALID_SIGNATURE ×4
Each of the four signing headers omitted in turn四个签名请求头逐个缺省
A-05 Rejection拒绝 Unknown merchant code商户编码不存在 Expected期望 INVALID_SIGNATURE
X-Merchant-Code: nobody, signed with the merchant's keyX-Merchant-Code: nobody,用本商户密钥签名
A-06 Boundary边界 Timestamp 6 minutes in the past时间戳早于当前 6 分钟 Expected期望 TIMESTAMP_EXPIRED
X-Timestamp = now − 360 s, re-signedX-Timestamp 取当前 − 360 秒并重新签名
A-07 Rejection拒绝 Repeated X-Request-IdX-Request-Id 重复 Expected期望 OK → DUPLICATE_REQUEST
The same signed request sent twice同一个已签名请求发送两次
A-08 Configuration配置 Source IP not whitelisted来源 IP 不在白名单 Expected期望 IP_NOT_ALLOWED
Whitelist replaced by [203.0.113.7]白名单改为 [203.0.113.7]
A-09 Configuration配置 Forged forwarding header伪造转发头 Expected期望 IP_NOT_ALLOWED
Whitelist [203.0.113.7]; X-Forwarded-For: 203.0.113.7白名单 [203.0.113.7];X-Forwarded-For: 203.0.113.7
A-10 Boundary边界 Body of 1 MiB + 1 byte正文 1 MiB + 1 字节 Expected期望 HTTP 413
Body of 1,048,577 bytes1,048,577 字节的正文
A-11 Boundary边界 Body of exactly 1 MiB正文恰好 1 MiB Expected期望 OK
Body of 1,048,576 bytes (a valid JSON object)1,048,576 字节的正文(合法 JSON 对象)
A-12 Boundary边界 Timestamp 295 seconds in the past时间戳早于当前 295 秒 Expected期望 OK
Inside the ±300 s window位于 ±300 秒窗口内侧
A-13 Boundary边界 Timestamp 295 seconds in the future时间戳晚于当前 295 秒 Expected期望 OK
Inside the ±300 s window位于 ±300 秒窗口内侧
A-14 Boundary边界 Timestamp 6 minutes in the future时间戳晚于当前 6 分钟 Expected期望 TIMESTAMP_EXPIRED
X-Timestamp = now + 360 s, re-signedX-Timestamp 取当前 + 360 秒并重新签名
A-15 Rejection拒绝 Malformed timestamp时间戳格式不合法 Expected期望 TIMESTAMP_EXPIRED ×2
Non-numeric value; seconds instead of milliseconds非数字;以秒而非毫秒表示
A-16 Boundary边界 X-Request-Id of 64 charactersX-Request-Id 长 64 个字符 Expected期望 OK
Maximum length长度上限
A-17 Boundary边界 X-Request-Id of 65 charactersX-Request-Id 长 65 个字符 Expected期望 INVALID_SIGNATURE
One character over the limit超出上限 1 个字符
A-18 Rejection拒绝 X-Request-Id with a spaceX-Request-Id 含空格 Expected期望 INVALID_SIGNATURE
Characters outside printable ASCII 0x21–0x7E含可见 ASCII(0x21–0x7E)以外的字符
A-19 Rejection拒绝 Malformed merchant code商户编码格式不合法 Expected期望 INVALID_SIGNATURE ×2
Code with a hyphen; code of 65 characters含连字符;长 65 个字符
A-20 Rejection拒绝 Merchant code in a different case商户编码大小写不一致 Expected期望 INVALID_SIGNATURE
X-Merchant-Code upper-casedX-Merchant-Code 转为大写
A-21 Normal正常 Signature in upper-case hex签名使用大写十六进制 Expected期望 OK
X-Signature upper-casedX-Signature 转为大写
A-22 Rejection拒绝 Signed for another path按其他路径签名 Expected期望 INVALID_SIGNATURE
Signed for /games/launch, sent to /games/list按 /games/launch 签名,发往 /games/list
A-23 Rejection拒绝 Signed with another merchant's key使用其他商户的密钥签名 Expected期望 INVALID_SIGNATURE
Own merchant code, another merchant's secret key本商户编码,其他商户的密钥
A-24 Configuration配置 Merchant disabled商户已停用 Expected期望 MERCHANT_DISABLED
Merchant disabled in the back office and published后台停用商户并发布
A-25 Configuration配置 Maintenance on signed endpoints维护期间调用业务接口 Expected期望 MAINTENANCE
merchant.maintenance = true, publishedmerchant.maintenance 设为 true 并发布
A-26 Rejection拒绝 Empty body正文为空 Expected期望 INVALID_PARAM
POST with a zero-length body, correctly signed正确签名、长度为 0 的 POST 正文

Your Wallet API is checked the other way round by the conformance test: group SIG in API conformance test.

反方向(商户钱包验平台的签名)由对接测试检查:见 接口对接测试 的 SIG 组。