OpenLive · 商户对接文档 v2(Dev)
游戏供应商对接与游戏数据监控平台。本文档定义了商户与 openlivegame 之间的 双向 HTTP 协议:商户通过 Provider API 获取游戏启动链接, openlivegame 通过 Seamless Wallet 回调实时完成余额变动。
| Provider API(商户 → openlivegame) | https://<PROVIDER_HOST>/provider/v2 |
| PP 玩家入口(gameURL 落地) | https://<CLIENT_HOST> |
| EVO 玩家入口(gameURL 落地) | https://<EVO_CLIENT_HOST> |
| Wallet 回调(openlivegame → 商户) | 由商户提供 callback_url,例如 https://merchant-wallet.example.com/wallet |
文档中所有 <...> 尖括号值都是占位符,代表接入时分配的实际主机名。实际域名由 openlivegame 在对接时提供,不同接入环境不同;请以我方给出的值为准,不要硬编码占位符本身或示例中出现的任何主机名。
<PROVIDER_HOST> | Provider API 基址主机(商户 → openlivegame) |
<CLIENT_HOST> | PP 玩家入口主机,同时用于局报表 URL |
<EVO_CLIENT_HOST> | EVO 玩家入口主机 |
<CDN_HOST> | 游戏图标 CDN 主机 |
主机名不参与签名计算,把占位符替换为实际域名不会影响本文档中的任何 hash 值。
概述 #
openlivegame 采用 Seamless Wallet(无缝钱包)集成模式。商户保留玩家资金账户的唯一真实来源, 玩家在游戏中的每一次下注 / 派奖 / 退款都由 openlivegame 实时回调商户钱包接口完成扣款与加款, openlivegame 自身不托管任何真实资金。
① 商户 → openlivegame
商户调用 Provider API 获取玩家游戏启动 URL,将玩家导向游戏客户端。
② openlivegame → 商户
openlivegame 在玩家下注结算过程中回调商户 Wallet API 完成资金变动。
③ 双向安全
两侧均使用统一的 MD5 签名算法 + 商户密钥验证请求真实性。
系统架构 #
整体交互分为同步链路(商户主动)与异步链路(openlivegame主动回调):
商户
openlivegame 平台
POST /provider/v2/{vendor}/url
返回 gameURL(含 JSESSIONID)
玩家跳转游戏客户端(WebSocket)
POST {callback_url}/authenticate
POST {callback_url}/bet / result / refund
返回 cash / transactionId
商户上下文字段
openlivegame 为每个商户保存以下关键配置,请在对接前向 openlivegame 索取或提交:
| 字段 | 用途 | 来源 |
|---|---|---|
secureLogin | 商户 API 身份标识,Provider API 中用来定位商户 | openlivegame 分配 |
secretKey | 双向签名密钥,禁止泄露 | openlivegame 分配 |
callback_url | 商户 Seamless Wallet 回调基础 URL,例如 https://merchant-wallet.example.com/wallet | 商户提供 |
ip_whitelist | 调用 Provider API 的出口 IP 白名单(逗号分隔,可选) | 商户提供 |
完整对接时序 #
token(建议 5–30 分钟有效期),
并将该 token 与玩家绑定,后续 Authenticate 回调凭此查询玩家。
POST /provider/v2/{vendor}/url
传入 secureLogin、token、externalPlayerId、gameId(必填),
以及按签名规则计算得到的 hash。
POST {callback_url}/authenticate
openlivegame 用同一份 token 反向询问商户,商户必须返回 userId(需 == externalPlayerId)、
currency 和当前余额 cash。
gameURL
URL 中附带平台会话 JSESSIONID,商户将玩家 302 跳转至该 URL 即可进入游戏。
/bet / /result / /refund),
商户执行账户变动后返回最新余额。
POST {callback_url}/balance 拉取最新余额。
货币支持 #
当前平台支持以下币种。authenticate 回调返回的 currency 必须取自此列表,
并作为玩家和会话的唯一币种来源:
| currency | 名称 | 符号 | 说明 |
|---|---|---|---|
USD | 美元 | $ | 基准币种 |
EUR | 欧元 | € | — |
BRL | 巴西雷亚尔 | R$ | — |
IDR | 印尼盾 | Rp | — |
INR | 印度卢比 | ₹ | — |
USDT | 泰达币 | $ | 按 1:1 锚定 USD 处理 |
- 币种需在 openlivegame 平台与游戏上游双侧开通后方可投运(限红、投注档位均按币种独立配置)。
authenticate 返回未开通的
currency会导致启动失败,请勿自行尝试列表外币种。 - 需要接入新币种时请提前联系 openlivegame 运营开通,开通后此列表同步更新。
- 一个
externalPlayerId终身绑定一种币种(详见 url 的「一用户一币种」约束)。
语言支持 #
玩家语言由 url 请求的 language 字段决定,
取值为下方表中的编码。语言只影响界面与桌名展示,不影响结算、派彩与钱包回调。
language 用来给新玩家(openlivegame 未见过的玩家)设定初始语言。
玩家已存在时,请求里的 language 被忽略,一律使用该玩家自己的语言——
首次启动时定下的、或玩家之后在游戏内切换的。玩家在游戏里换过语言,就不会被新的启动链接换回去。
| 优先级 | 使用的语言 |
|---|---|
| 1 | 玩家自己的语言——首次启动时定下的,或玩家在游戏内切换的 |
| 2 | 本次请求的 language(仅新玩家) |
| 3 | 系统默认 en-US |
匹配规则
- 取值必须是下表中的编码,按表中写法原样发送。匹配忽略两端空白与大小写,
en-US、en-us、EN-US是同一个编码。 - 不做主语言或区域回退:
en-GB、zh-CN、zh不在表内,一律识别不了,玩家得到en-US。 - 识别不了的值不会让请求失败,也不会被记录:玩家得到
en-US, 且因为没有记录,他仍然是新玩家——之后传对了照常生效。 我方不返回任何错误或提示,请照下表核对编码,不要依赖响应判断。 - 同一个语言在 PP 与 EVO 两侧、大厅与直达桌两条入口下保持一致。
支持的语言(36 种,PP 与 EVO 双侧可用)
| 编码 | 语言 | 编码 | 语言 | 编码 | 语言 |
|---|---|---|---|---|---|
ar | 阿拉伯语 | hy | 亚美尼亚语 | bn | 孟加拉语 |
bg | 保加利亚语 | hr | 克罗地亚语 | cs | 捷克语 |
da | 丹麦语 | nl | 荷兰语 | en-US | 英语(美国) |
et | 爱沙尼亚语 | fi | 芬兰语 | fr | 法语 |
ka | 格鲁吉亚语 | de | 德语 | el | 希腊语 |
hu | 匈牙利语 | id | 印尼语 | it | 意大利语 |
ja | 日语 | ko | 韩语 | lv | 拉脱维亚语 |
lt | 立陶宛语 | ms | 马来语 | no | 挪威语 |
pl | 波兰语 | pt-BR | 葡萄牙语(巴西) | ro | 罗马尼亚语 |
ru | 俄语 | sr-Latn | 塞尔维亚语(拉丁) | sk | 斯洛伐克语 |
es | 西班牙语 | sv | 瑞典语 | th | 泰语 |
tr | 土耳其语 | uk | 乌克兰语 | vi | 越南语 |
需要表中没有的语言?请联系 openlivegame 客服开通。
昵称支持 #
url 请求可以带上可选的 nickname,给还没有昵称的玩家直接定下昵称,
免去进游戏前先设名这一步。PP 与 EVO 两侧都生效。
nickname 被忽略,一律保留玩家自己的昵称——
无论那是更早的启动链接带来的,还是玩家在游戏内自己设的,都不会被覆盖。
| 优先级 | 使用的昵称 |
|---|---|
| 1 | 玩家自己的昵称——更早的启动链接带来的,或玩家在游戏内设的 |
| 2 | 本次请求的 nickname(仅限还没有昵称的玩家) |
| 3 | 空——由游戏引导玩家自己设置昵称 |
取值规则
- 最长 50 个字符。两端空白会被去掉,只有空白的值视同没传。
- 昵称不去重:允许两个玩家同名,重名不会返回错误。
- 控制字符、双向文本控制符、零宽空格一律拒绝——昵称会展示给同桌的其他玩家 (聊天、赢家榜、历史记录),这几类字符会破坏那几处展示。emoji 可以正常使用。
- 与
language不同,取值非法会让请求直接失败并返回error="2",而不是被忽略:昵称悄悄不生效,比请求被拒更难发现。 - 同一个昵称在 PP 与 EVO 两侧、大厅与直达桌两条入口下保持一致。
Provider API · openlivegame 提供给商户 #
基础地址:https://<PROVIDER_HOST>/provider/v2。PP 与 EVO 的目录和启动接口按厂商拆分,
局报表与健康检查挂在 v2 顶层。
| 方法 | 路径 | 用途 |
|---|---|---|
| POST | /pp/list | PP 游戏与大厅目录 |
| POST | /evo/list | EVO 游戏与大厅目录 |
| POST | /pp/url | 创建 PP 游戏或大厅启动链接 |
| POST | /evo/url | 创建 EVO 游戏或大厅启动链接 |
| POST | /report | 获取单局报表一次性 URL |
| GET | /health | 服务活性检测 |
POST 请求使用 application/x-www-form-urlencoded,签名字段为 hash;
返回 JSON,Provider error 为字符串,"0" 表示成功。业务错误同样返回 HTTP 200。
按厂商返回当前启用的真实游戏目录,以及厂商主大厅和分类页入口。请求不包含玩家、token、币种或会话上下文。
请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
secureLogin | string | 必填 | 商户 API 身份标识 |
hash | string | 必填 | 对 secureLogin 计算的请求签名 |
请求示例
curl -X POST https://<PROVIDER_HOST>/provider/v2/evo/list \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "secureLogin=merchant001" \
-d "hash=..."
响应字段(JSON)
| 字段 | 类型 | 说明 |
|---|---|---|
error | string | "0" 表示成功;其他值见错误码 |
description | string | 简短的处理结果说明 |
games | game[] | 当前启用的真实游戏条目 |
lobbies | lobby[] | 厂商主大厅与分类大厅入口 |
games[] 字段
| 字段 | 类型 | 说明 |
|---|---|---|
gameId | string | 原样传给同厂商 url 的启动选择器 |
name | string | 英文展示名 |
gameType | string | 游戏族 |
categoryIds | string[] | 厂商内的游戏分类,字段始终返回 |
vendorGameId | string | 上游厂商游戏 ID,字段始终返回 |
images | object | 按本条目 gameId 生成的 CDN 图片地址 |
lobbies[] 字段
| 字段 | 类型 | 说明 |
|---|---|---|
gameId | string | openlivegame 返回的不透明大厅启动 ID |
name | string | 英文大厅或分类入口名称 |
gameType | string | 固定为 lobby |
images | object | 按本条目 gameId 生成的 CDN 图片地址 |
images 字段
| 字段 | 类型 | 说明 |
|---|---|---|
square | string | 正方形图片地址 |
portrait | string | 竖版图片地址 |
landscape | string | 横版图片地址 |
list 只表达当前启用目录。具体玩家能否启动还取决于商户状态、token、玩家币种、桌台币种配置、
客户端配置和实时运行状态,必须以当前 url 响应为准。
gameId 是不透明字符串,必须从 list 读取并原样传给 url。
大厅条目不返回 categoryIds 和 vendorGameId。
gameId 固定生成;
图片文件可能后续补齐,加载失败时请使用商户侧默认图。
成功响应示例
{
"error": "0",
"description": "OK",
"games": [
{
"gameId": "evoCrazyTime0001",
"name": "Crazy Time",
"gameType": "crazytime",
"categoryIds": ["game_shows", "top_games"],
"vendorGameId": "CrazyTime0000001",
"images": {
"square": "https://<CDN_HOST>/openlive_games/gameicon/square/evoCrazyTime0001.jpg",
"portrait": "https://<CDN_HOST>/openlive_games/gameicon/portrait/evoCrazyTime0001.jpg",
"landscape": "https://<CDN_HOST>/openlive_games/gameicon/landscape/evoCrazyTime0001.jpg"
}
}
],
"lobbies": [
{
"gameId": "evo_lobby",
"name": "EVO Lobby",
"gameType": "lobby",
"images": {
"square": "https://<CDN_HOST>/openlive_games/gameicon/square/evo_lobby.jpg",
"portrait": "https://<CDN_HOST>/openlive_games/gameicon/portrait/evo_lobby.jpg",
"landscape": "https://<CDN_HOST>/openlive_games/gameicon/landscape/evo_lobby.jpg"
}
}
]
}
失败响应
{
"error": "5",
"description": "invalid signature"
}
gameId 是唯一启动选择器,可以指向真实游戏、厂商主大厅或分类页。
openlivegame 验证请求与目标后回调商户 authenticate,成功才创建会话并返回 gameURL。
请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
secureLogin | string | 必填 | 商户 API 身份标识 |
gameId | string | 必填 | 同厂商 list 返回的游戏或大厅条目 ID |
token | string | 必填 | 商户生成的短时玩家 token,原样透传给 authenticate |
externalPlayerId | string | 必填 | 商户玩家 ID,格式 ^[A-Za-z0-9_\-]{1,64}$ |
hash | string | 必填 | 请求签名 |
language | string | 可选 | 玩家语言编码,默认 en-US,仅对新玩家生效,取值见 语言支持;仅在非空发送时参与签名 |
nickname | string | 可选 | 玩家昵称,最长 50 个字符,仅对还没有昵称的玩家生效,规则见 昵称支持;仅在非空发送时参与签名 |
country | string | 可选 | ISO 国家代码;非空时参与签名 |
platform | string | 可选 | mobile / desktop 等;非空时参与签名 |
lobbyUrl | string | 可选 | 玩家退出后返回商户的绝对 URL |
cashierUrl | string | 可选 | 会话级收银台绝对 URL |
启动落点
| gameId 类型 | 玩家落点 |
|---|---|
games[] 条目 | 直接进入该真实桌台 |
lobbies[] 主大厅条目 | 厂商主大厅默认页 |
lobbies[] 分类大厅条目 | 厂商大厅对应分类页 |
| 未知、停用、过期或厂商不匹配 | 返回 error="4" |
请求示例
curl -X POST https://<PROVIDER_HOST>/provider/v2/evo/url \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "secureLogin=merchant001" \
-d "token=player-token-xyz" \
-d "externalPlayerId=player-001" \
-d "gameId=evo_lobby_game_shows" \
-d "language=en-US" \
-d "hash=..."
响应字段(JSON)
| 字段 | 类型 | 说明 |
|---|---|---|
error | string | "0" 表示成功;其他值见错误码 |
description | string | 简短的处理结果说明 |
gameURL | string | 玩家启动 URL,仅成功时返回 |
成功响应
{
"error": "0",
"description": "OK",
"gameURL": "https://<EVO_CLIENT_HOST>/frontend/evo/mini/?EVOSESSIONID=..."
}
失败响应
{
"error": "7",
"description": "unsupported field: currency"
}
userId 必须严格等于 externalPlayerId,否则返回 error=4;
authenticate 返回的 currency 是玩家和会话的唯一币种来源;为空时返回 error=20。
url 不接受 currency 字段,发送该字段返回 error=7。
externalPlayerId 终身绑定一种币种。多币种自然人必须使用不同 ID,
例如 u12345_USD 与 u12345_EUR。EVO 桌缺少该玩家币种配置时返回 error=7。
按钱包回调中的 16 位 roundId 获取短时、一次性的局报表 URL。
请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
secureLogin | string | 必填 | 商户 API 身份标识 |
roundId | string | 必填 | 钱包回调中的 16 位玩家业务局号 |
hash | string | 必填 | 请求签名 |
请求示例
curl -X POST https://<PROVIDER_HOST>/provider/v2/report \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "secureLogin=merchant001" \
-d "roundId=4827319501648205" \
-d "hash=..."
响应字段(JSON)
| 字段 | 类型 | 说明 |
|---|---|---|
error | string | "0" 表示成功;其他值见错误码 |
description | string | 简短的处理结果说明 |
url | string | 短时局报表 URL,仅成功时返回 |
成功响应
{
"error": "0",
"description": "OK",
"url": "https://<CLIENT_HOST>/reports/.../index.html?token=..."
}
失败响应
{
"error": "4",
"description": "round not found"
}
error=4,格式非法返回 error=7。
无需参数、无需签名。用于探测 Provider API 进程是否可访问。
请求参数
无请求字段。
请求示例
curl https://<PROVIDER_HOST>/provider/v2/health
响应字段(JSON)
| 字段 | 类型 | 说明 |
|---|---|---|
error | string | "0" 表示服务正常;维护中返回 "3" |
description | string | 简短的健康状态说明 |
成功响应
{
"error": "0",
"description": "OK"
}
失败说明
连接失败或 HTTP 5xx 表示服务不可用。系统维护时返回 HTTP 200 + {"error":"3","description":"System maintenance"}。请退避重试;持续失败时联系 openlivegame。
Seamless Wallet · 商户提供给 openlivegame #
基础地址:商户预留的 callback_url(例如 https://merchant-wallet.example.com/wallet)。
下文各端点路径(如 /authenticate)均为相对于该 callback_url 的相对路径,
实际请求 URL 形如 https://merchant-wallet.example.com/wallet/authenticate。
Dev 环境下 openlivegame 出口 IP 由出网网关决定,如商户在自己的 WAF / 防火墙上对入站做了白名单限制, 请向 openlivegame 索取最新的出口 IP 列表。
请求格式
- Method:
POST - Content-Type:
application/x-www-form-urlencoded - 签名字段:
hash(MD5,对全部非 hash 字段计算) - 超时时间:
/bet5 秒;其余接口 10 秒(openlivegame 侧 client timeout)
响应格式
- Content-Type:
application/json - HTTP Status: 业务错误也请使用
200 OK,错误信息写入error字段 - error 字段: 整型(注意:与 Provider API 的字符串错误码不同)
- 金额字段: JSON number,最多 2 位小数
bet / result / refund 三个接口必须基于 reference 字段实现幂等。
同一 reference 的重复请求必须返回 同一条流水(相同 transactionId 和 cash),
禁止重复扣款或重复加款。重复时建议 error=5 (duplicate),但返回 error=0 也视为幂等成功。
reference 由 openlivegame 生成,商户应视为不透明字符串:原样保存、原样查重,不要解析其格式,也不要自行拼接生成。
bet / result / refund 的 reference 彼此独立。
同时,同一玩家钱包 roundId 下每种交易类型最多只能有一笔有效流水:最多 1 笔 bet、1 笔 result、1 笔 refund。
关键字段说明
| 字段 | 说明 |
|---|---|
tableCode | 游戏机台代码,等于 games[].gameId,也是 url 的 gameId 入参。回调字段名是 tableCode,不是 tableId |
gameId | 供应商局号;与 tableCode 组合后定位桌台上的一局:(tableCode, gameId)。单独的 gameId 不是全局唯一,不同机台之间可能重复 |
roundId | openlivegame 业务局号(16 位纯数字),用于标识某个玩家在该局里的钱包流水链路。它由 (tableCode, gameId, userId) 派生:同三元组恒返同值;同一桌台同一供应商局号下,不同玩家的 roundId 不同 |
reference | openlivegame 为本次钱包回调生成的不透明幂等键。商户应原样保存,用于 UNIQUE (operator_id, reference);不要解析其内部格式,也不要自行生成。bet / result / refund 的 reference 彼此独立 |
openlivegame 在收到商户 url 请求后立即调用此接口,
用同一份 token 询问商户:此 token 是否有效、对应哪个玩家、当前余额是多少。
请求参数(form)
| 字段 | 类型 | 说明 |
|---|---|---|
token | string | 商户生成的玩家 token(与 url 的 token 相同) |
providerId | string | 固定值 ppgame |
hash | string | 请求签名 |
响应字段(JSON)
| 字段 | 类型 | 说明 |
|---|---|---|
userId | string | 玩家唯一 ID,必须等于 url 请求中的 externalPlayerId |
currency | string | 玩家账户币种,必填;它是 v2 启动的唯一币种来源,同一 userId 的 currency 一经确立不可更改 |
cash | number | 当前现金余额,2 位小数 |
error | int | 错误码,0 表示成功,见 错误码表 |
description | string | 错误描述 |
请求样例
POST /wallet/authenticate HTTP/1.1
Host: merchant-wallet.example.com
Content-Type: application/x-www-form-urlencoded
token=player-token-xyz&providerId=ppgame&hash=60dacf2fedfea2114586cea38f5685e0
# 示例 hash 以 secretKey=mysecretkey 计算
成功响应
{
"userId": "player-001",
"currency": "USD",
"cash": 1000.50,
"error": 0,
"description": "OK"
}
失败响应
{
"userId": "",
"currency": "",
"cash": 0,
"error": 3,
"description": "invalid token"
}
会话恢复、余额刷新等场景下 openlivegame 会调用此接口拉取商户侧玩家的最新现金余额。
请求参数(form)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
userId | string | 必填 | 玩家 ID(即 authenticate 返回的 userId) |
providerId | string | 必填 | 固定值 ppgame |
hash | string | 必填 | 请求签名 |
请求示例
curl -X POST https://merchant-wallet.example.com/wallet/balance \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "userId=player-001" \
-d "providerId=ppgame" \
-d "hash=..."
响应字段(JSON)
| 字段 | 类型 | 说明 |
|---|---|---|
currency | string | 币种 |
cash | number | 当前余额 |
error | int | 错误码 |
description | string | 错误描述 |
成功响应
{
"currency": "USD",
"cash": 1050.75,
"error": 0,
"description": "OK"
}
失败响应
{
"currency": "",
"cash": 0,
"error": 2,
"description": "user not found"
}
玩家在游戏中确认下注时调用,商户需从玩家余额中 扣减 amount。
请求参数(form)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
userId | string | 必填 | 玩家 ID(= externalPlayerId) |
tableCode | string | 必填 | 机台编号(= list 的 gameId / url 的 gameId 入参)。注意字段名是 tableCode,不是 tableId |
gameId | string | 必填 | 供应商局号;与 tableCode 组合后定位桌台上的一局:(tableCode, gameId)。不同机台之间 gameId 可能重复 |
roundId | string | 必填 | openlivegame 业务局号(16 位纯数字),用于标识该玩家本局的钱包流水链路。由 (tableCode, gameId, userId) 派生:同三元组恒返同值;同一桌台同一供应商局号下,不同玩家的 roundId 不同 |
amount | string(decimal) | 必填 | 下注金额,2 位小数字符串,如 "10.50" |
reference | string | 必填 | openlivegame 生成的幂等键。商户需原样保存并用于唯一查重;不要解析其内部格式,也不要自行拼接生成 |
providerId | string | 必填 | 固定值 ppgame |
timestamp | string(int64) | 必填 | 请求时间戳,毫秒 |
hash | string | 必填 | 请求签名 |
roundDetails | string(json) | 预留 | 局级附加信息(JSON 字符串)。当前版本不会发送,商户实现不必处理;如出现请按可选字段兼容(仍参与签名) |
请求示例
curl -X POST https://merchant-wallet.example.com/wallet/bet \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "userId=player-001" \
-d "tableCode=evoCrazyTime0001" \
-d "gameId=game-20260717-001" \
-d "roundId=4827319501648205" \
-d "amount=10.00" \
-d "reference=B4827319501648205" \
-d "providerId=ppgame" \
-d "timestamp=1784256000000" \
-d "hash=..."
响应字段(JSON)
| 字段 | 类型 | 说明 |
|---|---|---|
transactionId | string | 商户侧交易流水号 |
currency | string | 币种 |
cash | number | 扣款后的玩家余额 |
error | int | 错误码,余额不足时返回 1 |
description | string | 错误描述 |
成功响应
{
"transactionId": "txn-20260423-001",
"currency": "USD",
"cash": 989.50,
"error": 0,
"description": "OK"
}
余额不足响应
{
"transactionId": "",
"currency": "USD",
"cash": 5.00,
"error": 1,
"description": "insufficient balance"
}
cash 返回 当前真实余额,error=1。
openlivegame 将据此拒绝该笔下注并通知客户端。
/bet 的调用超时是 5 秒(下注在牌局倒计时内完成,无法长等)。
若商户响应超时、或返回 error=100 / 未知错误码,openlivegame 视本笔扣款为
状态不确定:会将该笔写入退款确认队列,由后台任务发起对应的
/refund 对冲(退款请求会携带独立的 reference)。
因此商户 /bet 必须保证「要么落账要么干净失败」,且 /refund 幂等必须健壮。
游戏结算后对每一个有成功下注的玩家调用,商户需将 amount 加到玩家余额上。
未中奖的局也会调用,此时 amount="0.00"——商户应照常落一条加款 0 的流水并返回成功,
以便双方对账确认该局已结算。
请求参数(form)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
userId | string | 必填 | 玩家 ID |
tableCode | string | 必填 | 机台编号(与 bet 同) |
gameId | string | 必填 | 供应商局号(与 bet 同) |
roundId | string | 必填 | 业务局号(与 bet 同;标识该玩家本局的钱包流水链路) |
amount | string(decimal) | 必填 | 派奖金额(≥ 0),2 位小数;未中奖为 "0.00" |
reference | string | 必填 | 派奖幂等键;由 openlivegame 生成,商户原样保存。与 bet 的 reference 不同,保证端点隔离 |
providerId | string | 必填 | 固定值 ppgame |
timestamp | string(int64) | 必填 | 毫秒时间戳 |
hash | string | 必填 | 请求签名 |
请求示例
curl -X POST https://merchant-wallet.example.com/wallet/result \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "userId=player-001" \
-d "tableCode=evoCrazyTime0001" \
-d "gameId=game-20260717-001" \
-d "roundId=4827319501648205" \
-d "amount=24.50" \
-d "reference=R4827319501648205" \
-d "providerId=ppgame" \
-d "timestamp=1784256005000" \
-d "hash=..."
响应字段(JSON)
| 字段 | 类型 | 说明 |
|---|---|---|
transactionId | string | 商户侧交易流水号;幂等重试必须稳定 |
currency | string | 玩家币种 |
cash | number | 派奖后的玩家余额 |
error | int | 0 表示成功;其他值见错误码 |
description | string | 简短的处理结果说明 |
成功响应
{
"transactionId": "result-20260423-001",
"currency": "USD",
"cash": 1014.50,
"error": 0,
"description": "OK"
}
失败响应
{
"transactionId": "",
"currency": "USD",
"cash": 989.50,
"error": 100,
"description": "internal error"
}
reference 做幂等。
/result 前强制校验本局存在对应的成功 /bet 扣款流水,
不存在则拒绝结算并转人工。商户不会收到「没有 bet 的 result」(如收到,按下文 refund 的建议返回 error=2)。
两种场景触发:① 整局被作废(游戏中断 / 上游取消本局);②
/bet 调用超时或返回不确定错误(error=100 / 未知码),平台无法确认扣款是否生效,
按「可能已扣款」入队退款。openlivegame 调用 refund 请求商户把 bet 扣掉的金额 退还给玩家。
请求参数(form)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
userId | string | 必填 | 玩家 ID |
tableCode | string | 必填 | 真实游戏机台代码 |
gameId | string | 必填 | 供应商局号 |
roundId | string | 必填 | 16 位玩家钱包业务局号 |
amount | string(decimal) | 必填 | 退款金额,必须为正数 |
reference | string | 必填 | 退款专用幂等键,与 bet/result 的 reference 不同 |
providerId | string | 必填 | 固定值 ppgame |
timestamp | string(int64) | 必填 | 毫秒时间戳 |
hash | string | 必填 | 请求签名 |
请求示例
curl -X POST https://merchant-wallet.example.com/wallet/refund \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "userId=player-001" \
-d "tableCode=evoCrazyTime0001" \
-d "gameId=game-20260717-001" \
-d "roundId=4827319501648205" \
-d "amount=10.00" \
-d "reference=F4827319501648205" \
-d "providerId=ppgame" \
-d "timestamp=1784256010000" \
-d "hash=..."
响应字段(JSON)
| 字段 | 类型 | 说明 |
|---|---|---|
transactionId | string | 商户侧交易流水号;幂等重试必须稳定 |
currency | string | 玩家币种 |
cash | number | 退款后的玩家余额 |
error | int | 0 表示成功;找不到原 bet 时返回 2 |
description | string | 简短的处理结果说明 |
成功响应
{
"transactionId": "refund-20260423-001",
"currency": "USD",
"cash": 1000.00,
"error": 0,
"description": "OK"
}
失败响应
{
"transactionId": "",
"currency": "USD",
"cash": 989.50,
"error": 2,
"description": "bet not found"
}
error=0 与 error=5(duplicate)都视为退款成功。
注意「bet 超时」场景:若商户实际并未扣款(bet 没有落账),收到对应 refund 时应返回
error=2 或携带原余额的失败响应,而不是凭空加款。
签名算法 #
双向通用(openlivegame验商户签名、商户验openlivegame签名)。签名字段名为 hash。
步骤
- 收集所有请求参数的 原始值(表单解码后),排除
hash本身。 - 对参数名按 字母序(ASCII 升序)排序。
- 按顺序拼接为
keyA=valueA&keyB=valueB&...。 - 在末尾 直接追加
secretKey(无分隔符)。 - 对整串做
MD5,取 小写十六进制。
Go 参考实现
func CalcSign(params map[string]string, secretKey string) string {
keys := make([]string, 0, len(params))
for k := range params {
if k == "hash" { continue }
keys = append(keys, k)
}
sort.Strings(keys)
var b strings.Builder
for i, k := range keys {
if i > 0 { b.WriteByte('&') }
b.WriteString(k)
b.WriteByte('=')
b.WriteString(params[k])
}
b.WriteString(secretKey)
sum := md5.Sum([]byte(b.String()))
return hex.EncodeToString(sum[:])
}
PHP 参考实现
function calcSign(array $params, string $secretKey): string {
unset($params['hash']);
ksort($params);
$parts = [];
foreach ($params as $k => $v) {
$parts[] = $k . '=' . $v;
}
return md5(implode('&', $parts) . $secretKey);
}
Python 参考实现
import hashlib
def calc_sign(params: dict, secret_key: str) -> str:
items = sorted((k, v) for k, v in params.items() if k != "hash")
payload = "&".join(f"{k}={v}" for k, v in items) + secret_key
return hashlib.md5(payload.encode()).hexdigest()
Node.js 参考实现
const crypto = require('crypto');
function calcSign(params, secretKey) {
const payload = Object.keys(params)
.filter(k => k !== 'hash')
.sort()
.map(k => `${k}=${params[k]}`)
.join('&') + secretKey;
return crypto.createHash('md5').update(payload).digest('hex');
}
完整示例
假设:
secureLogin | merchant001 |
token | player-token-123 |
externalPlayerId | player-001 |
gameId | evoCrazyTime0001 |
secretKey | mysecretkey |
排序后拼接:
externalPlayerId=player-001&gameId=evoCrazyTime0001&secureLogin=merchant001&token=player-token-123
追加 secretKey:
externalPlayerId=player-001&gameId=evoCrazyTime0001&secureLogin=merchant001&token=player-token-123mysecretkey
MD5 结果即为 hash:
28c33757235a1d47e8f169cf9733949d
你的签名实现对上述输入应输出完全相同的结果。本例未发送 language,因此 language 不参与签名; Provider 可选字段只在实际发送且非空时参与。
- 参数值若含
&/=/ 空格等字符,签名时使用 解码后的原始值,HTTP 传输时才 URL 编码。 - 商户验证openlivegame回调时:对收到的全部非 hash 字段计算(openlivegame发送什么就签什么,包括
timestamp)。 - 商户调用 Provider API 时:可选字段遵循各端点的「签名参与规则」,空值字段不发送、不计入。
- secretKey 只在签名末尾出现,不 通过 HTTP 传输。
错误码 #
Provider API 错误码(字符串类型)
用于openlivegame返回给商户的响应中的 error 字段。
| error | 简要说明 | 商户处理 |
|---|---|---|
| "0" | 成功 | 使用返回数据 |
| "1" | 服务异常 | 稍后重试;持续失败请联系支持 |
| "2" | 商户不存在 | 检查 secureLogin |
| "3" | 商户已禁用或系统维护中 | 维护时稍后重试;否则联系 openlivegame 运营 |
| "4" | 目标或玩家未通过 | 刷新目录并检查 token、玩家或数据归属 |
| "5" | 签名错误 | 按实际发送字段重新计算 hash |
| "7" | 参数无效或不支持 | 修正请求;不要原样重试 |
| "14" | 缺少必填字段 | 补齐字段后重新签名 |
| "20" | 玩家币种无效 | 返回有效 authenticate 币种或使用已绑定币种 |
Seamless Wallet 错误码(整型)
用于商户返回给openlivegame的响应中的 error 字段。
| error | 简要说明 | 必须处理 |
|---|---|---|
| 0 | 成功 | 返回已提交数据 |
| 1 | 余额不足 | 不扣款,返回当前余额 |
| 2 | 用户或原 bet 不存在 | 拒绝请求,不改变资金 |
| 3 | token 无效 | 拒绝 authenticate |
| 4 | 签名错误 | 拒绝请求并检查密钥 |
| 5 | 重复交易 | 返回原始落库结果 |
| 100 | 内部错误 | 回滚,不允许部分资金变更 |
幂等、重试与时序 #
幂等键设计建议
对 bet / result / refund,商户应把
(operatorId, reference) 作为联合唯一键。
收到请求时先查此唯一键,若已存在则直接返回该流水的 transactionId + cash_after,
避免重复扣加款。
超时设定
openlivegame 调用商户的 client timeout:/bet 5 秒,其余接口 10 秒。
商户回调接口应保证 P99 响应时间 < 3s;bet 超时会被视为状态不确定并触发 refund 对冲,
带来资金状态不一致风险。
重试策略
result 网络失败先即时重试(至多 3 次尝试);仍失败与 refund 任务一起进入
b_wallet_callback_tasks 重试队列(每 15 秒扫描,result 至多 5 次 / refund 至多 3 次),
超限转人工介入。因此商户必须对同一 reference 做严格幂等。
时序关系
同一 roundId 的正常时序为 bet → result(含 amount=0 的未中奖结算);
异常时序为 bet → refund(整局作废或 bet 状态不确定)。
正常情况下同一局不会同时出现 result 与 refund。
amount 为字符串(2 位小数)。商户实现必须使用 Decimal / BigDecimal
等精确类型计算,严禁用 float 做中间计算,以避免余额累积误差。
商户幂等实现(必读) #
openlivegame 的重试机制保证了失败请求一定会重试,商户若不做正确的幂等处理,会出现重复扣款 / 重复加款 / 资金对不平的严重事故。 本章给出强制性的实现要求。
1. 幂等键与唯一键
适用接口
/bet、/result、/refund 三个接口必须强制幂等。/authenticate 与 /balance 为只读接口,不写入状态,天然幂等。
幂等键
请求中的 reference 字段即幂等键,由 openlivegame 生成,在同一商户范围内全局唯一。商户应把它当作不透明字符串,原样保存、原样查重;不要依赖其内部格式,也不要自行拼接生成。
数据库唯一键
商户交易流水表(建议命名 wallet_transactions)必须建立 reference 的数据库级唯一索引,
并建议同时建立业务唯一键防御异常重复业务事件:
CREATE UNIQUE INDEX uk_operator_reference
ON wallet_transactions (operator_id, reference);
-- 同一玩家钱包 roundId 下,每种交易类型最多一笔有效流水
CREATE UNIQUE INDEX uk_user_round_type
ON wallet_transactions (user_id, round_id, txn_type);
说明:reference 在 openlivegame 侧对单个商户全局唯一,因此唯一键用 (operator_id, reference)
防止同一次回调重试重复落账;(user_id, round_id, txn_type) 用于保证同一玩家钱包局里同类交易不会因异常 reference 再生成第二笔有效流水。
如果商户库内有多 operator 共表,请把 operator_id 也加入业务唯一键。依靠代码层 if exists then skip
而没有 DB 唯一索引的实现,在并发场景下会失效。
2. 三种重复请求场景的正确响应
商户收到带有已见过的 reference 的请求时,必须按实际状态返回。不要重复扣 / 加款。
| 场景 | 识别条件 | 推荐响应 |
|---|---|---|
| A 已成功 | DB 中存在相同 reference 且状态为 success |
返回当时那笔流水的 transactionId 和成功时的余额 cash。error=0(推荐)或 error=5(duplicate,语义更准确),两者 openlivegame 都视为幂等成功。
|
| B 已失败 | DB 中存在相同 reference 且状态为 failed(如余额不足) |
返回与当时一致的失败响应(例如 error=1 + 当前余额)。不要重新执行业务逻辑,避免"之前余额不足、现在余额够了"导致迟到下注生效。
|
| C 并发中 | 两个线程几乎同时收到同一 reference 请求 | 依赖 DB 唯一索引兜底:一条成功插入,另一条插入时命中唯一冲突,按场景 A 回查并返回已有流水。 |
| D 同 round/type 异常重复 | 相同 roundId + txn_type,但 reference 不同 |
不得再次扣款或加款。返回已有流水(error=5 或 error=0)或拒绝请求,但余额不能变化。
|
3. 标准实现伪代码
Bet 接口的推荐流程
// 关键不变量:唯一键 (operator_id, reference) 限制下,一个 reference 只能有一条记录。
BEGIN TRANSACTION;
// Step 1: 先查幂等键,命中则直接返回该流水(场景 A / B)
tx := SELECT * FROM wallet_transactions
WHERE operator_id = $op AND reference = $ref
FOR UPDATE; // 行级锁保证并发安全
if tx exists {
return { transactionId: tx.id, cash: tx.cash_after,
currency: tx.currency, error: tx.error_code,
description: tx.description };
}
// Step 2: 业务唯一性防线:同一 roundId + txn_type 不能生成第二笔有效流水
sameRound := SELECT * FROM wallet_transactions
WHERE operator_id = $op
AND user_id = $userId
AND round_id = $roundId
AND txn_type = 'bet'
FOR UPDATE;
if sameRound exists {
return { transactionId: sameRound.id, cash: sameRound.cash_after,
currency: sameRound.currency, error: 5,
description: "duplicate transaction" };
}
// Step 3: 锁定玩家账户行
player := SELECT * FROM players
WHERE operator_id = $op AND user_id = $userId
FOR UPDATE;
// Step 4: 业务判断
if player.cash < $amount {
// 余额不足也要落一条失败流水,重试命中时场景 B 返回
INSERT INTO wallet_transactions(operator_id, reference, user_id,
table_code, game_id, round_id, txn_type, amount, cash_before, cash_after,
error_code, description, status)
VALUES ($op, $ref, $userId,
$tableCode, $gameId, $roundId, 'bet',
$amount, player.cash, player.cash,
1, 'insufficient balance', 'failed');
COMMIT;
return { cash: player.cash, error: 1,
description: "insufficient balance" };
}
// Step 5: 扣减余额 + 写流水(同一事务内)
UPDATE players SET cash = cash - $amount
WHERE id = player.id;
txId := INSERT INTO wallet_transactions(operator_id, reference, user_id,
table_code, game_id, round_id, txn_type, amount, cash_before, cash_after,
error_code, description, status)
VALUES ($op, $ref, $userId,
$tableCode, $gameId, $roundId, 'bet',
$amount, player.cash, player.cash - $amount,
0, 'OK', 'success')
ON CONFLICT (operator_id, reference) DO NOTHING
RETURNING id;
// 并发冲突兜底:另一个线程已插入 → 回滚本次扣款,查询已有流水返回(场景 C)
if txId is null {
ROLLBACK;
goto Step 1;
}
COMMIT;
return { transactionId: txId, cash: player.cash - $amount,
currency: player.currency, error: 0, description: "OK" };
Result 与 Refund 同构,区别仅是 txn_type 以及余额是加而非减,且无余额不足分支。
注意 result 的 amount 可能为 0(未中奖结算)——照常落流水、加 0、返回成功。
4. Refund 的特殊要求
(tableCode, gameId, userId) 三件套 + txn_type='bet' 定位原始 bet
(等价地,也可以基于 roundId + txn_type='bet',因为 roundId 是三件套的派生),校验后再按 refund 的 reference 做幂等。
- Refund 的
amount总是正数,表示加款金额 - 商户收到 Refund 时建议:定位 bet 流水 → 校验
amount <= bet.amount→ 写 refund 流水并加款 → 可选地把 bet 标记为 refunded - Refund 对应的 bet 可能在商户侧不存在(bet 超时、商户实际未落账的场景)。此时不要凭空加款,
应返回
error=2,openlivegame 侧按任务失败转人工核对
5. 常见反模式(不要这么写)
| 反模式 | 后果 | 正确做法 |
|---|---|---|
只在应用层 if exists,不建 DB 唯一索引 |
并发下两个请求都先查到不存在,双方都插入,扣两次款 | DB 唯一索引 + ON CONFLICT DO NOTHING 兜底 |
| 重复请求时"重新跑一遍业务" | 第一次余额不足失败,重试时因用户充值已成功,导致迟到下注生效 | 重复请求必须严格返回首次结果,包括首次的错误 |
| 扣款和写流水不在同一事务 | 扣款成功但流水写入失败 → 幂等失效 → 下次重试再扣一次 | 扣款、写流水必须在同一个 DB 事务内提交 |
| 用 float 做余额计算 | 10.10 - 10.00 ≠ 0.10,长期累计后余额对不平 | Decimal / BigDecimal / numeric(18,2) 等精确类型 |
| 失败请求返回 HTTP 5xx | openlivegame 无法区分业务失败与网络失败,全部进入重试队列,加剧雪崩 | 业务错误 一律 HTTP 200,错误码放 error 字段 |
| Authenticate 返回的 userId 与 externalPlayerId 不一致 | url 直接返回 error=4,玩家进不了游戏 | 严格使用同一份玩家标识,在 token 表和玩家表中保持映射一致 |
同一 externalPlayerId 在不同场次通过 authenticate 返回不同 currency |
url 返回 error=20,玩家进不了游戏 |
一个 externalPlayerId 终身绑定一种币种。多币种用户用
{uid}_{ccy} 这类派生 ID,由商户维护自然人与多 ID 的映射
|
把回调里的 gameId 当全局唯一局号入唯一索引 |
gameId 跨机台可重号,撞唯一键后丢单 | 桌台局维度请用 (tableCode, gameId);玩家钱包流水维度请用 roundId |
6. 每日对账
强烈建议商户实现与 openlivegame 的 T+1 对账机制(通过 openlivegame 运维后台的报表或导出功能)。
对账维度:(operator_id, date, txn_type) 下的总金额与总笔数。
任何差异都应在当日发现、当日修复,避免问题滚雪球。
上线前自查清单 #
商户侧实现核对
- ☐ 已与 openlivegame 协商并记录
secureLogin/secretKey/callback_url三项配置 - ☐ 实现了 5 个回调端点:
/authenticate//balance//bet//result//refund - ☐ 所有回调对
hash字段做了签名校验(对全部非 hash 字段计算),签名失败返回error=4 - ☐ 签名实现用本文档「完整示例」的输入跑出了相同的 MD5 结果
- ☐ 生成的 token 是一次性的、短期有效、与玩家绑定
- ☐ authenticate 返回的
userId一定等于商户传给 url 的externalPlayerId,且currency非空并作为启动币种 - ☐ 多币种用户已使用不同的
externalPlayerId(如uid_USD/uid_EUR),并在内部维护了自然人到多 ID 的映射 - ☐ 解析回调时机台字段读的是
tableCode(不是 tableId) - ☐ bet / result / refund 基于
reference实现了数据库唯一索引级幂等(UNIQUE (operator_id, reference)) - ☐ result 在
amount="0.00"(未中奖结算)时也能正确落流水并返回成功 - ☐ 扣款 / 加款与流水写入在同一 DB 事务内提交
- ☐ 重复请求不重跑业务,严格返回首次落库的流水与错误码
- ☐ 失败场景(余额不足等)也会落一条
status=failed流水,保证重试幂等 - ☐ 找不到对应 bet 的 refund 返回
error=2,不凭空加款 - ☐ 所有业务错误均使用 HTTP 200 + JSON
error字段返回,未用 HTTP 4xx/5xx 代替业务错误 - ☐ 余额计算使用 Decimal 类型,未使用 float
- ☐ 回调接口 P99 响应 < 3 秒(bet 的硬超时只有 5 秒)
- ☐ 玩家账户与资金流水表设计了幂等隔离,不会因并发下注导致透支
- ☐ 生产环境配好了服务器 IP 白名单(如有)并向 openlivegame 报备
- ☐ 已完成 openlivegame 提供的联调沙箱测试,包含成功、余额不足、重复 reference、退款等场景
建议的监控指标
- 回调接口 QPS / P99 延迟 / 错误率(按 endpoint 拆分)
error=1(余额不足)的日占比趋势error=5(重复交易)次数 —— 少量正常(对应openlivegame重试),激增需联系openlivegame排查- 未匹配到 bet 却请求 refund 的次数 —— 对应「bet 超时未落账」场景,激增说明商户侧 bet 链路过慢
- 签名校验失败次数 —— 可能是密钥泄露或被攻击