接入说明(必看)

商户须知、通用约定、签名算法及示例、响应格式

接入说明(必看) ​

本页说明所有接口共用的约定:商户须知、请求与响应格式、签名算法和幂等规则。开始对接前请完整阅读。

商户须知 ​

  • 时区:所有时间均为 UTC,格式为 RFC3339,例如 2026-09-23T08:00:00Z。统计和对账请按 UTC 处理。唯一例外是请求头 X-Timestamp,它使用 Unix 秒。
  • 凭据:开通后会交付商户 ID(X-Merchant-Id)和密钥(secret,43 位字符串)。密钥只能放在商户服务端,不能进入浏览器、App 或日志。
  • 钱包模式:每个商户开通一种模式:单一钱包(SEAMLESS)或转账钱包(TRANSFER)。可以调用 GET /v1/capabilities 确认。
  • 成功判断:没有数字业务成功码。HTTP 2xx 且响应带 data 为成功,响应带 error 为失败。资金类接口还要读取 status 字段,详见资金状态。
  • 地址占位:
占位符说明
{API_URL}平台 API 地址。测试环境和生产环境各一个,随凭据一起交付
{MERCHANT_URL}商户回调地址,只包含 HTTPS origin(例如 https://wallet.example.com)。平台只在这个地址后拼接固定路径,不跟随重定向

调用方向 ​

方向接口适用模式
商户 → 平台能力、游戏列表、玩家、启动、转账、查单、钱包、注单、现金奖励所有商户(转账接口仅限转账钱包)
平台 → 商户获取余额、改变余额、查询交易仅单一钱包
平台 → 商户对局结束通知所有商户

两个方向使用同一套请求头、签名算法、密钥、响应格式和错误格式,见回调签名。

公共请求头 ​

请求头必填说明
X-Merchant-Id是商户 ID
X-Timestamp是Unix 秒,10 位十进制字符串,例如 1790150400。与服务端时间相差超过 ±300 秒返回 401 REQUEST_EXPIRED
X-Signature是签名,64 位小写 hex,无前缀,见签名算法
Idempotency-Key写请求必填幂等键,字符集 [A-Za-z0-9._:-],长度 1–128。见幂等
If-Match部分接口必填仅在接口文档要求时发送
Content-Type有 body 时必填application/json
  • GET 请求没有 body,参数放在 query 中。POST 请求的 body 必须是 JSON 对象,编码为 UTF-8 且不带 BOM。
  • body 最大 1 MiB,超出返回 413。
  • 不接受重复的 query 参数、重复的 JSON 键、未声明的字段和压缩请求体。

签名算法 ​

text
X-Signature = lowercase_hex( HMAC-SHA256( secret 的 UTF-8 字节, 签名串 ) )

签名串由下面 5 行组成,每行都以一个换行符(\n,0x0A)结尾,最后再直接拼接原始 body 字节:

text
X-Timestamp\n
HTTP 方法(大写)\n
请求目标:原始 path + query(有 query 才带 ?;不含协议和域名;不排序、不解码)\n
Idempotency-Key(没有时为空行)\n
If-Match(没有时为空行)\n
原始 body 字节(GET 为空;body 后不加换行)

常见签名错误

  • 必须对最终发出的 body 字节签名。 签名后不能再重新序列化 JSON,否则字段顺序或空格会变化。
  • query 按实际发送的字节签名。 不排序,也不做 URL 解码。
  • 密钥是 43 位字符串本身。 HMAC 密钥就是这个字符串的 UTF-8 字节,不要先做 base64 解码。
  • 每次重试都要换新的 X-Timestamp 并重新签名。 Idempotency-Key 和业务 ID 保持不变。
  • 反向代理不能改写 path 或 query。

签名示例 ​

项值
密钥(示例)79QoN9Zotm0vXIlP8jrVvurbqMNY35BROsyd_Jfp9KM
请求POST {API_URL}/v1/transfers
X-Merchant-IdM123456789ABC
X-Timestamp1790150400
Idempotency-Keyorder-20260923-0001
Body{"external_id":"order-20260923-0001","player_id":"0192f6a4-3c1e-7b2a-9d40-5e8f1a2b3c4d","currency":"USD","direction":"IN","amount":"100.00"}

签名串(↵ 表示 \n):

text
1790150400↵
POST↵
/v1/transfers↵
order-20260923-0001↵
↵
{"external_id":"order-20260923-0001","player_id":"0192f6a4-3c1e-7b2a-9d40-5e8f1a2b3c4d","currency":"USD","direction":"IN","amount":"100.00"}

结果:

text
X-Signature: 83f744fcdadb9452d99be92347f22c0c8ab6070edbcf0cb0802f12bc664375a4

GET 示例:用相同的密钥和 X-Timestamp 签名 GET /v1/games?limit=100&status=AVAILABLE,没有幂等键和 body。结果是 4d0b3c1d96c30a78561074adaf4a4c86645428d919d15fdcd6f738bd7fe1ca10。

js
import crypto from 'node:crypto'

export function sign(secret, timestamp, method, target, idempotencyKey = '', ifMatch = '', body = '') {
  const head = [timestamp, method.toUpperCase(), target, idempotencyKey, ifMatch].join('\n') + '\n'
  const mac = crypto.createHmac('sha256', Buffer.from(secret, 'utf8'))
  mac.update(head)
  mac.update(body) // 与实际发送完全相同的字节
  return mac.digest('hex')
}
go
func Sign(secret, timestamp, method, target, idempotencyKey, ifMatch string, body []byte) string {
	head := strings.Join([]string{timestamp, strings.ToUpper(method), target, idempotencyKey, ifMatch}, "\n") + "\n"
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(head))
	mac.Write(body)
	return hex.EncodeToString(mac.Sum(nil))
}
python
import hmac, hashlib

def sign(secret, timestamp, method, target, idempotency_key="", if_match="", body=b""):
    head = "\n".join([timestamp, method.upper(), target, idempotency_key, if_match]) + "\n"
    return hmac.new(secret.encode("utf-8"), head.encode("utf-8") + body, hashlib.sha256).hexdigest()
php
function sign($secret, $timestamp, $method, $target, $idempotencyKey = '', $ifMatch = '', $body = '') {
    $head = implode("\n", [$timestamp, strtoupper($method), $target, $idempotencyKey, $ifMatch]) . "\n";
    return hash_hmac('sha256', $head . $body, $secret);
}

回调签名 ​

平台调用商户接口(对局结束通知和单一钱包回调)时,使用同样的请求头、同样的签名算法和同一个交付的 secret。

商户验签步骤:

步骤失败时返回
1. 按 X-Merchant-Id 找到密钥401 AUTHENTICATION_FAILED
2. 检查 X-Timestamp 在 ±300 秒内401 REQUEST_EXPIRED
3. 用原始 path、query 和 body 字节重算签名,并做常数时间比较401 AUTHENTICATION_FAILED

防重放依靠 ±300 秒时间窗口和业务幂等(transaction_id、external_id、Idempotency-Key),商户不需要额外存储。

密钥轮换:轮换期间平台同时接受新旧密钥。商户验证回调时,在平台告知的重叠期内也应同时接受新旧密钥。

平台接入时会自动检查第 3 步和第 2 步(发送过期的 X-Timestamp,要求返回 401 REQUEST_EXPIRED),见联调与上线检查。

通用响应格式 ​

响应类型为 application/json,编码为 UTF-8。响应只有两种形态:

json
{ "data": { }, "request_id": "req_01J..." }
json
{
  "error": { "code": "IDEMPOTENCY_CONFLICT", "message": "idempotency key reused with different request", "retryable": false, "details": {} },
  "request_id": "req_01J..."
}
字段类型说明
dataobject成功时返回的业务数据
error.codestring稳定的英文错误码,见通用错误码
error.messagestring供排查用的说明,不要用它做程序判断
error.retryableboolean仅表示这次传输可以重试,不说明资金是否已变化
error.detailsobject附加信息。资金请求被拒绝时会包含 transaction_id 和 external_id
request_idstring本次请求的 ID,排查问题时请提供
  • HTTP 状态:成功为 200,创建为 201,异步受理为 202,失败为 4xx 或 5xx。
  • 重放一个已完成的幂等请求时,返回首次的状态码和数据,并带响应头 Idempotency-Replayed: true。
  • 平台响应以后可能新增字段,客户端应忽略不认识的字段。但不认识的交易状态不能当作成功。
  • 商户实现的回调接口要严格按文档返回字段。 平台会按字段校验回调响应,响应中多出未声明的字段,或缺少 request_id,都会被视为协议错误。

通用数据约定 ​

类型规则
金额十进制字符串,例如 "100.00"。最多 4 位小数,并且不超过该币种的 decimal_scale。禁止使用浮点数、指数、+ 号和空白。单笔上限为 999999999999.9999
币种平台公布的币种代码,例如 USD,见币种与语言。一笔交易只能有一种币种,不会自动换汇
ID不透明字符串。player_id、transaction_id 等是平台生成的 UUID,不要从中解析任何信息
时间UTC RFC3339,响应中最多带 6 位小数
分页请求参数 limit(1–1000,默认 100)和 cursor。响应返回 data.items、data.next_cursor 和 data.has_more。next_cursor=null 表示已经读完。响应不返回总数

幂等 ​

所有写请求都必须带 Idempotency-Key:

场景结果
同一个键、同样的请求内容返回首次结果,带 Idempotency-Replayed: true
同一个键、不同的请求内容409 IDEMPOTENCY_CONFLICT,不会执行
首次请求还在处理中返回 202 和原资源,或返回 409 REQUEST_IN_PROGRESS

资金类请求(转账和现金奖励)还必须带 external_id,并且 Idempotency-Key 必须等于 external_id。同一个 external_id 永久只能对应一笔交易。

资金状态 ​

资金交易的 status 有 4 个值:

状态含义处理方式
PENDING已受理,正在处理按原 ID 查单
SUCCEEDED已确认成功终态
FAILED已确认失败,没有产生资金变化,原因见 failure.code终态
UNKNOWN暂时无法确认结果,不等于失败按原 ID 继续查单,直到进入终态

超时和未知结果

HTTP 超时、连接断开、5xx 或 UNKNOWN,都不能说明资金没有变化。这时应该用原来的 external_id 或 transaction_id 查单,确认结果后再决定下一步。不要换新 ID 重新发起,否则可能重复入账或扣款。

下一步 ​

最后更新于 12小时前