接入说明(必看)
商户须知、通用约定、签名算法及示例、响应格式
接入说明(必看)
本页说明所有接口共用的约定:商户须知、请求与响应格式、签名算法和幂等规则。开始对接前请完整阅读。
商户须知
- 时区:所有时间均为 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 键、未声明的字段和压缩请求体。
签名算法
X-Signature = lowercase_hex( HMAC-SHA256( secret 的 UTF-8 字节, 签名串 ) )签名串由下面 5 行组成,每行都以一个换行符(\n,0x0A)结尾,最后再直接拼接原始 body 字节:
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-Id | M123456789ABC |
| X-Timestamp | 1790150400 |
| Idempotency-Key | order-20260923-0001 |
| Body | {"external_id":"order-20260923-0001","player_id":"0192f6a4-3c1e-7b2a-9d40-5e8f1a2b3c4d","currency":"USD","direction":"IN","amount":"100.00"} |
签名串(↵ 表示 \n):
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"}结果:
X-Signature: 83f744fcdadb9452d99be92347f22c0c8ab6070edbcf0cb0802f12bc664375a4GET 示例:用相同的密钥和 X-Timestamp 签名 GET /v1/games?limit=100&status=AVAILABLE,没有幂等键和 body。结果是 4d0b3c1d96c30a78561074adaf4a4c86645428d919d15fdcd6f738bd7fe1ca10。
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')
}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))
}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()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。响应只有两种形态:
{ "data": { }, "request_id": "req_01J..." }{
"error": { "code": "IDEMPOTENCY_CONFLICT", "message": "idempotency key reused with different request", "retryable": false, "details": {} },
"request_id": "req_01J..."
}| 字段 | 类型 | 说明 |
|---|---|---|
data | object | 成功时返回的业务数据 |
error.code | string | 稳定的英文错误码,见通用错误码 |
error.message | string | 供排查用的说明,不要用它做程序判断 |
error.retryable | boolean | 仅表示这次传输可以重试,不说明资金是否已变化 |
error.details | object | 附加信息。资金请求被拒绝时会包含 transaction_id 和 external_id |
request_id | string | 本次请求的 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小时前