Seamless Wallet 运营商对接文档

PlayRouter Seamless Wallet(单一钱包)· 方向 A(你调我们的 15 个端点)+ 方向 B(我们调你的 5 个钱包回调)· 本页所有端点均含可交互 Playground · 接转账钱包?见 Transfer Wallet 文档站 ↗

这份文档面向运营商的后端工程师。你不需要读完整规范就能完成对接:本页从零开始讲每个字段是什么、你该传什么/返回什么、以及所有会出错的地方。所有示例都可以在页内 Playground 直接玩(纯前端模拟,不发网络请求)。

先记住四句话(其余都是细节)

① 上线必须只有 4 + 5(调 4 个端点、实现 5 个回调,其余用到再学)。② 双向共用同一组密钥、同一个签名算法。③ 方向 B 每个回调都回 HTTP 200 + JSON,同一个 transaction_id 只记一次账,credit 和 rollback 永远不许拒——唯二例外 player_not_found / currency_mismatch(见附录铁律注)。④ 方向 A 是真 HTTP 码——2xx 用 body、429 等着重试、5xx 同幂等键重试、其余 4xx 修请求。

接入之旅:全貌一张图

整套对接只有两个方向。方向 A:你调用归属服 {eu|sea|latam}.api.playrouter.io——启动游戏、拉目录、对账、管活动、彩池 feed;方向 B:我们调用你注册的钱包回调——查余额、扣款、入账、结算、冲正。端点按「洋葱」分层:接通核心层就可以上线——4 个端点 + 5 个回调;其余端点全部可接可不接(对账三件推荐接,上线后再补也行)。下面每个端点的标题上都标了必须 / 可选。

方向 A:你 ──调用──▶ {eu|sea|latam}.api.playrouter.io (15 个端点:启动 / 目录 / 对账 / 注单 / 活动 / 彩池) 方向 B:PlayRouter ──调用──▶ 你注册的回调 URL (5 个端点:余额 / 扣 / 入 / 结算 / 冲正)
展示 / 参考 · 会话辅助(可选) 活动模块(可选 · 启用才认证) 运营(强烈建议 · 可后补) 核心 · 上线必须 = 4 + 5 sessions/create · games/list providers/list · currencies + 方向 B 的 5 个钱包回调 transactions/list · get · records/list · get campaigns/create · cancel · options · list · get jackpots/feed · sessions/terminate
动图 · 接入分层洋葱:从内到外按需剥开,外圈永远不构成上线门槛
层端点什么时候需要
核心 · 上线必须sessions/create · games/list · providers/list · currencies(+ 5 个回调)第一天。认证套件核心卷只考这些
运营transactions/list · transactions/get · records/list · records/get想做每日对账、查投递日志时(强烈建议,可后补);想给玩家看游戏记录、按局做统计时接 records(可选)
活动模块campaigns/create · cancel · options · list · get要发免费旋转/免费筹码/白送功能购买时。不发活动永远不用碰
展示 / 参考jackpots/feed想拿彩池当前金额去展示时(跑马灯等是典型用途,不设限)
会话辅助sessions/terminate风控/自我排除要把人请出游戏时(best-effort)

钱包模式与拓扑

单一钱包(Seamless)模式下,玩家资金始终且只在你的系统里——没有厂商侧游戏钱包、没有划拨。玩家的每一笔投注/派彩,由 PlayRouter 实时回调你的钱包记账:钱不动,账单飞过来。代价与收益是同一件事:每一笔钱都过你的手(实时、可拒绝、账本天然一致),所以你要运营一个 7×24 可被调用的回调服务。

你(运营商) 玩家钱包 · 唯一账本 资金永远不出你的系统 PlayRouter 吸收厂商混乱 游戏厂商 (几十家) sessions/create(方向 A) wallet/* 逐笔回调(方向 B) 下注/派彩事件 启动游戏
图 · 单一钱包拓扑:资金停在你这侧,PlayRouter 把厂商事件翻译成 5 个标准回调打给你

三条你要提前知道的规则

规则含义
唯一账本玩家余额只有你这一份。我们的每个资金回调都带你答复的最新 balance——但记账的真相永远在你的库里。
7×24 回调服务你要暴露一个回调 base URL(沙箱与生产各一),我们向 {base}/wallet/* 五个固定路径发 POST。回调 base URL 须是最终地址:我们不跟随 HTTP 跳转(3xx),收到 3xx 按配置故障处理(见「错误与重试」)。宕机也不烧钱:我们按 HTTP 类别解读你的故障并按时间表重试(见「我们对你的承诺」)。
归属区生产三服 eu / sea / latam 全开放,固定用离你钱包后端最近的一个——方向 A 你调它,方向 B 回调从它发出,双向一个家,回调出口 IP 白名单只有一段。
与 Transfer Wallet 产品的关系
本文档自包含,你不需要看转账产品的任何文档。目录/会话/币种 5 个端点(sessions/create·terminate、games/list、providers/list、currencies)与转账产品字段逐字相同(错误码 currency_mismatch 两面判据不同:转账面按玩家钱包的锁定币种,本面按玩家在该厂商用过的币种),只是路径前缀不同(本面 /v1/seamless/…,转账面 /v1/transfer/…)——将来若增开转账,这 5 个端点切换只改前缀,密钥也是同一把(密钥账号级通用,2026-07-20 起):一把 pk_/sk_ 对两条产品线同时有效,增开转账无需换钥。你能调用哪些端点取决于账号的产品开通状态,与密钥无关——体现在路径前缀上:未开通的面返回 403 product_not_enabled。

五分钟快速开始

拿到密钥对

pk_test_…(Key-Id,出现在请求头里)和 sk_test_…(secret,签名/验签用,绝不出现在网络上)。双向共用这一组——方向 A 你签名我们验,方向 B 我们签名你验。

注册回调 base URL + 实现验签中间件

沙箱与生产各注册一个,例如 https://wallet.yourcasino.com。请填最终地址——不要填会返回 301 / 302 / 307 / 308 的地址(常见于 http 跳 https、换域名、路径末尾自动补斜杠)。我们向 {base}/wallet/* 五个固定路径发 POST;五个端点共用同一段验签逻辑(下节,三种语言现成代码)。

实现 5 个钱包回调

一张 processed_transactions 幂等表 + 直线记账代码,没有状态机(见方向 B 区,建表 SQL 现成)。

调通 4 个端点

providers/list + games/list 建 lobby(厂商分区 + 游戏目录)、currencies 拿金额精度表(方向 B 记账要用)→ sessions/create 启动第一局。启动时我们会先回调你的 wallet/balance 做预检——第一条回调就这么来(见全景时序)。

跑认证测试套件

沙箱后台一键运行,核心卷只考必须面(启用活动模块才加考活动卷)。全绿 = 自动获得生产密钥申请资格。

第一个请求(复制即跑)

配合下方「签名」一节的 signedHeaders,对沙箱调一次 providers/list——从密钥到 200 的最短路径:

const body = '{}'; // 序列化成最终字符串后再签名,不得再格式化 const res = await fetch('https://api.sandbox.playrouter.io/v1/seamless/providers/list', { method: 'POST', headers: { 'Content-Type': 'application/json', ...signedHeaders(body, 'pk_test_01H…', process.env.PR_SECRET) }, body }); console.log(await res.json()); // → { "data": [ { "code": "pg", "name": "PG Soft", … } ] }

三个核心概念(先建立心智模型)

整套回调只依赖三个概念。用你熟悉的电商类比一次讲清,后面所有字段表都会变得好读。

transaction_id = 支付流水号

每个资金回调是一笔流水,这是它的全局唯一编号(txn_ 前缀),也是幂等键:同一编号再来,答 duplicate:true、别记账。你的幂等表存的就是它。

round = 订单号(一局游戏)

slots 转一次轮、二十一点打一手=一局。一局产生多笔流水(扣注、派彩、冲正),全挂同一个 round.id(rnd_)。sequence=本局第几个事件;finished=这局完了没(每局恰好一次 true)。用途:游戏历史按局分组、未完成局管理、客服对坐标。不用它账也是对的——纯分组标签。注意:收口后仍可能到达冲正(rollback)——冲正不改变收口状态、不携带 finished:true,照常按 rollback 处理。

session = 一次游玩

玩家点开游戏到离开算一次游玩。session_id(ses_)是我们的编号——排障时报给支持,双方对同一坐标;session_payload 是你的字条——启动时你塞什么,每个回调原样带回什么(内部会话号/平台/品牌),零查表认路。两个都零处理义务,且都不是拒绝资金的理由。

附赠:campaign_id = 活动编号

钱源于你创建的活动(免费旋转、免费筹码、白送的功能购买)时出现(cmp_)——多活动并行时,这笔派彩算哪个活动的营销成本,到账即知。

全景时序:一笔钱怎么流动

四个角色:玩家在你的站点玩、游戏由厂商提供、PlayRouter 居中翻译、你只面对统一的 5 个回调。下图一次讲完启动、主流两段式厂商的一局、少数合笔厂商的一局——你会发现无论厂商怎么发,落到你这里只有 debit / credit / settle 三种直线调用。

玩家 游戏厂商 PlayRouter 你(运营商) ① 启动游戏 在你的站点点开「发财虎」 sessions/create(方向 A · 带 session_payload) wallet/balance 预检(你的第一个回调) 返回游戏 URL,玩家进入厂商游戏 ② 一局 · 两段式厂商(主流:下注和派彩分两次) 按下 spin 下注事件(各家格式不同) wallet/debit · amount "10.00" · source "bet" 200 ok · balance "90.00" 派彩事件 wallet/credit · "24.50" · finished:true 收口 200 ok · balance "114.50" ③ 一局 · 合笔厂商(少数:一局输赢单笔结算) 按下 spin 单笔结算:bet 10 · win 24.5 wallet/settle · bet "10.00" / win "24.50" · finished:true 200 ok · balance "114.50"(单 DB 事务) 厂商怎么发是我们的事;落到你这里永远是右侧这几根橙色箭头 · 乱序 / 冲正见下方「承诺」一节的动图
动图 · 全景:启动 → 两段式一局(主流) → 合笔一局(少数)。虚线 = 你的响应

签名(必读 · 双向同一套)

两个方向用同一组密钥、同一个算法:HMAC-SHA256 对「时间戳 + 原始请求体」签名,装在两个固定请求头里。方向 A 你给请求签名、我们验(失败 = 401,语义正确且唯一);方向 B 我们签名、你验(失败 = 200 + {"status":"error"})。验签是你唯一的安全义务——不需要校验会话、不需要校验 IP、不需要维护 token。

X-Playrouter-Key-Id: pk_live_01H8XGJWBW ← 用哪把钥匙(支持两把并行,轮换用) X-Playrouter-Signature: t=1719792000,v1=5257a869… ← t 是 Unix 秒,v1 是签名 # 验签三步: # 1. 检查 |服务器当前时间 - t| ≤ 300 秒(防重放) # 2. 拼接 signed_payload = t + "." + 原始请求体(逐字节,别重新序列化 JSON!) # 3. HMAC-SHA256(secret, signed_payload) 的 hex 小写 == v1 ?

A给你的请求签名(你 → 我们 · 三种语言)

const crypto = require('node:crypto'); function signedHeaders(rawBody, keyId, secret) { const t = Math.floor(Date.now() / 1000); const v1 = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex'); return { 'X-Playrouter-Key-Id': keyId, 'X-Playrouter-Signature': `t=${t},v1=${v1}` }; } // 先把 body 序列化成最终字符串再签名,然后原样发送——序列化后不得再格式化。 // 签错/时间戳出窗 → 我们返回 401(invalid_signature / invalid_timestamp / unknown_key)。

B验签(我们 → 你 · 三种语言)

const crypto = require('node:crypto'); function verify(rawBody, sigHeader, secret) { const m = /t=(\d+),v1=([0-9a-f]{64})/.exec(sigHeader); if (!m) return false; const [, t, v1] = m; if (Math.abs(Date.now()/1000 - Number(t)) > 300) return false; // 防重放窗 const expect = crypto.createHmac('sha256', secret) .update(t + '.' + rawBody).digest('hex'); return crypto.timingSafeEqual(Buffer.from(expect), Buffer.from(v1)); } // 注意:rawBody 必须是原始字节串。Express 用 express.raw(), // 不要先 JSON.parse 再 stringify——那会改变字节导致验签失败。
写错也不会出事故

验签失败你返回 200 + status:"error";就算你错手返回了 401 或 403——我们按类别解读:任何 4xx(429 除外——429 按 5xx 进重试时间表)都当作你的配置故障(告警 + 冻结投递,绝不重试轰炸),任何 5xx / 超时进重试时间表。没有会烧钱的错误答案。

幂等与重试(总纲)

一套规则、两种钥匙——按方向分工,两边都是「拿不到结果就同键重来,恒安全(在同一区内)」。这里给总图,细节在各自分区展开。

方向 A · 你是调用方

幂等键 = body 里的 idempotency_key(仅变更类端点,如 campaigns/create)。write-ahead 先落库再派生(推荐 fs-{内部ID}),超时/5xx 同 key 原样重发;命中重放返回原响应 + Idempotency-Replayed: true;同 key 不同 body → 409 idempotency_key_reused;同 key 同 body 但首请求仍在处理中 → 409 request_in_progress + Retry-After(按头值重试同 key 即命中重放);保留 7 天。严禁每次重试换新 key。详见方向 A 总览。

方向 B · 我们是调用方

幂等键 = transaction_id(我们生成,就是业务主键,没有第二个键)。你只需一张 processed_transactions 表做 effect-once:见过的编号答 200 + duplicate:true + 当前余额,不再记账、不要求存原响应。重试节奏由我们按公开时间表执行。详见通用约定与错误与重试。

重试判据只看 HTTP 层
两个方向同一句话:5xx / 429 / 超时 = 重试(方向 A 的 429 按 Retry-After;方向 B 我们按公开重试时间表);3xx 与其余 4xx = 修配置,不重试;2xx = 终局,按 body 处理。方向 B 你恒答 200(业务拒绝是 declined,不是错误)——所以你那侧永远不会触发我们的错误重试,宕机时基建吐的 5xx 恰好就是正确信号。

方向 A:你调用我们的 15 个端点 DIRECTION A

Base URL:https://{eu|sea|latam}.api.playrouter.io——三个归属服全开放,固定用离你钱包后端最近的一个,用哪个归属就在哪个(沙箱 https://api.sandbox.playrouter.io,单区)。改用别的区不需要新账号或新密钥,直接调用新区即可;数据按区存放、区与区不互通——旧区的会话与交易记录留在旧区,照常可查。拿不到响应要重试时,请留在同一个区:同一个 idempotency_key 只在同一区内去重,换到别的区重发会被当成一笔新请求再执行一次。一律 POST + JSON,带上一节的两个签名头。路径第二段 = 钱包模式面:本产品全部端点都在 /v1/seamless/<域>/<动作> 下(如 /v1/seamless/sessions/create、/v1/seamless/currencies);转账产品的同名端点在 /v1/transfer/… 下。账号开通了哪一面才能调哪个前缀——未开通的面一律返回 403 product_not_enabled,与端点是否"公共"无关。两面共有的 5 个目录/会话端点字段逐字相同,切换只改前缀。角色反转提醒:这一区你是调用方——幂等键是 body 里的 idempotency_key(仅变更类端点),错误是真 HTTP 码 + 语义字符串错误体,别把方向 B 的「恒 200」搬过来。15 个端点里必须的只有 4 个(sessions/create · games/list · providers/list · currencies),其余全部可选——每个端点标题都有标注。字段表读法(与方向 B 镜像):请求表的「必填 / 可选」说的是你要不要传;响应表的「总是有 / 不一定有」说的是我们给不给——「不一定有」判空再用即可。

错误处理:你只需要 4 个分支

2xx → 用 body,完事 (200 永远是成功语义,没有"错误藏在 200 里") 429 → 按 Retry-After 头等待后重试 5xx / 超时 → 用同一个 idempotency_key 重试 (幂等保障下安全) 其余 4xx → 终态:修请求 / 修配置,记日志 400 JSON 坏 · 401 签名 · 403 权限未开通 · 404 引用不存在 · 409 幂等键冲突/在途 · 422 字段校验失败

每个 4xx/5xx 都是固定形状的错误体——code 是语义字符串(不用数字),只进日志不要求逐个 catch;没见过的 code 按其 HTTP 码的动作处理即可(code 增补不是破坏性变更):

{ "error": { "code": "game_not_available", "message": "Game 'pg:fortune-tiger' is not enabled for your operator.", "doc_url": "https://docs.playrouter.io/errors/game_not_available", "request_id": "req_01H…", // 同响应头 X-Request-Id,报障用 "fields": { "game_code": "not enabled" } // 422 时逐字段明细 } }

完整错误码目录(30 个逐码表)收在 附录 · 速查。

幂等(变更类端点:campaigns/create · cancel)

body 必填 idempotency_key(1–128 字符 [A-Za-z0-9_.-])。推荐确定性派生(如 fs-issue-{你的内部记录ID}),严禁每次重试新生成。命中重放返回原响应 + 响应头 Idempotency-Replayed: true;同 key 不同 body → 409 idempotency_key_reused(绝不执行,修 bug);保留 7 天。5xx / 超时后放心用同 key 重试——绝不会发出两份活动。

连击(在途撞键):同 key 同 body、首请求尚在处理中(原响应还没产生)→ 409 request_in_progress + Retry-After 头(秒)——按头值用同一个 key 重试,届时命中重放取回原响应。判定顺序:先比 body(不同 → idempotency_key_reused,永久错误)、再看状态(终态 → 重放;在途 → request_in_progress,瞬态)。本语义适用于所有带 idempotency_key 的变更端点;transfers/create(转账钱包产品)例外——它有 pending 单据形态,撞键在途直接 200 + pending + Idempotency-Replayed 头。

限流

标准响应头 RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset;超限 429 + Retry-After。配额按 API Key 计(轮换期两把 key 各有独立额度),以下限保证公布——每个 API Key 每分钟至少:transactions/list 60 次、records/list 150 次(本面与转账钱包面的 records/list 两面合计——两面同开的账号共用这一档)、其余端点 1000 次;实际放行量可能更高,下限只会放宽不会收紧。RateLimit-* 头的值按端点给出(重查询端点配额更严),勿跨端点缓存;建议(非强制)按 RateLimit-Remaining 自适应节流。首次接入回补历史可联系临时提额。

POST/v1/seamless/sessions/create 上线必须

启动游戏,真钱/试玩合一。启动时我们统一先回调你的 wallet/balance 做预检(所有厂商一致,你无需感知)。不托管玩家资料:全 API 的玩家身份面只有一个不透明的 player_id——姓名/邮箱/生日/国籍一律不收、PII 不过境。

请求字段

字段类型必填说明示例
player_idstringreal 必填≤ 64 字符;你侧稳定玩家标识,方向 B 回调原样带回;demo 可省略(传了可做统一日志归因)"wang-123"
game_codestring必填<provider>:<游戏段>,来自 games/list——全 API 同一把游戏键。不透明键,原样回传:多产品线厂商(目前只有 db,DB Gaming)的游戏段形如 card-101(产品线-厂商游戏码),单产品线厂商的游戏段就是厂商游戏码原文;不要自行拆游戏段,产品维度用 provider_product 字段"pg:fortune-tiger" / "db:card-101"
currencystring必填real = 钱包币种,精确匹配永不降级;同一厂商(多产品线厂商按产品线)内,一个 player_id 只能使用一个币种,以首次真钱开局为准,沙箱与正式共用这一规则——之后用别的币种在同一厂商开局 → 422 currency_mismatch(message 点名该玩家在这个厂商的币种);要在同一厂商用多个币种,请按币种拆 player_id(如 wang-123-cny / wang-123-usd),不同厂商之间可以用不同币种(厂商 / 实例不收该币种 → 422 currency_not_supported,在建户 / 打厂商之前拒——如 saba 实例只登记 USD 时的 CNY;该币种平台侧暂时不可用时同码、fields.currency = temporarily unavailable,不要换币种、稍后重试);demo = 期望展示币种——厂商不支持则以厂商默认币种(通常 EUR)启动,静默降级不报错、不校验"BRL"
modestring必填real | demo——真钱/试玩合一端点,模式进字段"real"
localestring可选ISO 639-1 两字母小写;不支持的值静默降级 en"pt"
devicestring可选desktop | mobile;厂商需要时我们给默认值"mobile"
lobby_urlstring可选退出游戏的返回地址"https://op.com/lobby"
deposit_urlstring可选游戏内余额不足时厂商「去充值」按钮的跳转地址;不给则降级用 lobby_url 补位"https://op.com/cashier"
launch_modestring可选"url"(默认)| "html"——HTML 直出(抗封锁,见下图);游戏是否支持看 games/list 的 launch_modes;不支持时返 url 不报错"url"
ipstringreal 必填玩家 IP(IPv4/IPv6);demo 可省略"203.0.113.7"
session_payloadstring可选≤1024 字符透传串,方向 B 每个回调原样奉还(零查表认路)"wsess-8f2|ios"

响应字段

字段出现说明
session_id总是有我们的会话 ID(ses_)——排障 / terminate / 回调关联的锚点
url总是有恒在——HTML 直出型厂商由我们托管 launcher 包装成 URL,统一契约零分支。拿到后立即打开、只用一次:链接有效期由游戏厂商决定,短的只有约 1 分钟,部分厂商的链接只能打开一次;玩家每次进游戏(含刷新页面、重新打开)都重新调用 sessions/create 取新链接,不要缓存复用
html不一定有launch_mode:"html" 且厂商支持 HTML 直出时给——HTML 启动文档,从你的自有域名伺服。伺服禁缓存:每次启动重新取、响应带 Cache-Control: no-cache, no-store, must-revalidate(厂商启动令牌一次性,缓存复用会启动失败)
expires_at总是有启动 URL 最晚失效时刻;实际有效期由游戏厂商决定、可能早得多(见 url 行),不要据此缓存复用 url
降级三连(同一个哲学)

locale 不支持 → en;demo 币种不支持 → 厂商默认币种;launch_mode:"html" 厂商没有 → 只返 url。全部静默、不报错——凡是拒绝调用方毫无收益的地方,一律降级。你不用维护任何厂商白名单。

启动失败怎么处理
  • 422 game_not_available:游戏当前不对这个玩家开放(下架、地域限制、未到 released_at,或厂商拒绝该玩家进入)——重试同一请求不会成功,提示玩家换一款游戏。例外:fields.game_code = temporarily unavailable 表示平台侧暂时不可用、游戏本身没有下架——稍后重试同一请求即可,你不需要做任何配置,也不要把游戏从大厅撤下。
  • 422 demo_not_available:这款游戏不支持试玩。
  • 502 provider_error:临时故障,稍后用同一请求重试。
launch_mode: "url"(默认,恒可用) launch_mode: "html"(进阶 · 抗封锁) play.yourcasino.com iframe: 响应的 url 第三方游戏域名 ⚠ 域名可能被墙 (封锁单点在别人手里) play.yourcasino.com 直接伺服响应的 html 游戏从你的域名跑 ✓ 封锁面 = 你自己的域名
动图 · 两种启动形态。我们明令禁止 iframe 包装的假 HTML——iframe 里仍是第三方域名,抗封锁性为零
PLAYGROUND试一下 · 换厂商/币种/launch_mode 看降级行为
你 → PLAYROUTER
(点启动)
PLAYROUTER → 你

POST/v1/seamless/games/list 上线必须

游戏目录:cursor 分页 + updated_after 增量同步。图、语言、币种、投注面值、features、RTP、启动形态、状态一个对象拿全——没有独立面值端点、没有索引往返(campaigns/create 直接传金额字符串)。

请求字段

字段类型必填说明示例
cursorstring可选上页响应的 next_cursor;首页传 null 或省略。黑盒游标:内容不作承诺,原样传回即可"eyJ…"
limitint可选每页条数(≤2000)500
updated_afterstring可选增量同步:只拉此时刻后有变更的(厂商下架也是一次变更,status 变 recalled;从目录消失的游戏不在增量里,见响应 status 说明)"2026-07-01T00:00:00.000Z"
providerstring可选过滤:按厂商(厂商族码,值来自本面 providers/list 的 code)"pg"
categorystring可选过滤:按品类(live 是一等品类)"slots"
currencystring可选过滤:按币种可玩性"BRL"
带过滤条件做增量

带过滤条件做增量时,只返回当前仍匹配的游戏;游戏因品类或币种登记变化不再匹配时,不会出现在结果里。要跟踪这类变化,增量同步请不带过滤条件,按返回的 category / currencies 字段在本地筛(这些字段变化时 updated_at 会刷新)。

响应信封

字段出现说明
data总是有游戏对象数组(逐字段见下表)
next_cursor总是有下一页游标;末页为 null
has_more总是有还有没有下一页

响应的游戏对象(逐字段)

字段出现说明
game_code总是有<provider>:<游戏段>——启动、活动、对账全 API 同一把键(不透明、原样回传;多产品线厂商如 db:card-101)
name总是有游戏展示名
provider总是有厂商代号(厂商族码,来自本面 providers/list 的 code)
provider_product不一定有该游戏所属的厂商产品维度键,多产品线厂商才出现(db 的产品线:live 真人 / hash 哈希 / card DB 棋牌 / boyacard 博雅棋牌 / fishing DB 捕鱼 / boyafishing 博雅捕鱼 / slots 电子 / scratch 刮刮乐 / lottery 彩票 / esports 电竞 / sports 体育;其它多产品厂商沿用既有子钱包语义)。lobby 按产品线分区、对账按产品线归类用它;不要自行拆 game_code 的游戏段来反推
category总是有品类(live 是一等品类)。取值 = 游戏类型字典 17 码:sports, live, slots, lottery, card, poker, fishing, esports, virtual, originals, racing, cockfighting, hash, arcade, table, prediction, scratch;字典只增不改,未知值按「其它」兜底
images不一定有{square, landscape} 图片 URL——只在厂商提供时出现;缺省 = 厂商无图源,不编图(2026-09-19 起由「总是有」改为「不一定有」:7 家已接厂商中仅个别厂商的游戏列表带图)
locales总是有支持语言(ISO 639-1 两字母,真数组)。厂商实例级:由平台运营按厂商实例登记,同一厂商实例下所有游戏同值,不保证每款游戏各异;未登记时缺省 ["en"]。含义 = 该厂商启动接口可传的语言;不在表内的 locale 启动时静默降级 en
currencies总是有支持币种(ISO 4217 / 加密货币码,真数组)。厂商实例级:由平台运营按厂商实例登记,同一厂商实例下所有游戏同值,不保证每款游戏各异;未登记时缺省 ["CNY"]。以此判断 sessions/create 该传什么币种;实例真正接受的币种以 422 currency_not_supported 为准(fields.currency = temporarily unavailable 的除外:那是平台侧暂时不可用,不代表实例不收)(目录里出现该实例不接受的币种属运营登记错误,我方告警修正;目录只列部分接受币种是正常经营选择)
devices总是有支持设备(desktop / mobile)
launch_modes总是有["url"] 或 ["url","html"]——含 "html" 即支持 HTML 直出(配合 sessions/create 的 launch_mode)
features不一定有{demo, freespins, bonus_buy, jackpot} 能力布尔——只在厂商目录给出能力信息时出现;缺省 = 未知,不编假 false(2026-09-19 起由「总是有」改为「不一定有」)
bet_levels不一定有币种 → 合法面值数组(金额字符串),只在厂商提供时出现;缺省 = 厂商不给面值表,campaigns/create 的 bet_amount 按厂商文档自定——无索引、无独立端点、无工单
rtp不一定有RTP 百分数字符串("96.81"),只在厂商提供时出现;缺省 = 厂商不公开 RTP,不编数(2026-09-19 起由「总是有」改为「不一定有」:已接 7 家厂商的目录接口均不含 RTP)
volatility不一定有厂商波动率评级原值透传(各家口径不一,不强行归一)
released_at不一定有发布时刻——此前启动返回 game_not_available(「预告上架」排期数据源)
forbid_bonus_play不一定有true = 禁止用奖金资金玩(你自建奖金系统的合规输入)
status总是有active | recalled——目录只含运营已上架、且所属厂商(多产品线厂商按产品线)当前由平台开放的游戏;active = 厂商在售,recalled = 厂商已下架(下架即一次 update,增量同步天然捕获)。运营关闭上架的游戏、平台停用的厂商(产品线)的游戏会直接从目录消失,这类消失不经 updated_after 增量下发,由我方商务另行通知;也可自行核对:厂商(产品线)停用时它会从 providers/list 中消失,单款游戏关闭上架可不带 updated_after 全量拉取比对。平台恢复该厂商后,其游戏重新出现、updated_at 随之刷新
updated_at总是有最近变更时刻——增量同步(updated_after)的锚
同步姿势

首次:读响应头 X-Full-Dump-Url 拉每日全量快照(CDN,万级目录不用翻页);链接下载失败时,改用不带 updated_after 的分页全量。日常:updated_after 增量 + cursor 翻页。合规不用自算——不可用的游戏启动时返回 game_not_available,没有黑白名单矩阵要维护;平台停用的厂商(产品线)的游戏启动时返回 403 provider_not_enabled(游戏本身已下架或未上架的仍返回 game_not_available)。

PLAYGROUND试一下 · 过滤 + 翻页(沙箱目录 7 款游戏,每页 3 条)
你 → PLAYROUTER
(点查询)
PLAYROUTER → 你

POST/v1/seamless/providers/list 上线必须

零参数目录端点——请求就是空对象 {}(租户由签名头的 Key-Id 解析,任何端点都不用带租户字段),每个厂商族一行(pg、db、pp、saba……)。什么时候用:后台厂商开关页一次拿全;lobby 厂商分区要的展示名(games/list 只给 "pg" 这种机器码,"PG Soft" 在这里);厂商是否在开放(停用的厂商族直接从列表消失,整区下架以此为判断依据);多产品线厂商的产品线清单 products(lobby 二级分区、provider_product 取值发现)。lobby 厂商列表页是行业标配、这三样只有它给——所以它属必须(也是全 API 最便宜的端点:{} 进、对象数组出、日缓存足矣)。

请求字段

空对象 {}——零字段(租户由签名头的 Key-Id 解析)。

响应字段(data[] 的厂商对象)

字段出现说明示例
code总是有厂商族码——与 games/list.provider、campaigns/create.provider 同一把值,全 API 的 provider 值都来自这里"pg"
name总是有人读展示名(lobby 厂商分区用的就是它)"PG Soft"
categories总是有该厂商覆盖的品类 = 本面该族目录中(运营已上架)游戏 category 的去重集合;一个类目下最后一款下架即从此消失["slots"]
currencies总是有厂商级支持币种 = 本面该族各实例登记的 currencies 的并集(运营按实例登记,未登记缺省 CNY,与 games/list 同源)["BRL","USD"]
status总是有active | disabled——目前恒为 active:列表只含本面当前开放的厂商族;厂商族整体停用时直接从列表消失、某条产品线停用时从 products 中消失,lobby 整区下架以此为准(disabled 暂不下发)"active"
products不一定有多产品线厂商才出现:[{code, name}],供 lobby 分区与 provider_product 取值发现[{"code":"card","name":"DB Card"}]

示例

// POST /v1/seamless/providers/list 请求 {} { "data": [ { "code": "pg", "name": "PG Soft", "categories": ["slots"], "currencies": ["BRL","USD"], "status": "active" }, { "code": "db", "name": "DB Gaming", "categories": ["card","live","fishing"], "currencies": ["BRL","USD"], "status": "active", "products": [ { "code": "card", "name": "DB Card" }, { "code": "live", "name": "DB Live" } ] } ] }
PLAYGROUND试一下
你 → PLAYROUTER
(点查询)
PLAYROUTER → 你

POST/v1/seamless/currencies 上线必须

同样是零参数端点(请求 {})。方向 B 的地基:全 API 金额字符串的小数位权威表——回调金额解析与余额存储精度都依赖它(USDT 这类加密货币不在 ISO 4217 里,精度只能问它)。

请求字段

空对象 {}。

响应字段(data[] 的币种对象)

字段出现说明示例
code总是有币种码:法币 = ISO 4217 三字母;加密货币 = 3–4 位字母数字"BRL" / "USDT"
decimals总是有金额字符串的小数位数——全 API(含方向 B 回调)的解析与存储精度按此2 / 6
type总是有fiat | crypto"fiat"

示例

// POST /v1/seamless/currencies 请求 {} { "data": [ { "code": "BRL", "decimals": 2, "type": "fiat" }, { "code": "USDT", "decimals": 6, "type": "crypto" } ] }
PLAYGROUND试一下
你 → PLAYROUTER
(点查询)
PLAYROUTER → 你

POST/v1/seamless/transactions/list 可选 · 推荐

对账流:cursor 分页 + 时间窗过滤(from/to 必填),数据源是我们自己的账本(非厂商透传)。响应是 Transaction 对象分页(对象全貌见下一节 transactions/get)。

transactions 两端点是承诺的落点

方向 B 的三件事都在这里可见:①每日对账兜底(投递机制失手时,差异在这里捕获、事后 adjustment 修正);②delivery_status / delivery_attempts——每笔回调投没投到你、重试了几次,像 Stripe 的 webhook attempts 一样可查;③钱包回调里刻意不带的元数据(彩池贡献明细 jackpot、厂商活动明细 event)在 Transaction 对象上承接,数据不丢。

请求字段

字段类型必填说明示例
fromstring条件必填时间窗起点(RFC 3339,按 occurred_at 过滤);不带 round_id 时必填,带 round_id 时可省略(回合天然有界)"2026-07-02T00:00:00.000Z"
tostring条件必填时间窗终点(口径同 from)"2026-07-03T00:00:00.000Z"
round_idstring可选按回合过滤(rnd_ 前缀)——回合全貌在此取:回合内多笔按 sequence 排序即可(2026-07-15 修订,原 transactions/get 的回合查询挪到此处)"rnd_01H…"
cursorstring可选上页响应的 next_cursor;首页省略或传 null。黑盒游标:内容不作承诺,原样传回即可"eyJ…"
limitint可选每页条数(≤5000)2000
player_idstring可选≤ 64 字符;按玩家过滤"wang-123"
providerstring可选按厂商过滤(厂商族码,来自本面 providers/list 的 code)"pg"

响应字段

字段出现说明
data总是有Transaction 对象数组(逐字段见下一节 transactions/get)
next_cursor总是有下一页游标;末页为 null
has_more总是有还有没有下一页

示例

// POST /v1/seamless/transactions/list 请求:from/to 条件必填(带 round_id 可省),cursor/limit/player_id/provider/round_id 可选 { "from": "2026-07-02T00:00:00.000Z", "to": "2026-07-03T00:00:00.000Z", "limit": 2000 } // → { "data": [ /* Transaction 对象 */ ], "next_cursor": "…", "has_more": true }
PLAYGROUND试一下 · 拉一段时间窗的对账流
你 → PLAYROUTER
(点查询)
PLAYROUTER → 你

POST/v1/seamless/transactions/get 可选 · 推荐

按 transaction_id 查单笔详情(点查),最近 1 个月在线可查(更早归档调取,与列表查询窗口同口径)、带完整投递日志与 jackpot / event / campaign 元数据。回合维度查询走 transactions/list 的 round_id 过滤(2026-07-15 修订:单对象响应装不下回合多笔,get 语义回归点查)。

请求字段

字段类型必填说明示例
transaction_idstring必填查单笔(txn_ 前缀)"txn_01H…"

响应字段(data 的 Transaction 对象逐字段)

字段出现说明
transaction_id总是有交易 ID(txn_),与方向 B 回调里的同值
type总是有debit | credit | settle | rollback——对应四种资金回调
source总是有资金语义标签,与方向 B 的 source 同一枚举族
player_id总是有你侧玩家标识(与回调内同值)
currency总是有交易币种
bet不一定有投注金额(金额字符串;type = debit / settle 时出现)
win不一定有派彩金额(金额字符串;type = credit / settle 时出现)
round_id不一定有回合 ID(rnd_;无回合事件如小费不带)
sequence不一定有回合内单调递增序号
round_finished不一定有该回合是否已收口
game_code不一定有游戏标识(<provider>:<游戏段>,不透明键,原样回传)
provider不一定有厂商代号(厂商族码,来自本面 providers/list 的 code)
provider_product不一定有厂商产品维度键回显(多产品线厂商才出现,如 db 的 card)——按产品线对账用
session_id不一定有会话 ID(ses_)
occurred_at总是有事件发生时刻(RFC 3339)
jackpot不一定有彩池明细 {contribution, win, breakdown[{pool_id, contribution, win}]}——厂商给了才出现;钱包回调刻意不带,数据去处在此
campaign_id不一定有源于你的活动时出现(与回调内同值)
event不一定有厂商侧活动明细 {id, type, details}(锦标赛/奖品雨——非你发起,明细走对账不走回调)
delivery_status总是有pending | delivered | retrying | frozen | failed——这笔回调投没投到你。frozen = 你的端点返回了 3xx 跳转或非 429 的 4xx(配置故障),我们已暂停自动重试并告警,修复确认后恢复(v1.x 加法新增值,未识别请按未知值兜底)
delivery_attempts不一定有投递日志数组 {attempted_at, http_status, outcome}(像 Stripe 的 webhook attempts)
operator_transaction_id不一定有你在响应里回执过的账目 ID(对账互查)

示例

// POST /v1/seamless/transactions/get 请求: { "transaction_id": "txn_01H…" } // 回合维度用 transactions/list + round_id 过滤 // → Transaction 对象(节选关键字段): { "data": { "transaction_id": "txn_01H…", "type": "settle", "source": "game", "player_id": "wang-123", "currency": "BRL", "bet": "10.00", "win": "24.50", "round_id": "rnd_01H…", "sequence": 2, "round_finished": true, "jackpot": { "contribution": "0.02", "win": "0.00", "breakdown": [ { "pool_id": "grand", "contribution": "0.02", "win": "0.00" } ] }, "campaign_id": "cmp_01H…", // 源于你的活动时出现(与回调内同值) "event": { "id": "evt-prov-881", "type": "Tournament" }, // 厂商侧活动明细 "delivery_status": "delivered", // pending | delivered | retrying | frozen | failed "delivery_attempts": [ { "attempted_at": "…", "http_status": 504, "outcome": "timeout" }, { "attempted_at": "…", "http_status": 200, "outcome": "ok" } ], "operator_transaction_id": "op-ledger-991" } }
PLAYGROUND试一下 · 两笔样例交易,看投递日志的差别
你 → PLAYROUTER
(点查询)
PLAYROUTER → 你

POST/v1/seamless/records/list 可选 · 推荐

游戏记录(注单)流:每一局结束后,我们把你这位玩家在这一局的输赢汇成一条记录(部分游戏按手 / 按注给出),供玩家游戏记录、客服查询与按局统计用。请求字段、分页、时间窗与转账钱包面的 records/list 逐字相同;按 record_id 去重入库。可查窗口统一最近 1 个月(from 越界 422)。

注单不是对账依据

资金以钱包回调(方向 B)与 transactions/list 为准。注单只汇总已入账的资金流水(你拒掉的扣款不计);活动 / 奖励类入账(不挂在某一局上的奖金、返现等)不进注单。注单与资金流水的对应关系见下方 round_id 一行。

什么时候出现

注单在这一局结束后出现,通常几分钟内;部分游戏的注单可能在局结束后数小时才出现(这类游戏的「局结束」要等一段时间没有新的下注 / 派彩才能确认),这类游戏的注单自 2026-10-25 起提供。之后若同一局又有迟到的派彩或冲正,会按下方「改判」规则追加记录。部分彩票、体育类游戏目前只给出有派彩的注单,未中奖的注单暂不出现;这类游戏的下注额以 transactions/list 为准。

时间窗按记录进入本平台的时刻切

from / to 比较的是 recorded_at,不是 started_at——迟到的记录、改判追加的冲销 / 更正记录都出现在它们进入时刻所在的窗里,按时间窗向前增量拉取不会漏行。建议时间窗终点比当前时刻至少早 1 分钟(或相邻窗重叠 1 分钟,按 record_id 去重)。

记录粒度因游戏类型而异

多数游戏一局一条(同一局多人同桌时,每位玩家各一条);进桌带入、离桌带出的桌游(如棋牌)每手一条;每注独立结算的游戏(如电竞)每注一条,未结算时先给一条 unsettled 快照、结算后另追加一条 settled。

改判(追加式,与转账钱包面同一套规则)

原记录永不修改;改判时追加①冲销记录(source=adjustment、金额为被冲销记录的负值、ref_record_id 指向它)与②更正记录(改判后的全量值、ref_record_id 同指原记录)。金额求和只算 status=settled 的记录,净额恒正确。

请求字段

字段类型必填说明示例
fromstring必填开始时间(含),RFC 3339;比较记录的 recorded_at"2026-07-05T00:00:00.000Z"
tostring必填结束时间(不含):from ≤ recorded_at < to"2026-07-06T00:00:00.000Z"
cursorstring|null可选黑盒游标,原样传回null
limitinteger可选1–2000,缺省 5002000
player_idstring可选≤ 64 字符;按玩家过滤"wang-123"
providerstring可选按厂商过滤(厂商族码,来自本面 providers/list 的 code)"pg"

响应字段

字段出现说明
data总是有Record 对象数组(逐字段见下表)
next_cursor总是有下一页游标;末页为 null
has_more总是有还有没有下一页

Record 对象(records/list 与 records/get 同形)

字段名与转账钱包面的 Record 相同;round_id、valid_bet 两个字段在本面的含义与转账面不同,同时接两面的请特别留意。

字段类型出现说明示例
record_idstring总是有唯一 ID(rec_)——去重主键、records/get 查询键"rec_01H8XGJWBW"
ref_record_idstring不一定有冲销 / 更正记录指向被冲销原记录的 record_id"rec_01H8XGJWBW"
player_idstring总是有玩家标识"wang-123"
providerstring总是有厂商族码(来自本面 providers/list 的 code)"pg"
provider_productstring不一定有厂商产品维度键(多产品线厂商才出现,如 db 的 card)"card"
game_codestring总是有游戏键,与目录一致(不透明键,原样回传)"pg:fortune-tiger"
categorystring总是有品类(值域同 games/list);查不到时为空串 """slots"
currencystring总是有币种"BRL"
round_idstring不一定有回合 ID(rnd_)——与钱包回调里的 round.id、transactions/list 里的 round_id 同值。用它加上 player_id 调 transactions/list,即得这一行涉及的资金流水(同一局多人同桌时各玩家共用一个回合 ID,不带 player_id 会把同桌其他玩家的流水一起查出);本行的 bet / win 只计其中已入账的。不按局结算的游戏(进桌带入、离桌带出的桌游;每注独立结算的电竞)不带 round_id——这类游戏按整场(或整段)汇总核对"rnd_01H8XGJWBW"
betstring总是有投注额;仅冲销记录(source=adjustment)可为负"10.00"
valid_betstring不一定有有效投注额——只有游戏本身给出了有效投注时才出现;拿不到就不给(我们不拿投注额冒充)。不出现时请自定反水口径(通常按 bet)"9.50"
winstring总是有派彩额;玩家输赢 = win − bet"15.50"
sourcestring总是有game(常规局)/ adjustment(冲销 / 更正);可增补"game"
statusstring总是有unsettled | settled | canceled | refunded——口径同转账钱包面:金额求和只算 settled;canceled / refunded 金额保留原注额、不参与求和"settled"
provider_record_idstring不一定有厂商侧单号——客诉排障时报给我们"8865512900"
started_atstring总是有本局开始时间"2026-07-05T10:11:02.000Z"
settled_atstring不一定有结束 / 结算时间;unsettled 时缺省"2026-07-05T10:11:05.000Z"
recorded_atstring总是有记录进入 PlayRouter 的时刻——时间窗按它切"2026-07-05T10:12:30.000Z"

示例

// POST /v1/seamless/records/list 请求:from/to 必填,cursor/limit/player_id/provider 可选 { "from": "2026-07-05T00:00:00.000Z", "to": "2026-07-06T00:00:00.000Z", "limit": 2000 } // → { "data": [ { "record_id": "rec_01H8XGJWBW", "player_id": "wang-123", "provider": "pg", // "game_code": "pg:fortune-tiger", "category": "slots", "currency": "BRL", // "round_id": "rnd_01H8XGJWBW", "bet": "10.00", "win": "15.50", // "source": "game", "status": "settled", "provider_record_id": "8865512900", // "started_at": "…", "settled_at": "…", "recorded_at": "…" } ], // "next_cursor": "…", "has_more": true } // 这一局的资金流水:POST /v1/seamless/transactions/list {"round_id":"rnd_01H8XGJWBW","player_id":"wang-123"}
PLAYGROUND试一下 · 拉一页样例注单(含一条冲销 + 更正)
你 → PLAYROUTER
(点查询)
PLAYROUTER → 你

POST/v1/seamless/records/get 可选 · 推荐

客诉单查:按 record_id 查单笔游戏记录,最近 1 个月在线可查(更早归档调取,与列表查询窗口同口径)。查无此记录(含不属于你的记录)404 record_not_found。

请求字段

字段类型必填说明示例
record_idstring必填rec_ 前缀(records/list 返回)"rec_01H8XGJWBW"
localestring可选预留(详情页语言);目前无详情页,可省略"pt"

响应字段

字段出现说明
data总是有Record 对象(与 records/list 同一形状)
PLAYGROUND试一下 · 查一笔注单(含查无此记录)
你 → PLAYROUTER
(点查询)
PLAYROUTER → 你

POST/v1/seamless/campaigns/create 可选 · 发活动才接

一个模型发三种赠品:type: freespin | freechip | bonus_buy(与方向 B 的派彩 source 一一对称:freespin↔freespin_payout、freechip↔freechip_payout、bonus_buy↔bonus_buy_payout)。名单内嵌:player_ids 随 create 提交(≤1000/次)——给 1000 人发 = 1 次调用,逐玩家成败在响应 results 给出(整批 2xx,单人失败不连坐)。

type 决定两个骨架字段的语义

typebet_amount 是什么free_game_count 是什么典型 provider_options
freespin单次免费旋转的投注额旋转次数(必填)厂商倍数等
freechip单枚筹码面值(须 ≥ 游戏最小注,厂商侧校验)筹码枚数(必填)max_win 等
bonus_buy功能购买的基础投注额(成本 = 基础注 × 厂商 cost_multiplier)固定 1,可省略bonus_type(值域经 options 的 schema enum 发现)

请求字段

字段必填说明示例
idempotency_key必填幂等键(见方向 A 总览)——5xx/超时后同 key 重试,绝不发出两份活动"camp-2026w27-vip"
type必填freespin | freechip | bonus_buy"freespin"
name必填活动名——create 回执与对账日志里的可读标签"vip-july-freespins"
provider必填厂商显式单列(厂商族码,来自本面 providers/list 的 code;所有 game_codes 必须属于它,校验更早)"pg"
game_codes必填游戏数组(同一厂商)["pg:fortune-tiger"]
currency必填一活动一币种(bet_amount 语义无歧义);多币种发放 = 多个活动"BRL"
free_game_count按 type整个活动的总量——玩家可在 game_codes 内任意游戏消耗,不是每游戏各一份10
bet_amount必填金额字符串。games/list 该游戏带 bet_levels 时从中选值;不带(厂商不给面值表,属常态)则按厂商活动文档给的合法面值填——金额→厂商索引的翻译我们做"0.50"
starts_at必填活动开始时刻(起止都有 = 可预建定时活动)"2026-07-04T00:00:00.000Z"
ends_at必填活动截止时刻。厂商时长上限先查 options 的 constraints;违规 → 422 + fields.ends_at 明细"2026-07-11T00:00:00.000Z"
player_ids必填发放名单(≤1000/次)。活动 = 一次性名单快照——中途追加 = 另建活动["wang-123","li-456"]
provider_options可选类型/厂商专属参数,按 options 返回的 (provider, type) JSON Schema 校验;不合 → 422 provider_options_invalid{"multiplier":10}

响应字段

字段出现说明
data.campaign_id总是有活动 ID(cmp_)——cancel 的唯一引用键,须持久化
data.status总是有初始恒 pending;状态全枚举 pending | started | ended | cancelled(经 create / cancel / get / list 响应可见)
data.results[]总是有名单逐玩家答复 {player_id, status: ok|failed, code?}——整批 2xx、单人失败不连坐;code 见下表。create 首次应答只反映受理校验,厂商侧逐玩家结果在投放后经 campaigns/get 给出
响应仍须持久化 · 认证套件必测

响应里的 campaign_id 是 cancel 的唯一引用键。响应丢失时 7 天内可凭同 idempotency_key 重放找回;超期可经 campaigns/list 按名称/时间定位、campaigns/get 取回全量(v1.x 纯加法补齐,2026-07-09——查询面是兜底,不是持久化义务的替代)。

厂商暂不支持活动发放时

campaigns/options 对该厂商回空数组,即该厂商暂不支持活动发放;此时建活动返回 403 feature_not_enabled——不是你的配置问题,等我方通知开通即可。

results:200 里唯一的逐项业务失败

results[].code语义
player_id_invalidplayer_id 格式不合法(受理校验;预留码——目前名单含不合法 ID 时整单回 422 validation_failed)
campaign_player_limit_reached活动人数达厂商上限,本项未加入
provider_rejected厂商拒绝向该玩家发放——投放后才知道,经 campaigns/get 查看;本项未发放,名单内其余照常生效
你(运营商) PlayRouter 游戏厂商 campaigns/create · type + player_ids(≤1000)+ idempotency_key 受理校验名单 → 200 · campaign_id + results(全部 ok) 按投放批次逐玩家发放(翻译成厂商的单玩家调用 / 面值索引) 某个玩家被厂商拒绝 campaigns/get 查看:failed(provider_rejected) 单人失败不连坐——名单内其余照常生效 …几天后玩家把免费旋转玩完,厂商发结算… wallet/credit · source:"freespin_payout" · campaign_id ← 归因闭环 发放(方向 A)与结算(方向 B)由 campaign_id 串成闭环——营销成本归哪个活动,到账即知
动图 · 名单内嵌 create:1 次调用发千人,create 应答只反映受理校验,厂商侧逐玩家结果经 campaigns/get 查看,派彩回调自带归因
PLAYGROUND试一下 · 创建后点「同 key 重发」看幂等重放;名单里掺一个非法 ID(如 bad id!)看整单 422
你 → PLAYROUTER
(点创建)
PLAYROUTER → 你
pending started ended cancelled starts_at 到点 ends_at 到点 campaigns/cancel
活动状态机:pending → started → ended / cancelled(字符串枚举,不用数字码)。pending 必可取消;started 取消受理即回执终态 cancelled(厂商侧未使用发放的撤回由平台后台完成)——状态经 create / cancel / get / list 响应可见

POST/v1/seamless/campaigns/cancel 可选 · 发活动才接

取消活动(变更类,带幂等键)。请求就两个字段:idempotency_key + campaign_id(create 响应发的 cmp_,全局唯一,不需要厂商辅助定位)。

请求字段

字段类型必填说明示例
idempotency_keystring必填幂等键(变更类统一规则,见方向 A 总览)"cancel-cmp01H"
campaign_idstring必填要取消的活动(create 响应发的 cmp_,全局唯一)"cmp_01H…"

响应字段

字段出现说明
data.campaign_id总是有回显活动 ID
data.status总是有终态 "cancelled"——回执带终态,不用猜取消成没成
取消承诺

pending 状态必可取消(活动还没到厂商,我们自己说了算);started 取消受理即回执终态 cancelled,厂商侧未使用发放的撤回由平台后台异步完成(撤回失败自动重试;发放随活动截止时刻自然失效兜底),你无需跟进。422 campaign_state_invalid 只余两种形态:活动已 ended;或该厂商没有撤回能力(options 的 constraints.cancellable 为 false,可先查)时取消 started 活动。对已取消的活动再次取消,返回同一终态回执 cancelled,不报错。

PLAYGROUND试一下 · 取消两次,第二次看同一终态回执(不报错)
你 → PLAYROUTER
(选一个活动)
PLAYROUTER → 你

POST/v1/seamless/campaigns/options 可选 · 发活动才接

厂商参数发现,按 (provider, type) 组合返回两样东西:json_schema 管「怎么填合法」——SDK 发请求前本地校验 provider_options;provider_data 管「填哪个划算/可用」——bonus_buy 的逐游戏成本表 cost_multipliers、freechip 的动态面值 chip_values、通用 constraints(时长上限、可否取消等 JSON Schema 表达不了的约束)。data 恒为数组:指定 type = 单元素,缺省 = 该厂商全部 type 逐项。

回空数组 = 该厂商暂不支持活动发放

查选项对某厂商回空数组时,即该厂商暂不支持活动发放;此时对它建活动返回 403 feature_not_enabled——不是你的配置问题,等我方通知开通即可。

请求字段

字段类型必填说明示例
providerstring必填查哪家厂商(厂商族码,来自本面 providers/list 的 code)"pg"
typestring可选freespin | freechip | bonus_buy;缺省 = 返回该厂商全部 type 逐项"freespin"

响应字段(data[] 按 (provider, type) 逐项)

字段出现说明
provider总是有本项对应的厂商
type总是有本项对应的类型(freespin | freechip | bonus_buy)
json_schema总是有该组合下 provider_options 的 JSON Schema——SDK 发请求前本地校验;值域类信息(bonus_type 枚举、离散面值)经 enum 承载
provider_data总是有数据类发现信息,无数据时为 null(以下三个子字段按需出现)
provider_data.cost_multipliers[]不一定有type=bonus_buy——逐游戏成本表 {game_code, bonus_type, multiplier}(发放成本 = bet_amount × multiplier)
provider_data.chip_values不一定有type=freechip——动态面值厂商按币种的合法面值数组
provider_data.constraints不一定有通用厂商约束:max_duration_days(活动时长上限)· cancellable(可否取消)等——JSON Schema 表达不了的跨字段/能力类限制,创建前自查
PLAYGROUND试一下 · 换 (provider, type) 看 schema 与 provider_data 怎么变(沙箱示例数据)
你 → PLAYROUTER
(点查询)
PLAYROUTER → 你

POST/v1/seamless/campaigns/list 可选 · 发活动才接

活动列表查询(v1.x 纯加法补齐,2026-07-09——"最小面"原则下实需回补的演进案例):cursor 分页列出你创建过的活动,status / type / provider / 创建时间窗可过滤。典型用途:create 响应丢失且幂等超期后按名称/时间找回 campaign_id;营销后台的活动列表页。只读、无幂等键。返回活动骨架(不含逐玩家 results——全量用 campaigns/get)。

请求字段

字段类型必填说明示例
cursorstring可选上页 next_cursor 原样带回"eyJv…"
limitinteger可选每页条数,默认 20、上限 10020
statusstring可选过滤:pending | started | ended | cancelled"started"
typestring可选过滤:freespin | freechip | bonus_buy"freespin"
providerstring可选过滤:厂商代号(厂商族码,来自本面 providers/list 的 code)"pg"
fromstring可选创建时间窗起(RFC 3339,按 created_at 过滤)"2026-07-01T00:00:00.000Z"
tostring可选创建时间窗止"2026-07-09T00:00:00.000Z"

响应字段(data[] 的活动骨架;外层 next_cursor / has_more 与 transactions/list 同款)

字段出现说明
campaign_id总是有cmp_
name / type / provider / currency总是有create 时所传原值
status总是有当前状态(全枚举见 create)
starts_at / ends_at总是有create 时所传原值
created_at总是有create 受理时刻(本次加法新增字段,list/get 同出)
player_count总是有名单人数(逐玩家明细用 campaigns/get 取)

POST/v1/seamless/campaigns/get 可选 · 发活动才接

按 campaign_id 查单个活动全量(v1.x 纯加法补齐,2026-07-09):create 请求回显 + 当前 status + 名单逐玩家 results(与 create 响应同构,反映发放时点结果)。查无此单或不属于你 → 404 campaign_not_found(防枚举口径)。只读、无幂等键。

请求字段

字段类型必填说明示例
campaign_idstring必填create 响应发的 cmp_(或经 campaigns/list 找回)"cmp_01H…"

响应字段

字段出现说明
campaign_id / name / type / provider / game_codes / currency / bet_amount / free_game_count / starts_at / ends_at / provider_options总是有(free_game_count / provider_options 随 create 传否)create 请求回显
status总是有当前状态
created_at总是有create 受理时刻
results[]总是有名单逐玩家 {player_id, status: ok|failed, code?}——与 create 响应同构

POST/v1/seamless/jackpots/feed 可选

彩池当前金额的只读缓存 feed(TTL 分钟级)。典型用途是 lobby 展示彩池金额(跑马灯、游戏卡角标等)——数据怎么用是你的自由,我们只设一条硬约束:非结算依据(派彩以方向 B 的 credit(source=jackpot) 回调为准)。currency 必填单币种查询,amount 以请求币种计(换币种再查即可)。pool_id 与对账里 Transaction.jackpot.breakdown[].pool_id 同一命名空间——feed、派彩、对账三处对得上。无彩池数据的厂商游戏不出现在响应中。

请求字段

字段类型必填说明示例
currencystring必填查询币种——amount 以它计价(换币种再查)"BRL"
providerstring可选按厂商过滤(厂商族码,来自本面 providers/list 的 code)"pp"
game_codestring可选按游戏过滤(不透明键,原样传)"pp:jackpot-game"

响应字段(data[] 逐游戏条目)

字段出现说明
game_code总是有带彩池的游戏(与 games/list 同一把键)
provider总是有厂商代号(厂商族码,来自本面 providers/list 的 code)
pools[]总是有{pool_id, amount}——pool_id 与对账 Transaction.jackpot.breakdown[].pool_id 同一命名空间;amount 以请求币种计(金额字符串)
updated_at总是有数据时刻(缓存 TTL 分钟级)——非结算依据
PLAYGROUND试一下 · 换币种看 amount 换算(沙箱示例汇率),过滤厂商
你 → PLAYROUTER
(点查询)
PLAYROUTER → 你

POST/v1/seamless/sessions/terminate 可选 · best-effort

把玩家请出游戏(风控/自我排除场景)。请求给 player_id(终止其全部活跃会话)或 session_id(指名终止)二选一;非资金、天然幂等,不带 idempotency_key;响应 200 + {"data":{}}(受理即 200,不担保厂商侧结果)。

请求字段

字段类型必填说明示例
player_idstring二选一≤ 64 字符;终止该玩家的全部活跃会话"wang-123"
session_idstring二选一指名终止单个会话(ses_ 前缀,sessions/create 响应发的)"ses_01H…"

响应字段

字段出现说明
data总是有v1 为空对象 {}——受理即 200(best-effort,不担保厂商侧踢出结果);演进按加法补逐厂商扇出结果
语义边界 · 别搞混两层

这是尽力而为的踢出扇出(能否踢出、踢出后玩家能否重登,效果因厂商而异)。RG/自我排除的硬闸门永远在你的钱包侧:对 debit 答 declined(player_blocked)——实时、资金边界强制、不依赖任何厂商。terminate 是体验层(把人请出桌),player_blocked 是资金层(让他玩不了)。

PLAYGROUND试一下
你 → PLAYROUTER
(点查询)
PLAYROUTER → 你

方向 B:实现 5 个钱包回调 DIRECTION B

从这里到「错误与重试」都是方向 B——我们调用你注册的 {base}/wallet/*。角色再次反转:恒 200 + JSON(业务拒绝放 body)、幂等键是 transaction_id(我们生成)、你只会写三种响应。这 5 个回调全部属上线必须面——资金边界没有可选项。

响应契约:你只会写三种 JSON

// ① 办成了(每个响应必带 status 和 balance) 200 { "status": "ok", "balance": "114.50", "operator_transaction_id": "op-991" } // 末字段可选 // ② 业务上拒了(balance / debit / settle 可拒;credit / rollback 仅 player_not_found / currency_mismatch 可拒——账本无法定位) 200 { "status": "declined", "reason": "insufficient_funds", "balance": "3.20" } // ③ 验签失败(唯一的 error) 200 { "status": "error", "reason": "invalid_signature" }
reason(declined 时)什么时候用balance 要不要带
insufficient_funds余额不够扣必带(游戏客户端靠它刷新显示)
player_blocked玩家被禁投 / 自我排除必带
bet_limit_exceeded责任博彩限额(自设或监管)必带
player_not_found玩家在你系统里不存在可省(钱包定位不到)
currency_mismatch交易币种不在该玩家支持的币种范围内(玩家可多币种)可省

金额与时间

类型格式示例
金额(请求)字符串十进制,^\d{1,18}(\.\d{1,12})?$,恒非负——方向由端点表达(debit 恒扣、credit 恒入),你永远不用判断正负号"10.00" · "0.000001" · "0.00"
balance(响应)同上,但唯一允许负号(credit/rollback 强制执行后可能为负,见铁律①)"114.50" · "-95.00"
时间RFC 3339 UTC 恒 3 位毫秒,字段名以 _at 结尾"2026-07-03T12:00:00.000Z"
金额请按数值比较,不要按字符串比较

我们发出的金额(回调请求、各查询端点的响应)小数位按 currencies 公布的 decimals 写满,零也一样(2 位小数币种写 "0.00",6 位写 "0.000000")。判断「是否为零」「两笔是否相等」时,先解析成十进制数(Java BigDecimal.compareTo、Python Decimal、JS 用十进制库)再比较;amount == "0" 这类字符串比较会出错。

幂等:一张表就够

每个资金回调带全局唯一的 transaction_id(txn_ 前缀)。网络重试时同一笔会再来——你要保证只记一次账,重复的答 duplicate: true + 当前余额。不需要保存原响应体。

CREATE TABLE processed_transactions ( transaction_id VARCHAR(64) PRIMARY KEY, -- txn_… 直接做主键,天然去重 type VARCHAR(16) NOT NULL, -- debit | credit | settle | rollback player_id VARCHAR(100) NOT NULL, processed_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- handler 伪代码:INSERT … ON CONFLICT DO NOTHING; -- 冲突(已存在)→ 返回 { ok, 当前余额, duplicate:true },什么都不改。

收到任何回调时,按这个顺序判断

JSON 解析失败?

→ 返回 400(框架默认行为即可,零代码)。

验签失败 / 时间戳出窗?

→ 200 + {"status":"error","reason":"invalid_signature"}。

transaction_id 已处理过?

→ 200 + {"status":"ok","balance":当前余额,"duplicate":true},到此为止,什么都不改。

业务检查(仅 balance / debit / settle)

玩家存在吗?币种对吗?余额够吗?限额允许吗?不行 → 200 + declined + reason。credit 和 rollback 跳过这一步——它们没有拒绝路径。

单个 DB 事务里记账

动余额 + 写幂等表,一起提交。→ 200 + ok + 最新余额。

自己内部炸了?

→ 返回 5xx。我们会按 5s→1m→5m→…→24h 的时间表自动重试(全表见「错误与重试」;幂等表保证重试安全)。

我们对你的承诺(这些麻烦你永远见不到)

游戏厂商的事件是乱的:win 可能先于 bet 到达、rollback 可能引用一笔从未送达的交易。这些混乱全部在 PlayRouter 层终止——下面这张动图里,右边(你)永远只看到干净有序的流。

游戏厂商(混乱侧) PlayRouter(吸收层) 你(有序侧) ⚡ win #7 先到(它的 bet #6 还没来) credit txn_7(金额自足,直接入账) ⚡ bet #6 迟到 2.4 秒 debit txn_6(回合内 FIFO · sequence 对齐) ⚡ rollback(bet #88)——#88 从未送达过 ✕ 吸收 · 墓碑登记 (你这里什么都没发生)
动图 · 三条书面承诺:① 绝不对你未收到的交易发 rollback ② rollback 后原交易绝不再来 ③ 引用完整性 + 回合内按序投递(带单调 sequence)
对你意味着

不用写乱序状态机、不用建墓碑表、不用兜底"未知交易的冲正"——来一条记一条账。收口也有保证:每个回合最终恰好一次 round.finished: true(收口关闭的是游戏流;收口后若来迟到冲正,照常按 rollback 处理即可,不影响该保证)。

source 全解:每种钱是什么、走哪个端点

端点名回答"钱往哪个方向动"(debit 扣 / credit 入 / settle 一扣一入),source 回答"这是什么钱"。你的记账代码不需要按 source 分支——它只是写进账本的分类标签(报表、返水口径、风控用)。铁律③:没见过的 source 值照常入账/扣款。

端点 × source 矩阵

source走哪个端点这是什么钱带 round?带 campaign_id?
betdebit游戏下注(两段式厂商的扣款)有无
bonus_buydebit功能购买——玩家花钱直接买进免费特性玩法(如 100 倍注价直接进免费旋转)有无
tipdebit真人桌小费(给荷官)。不属于任何一局,可被冲正无无
entry_feedebit锦标赛/活动报名费无可能有
adjustmentdebit 或 credit事后更正(真人桌复核改判等):要收回走 debit、要补发走 credit——唯一双向的 source,方向永远由端点表达,你不用碰负数通常有无
game_wincredit游戏派彩(两段式厂商的赢钱),常带 related_transaction_id 指回原下注有无
freespin_payoutcredit免费旋转结算——你发的赠品旋转玩完,总赢额一笔入账(之前没有对应 debit:玩的是赠品不是玩家的钱)可能无有
freechip_payoutcredit免费筹码结算——同上,赠品形态是筹码(桌面/真人用)可能无有
bonus_buy_payoutcredit功能购买的派彩。玩家自费买的配对 bonus_buy 扣款;你活动白送的则无扣款、带 campaign_id有白送时有
promo_wincredit活动奖金:锦标赛奖金、奖品雨、厂商促销。不属于任何一局无你办的活动才有
jackpotcredit彩池派彩(累积奖金命中)。池明细不进回调,走对账 transactions/get有无
round_finishcredit零额回合收口——没钱要动但这局该关了(amount 为零,如 "0.00";余额不变),读 round.finished:true 关局即可有(finished:true)无
gamesettle合笔结算(少数厂商):一局的扣与派一笔完成。settle 只有这一个 source有无

实际调用长什么样(公共信封字段省略,只看区别)

// ① 普通一局(两段式厂商):一扣一入,related 指回原注 debit { "amount": "10.00", "source": "bet", "round": {"id":"rnd_X","sequence":1,"finished":false} } credit { "amount": "24.50", "source": "game_win", "round": {"id":"rnd_X","sequence":2,"finished":true}, "related_transaction_id": "txn_A" } // ② 同一局,换成合笔厂商:一笔完成 settle { "bet": "10.00", "win": "24.50", "source": "game", "round": {"id":"rnd_X","sequence":1,"finished":true} } // ③ 功能购买:买入 + 派彩,一对 debit { "amount": "100.00", "source": "bonus_buy", "round": {"id":"rnd_Y","sequence":1,"finished":false} } credit { "amount": "284.00", "source": "bonus_buy_payout", "round": {"id":"rnd_Y","sequence":2,"finished":true}, "related_transaction_id": "txn_C" } // ④ 免费旋转结算:只有入账、没有扣款(玩的是你送的赠品),campaign_id 告诉你是哪个活动 credit { "amount": "37.80", "source": "freespin_payout", "campaign_id": "cmp_vip_july" } // ⑤ 真人桌小费:无 round(不属于任何一局),之后可能被冲正 debit { "amount": "5.00", "source": "tip" } rollback { "original_transaction_id": "txn_E" } // 荷官没收到,退回小费 // ⑥ 锦标赛:报名扣费 → 得奖入账(两笔独立,无 round) debit { "amount": "20.00", "source": "entry_fee", "campaign_id": "cmp_weekly_t" } credit { "amount": "500.00", "source": "promo_win", "campaign_id": "cmp_weekly_t" } // ⑦ 事后更正(真人桌复核):补发走 credit、收回走 debit——同一个 source,方向在端点 credit { "amount": "1.11", "source": "adjustment", "round": {"id":"rnd_Z","sequence":3,"finished":true} } debit { "amount": "1.11", "source": "adjustment", "round": {"id":"rnd_Z","sequence":3,"finished":true} } // ⑧ 零额收口:这局没钱可动但该关了(厂商结束信号迟到时) credit { "amount": "0.00", "source": "round_finish", "round": {"id":"rnd_W","sequence":2,"finished":true} }
给记账代码的结论

八个场景,你的 handler 还是那三段直线代码:debit 扣、credit 入、settle 一扣一入。source 原样写进账本即可——它服务的是报表和运营(哪些钱算流水、哪些算营销成本、哪些是更正),不服务记账分支。

5 个回调 · 端点参考 + Playground

下面每个端点都有可交互 Playground,共享同一个沙箱钱包(就是这条常驻状态栏)。字段表读法:请求表的「总是有 / 不一定有」说的是我们传不传——「总是有」直接用、「不一定有」判空再用,两种都不构成拒绝理由;响应表的「必填 / 可选」说的才是你要不要返回。改改金额、连点两次同一笔、把余额打成负数——把所有边界都亲手试一遍。

沙箱钱包 · wang-123 · BRL 100.00 已处理 0 笔

POST{base}/wallet/balance 上线必须

查玩家当前余额。游戏启动前我们会先调它做预检(所有厂商统一),游戏内刷新余额也走它。只读、无幂等键、无副作用——五个端点里最简单的一个。

请求字段

字段类型出现说明示例
player_idstring总是有你系统里的玩家 ID——就是你在 sessions/create 时传给我们的那个,原样带回"wang-123"
currencystring总是有查哪个币种的钱包。法币 = ISO 4217 三字母;加密货币 3–4 位"BRL"
game_codestring不一定有有游戏上下文时给(<provider>:<游戏段>,不透明键)。有的厂商查余额不带游戏"pg:fortune-tiger"
session_idstring不一定有我们的会话 ID(ses_)。排障时报给支持用,不需要校验"ses_01H…"
session_payloadstring不一定有你启动游戏时塞的透传串,原样奉还(用于零查表路由回你的内部上下文)"wsess-8f2|ios"

你要返回什么

字段必填说明
status必填"ok";玩家不存在/币种不符可 "declined"
balance必填当前余额,字符串十进制
PLAYGROUND试一下
PLAYROUTER → 你
(点上面的按钮)
你 → PLAYROUTER

POST{base}/wallet/debit 上线必须

从玩家钱包扣款。方向永远是扣、金额永远非负——你不用判断正负号。业务上不能扣(余额不足等)就返回 declined,这是合法答复不是错误,我们收到就终止、绝不重试。

请求字段(完整)

字段类型出现说明示例
transaction_idstring总是有幂等键。全局唯一(txn_ 前缀)。见过 → 答 duplicate:true,别记账"txn_01H…"
player_idstring总是有你系统里的玩家 ID(sessions/create 时你传的,原样带回)"wang-123"
currencystring总是有交易币种(与本次会话/回合的币种一致)。玩家可支持多币种;此币种不在该玩家支持范围内 → declined currency_mismatch"BRL"
amountstring总是有扣多少。字符串十进制、非负"10.00"
sourcestring总是有这笔扣款是什么:bet 游戏下注 · bonus_buy 功能购买 · tip 真人桌小费 · entry_fee 锦标赛报名费 · adjustment 事后更正。没见过的值也照扣(我们上新类型你零改动)"bet"
roundobject不一定有{id, sequence, finished}——回合 ID、回合内序号(1,2,3…可用于断言顺序)、收口标志。小费等无回合事件不带{"id":"rnd_…","sequence":1,"finished":false}
campaign_idstring不一定有这笔钱源于你创建的活动时出现(cmp_),到账即可归因"cmp_01H…"
game_codestring不一定有游戏标识(<provider>:<游戏段>,不透明键,原样入账),记账/风控归因用;有游戏上下文时给"pg:fortune-tiger"
providerstring不一定有厂商代号(厂商族码,来自本面 providers/list 的 code;game_code 的前半段单独再给一份,省你拆字符串)"pg"
provider_productstring不一定有厂商产品维度键回显(多产品线厂商才出现,如 db 的 card)——按产品线记账/报表用,零处理义务"card"
session_idstring不一定有我们的会话 ID(ses_)。排障时报给支持用,零处理义务"ses_01H…"
session_payloadstring不一定有你启动游戏时塞的透传串,原样奉还(零查表路由回你的内部上下文),零处理义务"wsess-8f2|ios"
occurred_atstring总是有事件发生时刻(RFC 3339),直接可入账本"2026-07-03T12:00:00.000Z"

你要返回什么

字段必填说明
status必填"ok" 记账成功 · "declined" 业务拒绝(余额不足等)· "error" 验签失败
balance必填扣款后余额;declined 为 insufficient_funds / player_blocked / bet_limit_exceeded 时也必带
reasondeclined 时必填拒绝原因(见通用约定的 reason 表)
operator_transaction_id可选你侧账目 ID 回执,对账互查用
duplicate重复时幂等命中时为 true(返回当前余额、不记账)
PLAYGROUND试一下 · 试试扣一笔比余额大的
PLAYROUTER → 你
(点投递)
你 → PLAYROUTER

POST{base}/wallet/credit 上线必须

向玩家钱包入账。游戏派彩、免费旋转结算、活动奖金、彩池、零额收口——全走这一个端点,source 告诉你是哪种钱。入账逻辑只写一次,以后我们上任何新资金类型,对你都只是一个新的 source 字符串。

铁律 ① · 认证套件必测

credit 永远不许 declined(唯二例外 player_not_found / currency_mismatch——账本无法定位、拒收合理,PlayRouter 收到即进对账队列 + 双方告警,不重试不丢弃)。玩家赢的钱必须到账——哪怕他此刻不存在余额异常、哪怕入账后你想拒。无法处理(你内部故障)返回 5xx,我们自动重试直到送达。

请求字段(完整)

字段类型出现说明示例
transaction_idstring总是有幂等键。全局唯一(txn_ 前缀)。见过 → 答 duplicate:true,别记账"txn_01H…"
player_idstring总是有你系统里的玩家 ID(sessions/create 时你传的,原样带回)"wang-123"
currencystring总是有交易币种(与本次会话/回合一致)。玩家可支持多币种;不在其支持范围内 → declined currency_mismatch"BRL"
amountstring总是有入多少。字符串十进制、非负(source=round_finish 时为零,如 "0.00")"24.50"
sourcestring总是有game_win 游戏派彩 · freespin_payout 免费旋转结算 · freechip_payout 免费筹码结算 · bonus_buy_payout 功能购买派彩 · promo_win 活动奖金 · jackpot 彩池 · adjustment 事后更正 · round_finish 零额回合收口。未知值照收"game_win"
related_transaction_idstring不一定有这笔派彩对应的原下注(txn_)。出现则保证那笔已成功送达过你——放心用它做注赢配对,永不悬空"txn_01H…"
campaign_idstring不一定有源于你的活动时出现——两个活动并行时,这笔派彩算哪个活动的成本一目了然"cmp_01H…"
roundobject不一定有{id, sequence, finished}。活动奖金等无回合事件不带;round_finish 收口时 finished 恒为 true{"id":"rnd_…","sequence":2,"finished":true}
game_codestring不一定有游戏标识(<provider>:<游戏段>,不透明键,原样入账),有游戏上下文时给"pg:fortune-tiger"
providerstring不一定有厂商代号(厂商族码,来自本面 providers/list 的 code)"pg"
provider_productstring不一定有厂商产品维度键回显(多产品线厂商才出现,如 db 的 card)——按产品线记账/报表用,零处理义务"card"
session_idstring不一定有我们的会话 ID(ses_),排障用,零处理义务。异步派彩(免费旋转结算等)可能不带——会话早结束了"ses_01H…"
session_payloadstring不一定有你的透传串原样奉还,零处理义务"wsess-8f2|ios"
occurred_atstring总是有事件发生时刻(RFC 3339)"2026-07-03T12:00:00.000Z"

你要返回什么

字段必填说明
status必填"ok" 或 "error"(验签失败)。credit 没有 declined(铁律①)——内部故障返回 5xx 让我们重试
balance必填入账后余额(round_finish 时余额不变,照常返回)
operator_transaction_id可选你侧账目 ID 回执
duplicate重复时幂等命中时为 true
source = "round_finish" 是什么

回合结束但没有钱要动时(比如厂商的结束信号迟到),我们发一笔金额为零(如 amount:"0.00")的 credit 来收口。你按普通 credit 处理即可:入账 0(余额不变)、读 round.finished:true 关闭回合。收口保证:每个回合最终恰好一次 finished:true——放心在它上面挂"清理未完成回合表"这类逻辑。唯一例外语义:收口后仍可能到达迟到的 rollback(更正流,不属游戏流)——冲正不改变收口状态,照常处理。

PLAYGROUND试一下 · 切到 round_finish 看零额收口
PLAYROUTER → 你
(点投递)
你 → PLAYROUTER

POST{base}/wallet/settle 上线必须

一次调用完成"扣注 + 派彩"。这是少数厂商的形态:多数厂商把下注和派彩分成两次回调(走上面的 debit + credit),少数厂商把一局输赢合并成单笔结算发来——那部分流量走这里。五个端点认证时都要实现,但你实际收到多少 settle 取决于开通了哪些厂商。你在同一个 DB 事务里:扣 bet、加 win、返回最新余额。扣款部分不足 → 整体 declined,没有"扣了一半"这种状态。

请求字段(完整)

字段类型出现说明示例
transaction_idstring总是有幂等键。整局一笔、一个键"txn_01H…"
player_idstring总是有你系统里的玩家 ID"wang-123"
currencystring总是有交易币种(与本次会话/回合一致)。玩家可支持多币种;不在其支持范围内 → declined currency_mismatch"BRL"
betstring总是有本局下注额(要扣的)"10.00"
winstring总是有本局派彩额(要加的)。输了的回合照常发:win:"0.00",此时这笔的意义就是记下注 + 收口"24.50" / "0.00"
sourcestring总是有恒为 "game"(合笔结算只服务游戏回合)"game"
roundobject总是有{id, sequence, finished}。合笔结算通常即回合终局,finished 常为 true{"id":"rnd_…","sequence":1,"finished":true}
game_codestring不一定有游戏标识(<provider>:<游戏段>,不透明键,原样入账)"pg:fortune-tiger"
providerstring不一定有厂商代号(厂商族码,来自本面 providers/list 的 code)"pg"
provider_productstring不一定有厂商产品维度键回显(多产品线厂商才出现,如 db 的 card)——按产品线记账/报表用,零处理义务"card"
session_idstring不一定有我们的会话 ID(ses_),零处理义务"ses_01H…"
session_payloadstring不一定有你的透传串原样奉还,零处理义务"wsess-8f2|ios"
occurred_atstring总是有事件发生时刻(RFC 3339)"2026-07-03T12:00:00.000Z"

你要返回什么

字段必填说明
status必填"ok" · "declined"(扣款部分不足 → 整体拒绝,没有"扣一半")· "error"
balance必填结算后余额(= 原余额 − bet + win);insufficient_funds 拒绝时也必带
reasondeclined 时必填拒绝原因
operator_transaction_id可选你侧账目 ID 回执
duplicate重复时幂等命中时为 true
PLAYGROUND试一下 · 试试 win 填 "0.00"(输局)
PLAYROUTER → 你
(点投递)
你 → PLAYROUTER

POST{base}/wallet/rollback 上线必须

撤销一笔早前交易的余额影响:原来是扣的就退回去,原来是入的就收回来。厂商超时取消、真人桌判定作废时会发它。

铁律 ① 的另一半 · 认证套件必测

rollback 不许 declined,余额可以为负。典型场景:玩家赢 100 → 提现走了 → 该局被判作废 → 冲正要收回 100,余额只剩 5 → 记成 -95.00,返回 ok。负数不是系统错误,是在诚实记录玩家欠的钱(下次入金抵扣,你自行风控追偿)。若你的钱包表不支持负数:扣到 0 + 差额挂应收账,同样合规。

两个让你安心的保证

① original_transaction_id 必然是你成功处理过的交易——不存在"冲正一笔没见过的交易"这种事(厂商的乱序冲正我们吸收,见上文动图)。② rollback 之后,那笔原交易绝不会再投递给你。

请求字段(完整)

字段类型出现说明示例
transaction_idstring总是有幂等键——注意这是本次冲正自己的交易 ID(冲正也是一笔交易),别和下一行搞混"txn_01J…"
original_transaction_idstring总是有要撤销哪笔。查你的账本,按那笔的方向反向操作(原为扣则退、原为入则收回)。保证是你成功处理过的交易"txn_01H…"
player_idstring总是有你系统里的玩家 ID"wang-123"
currencystring总是有交易币种"BRL"
roundobject不一定有{id, sequence, finished}——回合可以以冲正收口(finished:true 随冲正携带)。小费冲正等无回合事件不带{"id":"rnd_…","sequence":3,"finished":true}
game_codestring不一定有游戏标识(<provider>:<游戏段>,不透明键)"pg:fortune-tiger"
providerstring不一定有厂商代号(厂商族码,来自本面 providers/list 的 code)"pg"
provider_productstring不一定有厂商产品维度键回显(多产品线厂商才出现,如 db 的 card)——按产品线记账/报表用,零处理义务"card"
session_idstring不一定有我们的会话 ID,零处理义务(冲正常发生在会话结束后,可能不带)"ses_01H…"
session_payloadstring不一定有你的透传串原样奉还,零处理义务"wsess-8f2|ios"
occurred_atstring总是有冲正事件时刻(RFC 3339)"2026-07-03T12:05:00.000Z"

你要返回什么

字段必填说明
status必填"ok" 或 "error"(验签失败)。rollback 没有 declined(铁律①)——余额不够收回也要执行
balance必填冲正后余额,可以为负(如 "-95.00"——钱包回调里唯一允许负号的字段)
operator_transaction_id可选你侧账目 ID 回执
duplicate重复时幂等命中时为 true
PLAYGROUND试一下 · 先在上面入账再提走,然后冲正它——看负余额
PLAYROUTER → 你
(选一笔交易)
你 → PLAYROUTER

错误与重试(我们侧的行为,写给你参考)

你返回我们做什么
200 + ok / declined终态。declined 绝不重试
200 + error(invalid_signature)配置故障桶:告警 + 冻结投递(修复确认后恢复),不重试轰炸
任何 4xx(429 除外——429 按 5xx 进重试时间表)同上(按类别解读,401 写成 403 也不出事故)
3xx(跳转)同上:我们不跟随跳转,按配置故障处理(告警 + 冻结投递,delivery_status 为 frozen);把注册地址改成最终地址、确认后恢复
5xx / 超时(10s)重试时间表:5s → 1m → 5m → 1h → 6h → 12h → 24h(共 8 次)。同回合后续事件排队等待、不跳队。全部失败 → 对账队列 + 双方告警 + delivery_status:"failed" 可查

附录 · 速查

写代码时贴在手边的三张表:ID 前缀、完整错误码、方向 B 的 declined reason。逐字段权威以规范 md / yaml 为准。

ID 前缀约定

前缀含义
txn_资金交易(方向 B 回调的幂等键,也是 transactions/get 的查询键)
rnd_游戏回合
rec_游戏记录(records/list 去重主键、records/get 查询键)
ses_游戏会话(sessions/create 返回)
cmp_活动(campaigns/create 返回,cancel 的唯一引用键)
req_请求追踪(错误体 / 响应头 X-Request-Id)

完整错误码目录 · 方向 A(30 个)

完整错误码目录(方向 A · 30 个)
HTTPcode触发场景
400malformed_requestbody 非合法 JSON
401invalid_signatureHMAC 对不上
401invalid_timestampt 出 ±300s 窗(重放或时钟漂移)
401unknown_keyKey-Id 不存在 / 已摘除
403account_suspended账号被停用
403feature_not_enabled功能未开通(如活动模块未启用)
403provider_not_enabled本面当前不提供该厂商(或产品线)——未接入本面,或已被平台停用;以本面 providers/list 为准
403product_not_enabled产品线未开通——账号未开通 Seamless 面时,/v1/seamless/… 前缀下的全部端点(含 sessions / games / providers / currencies)均返本码;已开通的 transfer 面前缀不受影响(契约 v2:面由路径前缀承载)
403ip_not_allowed来源 IP 不在你为该环境配置的白名单内(可选功能;名单为空不校验;无法判定来源 IP 时同码)——把服务器全部出口 IP(双栈含 v4 与 v6)加入 console「API settings」名单,约 30 秒生效
404not_found路径不存在
404game_not_foundgame_code 不存在
404campaign_not_found引用的活动不存在
404transaction_not_found引用的交易不存在
404record_not_foundrecords/get 查无此记录(含不属于你的记录)
404session_not_found引用的会话不存在
409idempotency_key_reused同 key 不同 body(绝不执行,修 bug)
409request_in_progress同 key 同 body、首请求在途(瞬态)——按 Retry-After 秒数用同 key 重试即命中重放
413payload_too_large请求 body 超过大小上限——报文瘦身后重试
422validation_failed字段缺失 / 类型错 / 超限(fields 逐字段明细)
422currency_not_supported平台 / 游戏 / 厂商三级币种不支持,一码讲完。实例:saba 实例只登记 USD,real 模式 saba:lobby + CNY → currency_not_supported(建户 / 打厂商之前拒);demo 不校验,展示币种由厂商侧静默降级。平台侧暂时不可用时 real 启动也返此码,fields.currency = temporarily unavailable(固定值,可按它程序判断)——此时不要换币种,稍后重试同一请求即可
422currency_mismatch该玩家在这个厂商(多产品线厂商按产品线)已使用其他币种——同一厂商内一个 player_id 只能使用一个币种。message 点名该币种("Player currency at this provider is CNY."),按它开局,或按币种拆 player_id。仅 sessions/create(real)返回
422game_not_available游戏存在但当前不可用(地域 / 下架 / 未到 released_at),或厂商拒绝该玩家进入(如玩家账号被停用)——重试同一请求不会成功。平台侧暂时不可用时 sessions/create 也返此码,fields.game_code = temporarily unavailable(固定值,可按它程序判断)——游戏没有下架,不要从大厅撤下,稍后重试同一请求即可
422demo_not_available游戏不支持试玩
422provider_options_invalid厂商参数不合 campaigns/options 的 schema
422campaign_state_invalid活动状态机不允许(活动已 ended,或厂商没有撤回能力时取消 started 活动)
429rate_limited超配额,按 Retry-After 重试
500internal_error我们的故障(带 request_id 报障)
502provider_error上游厂商故障(仅瞬态——上游的确定性拒绝会映射为对应 4xx 语义码,不会以 502 出现;收到 502 放心重试)
503service_unavailable维护 / 过载
504provider_timeout预留码,目前不会出现;如遇到,按 5xx 处理(变更类同 idempotency_key 重试)

注:405 / 415 由基础设施层返回、可能不带错误体——按 HTTP 码行动即可,未列入本表。
404 也用于存在但不属于你的资源(越权响应与"不存在"不可分辨——防跨商户枚举探测);403 只表示功能 / 厂商未开通类主体级权限,不涉及具体资源。

200 里唯一的业务失败:campaigns/create 名单逐项 results(3 个)

整批 HTTP 2xx,每个玩家的结果在 results[].status(ok | failed)逐项给出——这是逐项答复不是错误,名单内其余成员照常生效。create 首次应答只反映受理校验,厂商侧逐玩家结果在投放后经 campaigns/get 给出。

results[].code语义
player_id_invalidplayer_id 格式不合法(受理校验;预留码——目前名单含不合法 ID 时整单回 422 validation_failed)
campaign_player_limit_reached活动人数已达厂商上限,本项未加入
provider_rejected厂商拒绝向该玩家发放(投放后才知道,经 campaigns/get 查看),本项未发放

方向 B · declined reason 枚举(5 个)

全部通过 200 + {"status":"declined","reason":…} 返回——业务答复不是错误;我们收到即终态,绝不重试。

reason适用回调语义
insufficient_fundsdebit / settle余额不足以扣款(须附 balance)
player_not_found全部玩家在你系统中不存在(balance 可省略)
player_blockeddebit / settle玩家被禁投 / 自我排除 / 游戏对其禁用(须附 balance)
bet_limit_exceededdebit / settle责任博彩限额(玩家自设或监管强制)(须附 balance)
currency_mismatch全部交易币种不在该玩家支持的币种范围内(玩家可多币种;balance 可省略)
铁律回顾
credit / rollback 永不 declined(余额可负也必须收下;唯二例外 player_not_found / currency_mismatch——账本无法定位,我方进对账处理);禁止以会话过期为由拒钱;duplicate: true 是成功答复不是 decline;未知 reason 值我们照收并原样记录(前向兼容)。status 全枚举:ok(含 duplicate 重放)· declined(业务拒绝,终态)· error(非业务处理失败,非终态——进配置故障桶:告警 + 冻结投递)。

上线前,认证套件会测什么

幂等与签名

同 transaction_id 重复投递 → duplicate:true 且余额不变 · 篡改 body 验签拒绝 · 时间戳出窗拒绝 · 双 key 轮换期两把都能验

铁律三条

credit 在余额异常时照常入账 · rollback 打出负余额且返回 ok · 未知 source 值照收 · 会话过期后资金回调照常处理

回合与收口

settle win 为零("0.00")输局照常记账 · 零额 round_finish 正确关闭回合 · sequence 单调性 · finished 恰好一次 · 收口后迟到冲正照常处理

业务拒绝

余额不足 declined + 必附 balance · declined 后我们不重试 · 5xx 后同笔重试只记一次账

方向 A · 核心卷

providers/list · currencies 目录拉取 · games/list 增量与翻页正确 · sessions/create 三连降级(locale / demo 币种 / launch_mode)· 同一厂商换币种开局返回 currency_mismatch 并按 message 处理 · 429 按 Retry-After 退避 · 5xx 后同 idempotency_key 重试

方向 A · 活动卷(启用活动模块才考)

create 幂等重放答复一致 · 名单逐项 results 处理(failed 不连坐)· 422 厂商约束(时长 / provider_options schema)· 取消回执处理(started 取消即回 cancelled、再次取消返回同一回执)· create 响应持久化

FAQ

我的钱包表不允许负数怎么办?

扣到 0,差额记进对该玩家的应收账(debt ledger),下次入金先抵扣——协议要求的是 effect-once 记账并返回 ok,内部怎么表示是你的自由。参考实现里带这个模式。

会话过期了,资金回调要拒吗?

绝对不要。合法性只由签名决定。会话真伪我们在厂商边界已经验过了;异步派彩(免费旋转结算等)天然发生在会话死后,按会话拒钱会把玩家赢的钱卡在协议外。

收到不认识的 source 值?

照常入账/扣款,把值原样记进账本即可。这是前向兼容机制:我们上新资金类型时你零代码改动。

为什么响应恒 200,不用 HTTP 语义?

HTTP 码在这套协议里只回答"要不要重试":2xx=终态、5xx=重试、4xx=配置故障(429 除外——429 按 5xx 进重试时间表)。业务结果(ok/declined)是应用层语义,放 body。这样你写错分支也不会触发意外重试。

duplicate:true 需要返回当初的原响应吗?

不需要。返回当前余额 + duplicate:true 即可,你只需存"处理过的 ID",不用持久化响应体。

时钟不准会怎样?

签名头的 t 有 ±300 秒容忍窗。服务器 NTP 正常就没问题;若持续验签失败,先查时钟漂移。

round.sequence 我必须处理吗?

不必须。它是给你的可选防御工具:断言"同回合序号必须递增",若告警说明出了怪事。忽略它,账照样是对的。

注单(records)和交易流水(transactions)有什么区别,对账用哪个?

资金对账用 transactions/list——它是逐笔资金流水,和你收到的钱包回调一一对应。records/list 是按局汇总的游戏记录,给玩家游戏记录、客服查询、按局统计用;它不含活动 / 奖励类入账,也只计已入账的流水,所以不能当资金对账依据。两者用 round_id + player_id 关联。

campaigns/create 的响应丢了,活动找不回来了?

7 天内用同一个 idempotency_key 重发同一请求——拿回的是当初的原响应(带 Idempotency-Replayed: true),campaign_id 就在里面。超过 7 天用 campaigns/list 按名称/时间窗定位活动、再用 campaigns/get 取回全量(含逐玩家 results)。「持久化 create 响应」仍是接入要求,认证套件会测——查询面是兜底,不是替代。

为什么没有 players/add 这类端点?

最小面设计:名单随 create 内嵌(≤1000/次,一次调用发千人),中途追加 = 另建活动。活动查询面(list / get)是这一原则下的实证演进案例——v1 初版刻意不设,实需出现后按纯加法回补(2026-07-09),未发生破坏性变更。

launch_mode:"html" 什么时候用?

你的市场存在域名封锁风险时。默认 url 形态下游戏域名是第三方的,封锁单点不在你手里;html 直出让游戏从你自己的域名伺服(厂商支持时),封锁面收敛为你自己的域名。厂商不支持时自动降级返 url,不报错。游戏能力看 games/list 的 launch_modes。

方向 A 验签失败是 401,方向 B 为什么让我回 200?

角色不同。方向 A 你是调用方,我们用真 HTTP 码告诉你接下来该干什么(401 = 修配置)。方向 B 你是被调方,恒 200 是钱包回调的行业惯例——而且我们按类别解读你的答复:就算你回了 4xx(429 除外——429 按 5xx 进重试时间表)也只会进配置故障桶(告警 + 冻结投递),不会被重试轰炸。

下载与资源

⬇ 方向 A · OpenAPI 3.1(playrouter-seamless-wallet-api-v1.openapi.yaml) ⬇ 方向 B · OpenAPI 3.1(playrouter-seamless-wallet-callbacks-v1.openapi.yaml) ⬇ 本页 Markdown 版 · for LLM(playrouter-seamless-docs.llm.md)

两个 OpenAPI 文件可直接导入 Postman / Insomnia 生成请求集合;方向 A 可生成客户端 SDK 桩,方向 B 可生成服务端桩代码。Markdown 版与本页内容一致,专供投喂 AI 编码助手。