OpenLive · 商户对接文档 v2(Dev)

游戏供应商对接与游戏数据监控平台。本文档定义了商户与 openlivegame 之间的 双向 HTTP 协议:商户通过 Provider API 获取游戏启动链接, openlivegame 通过 Seamless Wallet 回调实时完成余额变动。

Dev 环境 HTTP / Form MD5 双向签名 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 值。

Machine-readable docs (for AI agents / LLMs): API spec (Markdown) · llms.txt

概述 #

openlivegame 采用 Seamless Wallet(无缝钱包)集成模式。商户保留玩家资金账户的唯一真实来源, 玩家在游戏中的每一次下注 / 派奖 / 退款都由 openlivegame 实时回调商户钱包接口完成扣款与加款, openlivegame 自身不托管任何真实资金。

① 商户 → openlivegame

商户调用 Provider API 获取玩家游戏启动 URL,将玩家导向游戏客户端。

② openlivegame → 商户

openlivegame 在玩家下注结算过程中回调商户 Wallet API 完成资金变动。

③ 双向安全

两侧均使用统一的 MD5 签名算法 + 商户密钥验证请求真实性。

系统架构 #

整体交互分为同步链路(商户主动)与异步链路(openlivegame主动回调):

商户

openlivegame 平台

1

POST /provider/v2/{vendor}/url

返回 gameURL(含 JSESSIONID)

2

玩家跳转游戏客户端(WebSocket)

3

POST {callback_url}/authenticate

4

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 白名单(逗号分隔,可选)商户提供

完整对接时序 #

1
商户生成玩家 token 商户为登录后的玩家生成一个短期有效、一次性的 token(建议 5–30 分钟有效期), 并将该 token 与玩家绑定,后续 Authenticate 回调凭此查询玩家。
2
商户调用 POST /provider/v2/{vendor}/url 传入 secureLogintokenexternalPlayerIdgameId(必填), 以及按签名规则计算得到的 hash
3
openlivegame 验签后回调商户 POST {callback_url}/authenticate openlivegame 用同一份 token 反向询问商户,商户必须返回 userId(需 == externalPlayerId)、 currency 和当前余额 cash
4
openlivegame 返回 gameURL URL 中附带平台会话 JSESSIONID,商户将玩家 302 跳转至该 URL 即可进入游戏。
5
游戏中实时资金交互 玩家每次下注、派奖、退款时,openlivegame 主动回调商户对应接口(/bet / /result / /refund), 商户执行账户变动后返回最新余额。
6
必要时查询余额 openlivegame 在会话恢复 / 余额异常场景会调用 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-USen-usEN-US 是同一个编码。
  • 不做主语言或区域回退:en-GBzh-CNzh 不在表内,一律识别不了,玩家得到 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/listPP 游戏与大厅目录
POST/evo/listEVO 游戏与大厅目录
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。

POST /pp/list · /evo/list 获取游戏与大厅目录

按厂商返回当前启用的真实游戏目录,以及厂商主大厅和分类页入口。请求不包含玩家、token、币种或会话上下文。

请求参数

字段类型必填说明
secureLoginstring必填商户 API 身份标识
hashstring必填对 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)

字段类型说明
errorstring"0" 表示成功;其他值见错误码
descriptionstring简短的处理结果说明
gamesgame[]当前启用的真实游戏条目
lobbieslobby[]厂商主大厅与分类大厅入口

games[] 字段

字段类型说明
gameIdstring原样传给同厂商 url 的启动选择器
namestring英文展示名
gameTypestring游戏族
categoryIdsstring[]厂商内的游戏分类,字段始终返回
vendorGameIdstring上游厂商游戏 ID,字段始终返回
imagesobject按本条目 gameId 生成的 CDN 图片地址

lobbies[] 字段

字段类型说明
gameIdstringopenlivegame 返回的不透明大厅启动 ID
namestring英文大厅或分类入口名称
gameTypestring固定为 lobby
imagesobject按本条目 gameId 生成的 CDN 图片地址

images 字段

字段类型说明
squarestring正方形图片地址
portraitstring竖版图片地址
landscapestring横版图片地址
目录不是启动承诺: list 只表达当前启用目录。具体玩家能否启动还取决于商户状态、token、玩家币种、桌台币种配置、 客户端配置和实时运行状态,必须以当前 url 响应为准。
大厅条目: 大厅 gameId 是不透明字符串,必须从 list 读取并原样传给 url。 大厅条目不返回 categoryIdsvendorGameId
图片地址: games 与 lobbies 都返回 square、portrait、landscape。URL 按当前 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"
}
POST /pp/url · /evo/url 创建玩家启动链接

gameId 是唯一启动选择器,可以指向真实游戏、厂商主大厅或分类页。 openlivegame 验证请求与目标后回调商户 authenticate,成功才创建会话并返回 gameURL

请求参数

字段类型必填说明
secureLoginstring必填商户 API 身份标识
gameIdstring必填同厂商 list 返回的游戏或大厅条目 ID
tokenstring必填商户生成的短时玩家 token,原样透传给 authenticate
externalPlayerIdstring必填商户玩家 ID,格式 ^[A-Za-z0-9_\-]{1,64}$
hashstring必填请求签名
languagestring可选玩家语言编码,默认 en-US,仅对新玩家生效,取值见 语言支持;仅在非空发送时参与签名
nicknamestring可选玩家昵称,最长 50 个字符,仅对还没有昵称的玩家生效,规则见 昵称支持;仅在非空发送时参与签名
countrystring可选ISO 国家代码;非空时参与签名
platformstring可选mobile / desktop 等;非空时参与签名
lobbyUrlstring可选玩家退出后返回商户的绝对 URL
cashierUrlstring可选会话级收银台绝对 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)

字段类型说明
errorstring"0" 表示成功;其他值见错误码
descriptionstring简短的处理结果说明
gameURLstring玩家启动 URL,仅成功时返回

成功响应

{
  "error": "0",
  "description": "OK",
  "gameURL": "https://<EVO_CLIENT_HOST>/frontend/evo/mini/?EVOSESSIONID=..."
}

失败响应

{
  "error": "7",
  "description": "unsupported field: currency"
}
关键校验: authenticate 返回的 userId 必须严格等于 externalPlayerId,否则返回 error=4; authenticate 返回的 currency 是玩家和会话的唯一币种来源;为空时返回 error=20url 不接受 currency 字段,发送该字段返回 error=7
一用户一币种: 每个 externalPlayerId 终身绑定一种币种。多币种自然人必须使用不同 ID, 例如 u12345_USDu12345_EUR。EVO 桌缺少该玩家币种配置时返回 error=7
POST /report 获取单局报表一次性 URL

按钱包回调中的 16 位 roundId 获取短时、一次性的局报表 URL。

请求参数

字段类型必填说明
secureLoginstring必填商户 API 身份标识
roundIdstring必填钱包回调中的 16 位玩家业务局号
hashstring必填请求签名

请求示例

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)

字段类型说明
errorstring"0" 表示成功;其他值见错误码
descriptionstring简短的处理结果说明
urlstring短时局报表 URL,仅成功时返回

成功响应

{
  "error": "0",
  "description": "OK",
  "url": "https://<CLIENT_HOST>/reports/.../index.html?token=..."
}

失败响应

{
  "error": "4",
  "description": "round not found"
}
短时与归属隔离: URL 不应缓存或分享,每次查看重新请求。本商户无权查看的局返回 error=4,格式非法返回 error=7
GET /health 活性检测

无需参数、无需签名。用于探测 Provider API 进程是否可访问。

请求参数

无请求字段。

请求示例

curl https://<PROVIDER_HOST>/provider/v2/health

响应字段(JSON)

字段类型说明
errorstring"0" 表示服务正常;维护中返回 "3"
descriptionstring简短的健康状态说明

成功响应

{
  "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 字段计算)
  • 超时时间: /bet 5 秒;其余接口 10 秒(openlivegame 侧 client timeout)

响应格式

  • Content-Type: application/json
  • HTTP Status: 业务错误也请使用 200 OK,错误信息写入 error 字段
  • error 字段: 整型(注意:与 Provider API 的字符串错误码不同)
  • 金额字段: JSON number,最多 2 位小数
强制幂等: bet / result / refund 三个接口必须基于 reference 字段实现幂等。 同一 reference 的重复请求必须返回 同一条流水(相同 transactionIdcash), 禁止重复扣款或重复加款。重复时建议 error=5 (duplicate),但返回 error=0 也视为幂等成功。 reference 由 openlivegame 生成,商户应视为不透明字符串:原样保存、原样查重,不要解析其格式,也不要自行拼接生成。 bet / result / refundreference 彼此独立。 同时,同一玩家钱包 roundId 下每种交易类型最多只能有一笔有效流水:最多 1 笔 bet、1 笔 result、1 笔 refund

关键字段说明

字段说明
tableCode游戏机台代码,等于 games[].gameId,也是 urlgameId 入参。回调字段名是 tableCode,不是 tableId
gameId供应商局号;与 tableCode 组合后定位桌台上的一局:(tableCode, gameId)。单独的 gameId 不是全局唯一,不同机台之间可能重复
roundIdopenlivegame 业务局号(16 位纯数字),用于标识某个玩家在该局里的钱包流水链路。它由 (tableCode, gameId, userId) 派生:同三元组恒返同值;同一桌台同一供应商局号下,不同玩家的 roundId 不同
referenceopenlivegame 为本次钱包回调生成的不透明幂等键。商户应原样保存,用于 UNIQUE (operator_id, reference);不要解析其内部格式,也不要自行生成。bet / result / refundreference 彼此独立
POST /authenticate 玩家身份校验 · 返回初始余额

openlivegame 在收到商户 url 请求后立即调用此接口, 用同一份 token 询问商户:此 token 是否有效、对应哪个玩家、当前余额是多少。

请求参数(form)

字段类型说明
tokenstring商户生成的玩家 token(与 url 的 token 相同)
providerIdstring固定值 ppgame
hashstring请求签名

响应字段(JSON)

字段类型说明
userIdstring玩家唯一 ID,必须等于 url 请求中的 externalPlayerId
currencystring玩家账户币种,必填;它是 v2 启动的唯一币种来源,同一 userId 的 currency 一经确立不可更改
cashnumber当前现金余额,2 位小数
errorint错误码,0 表示成功,见 错误码表
descriptionstring错误描述

请求样例

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"
}
POST /balance 查询玩家最新余额

会话恢复、余额刷新等场景下 openlivegame 会调用此接口拉取商户侧玩家的最新现金余额。

请求参数(form)

字段类型必填说明
userIdstring必填玩家 ID(即 authenticate 返回的 userId)
providerIdstring必填固定值 ppgame
hashstring必填请求签名

请求示例

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)

字段类型说明
currencystring币种
cashnumber当前余额
errorint错误码
descriptionstring错误描述

成功响应

{
  "currency":    "USD",
  "cash":        1050.75,
  "error":       0,
  "description": "OK"
}

失败响应

{
  "currency": "",
  "cash": 0,
  "error": 2,
  "description": "user not found"
}
POST /bet 下注扣款

玩家在游戏中确认下注时调用,商户需从玩家余额中 扣减 amount

请求参数(form)

字段类型必填说明
userIdstring必填玩家 ID(= externalPlayerId)
tableCodestring必填机台编号(= list 的 gameId / url 的 gameId 入参)。注意字段名是 tableCode,不是 tableId
gameIdstring必填供应商局号;与 tableCode 组合后定位桌台上的一局:(tableCode, gameId)。不同机台之间 gameId 可能重复
roundIdstring必填openlivegame 业务局号(16 位纯数字),用于标识该玩家本局的钱包流水链路。由 (tableCode, gameId, userId) 派生:同三元组恒返同值;同一桌台同一供应商局号下,不同玩家的 roundId 不同
amountstring(decimal)必填下注金额,2 位小数字符串,如 "10.50"
referencestring必填openlivegame 生成的幂等键。商户需原样保存并用于唯一查重;不要解析其内部格式,也不要自行拼接生成
providerIdstring必填固定值 ppgame
timestampstring(int64)必填请求时间戳,毫秒
hashstring必填请求签名
roundDetailsstring(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)

字段类型说明
transactionIdstring商户侧交易流水号
currencystring币种
cashnumber扣款后的玩家余额
errorint错误码,余额不足时返回 1
descriptionstring错误描述

成功响应

{
  "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 幂等必须健壮。
POST /result 结算派奖

游戏结算后对每一个有成功下注的玩家调用,商户需将 amount 加到玩家余额上。 未中奖的局也会调用,此时 amount="0.00"——商户应照常落一条加款 0 的流水并返回成功, 以便双方对账确认该局已结算。

请求参数(form)

字段类型必填说明
userIdstring必填玩家 ID
tableCodestring必填机台编号(与 bet 同)
gameIdstring必填供应商局号(与 bet 同)
roundIdstring必填业务局号(与 bet 同;标识该玩家本局的钱包流水链路)
amountstring(decimal)必填派奖金额(≥ 0),2 位小数;未中奖为 "0.00"
referencestring必填派奖幂等键;由 openlivegame 生成,商户原样保存。与 bet 的 reference 不同,保证端点隔离
providerIdstring必填固定值 ppgame
timestampstring(int64)必填毫秒时间戳
hashstring必填请求签名

请求示例

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)

字段类型说明
transactionIdstring商户侧交易流水号;幂等重试必须稳定
currencystring玩家币种
cashnumber派奖后的玩家余额
errorint0 表示成功;其他值见错误码
descriptionstring简短的处理结果说明

成功响应

{
  "transactionId": "result-20260423-001",
  "currency":      "USD",
  "cash":          1014.50,
  "error":         0,
  "description":   "OK"
}

失败响应

{
  "transactionId": "",
  "currency": "USD",
  "cash": 989.50,
  "error": 100,
  "description": "internal error"
}
重试策略: 网络层失败(超时 / 连接错误)时 openlivegame 即时重试,至多 3 次尝试(间隔 2s / 4s 递增); 业务错误码不做即时重试。最终失败的结算任务进入异步重试队列(约每 15 秒扫描一次,至多 5 次), 超过上限转人工介入。商户务必基于本次请求的 reference 做幂等。
资金安全保证: openlivegame 在调用 /result 前强制校验本局存在对应的成功 /bet 扣款流水, 不存在则拒绝结算并转人工。商户不会收到「没有 bet 的 result」(如收到,按下文 refund 的建议返回 error=2)。
POST /refund 下注回滚

两种场景触发:① 整局被作废(游戏中断 / 上游取消本局);② /bet 调用超时或返回不确定错误(error=100 / 未知码),平台无法确认扣款是否生效, 按「可能已扣款」入队退款。openlivegame 调用 refund 请求商户把 bet 扣掉的金额 退还给玩家。

请求参数(form)

字段类型必填说明
userIdstring必填玩家 ID
tableCodestring必填真实游戏机台代码
gameIdstring必填供应商局号
roundIdstring必填16 位玩家钱包业务局号
amountstring(decimal)必填退款金额,必须为正数
referencestring必填退款专用幂等键,与 bet/result 的 reference 不同
providerIdstring必填固定值 ppgame
timestampstring(int64)必填毫秒时间戳
hashstring必填请求签名

请求示例

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)

字段类型说明
transactionIdstring商户侧交易流水号;幂等重试必须稳定
currencystring玩家币种
cashnumber退款后的玩家余额
errorint0 表示成功;找不到原 bet 时返回 2
descriptionstring简短的处理结果说明

成功响应

{
  "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"
}
重试与终态: refund 任务由后台队列驱动(约每 15 秒扫描一次),失败自动重试至上限(作废退款 3 次), 超限标记失败并转人工介入。error=0error=5(duplicate)都视为退款成功。 注意「bet 超时」场景:若商户实际并未扣款(bet 没有落账),收到对应 refund 时应返回 error=2 或携带原余额的失败响应,而不是凭空加款。

签名算法 #

双向通用(openlivegame验商户签名、商户验openlivegame签名)。签名字段名为 hash

步骤

  1. 收集所有请求参数的 原始值(表单解码后),排除 hash 本身
  2. 对参数名按 字母序(ASCII 升序)排序。
  3. 按顺序拼接为 keyA=valueA&keyB=valueB&...
  4. 在末尾 直接追加 secretKey(无分隔符)。
  5. 对整串做 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');
}

完整示例

假设:

secureLoginmerchant001
tokenplayer-token-123
externalPlayerIdplayer-001
gameIdevoCrazyTime0001
secretKeymysecretkey

排序后拼接:

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 不存在拒绝请求,不改变资金
3token 无效拒绝 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。

金额精度: HTTP 传输层 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=5error=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 的特殊要求

三端点 reference 彼此独立, 所以 refund 的 reference 与 bet/result 的 reference 不同。商户判定某次 refund 是否合法时,不能直接用 refund 的 reference 去比对 bet 表。应基于 (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 链路过慢
  • 签名校验失败次数 —— 可能是密钥泄露或被攻击
OpenLive · Merchant Integration Guide · Dev · 2026