HashPlay 沙盒现已开放 — 申请接入凭证
Developers

开发文档

Hash Play 商户接入指南



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/gamehttps://api.example.com/prod-api/customer/app-api/api/v1/game
API Versionv1/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 Keyapi_key请求中公开携带的身份标识,置于请求头
API Secretapi_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. 安全机制:接口签名

双方之间的每一次通信均需通过同一套签名校验,用于确保:

  1. 请求来源可信(未被第三方伪造);
  2. 内容未被篡改(金额等参数在传输途中未被修改);
  3. 请求未被重放(历史请求无法被重复提交以触发存入或提走)。

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)。

  1. 取请求体(JSON)中的全部顶层字段,剔除值为空的字段
  2. 字段名字典顺序排序,拼接为 字段名=值&字段名=值&... 的形式;
  3. 末尾依次追加时间戳与随机串(以 & 连接)。

例如存入接口的请求体为:

json
{
  "user_id": "u_10086",
  "request_id": "dep_20260908_0001",
  "amount": "1000.00"
}

按字段名字典序排序拼接并追加时间戳、随机串后,待签名原文为:

code
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 重要注意事项(对接失败的常见原因)

  1. 时间戳单位统一为毫秒X-Timestamp 使用毫秒(13 位 Unix epoch 毫秒值)。校验窗口为 ±60 秒(60000 毫秒),请确保服务器时间为标准时间(建议开启 NTP 同步)。
  2. 禁止使用 JSON 数字类型传递金额amountbalancebet_amountwin_amountmult_value 等字段在请求与响应中均为 JSON 字符串(如 "1000.00")。禁止写成数字("amount": 10.00),否则平台按数字文本参与签名时可能变为 10.0,与商户签名串不一致而返回 4002。构造签名串时按请求体原样字符串拼接("1000.00"amount=1000.00),禁止数值格式化;前端展示时再 Number() 转换。
  3. 空值剔除:值为 null、空字符串的字段不参与签名串构造(平台侧同样跳过)。因此 "amount": null 与不传该字段等价。
  4. 字段名大小写敏感:签名串与请求体字段名均使用小写下划线风格(user_idrequest_idgame_code…),请勿写成驼峰。

5.4 验签失败的统一响应

Hash Play 校验商户请求验签未通过时,返回:

json
{
  "code": 4002,
  "message": "签名验证失败"
}

message 为本地化提示文本(可能随语言变化),请始终以 code 作为判定依据。例外:游戏列表接口 /list 在验签失败时固定返回 {"code": 4002, "message": "SIGNATURE_VERIFY_FAILED"}

5.5 响应结构与两类错误

业务接口的响应统一为扁平 JSON,成功与业务失败均返回 HTTP 200,以 code 区分(code = 0 表示成功):

json
{ "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(游戏列表):无请求体,签名串退化为 '&timestamp&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 业务流程

  1. 玩家在商户平台点击游戏入口;
  2. 商户后端(不得从前端直接调用,否则将暴露 Secret)向 Hash Play 申请游戏入场链接;
  3. Hash Play 校验签名与参数后,为该 (operator_id, user_id) 查找或创建平台用户与托管钱包账户,并生成一次性入场链接(内含一次性票据 ticket)返回,同时返回该玩家当前的平台内托管余额;
  4. 商户将链接交由玩家浏览器打开(新窗口或内嵌均可),玩家进入游戏。

入场链接每次申请仅有效一次且具有时效(票据 ticket 有效期 5 分钟,兑换一次后立即失效),不得缓存复用,亦不得多玩家共用。玩家每次进入游戏重新申请即可,该接口开销很小。

幂等提示/launch(operator_id, user_id) 查找或创建平台用户与托管钱包,重复调用不会新建用户,也不会改变余额,可以放心重复调用(但每次都要重新打开返回的 game_url)。

余额来源:返回的 balance 是玩家当前的平台托管余额(字符串)。首次启动通常为 "0",请随后调用 /wallet/deposit 为玩家充值(见第 8 章)。

7.1.1 game_code 不传时会发生什么

game_code可选字段,语义分两种情况:

是否传 game_code平台行为
传了(如 Dicegame_type 字典解析游戏标识 → 校验「商户 × 游戏」权限(无权限 4006)与限额配置(未配置 4008)→ 返回该游戏的入场链接
不传 / 传空串视为「游戏待定」→ 跳过游戏维度的权限与限额校验(仅保留签名、商户状态、IP 白名单三道)→ 返回游戏大厅的通用地址,玩哪款游戏由玩家进入后自行选择

因此若商户希望在大厅中屏蔽未开通的游戏入口,仍需传入 game_code 走权限校验;而不传 game_code 时,玩家进入大厅后能否开局由平台按游戏权限另行判定。

game_codegame_type 字典的标签(dictLabel)做大小写不敏感匹配crash / CRASH / Crash 等价);若某个标签自身含空格,则允许只传其第一个单词作为简写。最稳妥的做法始终是:直接用 游戏列表 API 返回的 gameCode 原值,不要自己改写。

7.2 接口定义

商户调用:

code
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-CNpt-BRth-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 确认该语言的游戏语言包是否就绪;传入未支持的语言时,游戏将回退至英文界面。

请求示例:

json
{
  "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 返回:

json
{
  "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);不可重试,修正后重发
4004IP 不在白名单商户出口 IP 未登记或已变更,联系 Hash Play 核对;不可重试
4005商户无效或停用核对 operator_id 与商户状态,联系 Hash Play
4006游戏不存在或商户无该游戏权限传入的 game_code 未在字典中,或该商户未开通;改用 游戏列表 APIstatus=1 的游戏,或不传 game_code 走大厅
4008游戏限额未配置该「商户 × 游戏」未配置限额,联系 Hash Play 开通
4009游戏地址未配置或已停用该游戏的入场地址未配置,联系 Hash Play
4010玩家已被禁用user_id 对应的平台用户处于禁用/删除状态,按业务提示玩家
500系统异常参考 message 向玩家提示(如"游戏暂不可用"),可稍后重试

500 外,上述失败重试同一请求大概率仍会失败,请按上表定位原因后再重发,避免无脑轮询。玩家侧统一提示为「游戏暂不可用,请稍后再试」即可。


8. 钱包接口

玩家进入游戏后,游戏内的一切资金变动(投注扣减、派奖入账、异常局次退款)均由 Hash Play 在该玩家的托管钱包内实时本地结算,商户无需感知、无需提供回调。商户的职责是按本节规范调用 6 个钱包接口,管理玩家在平台内的托管资金。

8.1 六个钱包接口的语义

接口方式调用时机说明
存入 /wallet/depositPOST玩家进入游戏前后,商户决定为玩家充值时向该玩家的托管余额累加 amount,写入 DEPOSIT 流水
提走 /wallet/withdrawPOST玩家退出游戏、商户回收资金时提现指定金额或全部余额,写入 WITHDRAW 流水
余额查询 /wallet/balancePOST任意时刻(如商户平台展示余额、风控巡检)查询该玩家当前托管余额
流水查询 /wallet/transactionsPOST对账、客诉核查时按时间范围查询该玩家的全部金额变动流水(含游戏内投注/派奖/撤销)
订单查询 /wallet/ordersPOST对账、客诉核查、投注行为分析时按时间范围查询该玩家的投注订单(含游戏编码、投注/派奖金额、结算状态、乘数等局次详情)
存取状态 /wallet/operation/statusPOSTdeposit/withdraw 超时或未知结果后核对按商户订单号 request_id + 类型查询是否成功

六个接口统一位于 {base_url}/api/v1/game/wallet 下。除存取状态外,均按 (operator_id, user_id) 寻址玩家(operator_idX-API-Key 对应的凭证自动确定,请求体中无需传);存取状态按 (operator_id, request_id, operation_type) 查询;流水/订单查询的 user_id 可不传,缺省查询该商户全部用户(响应 records 中含 user_id 供区分)。鉴权与 /launch 完全一致(四请求头验签 + IP 白名单)。

8.2 存入(Deposit)——为玩家充值

商户调用:

code
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 拒绝。

请求示例:

json
{
  "user_id": "u_10086",
  "request_id": "dep_20260908_0001",
  "amount": "1000.00"
}

Hash Play 返回:

json
{
  "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,修正参数后重试
4011user_id 为空补充参数后重试
4012user_id 无效(operator_id, user_id) 未找到用户——请先调 /launch 创建用户
4013request_id 为空补充参数后重试
4014request_id 已成功执行过禁止重复执行:改用 /wallet/operation/status 核对,或换新 request_id
4015钱包操作执行失败失败已留痕(status=0),可用原 request_id 重试

8.3 提走(Withdraw)——回收余额

商户调用:

code
POST {base_url}/wallet/withdraw

请求参数:

参数必填说明
user_id玩家在商户平台的账号标识(须与 /launch 传入的一致)
request_id商户侧订单号,本次提取的唯一标识(建议 wd_日期_序号)。平台按 (operator_id, request_id) 幂等防重
amount提现金额,字符串,正数,保留两位小数(如 "500.00");不传则提走全部余额并清零

全额提走时不要传 amount(或传空串),不要自己先查余额再传入该数值——两次调用之间可能会发生游戏结算,导致金额不匹配而返回 4001

请求示例(部分提现):

json
{
  "user_id": "u_10086",
  "request_id": "wd_20260908_0001",
  "amount": "500.00"
}

请求示例(全额提走):

json
{
  "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 / 4012user_id 为空 / 无效补参或先调 /launch 创建用户
4013request_id 为空补充参数后重试
4014request_id 已成功执行过改用 /wallet/operation/status 核对,或换新 request_id
4015钱包操作执行失败失败已留痕(status=0),可用原 request_id 重试

8.4 余额查询(Balance)

商户调用:

code
POST {base_url}/wallet/balance

请求参数:

json
{ "user_id": "u_10086" }

user_id 必填(同 8.3)。余额查询不涉及动账,无需 request_id

Hash Play 返回:

json
{
  "code": 0,
  "message": "success",
  "currency": "USDT",
  "balance": "1015.00"
}

balance字符串(如 "1015.00"),商户前端展示前请自行 Number() / BigDecimal 转换,勿用 typeof === 'number' 判断"平台未返回"。

可能返回的错误码: 4011 / 4012user_id 为空 / 无效——通常意味着该玩家尚未通过 /launch 在平台建档)。

8.5 流水查询(Transactions)——对账

商户调用:

code
POST {base_url}/wallet/transactions

请求参数:

参数必填说明
user_id玩家在商户平台的账号标识;不传则查询该商户全部用户的流水
start_time起始时间(毫秒时间戳,含)
end_time截止时间(毫秒时间戳,含;不得早于 start_time
page_num页码(从 1 开始,默认 1)
page_size每页条数(默认 10,最大 500)

Hash Play 返回:

json
{
  "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)——投注订单核查

商户调用:

code
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 返回:

json
{
  "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)——核对商户订单是否成功

商户调用:

code
POST {base_url}/wallet/operation/status

请求参数:

参数必填说明
request_id商户侧订单号(与 deposit/withdraw 时传入的 request_id 一致)
operation_type类型:DEPOSIT(存入)/ WITHDRAW(提走);大小写不敏感,其他取值返回 4017

请求示例:

json
{
  "request_id": "dep_20260908_0001",
  "operation_type": "DEPOSIT"
}

本接口不需要传 user_id——request_id 在商户维度内唯一即可定位。

Hash Play 返回:

json
{
  "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
}
字段说明
status1 成功 / 0 失败(同一 request_id 若曾成功则优先返回成功记录)
amount操作金额(字符串;全额提现时为实际提走金额)
txn_id成功时关联的钱包流水号;失败时为 null
error_msg失败原因;成功时为 null
create_time该条记录创建时间(毫秒时间戳)

可能返回的错误码: 4016(未找到该 request_id 的记录——包含从未发起、request_id 拼写错误两种情况)/ 4017operation_typeDEPOSIT/WITHDRAW)。

超时或未知结果时,请先调本接口核对:status=1 表示已成功入账/扣款,勿用同一 request_id 重发;status=0 或返回 4016(记录不存在)时,可用原 request_id 重试。

8.8 统一返回码约定

返回码含义商户处理
0成功附带最新 balance(涉及动账时附 amounttxn_id
4001金额非法deposit 未传 amount 或 ≤ 0;withdraw 的 amount > 当前余额。修正参数后重试
4002验签失败检查签名实现(见 5.3 注意事项)
4003托管余额为零仅 withdraw 可能返回,无资金可提,按业务处理
4004IP 不在白名单商户出口 IP 未登记,联系 Hash Play 核对
4005商户无效或停用核对 API Key 与商户状态,联系 Hash Play
4006时间范围非法仅 transactions / orders 可能返回(start_time > end_time,或时间戳非毫秒数值)
4011user_id 为空补充参数后重试
4012user_id 无效(operator_id, user_id) 未找到对应用户——请先调 /launch 创建用户
4013request_id 为空补充参数后重试
4014request_id 已成功执行禁止重复执行,改用本接口核对或换新订单号
4015钱包操作执行失败失败已留痕,可用原 request_id 重试
4016存取操作记录不存在/wallet/operation/status:订单号/类型无匹配记录
4017operation_type 非法/wallet/operation/status:仅支持 DEPOSIT / WITHDRAW

同名不同义4006401140124013/launch/wallet/* 下含义不同(前者报的是票号/游戏权限类问题,后者报的是参数类问题),部分文案还受多语言影响。请按"接口 + code"两部分一起判断,不要只看 code

code/launch 下的含义/wallet/* 下的含义
4006商户无该游戏权限(见 7.2)transactions/orders 时间范围非法
4011ticket 为空(仅 /exchangeuser_id 为空
4012ticket 无效或已过期user_id 无效(未建档)
4013ticket 对应用户不存在request_id 为空

9. 完整交互示例

以玩家(商户为其首次充值 1000.00 USDT)进行一局 Crash 游戏为例:

点击放大

对账公式(任意时刻均应成立,供商户财务核验):

code
所有存入(Deposit) − 所有提走(Withdraw) − 所有投注(Bet) + 所有派奖(Win) + 所有退款(Rollback) = 当前托管余额(Balance)

上图示例:商户 deposit 1000(余额 0 → 1000.00)、玩家游戏内投注 Debit 10(→ 990.00)、派奖 Win 25(→ 1015.00),玩家退出后商户 withdraw 提走全部 1015.00(余额清零)。若局次异常被取消,平台自动追加一笔 Rollback 把投注本金原路退回托管余额,无需商户介入。


10. 商户系统技术要求

以下为余额托管模式的硬性要求,直接关系到玩家体验与双方资金安全:

  1. 资金变动以 Hash Play 流水为准:游戏内动账全部发生在平台侧,商户财务请以 /wallet/transactions 流水 + /wallet/balance 余额为对账依据,公式见第 9 章。
  2. user_id 稳定唯一:托管钱包以 (operator_id, user_id) 寻址。同一玩家必须始终使用同一 ID,错用其他玩家的 ID 将操作到他人钱包。
  3. 充值前置:玩家投注使用托管余额,余额不足时投注将被平台拒绝并向玩家提示。请确保玩家进入游戏前或余额耗尽时及时 deposit。
  4. 提现支持部分金额:withdraw 传入 amount 提取指定金额,不传则提走全部余额并清零;如遇 4003 表示余额已为零,属正常业务结果而非故障。
  5. 响应性能:deposit/balance 位于玩家进入游戏的关键路径上,请确保商户侧发起调用的链路稳定;Hash Play 侧目标 500 毫秒内响应。
  6. 先签验后调用:所有请求必须携带合法签名(规则见第 5 章;失败返回 4002)。
  7. IP 白名单必配:商户出口 IP 须在 Hash Play 侧登记,未登记时全部接口返回 4004。支持 IPv4 / CIDR;出口变更须提前通知。
  8. 时钟准确:服务器须同步 NTP;时间戳与窗口规则见 5.3
  9. 金额类型:禁止使用 JSON 数字传递金额,规则见 5.3
  10. 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 上线新游戏后无需等待文档重发,接口即刻可见:

商户调用:

code
GET {base_url}/api/v1/game/list?language=zh-CN
参数必填说明
language游戏名称的国际化语言,格式同 /launchlanguage(如 zh-CN)。不传默认英文

签名:与 /launch 完全一致(X-API-Key / X-Timestamp 毫秒级 ±60 秒 / X-Nonce / X-Signature)。GET 请求无请求体,签名串退化为 &时间戳&随机串(见 5.2)。

Hash Play 返回:

json
{
  "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 国际化,无翻译时回退英文)
status1 = 贵商户已开通该游戏;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,运营侧会随游戏上下架、改名而调整(历史上 CoinflipflipCoinXoc DiaXocdiaCock fightingCockFight 都发生过)。请一律以 13.1 的 /api/v1/game/list 接口为准,本表仅用于理解命名风格与联调时人工核对。

序号游戏名称(中文 / 英文)game_code
1爆点 / CrashCrash
2骰宝 / DiceDice
3希洛 / HiLoHilo
4过纸牌 / BetweenBetween
5翻转硬币 / Flip CoinflipCoin
8轮盘赌 / RouletteRoulette
9哈希28 / Hash 28TwentyEight
10极速倍率 / LimboLimbo
11扫雷 / MineMine
12视频扑克 / Video PokerVideoPoker
13生肖扫雷 / ZodiacZodiac
14传奇之塔 / TowerTower
15虾蟹 / Xoc DiaXocdia
16牛牛 / BullBull
17百家乐 / BaccaratBaccarat
18百家乐(变体)/ BaccaratsBaccarats
19博状元 / Bo BingBoBing
20斗鸡 / Cock FightingCockFight
21无佣百家乐 / No Commission BaccaratNCBaccarat
22黄金峡谷 / Golden CanyongoldenCanyon
23赛博朋克 / Cyberpunkcyberpunk
24财神 / Fortune GodfortuneGod
25火龙 / Fiery DragonfieryDragon
26太空入侵者 / Space InvaderspaceInvader

上表"序号"即平台 game_type 字典的 dictValue(内部编号,商户无需传该编号,/launch 只认 game_code 标签)。编号 6、7 已废弃。

匹配规则回顾:大小写不敏感crashCRASHCrash 等价);若标签自身含空格,允许只传首单词。除此之外的拼写差异(例如把 flipCoin 写成历史名 Coinflip)会匹配失败并返回 4006