1. 接入概述
Hash Play 是游戏内容供应商,向商户提供游戏产品(Crash、Dice 等 Hash Play 游戏)。双方采用**余额托管(Hosted Wallet)**合作模式:
- 玩家游戏资金由 Hash Play 托管。 每个玩家在平台内拥有一个独立的托管钱包账户,以
(operator_id, user_id)寻址,与商户平台的玩家一一对应; - 商户在玩家进入游戏前后,通过钱包接口向该托管账户存入(deposit)资金;玩家游戏内的投注、派奖、局次撤销等全部资金变动均由 Hash Play 在托管账户内实时本地结算,不回调商户;
- 商户可随时**查询(balance)**托管余额、**对账(transactions)资金流水、核查投注订单(orders),并在玩家需要回收资金时提走(withdraw)**余额(支持部分提现);
- 上述过程对玩家透明,玩家在游戏内看到的是托管钱包余额的实时变动。
整个接入只涉及一个方向的交互——全部接口均由商户主动调用 Hash Play:
| 动作 | 方向 | 说明 |
|---|---|---|
| 启动游戏 | 您的平台 → Hash Play | 玩家点击游戏入口后,商户系统向 Hash Play 申请游戏入场链接,交由玩家浏览器打开 |
| 钱包存取 | 您的平台 → Hash Play | 商户按需调用存入、提走、余额、流水、订单、存取状态等接口,管理玩家的托管钱包 |
| 游戏内结算 | Hash Play内部 | 玩家投注、派奖、撤销均在平台托管账户内完成,商户无需提供任何回调接口,也无需感知每一笔局次交易 |
整体交互流程
点击放大
说明:蓝色区域为启动阶段,玩家每次进入游戏时执行一次;绿色区域为游戏与资金管理阶段——玩家进入游戏后直接与 Hash Play交互完成投注与结算(托管钱包内实时动账,不经过商户);仅当玩家需要充值或提现时,才经由商户平台中转(商户调用
/wallet/deposit、/wallet/withdraw)。余额查询与流水对账由商户按需随时发起。
2. 双方职责划分
Hash Play 负责:
- 提供游戏内容及游戏内全部逻辑(投注判定、开奖、派奖金额计算);
- 提供游戏启动接口(
/launch)与钱包存取接口(/wallet/*); - 维护每个玩家的托管钱包账户与全部资金流水,保证账务一致;
- 提供游戏编码清单、联调环境与技术支持。
商户负责:
- 玩家账户体系:在商户平台维护玩家,并以稳定的
user_id与 Hash Play 侧托管账户对应; - 在商户平台提供游戏入口,调用启动接口并向玩家分发游戏链接;
- 根据业务节奏调用钱包接口完成存入、提走、余额查询、流水对账与投注订单核查;
- 妥善保管 API Secret,实现签名调用,确保仅商户服务器可以操作本商户的资金。
商户无需对外提供任何钱包回调接口,也无需实现逐笔投注的扣款/入账/撤销逻辑——这些全部由 Hash Play 在托管账户内完成。
3. 环境信息
商户开发前请向 Hash Play 索取下表信息,所有环境信息集中于此处核对:
| 配置项 | Sandbox(联调环境) | Production(生产环境) |
|---|---|---|
Hash Play 网关 Base URL(/launch、/list、/wallet/* 使用) | 联调时由 Hash Play 提供,例如 https://sandbox.example.com/prod-api/customer/app-api/api/v1/game | https://api.example.com/prod-api/customer/app-api/api/v1/game |
| API Version | v1(/api/v1/game/launch、/api/v1/game/list、/api/v1/game/wallet/*) | 同左 |
测试 operator_id | 由 Hash Play 分配(示例 M001) | 正式分配的商户编码 |
测试 api_key / api_secret | 由 Hash Play 分配 | 生产凭证单独签发,严禁混用 |
| 测试游戏 | Dice(Dice 游戏编码与名称同名)等,见 第 13 章游戏列表 | 按商户签约游戏范围开通 |
| 测试币种 | USDT(当前 B2B 仅支持 USDT) | USDT |
| 商户出口 IP(Hash Play 白名单,必配) | 由商户提供,Hash Play 侧登记;未登记时全部请求返回 4004 | 由商户提供,Hash Play 侧登记 |
| 签名时间戳窗口 | ±60 秒(X-Timestamp 为毫秒级时间戳,与服务器时间偏差须 ≤ 60000ms) | 同左 |
| Nonce 去重窗口 | 同一随机串约 2 分钟内重复即被判定为重放并拒绝 | 同左 |
环境信息变更(如网关地址、IP 白名单)将由 Hash Play 提前通知;请勿将沙箱凭证用于生产。
关于 Base URL 的组成:Base URL 由「网关域名 + nginx 前缀
prod-api+ 网关路由customer+ 应用前缀app-api+api/v1/game」拼成。文档其余章节用{base_url}代指它,具体接口路径在其后拼接:
- 启动游戏
{base_url}/launch- 游戏列表
{base_url}/list- 钱包接口
{base_url}/wallet/deposit、/wallet/withdraw、/wallet/balance、/wallet/transactions、/wallet/orders、/wallet/operation/status联调地址以 Hash Play 提供为准,禁止自行拼装路径。
4. 接入凭证与配置
签约后,Hash Play 为商户分配身份凭证。商户无需向 Hash Play 登记任何回调地址。
4.1 Hash Play 分配的凭证
| 凭证 | 作用 |
|---|---|
商户编码(operator_id,示例 M001) | 商户在 Hash Play 的唯一身份标识 |
API Key(api_key) | 请求中公开携带的身份标识,置于请求头 |
API Secret(api_secret) | 签名用的共享密钥,仅存于双方服务器,不通过网络传输 |
安全要求:API Secret 等同于资金接口的访问密钥,请仅保存在服务端安全存储中,禁止写入前端代码、客户端安装包、日志或代码仓库。如怀疑泄露,请立即联系 Hash Play 轮换密钥。
4.2 商户需提供的配置
商户无需提供钱包接口地址,配置仅有一项:
| 配置项 | 说明 |
|---|---|
| 商户服务器出口 IP | 由商户提供给 Hash Play 登记白名单;必填。仅白名单内的 IP 可调用本商户对接的 Hash Play 的 /launch、/list 与 /wallet/* 接口 |
IP 白名单是必需项,不是可选项:平台侧查不到该商户的启用白名单记录时,会直接判定 IP 校验不通过并返回
4004。白名单支持精确 IPv4 与 CIDR 网段(如203.0.113.0/24)。商户出口 IP 发生变化(扩容、迁移机房、NAT 出口调整)时请提前通知 Hash Play 更新,否则全部接口将不可用。沙箱联调阶段同样需要登记 IP 白名单,请把联调机/联调出口 IP 一并提供给 Hash Play。
5. 安全机制:接口签名
双方之间的每一次通信均需通过同一套签名校验,用于确保:
- 请求来源可信(未被第三方伪造);
- 内容未被篡改(金额等参数在传输途中未被修改);
- 请求未被重放(历史请求无法被重复提交以触发存入或提走)。
5.1 通信规则
商户调用 Hash Play 的每个请求均携带 4 个专用请求头:
| 请求头 | 含义 | 说明 |
|---|---|---|
X-API-Key | 商户身份标识 | 填商户 API Key |
X-Timestamp | 请求时间戳 | 统一为毫秒级时间戳(13 位,即 Unix epoch 毫秒)。与服务器时间偏差超过 ±60 秒(60000ms)的请求将被拒收 |
X-Nonce | 一次性随机串 | 每个请求唯一(建议 32 位随机十六进制,如 UUID 去横线)。平台侧会缓存已用过的随机串(约 2 分钟),同一随机串再次出现即判定为重放攻击并直接拒绝 |
X-Signature | 签名 | 按 5.2 规则以 API Secret 计算所得 |
验签通过后平台才会继续校验商户状态(
4005)与出口 IP 白名单(4004),因此签名错误会先于 IP 问题暴露——排查时请按4002 → 4005 → 4004的顺序逐项确认。
5.2 签名计算方法(唯一算法,双方一致)
签名算法为业界通用的 HMAC-SHA256,主流编程语言均有标准实现。计算分两步:
第一步:构造规范化签名串(canonical string)。
- 取请求体(JSON)中的全部顶层字段,剔除值为空的字段;
- 按字段名字典顺序排序,拼接为
字段名=值&字段名=值&...的形式; - 末尾依次追加时间戳与随机串(以
&连接)。
例如存入接口的请求体为:
{
"user_id": "u_10086",
"request_id": "dep_20260908_0001",
"amount": "1000.00"
}
按字段名字典序排序拼接并追加时间戳、随机串后,待签名原文为:
amount=1000.00&request_id=dep_20260908_0001&user_id=u_10086&1712345678123&f3a9c2e8d1b4...
无请求体的接口(如
GET /list):签名串中没有字段部分,退化为&{timestamp}&{nonce}(即以&开头)。这是常见踩坑点——此时不要在开头凭空拼一个&之外的字符。
第二步:以 API Secret 为密钥,对原文执行 HMAC-SHA256 运算,结果以十六进制小写表示,即为 X-Signature 的值。
关于"原始报文"的说明(消除歧义):签名对象永远是上面的规范化签名串,而非 HTTP 报文的原始字节。因此 JSON 的空格、字段顺序、大小写差异均不影响验签结果。推荐实现方式是:解析 JSON → 提取字段 → 重新构造规范化签名串(见第 6 章代码示例)。所谓"不要重新序列化"指的是不要把解析后的对象直接转回 JSON 字符串再拿去 HMAC——那不是本协议的签名对象。
5.3 重要注意事项(对接失败的常见原因)
- 时间戳单位统一为毫秒:
X-Timestamp使用毫秒(13 位 Unix epoch 毫秒值)。校验窗口为 ±60 秒(60000 毫秒),请确保服务器时间为标准时间(建议开启 NTP 同步)。 - 禁止使用 JSON 数字类型传递金额:
amount、balance、bet_amount、win_amount、mult_value等字段在请求与响应中均为 JSON 字符串(如"1000.00")。禁止写成数字("amount": 10.00),否则平台按数字文本参与签名时可能变为10.0,与商户签名串不一致而返回4002。构造签名串时按请求体原样字符串拼接("1000.00"→amount=1000.00),禁止数值格式化;前端展示时再Number()转换。 - 空值剔除:值为
null、空字符串的字段不参与签名串构造(平台侧同样跳过)。因此"amount": null与不传该字段等价。 - 字段名大小写敏感:签名串与请求体字段名均使用小写下划线风格(
user_id、request_id、game_code…),请勿写成驼峰。
5.4 验签失败的统一响应
Hash Play 校验商户请求验签未通过时,返回:
{
"code": 4002,
"message": "签名验证失败"
}
message 为本地化提示文本(可能随语言变化),请始终以 code 作为判定依据。例外:游戏列表接口 /list 在验签失败时固定返回 {"code": 4002, "message": "SIGNATURE_VERIFY_FAILED"}。
5.5 响应结构与两类错误
业务接口的响应统一为扁平 JSON,成功与业务失败均返回 HTTP 200,以 code 区分(code = 0 表示成功):
{ "code": 0, "message": "success", "...": "各接口自有字段" }
需注意两类错误的字段名不一致,解析时请同时兼容:
| 错误来源 | 响应形态 | 说明 |
|---|---|---|
| 业务校验失败(验签、商户、IP、金额、余额、参数语义等) | { "code": <4001~4017>, "message": "..." } | 见 8.8 统一返回码约定 |
| 框架级错误(参数校验/反序列化失败、系统异常等) | { "code": 500, "msg": "..." } | 注意字段名是 msg 而非 message |
分工说明:
/launch、/exchange对请求体做了注解校验,漏传operator_id/user_id/currency这类必填项时会走框架级错误({code:500, msg}),而不是业务错误码;/wallet/*未做注解校验,字段缺失一般返回业务码(如4011),只有请求体整体缺失或不是合法 JSON 时才返回{code:500, msg}。实现建议:先判 HTTP 状态码,再判
code === 0;取提示文案时用data.message ?? data.msg。
6. 代码示例
以下示例覆盖签名生成、Launch 调用与钱包存取调用,可直接参考实现。
const crypto = require('crypto');
// 签名生成:body 为请求体对象
function generateSignature(apiSecret, timestampMs, nonce, body) {
const sorted = {};
Object.keys(body || {})
.filter((k) => body[k] !== null && body[k] !== undefined && body[k] !== '')
.sort()
.forEach((k) => (sorted[k] = String(body[k])));
const canonical = Object.entries(sorted)
.map(([k, v]) => `${k}=${v}`)
.join('&');
const signText = `${canonical}&${timestampMs}&${nonce}`;
return crypto
.createHmac('sha256', apiSecret)
.update(signText, 'utf8')
.digest('hex'); // 十六进制小写
}
// 调用 /launch(钱包 /wallet/* 接口同理,仅换路径与请求体)
const axios = require('axios');
async function launch(apiSecret, apiKey, payload) {
const timestamp = String(Date.now()); // 毫秒级(13 位)
const nonce = crypto.randomUUID().replace(/-/g, '');
const signature = generateSignature(apiSecret, timestamp, nonce, payload);
const resp = await axios.post(
'https://api.example.com/prod-api/customer/app-api/api/v1/game/launch',
payload,
{
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
},
}
);
return resp.data; // { code: 0, message: 'success', game_url: '...', balance: '0' }
}
// 调用 /wallet/deposit:为玩家托管钱包充值
// request_id 为本次存入的唯一订单号(商户侧生成),平台按 (operator_id, request_id) 幂等防重
async function deposit(apiSecret, apiKey, userId, requestId, amount) {
// 金额用字符串传入,避免浮点序列化差异导致验签失败
const payload = { user_id: userId, request_id: requestId, amount: String(amount) };
const timestamp = String(Date.now());
const nonce = crypto.randomUUID().replace(/-/g, '');
const signature = generateSignature(apiSecret, timestamp, nonce, payload);
const resp = await axios.post(
'https://api.example.com/prod-api/customer/app-api/api/v1/game/wallet/deposit',
payload,
{
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
},
}
);
return resp.data; // { code: 0, message: 'success', currency: 'USDT', amount: '1000.00', balance: '1000.00', txn_id: '...' }
}
// 调用 /wallet/operation/status:按商户订单号核对存取是否成功(超时/未知结果时使用)
async function operationStatus(apiSecret, apiKey, requestId, operationType) {
const payload = { request_id: requestId, operation_type: operationType }; // 'DEPOSIT' | 'WITHDRAW'
const timestamp = String(Date.now());
const nonce = crypto.randomUUID().replace(/-/g, '');
const signature = generateSignature(apiSecret, timestamp, nonce, payload);
const resp = await axios.post(
'https://api.example.com/prod-api/customer/app-api/api/v1/game/wallet/operation/status',
payload,
{
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
},
}
);
return resp.data; // { code: 0, status: 1, amount: '1000.00', txn_id: '...' }
}
// 调用 GET /list(游戏列表):无请求体,签名串退化为 '×tamp&nonce'
async function gameList(apiSecret, apiKey, language = 'zh-CN') {
const timestamp = String(Date.now());
const nonce = crypto.randomUUID().replace(/-/g, '');
const signature = generateSignature(apiSecret, timestamp, nonce, null); // body 传 null
const resp = await axios.get(
`https://api.example.com/prod-api/customer/app-api/api/v1/game/list?language=${encodeURIComponent(language)}`,
{
headers: {
'X-API-Key': apiKey,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
},
}
);
return resp.data; // { code: 0, games: [...] }
}7. 游戏启动
7.1 业务流程
- 玩家在商户平台点击游戏入口;
- 商户后端(不得从前端直接调用,否则将暴露 Secret)向 Hash Play 申请游戏入场链接;
- Hash Play 校验签名与参数后,为该
(operator_id, user_id)查找或创建平台用户与托管钱包账户,并生成一次性入场链接(内含一次性票据 ticket)返回,同时返回该玩家当前的平台内托管余额; - 商户将链接交由玩家浏览器打开(新窗口或内嵌均可),玩家进入游戏。
入场链接每次申请仅有效一次且具有时效(票据 ticket 有效期 5 分钟,兑换一次后立即失效),不得缓存复用,亦不得多玩家共用。玩家每次进入游戏重新申请即可,该接口开销很小。
幂等提示:
/launch按(operator_id, user_id)查找或创建平台用户与托管钱包,重复调用不会新建用户,也不会改变余额,可以放心重复调用(但每次都要重新打开返回的game_url)。余额来源:返回的
balance是玩家当前的平台托管余额(字符串)。首次启动通常为"0",请随后调用/wallet/deposit为玩家充值(见第 8 章)。
7.1.1 game_code 不传时会发生什么
game_code 是可选字段,语义分两种情况:
是否传 game_code | 平台行为 |
|---|---|
传了(如 Dice) | 按 game_type 字典解析游戏标识 → 校验「商户 × 游戏」权限(无权限 4006)与限额配置(未配置 4008)→ 返回该游戏的入场链接 |
| 不传 / 传空串 | 视为「游戏待定」→ 跳过游戏维度的权限与限额校验(仅保留签名、商户状态、IP 白名单三道)→ 返回游戏大厅的通用地址,玩哪款游戏由玩家进入后自行选择 |
因此若商户希望在大厅中屏蔽未开通的游戏入口,仍需传入
game_code走权限校验;而不传game_code时,玩家进入大厅后能否开局由平台按游戏权限另行判定。
game_code与game_type字典的标签(dictLabel)做大小写不敏感匹配(crash/CRASH/Crash等价);若某个标签自身含空格,则允许只传其第一个单词作为简写。最稳妥的做法始终是:直接用 游戏列表 API 返回的gameCode原值,不要自己改写。
7.2 接口定义
商户调用:
POST {base_url}/api/v1/game/launch
请求参数(JSON 格式,字段名为小写下划线风格):
| 参数 | 必填 | 展示对象 | 说明 |
|---|---|---|---|
operator_id | 是 | 系统 | 商户编码,示例 M001 |
user_id | 是 | 系统 | 玩家在商户平台的唯一账号标识。同一玩家须始终传同一 ID,Hash Play 以 (operator_id, user_id) 定位(或创建)平台用户及其托管钱包 |
game_code | 否 | 系统 | 游戏编码,由 Hash Play 提供清单,示例 Crash(完整清单与实时查询接口见第 13 章),不传直接跳转游戏大厅 |
currency | 是 | 系统 | 玩家使用的货币代码,必须传 USDT。当前 B2B 为 USDT 单币种,平台侧忽略其取值(传其他币种不会改变结算币种),但该字段仍必填 |
username | 否 | 玩家 | 玩家昵称。传入后于游戏内展示;不传则显示系统默认值 |
avatar | 否 | 玩家 | 玩家头像图片地址(URL)。传入后于游戏内展示 |
language | 否 | 玩家 | 游戏界面语言。不传默认英文。取值请使用 SimpleLocalize 语言代码表 中的标识,格式为 语言-国家(如 zh-CN、pt-BR、th-TH),常用取值见下方附表 |
return_url | 否 | 玩家 | 玩家退出游戏后返回商户平台的地址,示例 https://yourdomain.com/lobby |
language 常用取值(完整列表见 SimpleLocalize 语言代码表):
| 语言 | 代码 | 语言 | 代码 | 语言 | 代码 |
|---|---|---|---|---|---|
| 简体中文 | zh-CN | 繁体中文(台湾) | zh-TW | 繁体中文(香港) | zh-HK |
| 英语(美国) | en-US | 英语(英国) | en-GB | 西班牙语(西班牙) | es-ES |
| 日语 | ja-JP | 韩语 | ko-KR | 越南语 | vi-VN |
| 泰语 | th-TH | 印尼语 | id-ID | 俄语 | ru-RU |
| 土耳其语 | tr-TR | 阿拉伯语 | ar-SA | 印地语 | hi-IN |
传入前请与 Hash Play 确认该语言的游戏语言包是否就绪;传入未支持的语言时,游戏将回退至英文界面。
请求示例:
{
"operator_id": "M001",
"user_id": "u_10086",
"game_code": "Crash",
"currency": "USDT",
"username": "玩家A",
"language": "zh-CN",
"return_url": "https://yourdomain.com/lobby"
}
Hash Play 返回:
{
"code": 0,
"message": "success",
"game_url": "https://game.hashplay.io/hashWeb/#/zh-CN?ticket=8f3e...c21a",
"balance": "1000.00"
}
| 字段 | 说明 |
|---|---|
game_url | 一次性入场链接,交由玩家浏览器打开 |
balance | 该玩家当前的平台托管余额(字符串,如 "1000.00";首启通常为 "0") |
失败返回({code, message, game_url: null}):
| 返回码 | 含义 | 商户处理 |
|---|---|---|
0 | 成功 | 将 game_url 交付前端打开;balance 为该玩家当前托管余额(首启通常为 "0",可据此决定是否提示充值) |
4002 | 验签失败 | 检查签名实现(见 5.3);不可重试,修正后重发 |
4004 | IP 不在白名单 | 商户出口 IP 未登记或已变更,联系 Hash Play 核对;不可重试 |
4005 | 商户无效或停用 | 核对 operator_id 与商户状态,联系 Hash Play |
4006 | 游戏不存在或商户无该游戏权限 | 传入的 game_code 未在字典中,或该商户未开通;改用 游戏列表 API 中 status=1 的游戏,或不传 game_code 走大厅 |
4008 | 游戏限额未配置 | 该「商户 × 游戏」未配置限额,联系 Hash Play 开通 |
4009 | 游戏地址未配置或已停用 | 该游戏的入场地址未配置,联系 Hash Play |
4010 | 玩家已被禁用 | 该 user_id 对应的平台用户处于禁用/删除状态,按业务提示玩家 |
500 | 系统异常 | 参考 message 向玩家提示(如"游戏暂不可用"),可稍后重试 |
除
500外,上述失败重试同一请求大概率仍会失败,请按上表定位原因后再重发,避免无脑轮询。玩家侧统一提示为「游戏暂不可用,请稍后再试」即可。
8. 钱包接口
玩家进入游戏后,游戏内的一切资金变动(投注扣减、派奖入账、异常局次退款)均由 Hash Play 在该玩家的托管钱包内实时本地结算,商户无需感知、无需提供回调。商户的职责是按本节规范调用 6 个钱包接口,管理玩家在平台内的托管资金。
8.1 六个钱包接口的语义
| 接口 | 方式 | 调用时机 | 说明 |
|---|---|---|---|
存入 /wallet/deposit | POST | 玩家进入游戏前后,商户决定为玩家充值时 | 向该玩家的托管余额累加 amount,写入 DEPOSIT 流水 |
提走 /wallet/withdraw | POST | 玩家退出游戏、商户回收资金时 | 提现指定金额或全部余额,写入 WITHDRAW 流水 |
余额查询 /wallet/balance | POST | 任意时刻(如商户平台展示余额、风控巡检) | 查询该玩家当前托管余额 |
流水查询 /wallet/transactions | POST | 对账、客诉核查时 | 按时间范围查询该玩家的全部金额变动流水(含游戏内投注/派奖/撤销) |
订单查询 /wallet/orders | POST | 对账、客诉核查、投注行为分析时 | 按时间范围查询该玩家的投注订单(含游戏编码、投注/派奖金额、结算状态、乘数等局次详情) |
存取状态 /wallet/operation/status | POST | deposit/withdraw 超时或未知结果后核对 | 按商户订单号 request_id + 类型查询是否成功 |
六个接口统一位于 {base_url}/api/v1/game/wallet 下。除存取状态外,均按 (operator_id, user_id) 寻址玩家(operator_id 由 X-API-Key 对应的凭证自动确定,请求体中无需传);存取状态按 (operator_id, request_id, operation_type) 查询;流水/订单查询的 user_id 可不传,缺省查询该商户全部用户(响应 records 中含 user_id 供区分)。鉴权与 /launch 完全一致(四请求头验签 + IP 白名单)。
8.2 存入(Deposit)——为玩家充值
商户调用:
POST {base_url}/wallet/deposit
请求参数:
| 参数 | 必填 | 说明 |
|---|---|---|
user_id | 是 | 玩家在商户平台的账号标识(须与 /launch 传入的一致) |
request_id | 是 | 商户侧订单号,本次存入的唯一标识(建议 dep_日期_序号)。平台按 (operator_id, request_id) 幂等防重,同一个成功的 request_id 不可重复使用 |
amount | 是 | 存入金额,字符串,正数,保留两位小数(如 "1000.00") |
request_id是存取接口的幂等键,也是后续用/wallet/operation/status核对结果的查询键。请由商户侧生成并落库(与自己的充值订单绑定),长度建议 ≤ 64。
request_id不是随机串:它必须能唯一标识"这一笔业务"。网络超时后重发时沿用同一个request_id;而一次全新的充值必须换新号,否则会被4014拒绝。
请求示例:
{
"user_id": "u_10086",
"request_id": "dep_20260908_0001",
"amount": "1000.00"
}
Hash Play 返回:
{
"code": 0,
"message": "success",
"currency": "USDT",
"amount": "1000.00",
"balance": "1000.00",
"txn_id": "txn_20260904_0001"
}
amount 为本次存入金额,balance 为存入后的最新托管余额,txn_id 为本笔流水号(由 Hash Play 统一生成,供对账使用)。金额字段均为字符串(见 5.3)。
deposit 可能返回的错误码:
| 返回码 | 含义 | 商户处理 |
|---|---|---|
4001 | 金额非法 | 未传 amount 或 ≤ 0,修正参数后重试 |
4011 | user_id 为空 | 补充参数后重试 |
4012 | user_id 无效 | 该 (operator_id, user_id) 未找到用户——请先调 /launch 创建用户 |
4013 | request_id 为空 | 补充参数后重试 |
4014 | 该 request_id 已成功执行过 | 禁止重复执行:改用 /wallet/operation/status 核对,或换新 request_id |
4015 | 钱包操作执行失败 | 失败已留痕(status=0),可用原 request_id 重试 |
8.3 提走(Withdraw)——回收余额
商户调用:
POST {base_url}/wallet/withdraw
请求参数:
| 参数 | 必填 | 说明 |
|---|---|---|
user_id | 是 | 玩家在商户平台的账号标识(须与 /launch 传入的一致) |
request_id | 是 | 商户侧订单号,本次提取的唯一标识(建议 wd_日期_序号)。平台按 (operator_id, request_id) 幂等防重 |
amount | 否 | 提现金额,字符串,正数,保留两位小数(如 "500.00");不传则提走全部余额并清零 |
全额提走时不要传
amount(或传空串),不要自己先查余额再传入该数值——两次调用之间可能会发生游戏结算,导致金额不匹配而返回4001。
请求示例(部分提现):
{
"user_id": "u_10086",
"request_id": "wd_20260908_0001",
"amount": "500.00"
}
请求示例(全额提走):
{
"user_id": "u_10086",
"request_id": "wd_20260908_0002"
}
Hash Play 返回:
- 提现成功:
{ "code": 0, "message": "success", "currency": "USDT", "amount": "500.00", "balance": "515.00", "txn_id": "txn_20260904_0002" }(amount为本次提现金额,balance为提现后最新余额;全额提走时balance为"0.00") - 余额为零:
{ "code": 4003, "message": "WALLET_BALANCE_ZERO", "currency": "USDT", "balance": "0.00" }(无可提资金,按业务场景处理即可)
Withdraw 支持部分提现:传入
amount提取指定金额;不传amount则提走全部余额并清零。提现金额不得超过当前余额,否则平台返回4001金额非法。
withdraw 可能返回的错误码:
| 返回码 | 含义 | 商户处理 |
|---|---|---|
4001 | 金额非法 | 传入的 amount ≤ 0 或大于当前托管余额(并发结算下余额已变动)。建议改为不传 amount 全额提走 |
4003 | 托管余额为零 | 无可提资金,按业务场景处理(一般视为已提完) |
4011 / 4012 | user_id 为空 / 无效 | 补参或先调 /launch 创建用户 |
4013 | request_id 为空 | 补充参数后重试 |
4014 | 该 request_id 已成功执行过 | 改用 /wallet/operation/status 核对,或换新 request_id |
4015 | 钱包操作执行失败 | 失败已留痕(status=0),可用原 request_id 重试 |
8.4 余额查询(Balance)
商户调用:
POST {base_url}/wallet/balance
请求参数:
{ "user_id": "u_10086" }
user_id 必填(同 8.3)。余额查询不涉及动账,无需 request_id。
Hash Play 返回:
{
"code": 0,
"message": "success",
"currency": "USDT",
"balance": "1015.00"
}
balance为字符串(如"1015.00"),商户前端展示前请自行Number()/BigDecimal转换,勿用typeof === 'number'判断"平台未返回"。
可能返回的错误码: 4011 / 4012(user_id 为空 / 无效——通常意味着该玩家尚未通过 /launch 在平台建档)。
8.5 流水查询(Transactions)——对账
商户调用:
POST {base_url}/wallet/transactions
请求参数:
| 参数 | 必填 | 说明 |
|---|---|---|
user_id | 否 | 玩家在商户平台的账号标识;不传则查询该商户全部用户的流水 |
start_time | 否 | 起始时间(毫秒时间戳,含) |
end_time | 否 | 截止时间(毫秒时间戳,含;不得早于 start_time) |
page_num | 否 | 页码(从 1 开始,默认 1) |
page_size | 否 | 每页条数(默认 10,最大 500) |
Hash Play 返回:
{
"code": 0,
"message": "success",
"currency": "USDT",
"page_num": 1,
"page_size": 10,
"total": 3,
"records": [
{
"user_id": "u_10086",
"txn_id": "txn_20260904_0001",
"txn_type": "DEPOSIT",
"amount": "1000.00",
"balance_before": "0.00",
"balance_after": "1000.00",
"round_id": null,
"ref_txn_id": null,
"remark": "deposit:M001",
"create_time": 1756948800000
},
{
"user_id": "u_10086",
"txn_id": "txn_20260904_0003",
"txn_type": "BET",
"amount": "10.00",
"balance_before": "1000.00",
"balance_after": "990.00",
"round_id": "r_20260904_0001",
"ref_txn_id": null,
"remark": null,
"create_time": 1756948900000
},
{
"user_id": "u_10086",
"txn_id": "txn_20260904_0004",
"txn_type": "WIN",
"amount": "25.00",
"balance_before": "990.00",
"balance_after": "1015.00",
"round_id": "r_20260904_0001",
"ref_txn_id": "txn_20260904_0003",
"remark": null,
"create_time": 1756948950000
}
]
}
| 字段 | 说明 |
|---|---|
page_num / page_size / total | 分页页码、每页条数、总记录数 |
user_id | 玩家在商户平台的账号标识(即请求传入的 user_id;跨用户查询时用于区分归属) |
txn_id | 流水号(全局唯一) |
txn_type | 流水类型:DEPOSIT(商户存入)/ WITHDRAW(商户提走)/ BET(投注)/ WIN(派奖)/ ROLLBACK(异常局退款)/ 其他游戏内动账类型 |
amount | 变动金额(字符串,正数) |
balance_before / balance_after | 变动前后余额(字符串) |
round_id | 关联游戏局号(存取款类流水为 null) |
ref_txn_id | 关联的原交易流水号(如派奖对应投注、退款对应投注) |
remark | 备注信息 |
create_time | 流水发生时间(毫秒时间戳) |
8.6 订单查询(Orders)——投注订单核查
商户调用:
POST {base_url}/wallet/orders
请求参数(与 8.5 流水查询一致):
| 参数 | 必填 | 说明 |
|---|---|---|
user_id | 否 | 玩家在商户平台的账号标识;不传则查询该商户全部用户的订单 |
start_time | 否 | 起始时间(毫秒时间戳,含) |
end_time | 否 | 截止时间(毫秒时间戳,含;不得早于 start_time) |
page_num | 否 | 页码(从 1 开始,默认 1) |
page_size | 否 | 每页条数(默认 10,最大 500) |
Hash Play 返回:
{
"code": 0,
"message": "success",
"currency": "USDT",
"page_num": 1,
"page_size": 10,
"total": 1,
"records": [
{
"user_id": "u_10086",
"round_id": "r_20260904_0001",
"game_code": "Crash",
"currency": "USDT",
"bet_amount": "10.00",
"win_amount": "25.00",
"status": "SETTLED",
"settle_result": 1,
"mult_value": "2.5",
"settle_time": 1756948950000,
"create_time": 1756948900000,
"game_detail": "{...}"
}
]
}
| 字段 | 说明 |
|---|---|
page_num / page_size / total | 分页页码、每页条数、总记录数 |
user_id | 玩家在商户平台的账号标识(即请求传入的 user_id;跨用户查询时用于区分归属) |
round_id | 游戏局号(与 /wallet/transactions 流水中的 round_id 对应,可交叉核对) |
game_code | 游戏编码(与 /launch、游戏列表 API 的取值一致) |
currency | 币种 |
bet_amount | 本局总投注金额(字符串) |
win_amount | 本局派奖金额(字符串) |
status | 局次状态:OPEN(进行中)/ SETTLED(已结算)/ CANCELLED(已取消) |
settle_result | 结算结果:1 赢 / 0 输(仅 SETTLED 状态有值) |
mult_value | 结算乘数(字符串,如 "2.5";按游戏玩法提供,可能为空) |
settle_time | 结算时间(毫秒时间戳) |
create_time | 局次创建时间(毫秒时间戳) |
game_detail | 游戏局次详情(游戏玩法相关的 JSON,供客诉核查,格式因游戏而异) |
流水(8.5)记录的是每一笔资金变动(含存取款),订单(本节)记录的是每一局游戏的投注与结算结果。两者通过
round_id关联,配合使用可完整还原玩家行为与资金变化。
8.7 存取状态查询(Operation Status)——核对商户订单是否成功
商户调用:
POST {base_url}/wallet/operation/status
请求参数:
| 参数 | 必填 | 说明 |
|---|---|---|
request_id | 是 | 商户侧订单号(与 deposit/withdraw 时传入的 request_id 一致) |
operation_type | 是 | 类型:DEPOSIT(存入)/ WITHDRAW(提走);大小写不敏感,其他取值返回 4017 |
请求示例:
{
"request_id": "dep_20260908_0001",
"operation_type": "DEPOSIT"
}
本接口不需要传
user_id——request_id在商户维度内唯一即可定位。
Hash Play 返回:
{
"code": 0,
"message": "success",
"currency": "USDT",
"request_id": "dep_20260908_0001",
"operation_type": "DEPOSIT",
"status": 1,
"amount": "1000.00",
"txn_id": "txn_20260904_0001",
"error_msg": null,
"create_time": 1756948800000
}
| 字段 | 说明 |
|---|---|
status | 1 成功 / 0 失败(同一 request_id 若曾成功则优先返回成功记录) |
amount | 操作金额(字符串;全额提现时为实际提走金额) |
txn_id | 成功时关联的钱包流水号;失败时为 null |
error_msg | 失败原因;成功时为 null |
create_time | 该条记录创建时间(毫秒时间戳) |
可能返回的错误码: 4016(未找到该 request_id 的记录——包含从未发起、request_id 拼写错误两种情况)/ 4017(operation_type 非 DEPOSIT/WITHDRAW)。
超时或未知结果时,请先调本接口核对:
status=1表示已成功入账/扣款,勿用同一request_id重发;status=0或返回4016(记录不存在)时,可用原request_id重试。
8.8 统一返回码约定
| 返回码 | 含义 | 商户处理 |
|---|---|---|
0 | 成功 | 附带最新 balance(涉及动账时附 amount、txn_id) |
4001 | 金额非法 | deposit 未传 amount 或 ≤ 0;withdraw 的 amount > 当前余额。修正参数后重试 |
4002 | 验签失败 | 检查签名实现(见 5.3 注意事项) |
4003 | 托管余额为零 | 仅 withdraw 可能返回,无资金可提,按业务处理 |
4004 | IP 不在白名单 | 商户出口 IP 未登记,联系 Hash Play 核对 |
4005 | 商户无效或停用 | 核对 API Key 与商户状态,联系 Hash Play |
4006 | 时间范围非法 | 仅 transactions / orders 可能返回(start_time > end_time,或时间戳非毫秒数值) |
4011 | user_id 为空 | 补充参数后重试 |
4012 | user_id 无效 | 该 (operator_id, user_id) 未找到对应用户——请先调 /launch 创建用户 |
4013 | request_id 为空 | 补充参数后重试 |
4014 | request_id 已成功执行 | 禁止重复执行,改用本接口核对或换新订单号 |
4015 | 钱包操作执行失败 | 失败已留痕,可用原 request_id 重试 |
4016 | 存取操作记录不存在 | 仅 /wallet/operation/status:订单号/类型无匹配记录 |
4017 | operation_type 非法 | 仅 /wallet/operation/status:仅支持 DEPOSIT / WITHDRAW |
同名不同义:
4006、4011、4012、4013在/launch与/wallet/*下含义不同(前者报的是票号/游戏权限类问题,后者报的是参数类问题),部分文案还受多语言影响。请按"接口 + code"两部分一起判断,不要只看 code:
code /launch下的含义/wallet/*下的含义4006商户无该游戏权限(见 7.2) transactions/orders 时间范围非法 4011ticket为空(仅/exchange)user_id为空4012ticket无效或已过期user_id无效(未建档)4013ticket对应用户不存在request_id为空
9. 完整交互示例
以玩家(商户为其首次充值 1000.00 USDT)进行一局 Crash 游戏为例:
点击放大
对账公式(任意时刻均应成立,供商户财务核验):
所有存入(Deposit) − 所有提走(Withdraw) − 所有投注(Bet) + 所有派奖(Win) + 所有退款(Rollback) = 当前托管余额(Balance)
上图示例:商户 deposit 1000(余额 0 → 1000.00)、玩家游戏内投注 Debit 10(→ 990.00)、派奖 Win 25(→ 1015.00),玩家退出后商户 withdraw 提走全部 1015.00(余额清零)。若局次异常被取消,平台自动追加一笔 Rollback 把投注本金原路退回托管余额,无需商户介入。
10. 商户系统技术要求
以下为余额托管模式的硬性要求,直接关系到玩家体验与双方资金安全:
- 资金变动以 Hash Play 流水为准:游戏内动账全部发生在平台侧,商户财务请以
/wallet/transactions流水 +/wallet/balance余额为对账依据,公式见第 9 章。 user_id稳定唯一:托管钱包以(operator_id, user_id)寻址。同一玩家必须始终使用同一 ID,错用其他玩家的 ID 将操作到他人钱包。- 充值前置:玩家投注使用托管余额,余额不足时投注将被平台拒绝并向玩家提示。请确保玩家进入游戏前或余额耗尽时及时 deposit。
- 提现支持部分金额:withdraw 传入
amount提取指定金额,不传则提走全部余额并清零;如遇4003表示余额已为零,属正常业务结果而非故障。 - 响应性能:deposit/balance 位于玩家进入游戏的关键路径上,请确保商户侧发起调用的链路稳定;Hash Play 侧目标 500 毫秒内响应。
- 先签验后调用:所有请求必须携带合法签名(规则见第 5 章;失败返回
4002)。 - IP 白名单必配:商户出口 IP 须在 Hash Play 侧登记,未登记时全部接口返回
4004。支持 IPv4 / CIDR;出口变更须提前通知。 - 时钟准确:服务器须同步 NTP;时间戳与窗口规则见 5.3。
- 金额类型:禁止使用 JSON 数字传递金额,规则见 5.3。
request_id与重试:deposit/withdraw 的request_id须落库并作为幂等键(规则见 8.2);超时或未知结果时先调/wallet/operation/status核对,禁止未核对就换新号重发。
11. 接入清单
- 准备:从 Hash Play 获取商户编码、API Key、API Secret、网关地址(沙箱/生产,见第 3 章环境信息)
- 准备:向 Hash Play 提供商户服务器出口 IP(必配,支持 IPv4 / CIDR;不登记则全部接口返回
4004) - 准备:调
/api/v1/game/list拉取当前已开通的游戏清单,不要写死本地游戏编码表(清单会随平台上下架变动) - 开发:实现 HMAC-SHA256 签名工具,通过 Hash Play 提供的签名自测用例(重点验证:字段字典序、空值剔除、金额按原样字符串拼接、毫秒级时间戳)
- 开发:实现游戏启动调用(服务端调
/launch、向前端分发game_url) - 开发:实现钱包接口调用(
/wallet/deposit、/wallet/withdraw、/wallet/balance、/wallet/transactions、/wallet/orders、/wallet/operation/status) - 开发:
request_id生成规则 + 落库;deposit/withdraw 前必填校验;超时后走/wallet/operation/status核对 - 开发:金额字段按 5.3 以字符串全链路处理(展示前再
Number()) - 自测:正常场景——充值 → 余额查询 → 部分提现 / 全额提走
- 自测:异常场景——验签失败(4002)、IP 未登记(4004)、余额为零提走(4003)、
user_id无效(4012)、金额非法(4001)、request_id缺失(4013)、request_id重复(4014) - 自测:时间戳超出 ±60 秒窗口 → 确认返回
4002且能通过 NTP 修复 - 联调:与 Hash Play 共同跑通全流程:启动 → 充值 → 投注 → 派奖 → 流水核对 → 提走
- 上线:生产凭证切换、IP 白名单确认、上线首日对账
12. 常见问题
按以下顺序排查:
①时间戳:须为毫秒级 13 位,与 Hash Play 偏差 ≤ 60 秒(建议 NTP);
②签名串:字段字典序、空值剔除、末尾 &{timestamp}&{nonce};
③金额类型:禁止使用 JSON 数字,须传字符串(详见 5.3);
④签名大小写:十六进制须小写;
⑤nonce:约 2 分钟内不可复用;
⑥GET 无请求体(如 /list):签名串退化为 &{timestamp}&{nonce}。
请对照第 6 章示例自测。
13. 游戏列表
13.1 游戏列表 API(推荐,实时获取)
游戏清单持续更新,请优先通过本接口实时获取,不要把清单写死在您的系统里——Hash Play 上线新游戏后无需等待文档重发,接口即刻可见:
商户调用:
GET {base_url}/api/v1/game/list?language=zh-CN
| 参数 | 必填 | 说明 |
|---|---|---|
language | 否 | 游戏名称的国际化语言,格式同 /launch 的 language(如 zh-CN)。不传默认英文 |
签名:与 /launch 完全一致(X-API-Key / X-Timestamp 毫秒级 ±60 秒 / X-Nonce / X-Signature)。GET 请求无请求体,签名串退化为 &时间戳&随机串(见 5.2)。
Hash Play 返回:
{
"code": 0,
"message": "success",
"games": [
{
"gameCode": "Crash",
"gameName": "爆点",
"status": 1,
"supportedCurrency": "USDT",
"thumbnail": "https://resource.hashplay.io/xxx.png",
"isMultiplayer": 0,
"sort": 1
}
]
}
| 字段 | 说明 |
|---|---|
gameCode | 游戏编码,即 /launch 入参 game_code 的取值。大小写不敏感;若标签自身含空格,支持只传首单词作为简写。该值取自平台 game_type 字典的标签,运营侧可能改名,请勿缓存到本地代码 |
gameName | 游戏名称(按 language 国际化,无翻译时回退英文) |
status | 1 = 贵商户已开通该游戏;0 = 平台已上架但贵商户未开通 |
supportedCurrency | 支持币种(当前 B2B 仅 USDT) |
thumbnail | 游戏缩略图完整 URL,可用于商户前端游戏大厅展示 |
isMultiplayer | 是否多人游戏(0 否 / 1 是) |
sort | 排序号,越小越靠前(可按此字段排游戏大厅顺序) |
本接口返回平台全量上架游戏,
status表示"贵商户是否已开通"。请只对status=1的游戏展示入口——传入未开通游戏的game_code调/launch会返回4006。
错误码:验签失败、商户停用、IP 未登记时返回 { "code": <4002|4005|4004>, "message": "SIGNATURE_VERIFY_FAILED" 或对应文案, "games": null }。注意本接口的失败文案不跟随语言(验签失败固定为字面量 SIGNATURE_VERIFY_FAILED),请以 code 为准。
建议商户每次玩家进入游戏大厅时调用本接口(或短 TTL 缓存,如 5 分钟),
status=1的游戏才展示入口。
13.2 附录:游戏编码速查表(快照,仅供参考)
本表只是某一时点的快照,不是契约。
game_code的真实来源是平台game_type字典的dictLabel,运营侧会随游戏上下架、改名而调整(历史上Coinflip→flipCoin、Xoc Dia→Xocdia、Cock fighting→CockFight都发生过)。请一律以 13.1 的/api/v1/game/list接口为准,本表仅用于理解命名风格与联调时人工核对。
| 序号 | 游戏名称(中文 / 英文) | game_code |
|---|---|---|
| 1 | 爆点 / Crash | Crash |
| 2 | 骰宝 / Dice | Dice |
| 3 | 希洛 / HiLo | Hilo |
| 4 | 过纸牌 / Between | Between |
| 5 | 翻转硬币 / Flip Coin | flipCoin |
| 8 | 轮盘赌 / Roulette | Roulette |
| 9 | 哈希28 / Hash 28 | TwentyEight |
| 10 | 极速倍率 / Limbo | Limbo |
| 11 | 扫雷 / Mine | Mine |
| 12 | 视频扑克 / Video Poker | VideoPoker |
| 13 | 生肖扫雷 / Zodiac | Zodiac |
| 14 | 传奇之塔 / Tower | Tower |
| 15 | 虾蟹 / Xoc Dia | Xocdia |
| 16 | 牛牛 / Bull | Bull |
| 17 | 百家乐 / Baccarat | Baccarat |
| 18 | 百家乐(变体)/ Baccarats | Baccarats |
| 19 | 博状元 / Bo Bing | BoBing |
| 20 | 斗鸡 / Cock Fighting | CockFight |
| 21 | 无佣百家乐 / No Commission Baccarat | NCBaccarat |
| 22 | 黄金峡谷 / Golden Canyon | goldenCanyon |
| 23 | 赛博朋克 / Cyberpunk | cyberpunk |
| 24 | 财神 / Fortune God | fortuneGod |
| 25 | 火龙 / Fiery Dragon | fieryDragon |
| 26 | 太空入侵者 / Space Invader | spaceInvader |
上表"序号"即平台
game_type字典的dictValue(内部编号,商户无需传该编号,/launch只认game_code标签)。编号 6、7 已废弃。匹配规则回顾:大小写不敏感(
crash、CRASH、Crash等价);若标签自身含空格,允许只传首单词。除此之外的拼写差异(例如把flipCoin写成历史名Coinflip)会匹配失败并返回4006。