Transfer Wallet 运营商对接文档

PlayRouter Transfer Wallet(转账钱包)· 11 端点 · 零回调 · 2026-07-06 · Seamless Wallet 产品见 对应文档站 ↗

四句话读懂本产品
① 调 7 个端点、实现 0 个回调就能上线(游戏期间我们不会调用你的任何接口)。② 玩家玩哪家厂商,钱就划到哪家——PlayRouter 只转发划拨指令,不持有玩家资金。③ 输赢经 records/list 拉取——这是你知道玩家输赢的唯一渠道。④ 拿不到响应就同 idempotency_key 重发,恒安全(在同一区内)——没有任何"重发前先查单"的仪式。

接入之旅:全貌一张图

整套对接只有一个方向:全部端点都是你调用归属服 {eu|sea|latam}.api.playrouter.io——划拨、启动、目录、对账。零回调:游戏期间我们不会调用你的任何接口,你不需要暴露任何服务。端点按「洋葱」分层:接通必须层就可以上线——7 个端点 + 0 个回调;查单两件推荐接(异常处理更从容),terminate 按需。下面每个端点的标题上都标了必须 / 可选。

唯一方向:你 ──调用──▶ {eu|sea|latam}.api.playrouter.io (11 个端点:划拨 / 启动 / 目录 / 对账) 没有方向 B —— 游戏期间 PlayRouter 不会调用你(对比 Seamless 的 5 个回调)
会话辅助(可选) 查单(可选 · 推荐——pending 轮询与人工排障) 必须 · 上线门槛 = 7 + 0 sessions/create · games/list · providers/list · currencies transfers/create · wallet/balance records/list + 0 个回调 —— 没有要实现的服务端 transfers/get · records/get · wallet/balances(厂商内余额归集) sessions/terminate(best-effort)
动图 · 接入分层洋葱:从内到外按需剥开,外圈永远不构成上线门槛
层端点什么时候需要
必须 · 上线门槛sessions/create · games/list · providers/list · currencies
transfers/create · wallet/balance · records/list
第一天。认证套件只考这 7 个(+ 0 个回调)
查单transfers/get · records/get · wallet/balancespending 划拨轮询终态、客服按单号排障、厂商内多产品/场馆余额归集时(推荐,可后补)
会话辅助sessions/terminate风控/自我排除要把人请出游戏时(best-effort)

钱包模式与拓扑

转账钱包模式下,玩家在你系统里的余额和在厂商侧游戏钱包里的余额各自独立。玩家玩游戏前你先"上分"(把钱划进厂商侧游戏钱包),玩完"下分"取回。PlayRouter 是中间的统一转发层:一套 API、一套签名、一套幂等,替你对接所有转账形态的厂商。

你(运营商) 玩家主钱包 上分后那部分钱就不在这了 PlayRouter 纯转发 · 不持资金 PP 游戏钱包 JDB 游戏钱包 SABA 游戏钱包 示例 · 游戏期间,钱停在这里(逐厂商一个) 上分 transfers/create(deposit) 下分 transfers/create(withdraw) records/list 输赢明细(唯一渠道) 划入 / 划回 provider 必填
图 1 · 纯转发拓扑:上分把钱划进指定厂商的游戏钱包、下分划回,PlayRouter 不是托管方——划拨、查余额都要指定 provider;输赢只能事后拉 records/list

三条你要提前知道的规则

规则含义
逐厂商钱包玩家在每家厂商各有一个游戏钱包。跨厂商换游戏 = 先从 A 家 withdraw、再向 B 家 deposit(两笔独立划拨)。
单币种钱包每个 player_id 首次划拨/启动时确定币种,此后不可改。多币种玩家请拆多个 player_id(如 wang-123-brl / wang-123-usd)。
目录边界games/list 只含转账形态厂商。仅有 seamless 形态的厂商在本产品不可见——没有任何一方持有它们要调用的钱包。
与 Seamless Wallet 产品的关系
本文档自包含,你不需要看 seamless 产品的任何文档。目录/会话/币种 5 个端点(sessions/create·terminate、games/list、providers/list、currencies)与 seamless 产品字段逐字相同,只是路径前缀不同(本面 /v1/transfer/…,seamless 面 /v1/seamless/…)——将来若升级 seamless,这 5 个端点切换只改前缀,密钥也是同一把(密钥账号级通用,2026-07-20 起):一把 pk_/sk_ 对两条产品线同时有效,升级 seamless 无需换钥。你能调用哪些端点取决于账号的产品开通状态,与密钥无关——体现在路径前缀上:未开通的面返回 403 product_not_enabled。

五分钟快速开始

拿沙箱密钥

控制台注册 → 创建密钥 → 得 pk_test_… / sk_test_…(账号级,两条产品线通用)。沙箱与生产完全隔离。

调 ping 级验证:providers/list + currencies

两个零参数端点(body 传 {}),验证签名代码是否正确,同时拿到厂商目录与币种精度表。

同步游戏目录

games/list 首次用响应头 X-Full-Dump-Url 的每日快照,日常用 updated_after 增量。

上分 → 启动 → 下分

transfers/create(deposit) 划 100 进厂商钱包 → sessions/create 拿启动 URL → 玩家玩完 wallet/balance 看 transferable → transfers/create(withdraw) 取回。

接对账流

定时拉 records/list(这是你知道输赢的唯一渠道),按 record_id 去重入库。到这里,7 个必须端点全部接完,可申请认证。

第一个请求(复制即跑)

// Node 18+ · 对 providers/list 发一个已签名请求 import crypto from "node:crypto"; const KEY_ID = "pk_test_xxx", SECRET = "sk_test_xxx"; const body = "{}"; // raw body,序列化后不得再改 const t = Math.floor(Date.now() / 1000); const v1 = crypto.createHmac("sha256", SECRET) .update(`${t}.${body}`).digest("hex"); const res = await fetch("https://api.sandbox.playrouter.io/v1/transfer/providers/list", { method: "POST", headers: { "Content-Type": "application/json; charset=utf-8", "X-Playrouter-Key-Id": KEY_ID, "X-Playrouter-Signature": `t=${t},v1=${v1}` }, body }); console.log(await res.json()); // → { data: [{ code:"pg", name:"PG Soft", ... }] }

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

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

transfer = 转账凭证(划拨单)

每次上分/下分是一张划拨单,transfer_id(trf_)是它的全局唯一编号。三态终局:completed / failed(带 reason)/ pending(轮询 transfers/get 等终态)。重发靠 idempotency_key(推荐 tf-{内部订单ID})——拿不到响应同 key 重发恒安全(在同一区内)。

record = 输赢流水(注单)

玩家在厂商侧玩的每一局,事后以一条记录出现在 records/list 里,record_id(rec_)是它的编号,也是你入库的去重键——转账产品没有 transaction 回调,rec_ 就是你这侧的"流水号"。这是你知道输赢的唯一渠道,按分钟级拉取。

逐厂商钱包 + 全盲期

玩家在每家厂商各有一个游戏钱包(划拨和查余额都要指定 provider)。钱划过去之后、拉到记录之前,你是全盲的——游戏期间零回调,余额问 wallet/balance、输赢等 records/list。这是转账形态的本质代价,换来的是零服务端成本。

附赠:transferable = 可取余额预览

wallet/balance 响应里的可划回金额(局内锁定部分不含)。支持的厂商给这个字段,下分前先看它,一次取准;不支持的厂商按 balance 试、按 reason 重试(见「上分 / 下分流程」的取回全部循环)。

全景时序:一次上分到下分的一生

四个角色:玩家在你的站点玩、游戏由厂商提供、PlayRouter 居中转发、你只发起调用。下图一次讲完上分、全盲游玩、下分与对账——注意中段:那里没有任何指向你的箭头,这就是零回调。

玩家 游戏厂商 PlayRouter 你(运营商) ① 上分 + 启动 在你的站点点「上分 100 并进入游戏」 先扣主钱包 → transfers/create(deposit) 划入厂商侧游戏钱包 200 · status "completed" · trf_01H… sessions/create(拿游戏启动 URL) 游戏 URL 经你返回,玩家进入厂商游戏 ② 游玩(全盲期 —— 没有任何指向你的箭头) spin · spin · spin … 输赢直扣 / 直入厂商侧游戏钱包 你这侧:静默。想看余额可随时 wallet/balance ③ 下分 + 对账(还是你发起) wallet/balance(先问能取多少) transferable "124.50" transfers/create(withdraw "124.50") 200 · "completed" —— 此刻才给玩家主钱包入账 records/list(定时拉取) 输赢明细(rec_ 去重入库)—— 你知道输赢的唯一渠道 顺序铁律:上分先扣自己、后划厂商;下分先划回来、确认成功再入账(详见「上分 / 下分流程」)
动图 · 全景:① 上分+启动 → ② 全盲游玩 → ③ 下分+对账。流动实线 = 请求(箭头指向接收方),细虚线 = 响应

签名(必读 · 你唯一的安全动作)

本产品只有一个方向要签:你给每个请求签名、我们验(失败 = 401,语义正确且唯一)。零回调意味着没有验签义务、没有入站白名单、不需要维护 token——把请求签对,安全面就完整了。算法与 PlayRouter 全线产品逐字相同:HMAC-SHA256 对「时间戳 + 原始请求体」签名,装在两个固定请求头里。

X-Playrouter-Key-Id: pk_live_01H8XGJWBW ← 用哪把钥匙(支持两把并行,轮换用) X-Playrouter-Signature: t=1719792000,v1=5257a869… ← t 是 Unix 秒,v1 是签名 # 签名三步: # 1. body 序列化成最终字符串——此后不得再格式化,签名对象是原始字节 # 2. 拼接 signed_payload = t + "." + raw_body # 3. v1 = HMAC-SHA256(secret, signed_payload) 的 hex 小写;|服务器时间 − t| > 300 秒会被拒(防重放)

给你的请求签名(三种语言)

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)。
签错会发生什么
答复有且只有 401,错误体的 code 告诉你错在哪一步:invalid_signature(v1 不匹配,九成是 body 被二次格式化)· invalid_timestamp(t 出 ±300 秒窗,查服务器时钟)· unknown_key(Key-Id 打错或环境不对)。这是配置故障:修好再发,不要重试轰炸;报障带上响应头 X-Request-Id。
防重放与换钥
签名带 timestamp 防重放;Key-Id 支持双钥轮换——每个环境最多 2 把 active key 并行,换钥零停机。

幂等与重试

全产品唯一要背的重试规则:拿不到响应(超时/断连/5xx),同一个 idempotency_key 原样重发,恒安全(在同一区内)。命中重放会返回原响应 + 响应头 Idempotency-Replayed: true;同 key 不同 body 会被 409 拒绝(防复制粘贴事故);键保留 7 天。

推荐派生方式

  1. 内部划拨订单先落库(write-ahead),拿到订单号
  2. 幂等键 = tf-{内部订单ID}(确定性派生,天然防重)
  3. 严禁每次重试重新生成随机键——那等于没有幂等

你不需要做的事

  1. 不需要背"哪些错误态可以重发"的清单
  2. 不需要重发前先调查单接口"确认状态"
  3. 不需要在单号里内嵌时间戳、凑格式、赶 5 分钟窗口

上分 / 下分流程

铁律一句话:上分先扣自己、后划厂商(失败退回);下分先划回来、确认成功再入账。顺序错了就是双花或丢分。

上分(deposit)

  1. 创建内部订单(状态"新建")
  2. 先扣你侧玩家主钱包——扣不动就停
  3. 调 transfers/create(type:"deposit")
  4. completed → 订单完结;failed → 退回主钱包扣款;pending → 进异常队列,transfers/get 轮询终态

下分(withdraw)

  1. 创建内部订单;先查 wallet/balance 拿 transferable(有此字段的厂商)
  2. 调 transfers/create(type:"withdraw"),金额 ≤ transferable
  3. completed → 此刻才给玩家主钱包入账;failed → 按 reason 分支(枚举见「错误与 reason」);pending → 轮询,终态前不入账

「取回全部」参考流程(我们有意不做 withdraw_all)

为什么没有一键取光
"取回全部"只有在钱包静止时才有确定含义,而没有任何厂商提供跨局静止保证(玩家关闭游戏窗口 ≠ 本局已结束)。假装能原子取光是我们不愿意卖的假能力。下面的循环就是诚实答案——你写一次,对所有厂商通用:
# 伪码:取回玩家在某厂商的全部余额 def recall_all(player, provider, recall_id): # recall_id:本次取回先落库的内部单号,每次取回新生成 for attempt in range(3): bal = wallet_balance(player, provider) amount = bal.transferable or bal.balance # 有预览用预览,没有就按余额试 if Decimal(amount) == 0: return DONE # 按数值比较:零写作 "0.00",别按字符串比 r = transfers_create(player, provider, "withdraw", amount, key=f"recall-{recall_id}-{attempt}") if r.status == "completed": return DONE # 大多数一轮就结束(此刻再给玩家主钱包入账) if r.status == "pending": return WAIT(r.transfer_id) # 处理中(极少):先不入账,交给异常订单队列轮询到终态 if r.reason in ("game_limit", "player_in_game"): sleep(60); continue # 局内锁:等局结束再来 if r.reason == "insufficient_funds": continue # 余额刚变了:重查再取 if r.reason in ("provider_unavailable", "not_executed"): sleep(30); continue # 资金未动:下一轮换新 key 再取 return ESCALATE(r) # 其余 reason 进人工 return ESCALATE(r) # 三轮用尽仍没取完:进人工

recall_id 必须每次取回新生成并先落库(同上文「推荐派生方式」):键保留 7 天,recall-{player}-{attempt} 这类下次还会再算出来的键,会让下一次取回撞上上一次的键——金额相同就直接重放上次的结果(上次是 completed,你就会再入账一次),金额不同则被 409 拒绝。

WAIT(处理中)怎么收尾
这笔下分还没有结论——先不要给玩家主钱包入账,也不必转人工。把它放进你的异常订单队列,用 transfers/get 按 ≥30 秒间隔轮询(1 小时后可降频);终态一定会给出(超过 1 小时我们会人工核对后落终态,无需你处理)。终态出来以后:
  • completed → 此刻给玩家主钱包入账;需要的话再调一次 recall_all(新的 recall_id)取剩余。
  • failed 且 reason 是 not_executed 或 provider_unavailable → 资金没动、不入账;需要的话用新的 recall_id 再调一次 recall_all。
  • failed 且 reason 是 insufficient_funds、game_limit 或 player_in_game → 不入账;稍后用新的 recall_id 再调一次 recall_all(局内锁要等局结束)。
  • 其余 reason → 记录并转人工。

循环里出现的各 reason 含义与处置,见「错误与 reason」的枚举表。

方向 A · 你调我们(11 端点)

Base URL:https://{eu|sea|latam}.api.playrouter.io(沙箱 https://api.sandbox.playrouter.io,单区)。改用别的区不需要新账号或新密钥;数据按区存放、区与区不互通——旧区的划拨与注单记录留在旧区,照常可查。玩家已在某个区上分进游戏的余额,请在同一个区查询和取回,取回后再到新区上分。拿不到响应要重试时,也请留在同一个区:同一个 idempotency_key 只在同一区内去重,换到别的区重发会被当成一笔新请求再执行一次。路径第二段 = 钱包模式面:本产品全部端点都在 /v1/transfer/<域>/<动作> 下(如 /v1/transfer/transfers/create、/v1/transfer/currencies);seamless 产品的同名端点在 /v1/seamless/… 下。账号开通了哪一面才能调哪个前缀——未开通的面一律返回 403 product_not_enabled,与端点是否"公共"无关。两面共有的 5 个目录/会话端点字段逐字相同,切换只改前缀。

PLAYGROUND 沙箱钱包 PG Soft:0.00 PP:0.00 wang-123 · BRL · 下面所有 Playground 共享此状态

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

启动游戏,真钱/试玩合一。转账模式注意:启动前先上分——余额 0 也能进游戏,但玩家会看到 0 余额。game_code 的厂商段(冒号前)决定厂商,具体钱包由 provider_product 决定(多产品线厂商)。

请求字段

字段类型必填说明示例
player_idstringreal 必填≤ 64 字符;你侧稳定玩家标识;demo 可省。不收 PII。单币种钱包:一个 player_id 一个币种"wang-123"
game_codestring必填<provider>:<游戏段>,与 games/list 同一把键。不透明键,原样回传:多产品线厂商(目前只有 db,DB Gaming)的游戏段形如 card-101(产品线-厂商游戏码),单产品线厂商的游戏段就是厂商游戏码原文;不要自行拆游戏段,产品维度用 provider_product 字段"pg:fortune-tiger" / "db:card-101"
currencystring必填real = 钱包币种(精确匹配永不降级;厂商 / 实例不收该币种 → 422 currency_not_supported,在建户 / 打厂商之前拒,如 saba 只登记 USD 时的 CNY;该币种平台侧暂时不可用时同码、fields.currency = temporarily unavailable,不要换币种、稍后重试);demo = 期望展示币种(不支持则静默降级厂商默认,不校验)"BRL"
modestring必填real | demo"real"
ipstringreal 必填玩家 IP,IPv4/IPv6"203.0.113.7"
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 直出(抗封锁);厂商不支持时声明降级"url"
session_payloadstring可选≤1024 字符透传串,出现在 records 关联与排障日志"wsess-8f2|ios"

响应字段

字段类型出现说明示例
session_idstring总是有会话锚点(ses_),排障/终止用"ses_01H8XGJWBW"
urlstring总是有启动 URL 恒在(HTML 型厂商由我们包装成 URL)。拿到后立即打开、只用一次:链接有效期由游戏厂商决定,短的只有约 1 分钟,部分厂商的链接只能打开一次;玩家每次进游戏(含刷新页面、重新打开)都重新调用 sessions/create 取新链接,不要缓存复用"https://launch..."
htmlstring不一定有仅 launch_mode:"html" 且厂商支持时出现;从你的自有域名伺服"<!DOCTYPE html>..."
expires_atstring总是有启动 URL 最晚失效时刻(RFC 3339);实际有效期由游戏厂商决定、可能早得多(见 url 行),不要据此缓存复用 url"2026-07-06T12:30:00.000Z"
PLAYGROUNDsessions/create · 启动游戏(试 launch_mode 降级)
REQUEST
// 点「启动」
RESPONSE
//
启动失败怎么处理
  • 422 game_not_available:游戏当前不对这个玩家开放(下架、地域限制、未到 released_at,或厂商拒绝该玩家进入)——重试同一请求不会成功,提示玩家换一款游戏。例外:fields.game_code = temporarily unavailable 表示平台侧暂时不可用、游戏本身没有下架——稍后重试同一请求即可,你不需要做任何配置,也不要把游戏从大厅撤下。
  • 422 demo_not_available:这款游戏不支持试玩。
  • 502 provider_error:临时故障,稍后用同一请求重试。

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

游戏目录:对象数组 + cursor 分页 + updated_after 增量。首次同步用响应头 X-Full-Dump-Url 的每日全量快照;链接下载失败时,改用不带 updated_after 的分页全量。目录只含转账形态厂商。

请求字段

字段类型必填说明示例
cursorstring|null可选黑盒游标:上页 next_cursor 原样传回;首页省略null
limitinteger可选1–2000500
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 会刷新)。

响应字段(game 对象)

字段类型出现说明
game_codestring总是有全 API 同一把游戏键(<provider>:<游戏段>,不透明、原样回传;多产品线厂商如 db:card-101)
namestring总是有官方展示名
providerstring总是有厂商代号(厂商族码,来自本面 providers/list 的 code)——转账模式下同时是钱包定位键
provider_productstring不一定有该游戏所属的厂商产品维度键——transfers/create 与 wallet/balance 同名参数的值域来源(多产品线厂商才出现;db 的产品线:live 真人 / hash 哈希 / card DB 棋牌 / boyacard 博雅棋牌 / fishing DB 捕鱼 / boyafishing 博雅捕鱼 / slots 电子 / scratch 刮刮乐 / lottery 彩票 / esports 电竞 / sports 体育;其它多产品厂商沿用既有子钱包语义)。不要自行拆 game_code 的游戏段来反推它
categorystring总是有品类(live 是一等品类)。取值 = 游戏类型字典 17 码:sports, live, slots, lottery, card, poker, fishing, esports, virtual, originals, racing, cockfighting, hash, arcade, table, prediction, scratch;字典只增不改,未知值按「其它」兜底
images.squarestring不一定有方图 URL——只在厂商提供时出现;缺省 = 厂商无图源,不编图
images.landscapestring不一定有横图 URL——同上
localesarray总是有支持语言(ISO 639-1 两字母,真数组)。厂商实例级:平台运营按厂商实例登记,同一实例下所有游戏同值,不保证每款游戏各异;未登记缺省 ["en"]。不在表内的 locale 启动时静默降级 en
currenciesarray总是有支持币种(ISO 4217 / 加密货币码,真数组)。厂商实例级:平台运营按厂商实例登记,同一实例下所有游戏同值,不保证每款游戏各异;未登记缺省 ["CNY"]。以此判断 sessions/create · transfers/create 该传什么币种;实例真正接受的币种以 422 currency_not_supported 为准(fields.currency = temporarily unavailable 的除外:那是平台侧暂时不可用,不代表实例不收)(目录里出现该实例不接受的币种属运营登记错误,我方告警修正;目录只列部分接受币种是正常经营选择)
devicesarray总是有desktop / mobile
launch_modesarray总是有启动形态能力:含 "html" 即支持 HTML 直出(抗封锁)
featuresobject不一定有demo / freespins / bonus_buy / jackpot 能力布尔——只在厂商目录给出能力信息时出现;缺省 = 未知,不编假 false(2026-09-19 起由「总是有」改为「不一定有」)
bet_levelsobject不一定有币种 → 合法投注面值(金额字符串数组),只在厂商提供时出现
rtpstring不一定有RTP 百分数字符串,只在厂商提供时出现;已接 7 家厂商的目录接口均不含 RTP,缺省不编数
volatilitystring不一定有波动率评级,厂商原值透传(不强行归一枚举)
released_atstring不一定有发布时刻——此前启动返回 game_not_available
forbid_bonus_playboolean不一定有true = 禁止用奖金资金游玩(自建奖金系统的合规输入)
statusstring总是有active | recalled——目录只含运营已上架、且所属厂商(多产品线厂商按产品线)当前由平台开放的游戏;active = 厂商在售,recalled = 厂商已下架(下架也是一次变更,增量同步天然捕获)。运营关闭上架的游戏、平台停用的厂商(产品线)的游戏会直接从目录消失,这类消失不经 updated_after 增量下发,由我方商务另行通知;也可自行核对:厂商(产品线)停用时它会从 providers/list 中消失,单款游戏关闭上架可不带 updated_after 全量拉取比对。平台恢复该厂商后,其游戏重新出现、updated_at 随之刷新
updated_atstring总是有最后变更时刻——增量同步(updated_after)的锚点
PLAYGROUNDgames/list · 拉目录(试过滤)
REQUEST
// 点「拉取」
RESPONSE
//

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

厂商目录,每个厂商族一行(pg、db、pp、saba……)。请求空对象 {}。转账模式下 code 还是划拨/余额的 provider 参数取值。低频,日缓存。

响应字段(provider 对象)

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

示例

// POST /v1/transfer/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" } ] } ] }
PLAYGROUNDproviders/list · 厂商目录(零参数)
REQUEST
// 点「发送」
RESPONSE
//

POST/v1/transfer/currencies 上线必须

币种目录 + 每币种 decimals——全 API 金额字符串的精度权威表。请求空对象 {}。没有 K 货币单位:IDR/VND/COP 型大面值币种直接按真实面值传,不需要 ÷1000 换算。

金额请按数值比较,不要按字符串比较
我们返回的金额小数位按 decimals 写满,零也一样(2 位小数币种写 "0.00",如从未上分玩家的 balance)。判断「是否为零」「是否相等」时先解析成十进制数(Java BigDecimal.compareTo、Python Decimal、JS 用十进制库)再比较;amount == "0" 这类字符串比较会出错。

响应字段(currency 对象)

字段类型出现说明示例
codestring总是有法币 ISO 4217 三字母;加密货币 3–4 位字母数字(如 USDT)"BRL"
decimalsinteger总是有金额字符串小数位数2
typestring总是有fiat | crypto"fiat"
PLAYGROUNDcurrencies · 币种精度表(注意 IDR 的 decimals=0——没有 K 单位)
REQUEST
// 点「发送」
RESPONSE
//

POST/v1/transfer/transfers/create 上线必须

划拨:上分/下分合一,方向在 type,金额恒正。不需要预注册玩家——deposit 自动建户(上游要显式建户的厂商由我们内部代办)。业务结局不是 HTTP 错误:余额不足、局内锁都是 200 + status:"failed" + reason。从未上分(在该厂商尚无钱包)的玩家发起 withdraw 也是业务结局:200 + status:"failed" + reason:"insufficient_funds"(余额 0 不够下),不是 404——404 player_not_found 只在本平台从未见过该 player_id 时出现,且只发生在 wallet/balance · wallet/balances(见错误码目录)。

请求字段

字段类型必填说明示例
player_idstring必填≤ 64 字符;首笔划拨确定钱包币种"wang-123"
providerstring必填钱去哪家 / 从哪家取回(厂商族码,来自本面 providers/list 的 code)"pg"
provider_productstring多产品线厂商必填钱进哪个产品线钱包。多产品线厂商必填(如 db),值取 games/list 回显的 provider_product 或 providers/list.products[].code;单产品线厂商省略。缺省 → 422 validation_failed(fields.provider_product = required);厂商已开通但值不是它的产品线(如 db + "bogus")→ 422 validation_failed(fields.provider_product = invalid);厂商族本身不在本面提供(未接入或已被平台停用)→ 403 provider_not_enabled。三者都不会挂单"card"
typestring必填deposit(上分)| withdraw(下分)"deposit"
amountstring必填恒正金额字符串,精度按 currencies。个别游戏厂商的接口能接受的小数位少于币种目录(例如最多 3 位),超出时 transfers/create 当场 422 validation_failed,fields.amount 写明上限(exceeds N decimals accepted by provider),不建单、资金未动;改成该位数以内重新发起即可"100.00"
currencystring必填一致性防呆:与该玩家已锁定的钱包币种不符 → 422 currency_mismatch;厂商 / 实例本身不收该币种 → 422 currency_not_supported(两码分工见错误码目录)"BRL"
idempotency_keystring必填推荐 tf-{内部订单ID};拿不到响应同 key 重发恒安全(在同一区内)"tf-ord-889231"

响应字段(Transfer 对象;transfers/get 同形)

字段类型出现说明示例
transfer_idstring总是有划拨 ID(trf_),请持久化——transfers/get 的主键"trf_01H8XGJWBW"
player_idstring总是有玩家标识回带"wang-123"
providerstring总是有资金所在厂商(厂商族码,来自本面 providers/list 的 code)"pg"
provider_productstring不一定有生效的厂商产品维度键(多产品线厂商时出现)——钱实际进/出的产品线钱包,自记对账以此为准"card"
typestring总是有deposit | withdraw"deposit"
amountstring总是有划拨金额(恒正)"100.00"
currencystring总是有钱包币种"BRL"
statusstring总是有completed | failed 终态不再变;pending 极少出现 → transfers/get 轮询(≥30s 间隔)"completed"
reasonstring不一定有仅 failed 时出现;枚举见「错误与 reason」一节"insufficient_funds"
balancestring不一定有划拨后(或紧随其后查到的)游戏钱包余额;completed 时通常带上,个别情况下可能缺省(不影响划拨结论)——需要当前余额请调 wallet/balance;failed 时给得出则附上"100.00"
transferablestring不一定有可下分预览,厂商有此口径才出现"100.00"
created_atstring总是有受理时刻(RFC 3339)"2026-07-06T12:00:00.000Z"
completed_atstring不一定有终态时刻——completed / failed 时出现,pending 时缺省"2026-07-06T12:00:00.180Z"
PLAYGROUNDtransfers/create · 划拨(沙箱钱包共享状态)
REQUEST
// 点「发送」
RESPONSE
//

POST/v1/transfer/wallet/balance 上线必须

查玩家在指定厂商侧游戏钱包的余额。transferable(可下分余额)按厂商能力出现:有此口径的厂商透传,给不出的厂商缺省该字段——我们不编数。注意:玩家关闭游戏窗口 ≠ 本局结束,局内锁定余额要等结算。

从未上分的玩家不是错误:玩家在该厂商尚无钱包(首笔 deposit / 首次真钱启动才自动建户)→ 200 + balance:"0.00"(transferable 缺省)——0 是事实不是编数,别据此判定玩家不存在。404 player_not_found 只在本平台从未见过该 player_id 时出现:你从未用它发起过一次被受理的 transfers/create(返回 200,不论厂商、不论 status 是 completed 还是 failed),也从未用它成功建过真钱会话(sessions/create 200);一旦有过任一次,此后查任何厂商都不会再 404,最多是 0.00。被 4xx 拒绝的请求(422 validation_failed / currency_not_supported、403)在挂单之前就被拦下,不登记玩家。

请求字段

字段类型必填说明示例
player_idstring必填≤ 64 字符"wang-123"
providerstring必填逐厂商钱包:查哪家指定哪家(厂商族码,来自本面 providers/list 的 code)"pg"
provider_productstring多产品线厂商必填查哪个产品线钱包的余额。多产品线厂商必填(如 db),值取 games/list 回显的 provider_product 或 providers/list.products[].code;单产品线厂商省略。缺省 → 422 validation_failed(fields.provider_product = required);厂商已开通但值不是它的产品线 → 422 validation_failed(fields.provider_product = invalid);厂商族本身不在本面提供(未接入或已被平台停用)→ 403 provider_not_enabled。语义同 transfers/create 同名字段"card"

响应字段

字段类型出现说明示例
player_idstring总是有玩家标识回带"wang-123"
providerstring总是有钱包所在厂商(厂商族码,来自本面 providers/list 的 code)"pg"
provider_productstring不一定有生效的厂商产品维度键(多产品线厂商时出现)——本快照属于哪个产品线钱包"card"
currencystring总是有钱包币种(单币种钱包)"BRL"
balancestring总是有当前余额"95.00"
transferablestring不一定有可下分预览;缺省 = 该厂商无此口径,按 balance 试错 + reason 重试"80.00"
as_ofstring总是有快照时刻"2026-07-06T12:00:00.000Z"
PLAYGROUNDwallet/balance · 查余额(换厂商看 transferable 降级)
REQUEST
// 点「查询」
RESPONSE
//

POST/v1/transfer/wallet/balances 可选

一次查玩家在同一厂商全部产品/场馆游戏钱包的余额。缺省扇出范围 = 该玩家在该厂商有过划拨记录的产品/场馆 ∪ 平台缺省产品;也可用 products 显式指定(值域 = games/list 回显的 provider_product,或 providers/list.products[].code)。按产品线拆分的厂商(如 db)用 products 指定要归集的产品线。单产品/无产品维度厂商退化为单行——等价 wallet/balance。

边界:厂商内归集。不解决跨厂商总资产——要看玩家全部厂商的资金,逐厂商各调一次;跨厂商合计(币种折算、时点语义)由你自己定义。

partial 反模式警示(务必阅读)
余额是逐产品顺序查询的,每行有自己独立的 as_of(我们不假装这是同一时点的原子快照)。任何一行失败:该行 available=false(保留在列表里,不剔除)、顶层 partial=true、total_balance 缺省。切勿把可得行的 balance 求和当总资产——缺的那行可能正是钱所在的场馆。正确姿势:partial=true 时按行重试(用 products 指定失败行重查),全部可得后再用 total_balance。
另:本端点较重(要逐个产品查询),勿高频轮询——同一玩家建议间隔 ≥30 秒;高频场景用 wallet/balance 点查目标产品。

请求字段

字段类型必填说明示例
player_idstring必填≤ 64 字符"wang-123"
providerstring必填归集范围恒为单厂商(厂商族码,来自本面 providers/list 的 code)"db"
productsstring[]可选显式扇出集(≤32 值,值域 = providers/list.products[].code);任何非法值(含厂商已开通但不是它的产品线,如 db + "bogus")→ 422 validation_failed(fields.products),不部分执行;厂商族本身不在本面提供(未接入或已被平台停用)→ 403 provider_not_enabled。缺省 = 划拨触达集 ∪ 平台缺省产品["card", "live"]

响应字段

字段类型出现说明示例
player_idstring总是有玩家标识回带"wang-123"
providerstring总是有钱包所在厂商(厂商族码,来自本面 providers/list 的 code)"db"
currencystring总是有钱包币种(单币种钱包)"BRL"
walletsarray总是有逐产品/场馆一行;失败行保留(available=false),不剔除—
wallets[].provider_productstring不一定有该行的厂商产品维度键(无维度厂商的单行不出)"card"
wallets[].balancestring不一定有该产品钱包余额;仅 available=true 时出现"95.00"
wallets[].transferablestring不一定有可下分预览(厂商有此口径且行可得时)"80.00"
wallets[].as_ofstring总是有本行取数时刻(逐行独立,非全响应统一时点)"2026-07-06T12:00:00.000Z"
wallets[].availableboolean总是有false = 本行暂不可得(厂商不可达/超时/预算耗尽),balance 缺省true
total_balancestring不一定有全部行可得时的合计。partial=true 时恒缺省——勿以可得行之和充总资产"97.00"
partialboolean总是有true = 至少一行不可得;按行重试收敛false

POST/v1/transfer/records/list 上线必须

转账模式下游戏期间你是全盲的——这个端点是你知道玩家输赢的唯一渠道,是主账本不是复核视图。定时批量拉取,按 record_id 去重入库。数据从各厂商拉取归一:新鲜度分钟级;可查窗口统一最近 1 个月(from 越界请求 422)。个别记录可能因平台侧处理延后出现,处理完成后自动补入、无需商户操作;补入的记录 recorded_at 为补入时刻,出现在该时刻所在的时间窗——按时间窗增量拉取不会漏,按 record_id 去重即可。顺序稳定但不保证按时间排。

时间窗按记录进入本平台的时刻切(2026-10-12)
from / to 比较的是每条记录的 recorded_at(进入 PlayRouter 的时刻),不是 started_at。改判追加的冲销 / 更正记录、体育结算记录、因厂商延迟晚到的记录,都出现在它们进入时刻所在的时间窗——按时间窗向前增量拉取不会漏行,无需回拉旧窗口。建议时间窗终点比当前时刻至少早 1 分钟(或相邻时间窗重叠 1 分钟,按 record_id 去重)。
对账铁律
计算反水或杀率务必用 valid_bet(有效投注,已排除对押/刷水),不要用 bet。记录粒度因游戏类型而异:多数游戏一局一条;捕鱼/老虎机可能整段游玩合并一条;体育投注单与结算单各一条。
改判/重结算(追加式,2026-07-19)
厂商终局后改判时原记录永不修改——改判发生的时间窗里会追加两条新记录:①冲销记录(source=adjustment、金额为被冲销记录的负值、ref_record_id 指向它);②更正记录(改判后的全量值、ref_record_id 同指原记录)。你按时间窗增量拉取天然拿到修正,不用回拉旧窗口;对 status=settled 的记录求和(Σwin−Σbet、Σvalid_bet)净额恒正确——负值就是为了让最朴素的求和也不出错。示例(原局赢 10 改判为输):原 {bet 10.00, win 20.00} → 追加 {bet -10.00, win -20.00, source adjustment, ref_record_id} 与 {bet 10.00, win 0.00, ref_record_id}。负值只出现在 source=adjustment 的冲销记录上,其余一切金额恒非负。
状态与求和口径(2026-07-20 语义收窄)
金额求和只算 status=settled 的记录。unsettled 是体育型投注单的首见快照(bet=原注额、win=0、无 settled_at),首见后不再更新——结算时另追加一条 settled 全量结算记录(同 provider_record_id/round_id,「投注单与结算单各一条」说的就是这两条);canceled / refunded 记录金额保留原注额、只是事实快照——照单全收后务必按 status 过滤,别把作废单的 bet 加进流水。冲销记录的 status 与被冲销记录一致,同进同出。部分退款不用 refunded:以 settled + 调整后金额的更正记录下发(refunded 仅限全额退)。

请求字段

字段类型必填说明示例
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–20002000
player_idstring可选≤ 64 字符;按玩家过滤(客诉排查免全量下载)"wang-123"
providerstring可选按厂商过滤(厂商族码,来自本面 providers/list 的 code)"pg"

响应字段(Record 对象;records/get 同形)

字段类型出现说明示例
record_idstring总是有唯一 ID(rec_)——去重主键、records/get 查询键"rec_01H8XGJWBW"
player_idstring总是有玩家标识"wang-123"
providerstring总是有厂商代号(厂商族码,来自本面 providers/list 的 code)"pg"
provider_productstring不一定有厂商产品维度键回显(多产品线厂商才出现,如 db 的 card)——按产品线对账用"card"
game_codestring总是有游戏键,与目录一致(不透明键,原样回传,不要自行拆游戏段)"pg:fortune-tiger"
categorystring总是有品类(值域同 games/list)。按记录的 game_code 查目录得到、入账时快照(日后类目改映不追溯);目录查不到该码时若该厂商实例只有一个品类(体育 / 真人类大厅厂商)则取它,否则为空串 ""——键恒在、不编"slots"
game_detailstring不一定有可选。记录带有比游戏键更细的玩法信息(体育项目、桌台、彩种等)时给出其可读名称,只供显示;不要用作分组或关联键,分组请用 game_code / category。"Soccer" / "BaccaratClassic"
currencystring总是有币种"BRL"
round_idstring总是有厂商侧回合标识(归一后)。注意:单一钱包面(/v1/seamless/records/*)同名字段是我方回合 ID(rnd_,与钱包回调同值),两面含义不同,同时接两面时请分开处理"rnd_9f2k1"
betstring总是有原始投注额(未排除对押);仅冲销记录(source=adjustment)可为负"10.00"
valid_betstring总是有有效投注额——反水/杀率用它;冲销记录的负值使求和净额天然正确"10.00"
winstring总是有派彩额。玩家输赢 = win − bet(可推导,不设差额字段;对负值冲销行同样成立)"15.50"
sourcestring总是有game | freespin | promo | adjustment(补偿更正——含冲销记录:金额为负、恒带 ref_record_id;可增补)"game"
statusstring总是有unsettled | settled | canceled | refunded"settled"
jackpot.winstring不一定有仅该局含彩池派彩时出现(没有平铺恒 0 列)"3.00"
provider_record_idstring不一定有厂商侧原始单号——客诉排障时报给我们;改判时同一单号对应多条记录"8865512900"
ref_record_idstring不一定有冲销/更正记录指向被冲销原记录的 record_id(2026-07-19 加法新增);原记录永不被修改或删除"rec_01H8XGJWBW"
session_idstring不一定有能归属时给(ses_)"ses_01H8XGJWBW"
started_atstring总是有本局开始时间;合并型记录 = 进房时间(照厂商口径)"2026-07-05T10:11:02.000Z"
settled_atstring不一定有结算时间;unsettled 时缺省"2026-07-05T10:11:05.000Z"
recorded_atstring总是有记录进入 PlayRouter 的时刻(2026-10-12 新增)——时间窗按它切;改判 / 结算 / 厂商延迟晚到的记录晚于 started_at"2026-07-05T10:12:30.000Z"
PLAYGROUNDrecords/list · 拉一页样例记录
REQUEST
// 点「拉取」
RESPONSE
//

POST/v1/transfer/transfers/get 可选 · 推荐

按 transfer_id 或 idempotency_key 查单笔划拨,最近 1 个月在线可查(更早归档调取,与列表查询窗口同口径)。这不是重试的前置仪式——拿不到响应直接同 key 重发 create;本端点服务两个场景:pending 收尾轮询、运维排障反查。查无此单 404 transfer_not_found。

请求字段

字段类型必填说明示例
transfer_idstring二选一trf_ 前缀"trf_01H8XGJWBW"
idempotency_keystring二选一用创建时的幂等键反查"tf-ord-889231"

响应 = { "data": Transfer },与 transfers/create 响应同一对象(不搞两套 schema)。

PLAYGROUNDtransfers/get · 查单笔划拨(先在上面的划拨 Playground 发几笔,再用它的 trf_ id 或幂等键反查)
REQUEST
// 点「查询」
RESPONSE
//

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

客诉单查:按 record_id 查单笔游戏记录,最近 1 个月在线可查(更早归档调取,与列表查询窗口同口径)——不用重拉整段列表定位那一条。响应在 Record 对象之外可选给 detail_url(厂商牌局详情页,给客服/玩家看的证据页);厂商给不出则缺省。查无此单 404 record_not_found。

请求字段

字段类型必填说明示例
record_idstring必填rec_ 前缀(records/list 返回)"rec_01H8XGJWBW"
localestring可选详情页语言;不支持降级 en"pt"

响应字段

字段类型出现说明示例
(Record 全字段)—总是有与 records/list 同一对象—
detail_urlstring不一定有厂商牌局详情页链接;厂商无详情页则缺省(不编造)"https://records...."
PLAYGROUNDrecords/get · 客诉单查(rec_sbx002 演示 detail_url 降级缺省)
REQUEST
// 点「查询」
RESPONSE
//

POST/v1/transfer/sessions/terminate 可选

尽力而为的踢出扇出(player_id 或 session_id 二选一)。能否踢出、踢出后玩家能否重登,效果因厂商而异。RG/自我排除的硬闸门在转账模式 = 拒绝该玩家后续 deposit(划拨在你手里)+ 我们对厂商侧封禁通道的扇出。注意:踢出不使游戏提前结算——局内锁定余额仍要等局结束才能下分。

请求字段

字段类型必填说明示例
player_idstring二选一≤ 64 字符;终止该玩家全部活跃会话"wang-123"
session_idstring二选一指名终止单个会话"ses_01H8XGJWBW"

响应 = { "data": {} }(200 表示扇出已执行,不担保厂商侧踢出结果)。非资金、天然幂等,不带 idempotency_key。

PLAYGROUNDsessions/terminate · 踢出(best-effort)
REQUEST
// 点「终止」
RESPONSE
//

错误与 reason

两层分开记:HTTP 错误(传输/配置问题,body 为固定 ErrorResponse)和 reason(划拨的业务结局,藏在 200 的 status:"failed" 里,枚举见下表)。HTTP 层你只需要 4 个分支:

HTTP动作本产品会出现的 code
2xx用 body(含 status:"failed" 的业务结局)—
429按 Retry-After 重试rate_limited
5xx / 超时同 idempotency_key 重试internal_error / provider_error / service_unavailable(provider_timeout 为预留码,目前不会出现)
其余 4xx终态:记日志等人修配置或请求问题——逐码表见 附录 · 速查

reason 枚举(划拨的业务结局 · 统一所有厂商的失败形态)

只在 200 + status:"failed" 里出现——这不是错误,是合法业务答复;处置分支在「上分 / 下分流程」的取回全部循环里已写全。

reason含义你该做什么
insufficient_funds(下分)游戏钱包余额不足——通常是查询与取款之间玩家有一局结算了重查 balance 再取
game_limit该游戏局内禁划拨(斗地主/抢庄牛牛型对局游戏)提示玩家退出游戏,或等局结束重试
player_in_game玩家正在游戏中,此时不能划拨稍后重试(建议 ≥60s)
player_blocked玩家被厂商或风控封禁终态;人工处理
amount_out_of_range超出厂商单笔/余额上限(目前基本不出现:只在我方人工核对后落终态时可能给出)改金额后用新 idempotency_key 重新发起
provider_unavailable厂商暂不可用(维护/故障)——资金未动稍后用新 idempotency_key 重新发起(同 key 只会重放这次 failed)
provider_rejected厂商确定性拒绝(参数/单号/风控类,重试同样被拒)终态;记录并人工处理,带 X-Request-Id 报障
not_executed这笔划拨确认未被执行——资金未动终态,不会再变;上分退回主钱包扣款、下分不入账;可用新 idempotency_key 重新发起

附录 · 速查

写代码时贴在手边的两张表。逐字段权威以 playrouter-transfer-wallet-v1.openapi.yaml 为准。

ID 前缀约定

前缀含义
trf_划拨(transfers/create 返回,transfers/get 的查询主键——请持久化)
rec_游戏记录(records/list 去重主键、records/get 查询键)
rnd_游戏回合(records 内的厂商回合标识,归一后)
ses_游戏会话(sessions/create 返回)
req_请求追踪(错误体 / 响应头 X-Request-Id)

没有 txn_(零回调,无交易流——输赢在 rec_ 记录里);没有 cmp_(转账产品无活动模块)。

完整错误码目录(27 个)

与 Seamless 产品共用同一错误模型;转账专属 3 码已标注 ★。业务结局(余额不足、局内锁)不在此表——那是 200 + status:"failed" + reason,枚举见「错误与 reason」。

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产品线未开通——账号未开通 Transfer 面时,/v1/transfer/… 前缀下的全部端点(含 sessions / games / providers / currencies)均返本码;已开通的 seamless 面前缀不受影响(契约 v2:面由路径前缀承载)
403ip_not_allowed来源 IP 不在你为该环境配置的白名单内(可选功能;名单为空不校验;无法判定来源 IP 时同码)——把服务器全部出口 IP(双栈含 v4 与 v6)加入 console「API settings」名单,约 30 秒生效
404not_found路径不存在
404game_not_foundgame_code 不存在
404player_not_found ★本平台从未见过该 player_id(你从未用它发起过一次被受理的 transfers/create——返回 200、不论 completed / failed——也从未用它成功建过真钱会话;被 4xx 拒的请求不登记玩家)——只在 wallet/balance · wallet/balances 出现。不是「在该厂商尚无钱包」:玩家已在平台登记、只是从未在该厂商上分 → balance 回 200 + balance:"0.00",withdraw 回 200 + status:"failed" + reason:"insufficient_funds"(用户裁决 2026-09-17)
404transfer_not_found ★transfers/get 查无此单
404record_not_foundrecords/get 查无此记录
404session_not_found引用的会话不存在
409idempotency_key_reused同 key 不同 body——修 bug,绝不执行
413payload_too_large请求 body 超过大小上限——报文瘦身后重试
422validation_failed字段缺失 / 类型错 / 超限(fields 逐字段给出,含时间窗越界)。transfers/create 的 amount 小数位超过该厂商接口能接受的位数(个别厂商少于币种目录,例如最多 3 位)也返本码,fields.amount = exceeds N decimals accepted by provider——不建单、资金未动,改成该位数以内重新发起即可
422currency_not_supported平台 / 游戏 / 厂商不支持该币种。实例:saba 实例只登记 USD,CNY 上分或 real 启动 → currency_not_supported——处置是换成该厂商支持的币种(看 providers/list.currencies),不是换成玩家钱包币种。平台侧暂时不可用时 real 启动也返此码,fields.currency = temporarily unavailable(固定值,可按它程序判断)——此时不要换币种(钱包币种在首次划拨 / 首次真钱启动时锁定:换币种要么撞 currency_mismatch,要么把新玩家锁成错误币种),稍后重试同一请求即可;上下分不受影响。玩家钱包已锁定其它币种时先返 currency_mismatch
422currency_mismatch ★请求币种 ≠ 该玩家已锁定的钱包币种(单币种钱包防呆)。实例:玩家首笔划拨锁定 CNY 钱包后再发 USD 请求 → currency_mismatch,message 点名钱包币种("Wallet currency is CNY."),按它重发
422game_not_available游戏存在但当前不可用(下架 / 地域 / 未到 released_at),或厂商拒绝该玩家进入(如玩家账号被停用)——重试同一请求不会成功。平台侧暂时不可用时 sessions/create 也返此码,fields.game_code = temporarily unavailable(固定值,可按它程序判断)——游戏没有下架,不要从大厅撤下,稍后重试同一请求即可;上下分不受影响
422demo_not_availabledemo 请求但游戏不支持
429rate_limited超配额——按 Retry-After 重试。配额按 API Key 计、以下限保证公布:每 key 每分钟至少 records/list 150 次(本面与单一钱包面的 records/list 两面合计)、transactions/list 60 次(单一钱包端点,跨产品面调用同受此档)、其余端点 1000 次(实际可能更高,下限只放宽不收紧);RateLimit-* 头按端点给出勿跨端点缓存,建议(非强制)按 RateLimit-Remaining 自适应节流;回补历史可联系临时提额
500internal_error我们的故障——同 idempotency_key 重试安全
502provider_error上游厂商故障(仅瞬态——上游的确定性拒绝会映射为对应 4xx 语义码,不会以 502 出现;收到 502 放心重试)
503service_unavailable维护 / 过载
504provider_timeout预留码,目前不会出现;如遇到,同 idempotency_key 重试即可

划拨业务结局 reason 速查(8 个 · 只在 200 + status:"failed" 出现,正文「错误与 reason」为准)

reason含义你该做什么
insufficient_funds(下分)游戏钱包余额不足重查 balance 再取
game_limit该游戏局内禁划拨等局结束重试
player_in_game玩家正在游戏中,此时不能划拨稍后重试(建议 ≥60s)
player_blocked玩家被厂商或风控封禁终态;人工处理
amount_out_of_range超出厂商单笔/余额上限(目前基本不出现)改金额后换新 key 重新发起
provider_unavailable厂商暂不可用(资金未动)稍后换新 key 重新发起
provider_rejected厂商确定性拒绝(重试同样被拒)终态;人工处理
not_executed这笔划拨确认未被执行(资金未动)终态,不会再变;退回 / 不入账,可换 key 重新发起

status 全枚举:completed · pending(≥30s 轮询 transfers/get 至终态)· failed(上表 reason)。

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

FAQ

为什么没有"一键取回全部"(withdraw_all)?

因为它是我们给不出的承诺:"全部"只有在钱包静止时才有确定值,而没有任何厂商保证跨局静止(玩家关窗 ≠ 局结束)。诚实的答案是「取回全部」参考循环——你写一次,全厂商通用,每一步的失败原因都有统一 reason 可分支。

划拨超时没拿到响应,我该重发还是先查单?

直接同 idempotency_key 重发,恒安全(在同一区内)——命中会重放原响应并带 Idempotency-Replayed: true 头。transfers/get 是给 pending 收尾和排障用的,不是重试的前置步骤。

status:"pending" 多久出现一次?要怎么处理?

极少(表示这笔划拨仍在确认中,我方会给出终态)。出现时进你的异常订单队列,用 transfers/get 以 ≥30 秒间隔轮询,终态必然落到 completed 或 failed;终态前不要给玩家主钱包入账。时限承诺:pending 超过 1 小时未收敛即转我方人工介入(我方核对后落终态,无需你处理),终态仍通过 transfers/get 给出;受理后如确认这笔划拨未被执行,会以 failed / not_executed 结束(通常在受理后十余分钟内);资金未动:上分退回主钱包扣款、下分不入账,可换新的 idempotency_key 重新发起;这一结论是终态,不会再变。你侧轮询可在 1 小时后降频,不必更密。另:极少数情况下(厂商暂不可用)划拨会直接答 failed / provider_unavailable,上下分皆然;这笔首发还在进行时你同 key 重发或查单,可能先看到 pending、随后变为 failed / provider_unavailable。这一笔资金未动、之后也不会再被执行,按 failed 处置(上分退回主钱包扣款、下分不入账),稍后用新的 idempotency_key 重新发起即可。

玩家换游戏(跨厂商)时余额怎么办?

逐厂商钱包:先 withdraw 从 A 家取回、确认 completed,再 deposit 到 B 家。同厂商内换游戏不需要划拨。建议在玩家离开某厂商游戏时自动触发「取回全部」流程,避免余额长期停在厂商侧。

为什么 records/list 是必须?我能不能不接?

不能。转账模式下游戏期间我们不回调你——玩家输赢、有效投注、彩池派彩全部只能从 records/list 拉。不接它你就没有账本,反水、风控、财务对账都无从谈起。

transferable 缺省的厂商怎么安全下分?

按 balance 发起 withdraw,失败时看 reason:insufficient_funds 重查再取(余额刚变了)、game_limit / player_in_game 等局结束或稍后重试。「取回全部」参考循环已覆盖这些分支。

大面值币种(IDR/VND/COP)要除以 1000 吗?

不要。没有 K 货币单位——金额是字符串十进制,按 /v1/transfer/currencies 公布的 decimals 直接传真实面值。如果你的系统里有 ÷1000/×1000 的换算层,接入时请去掉,那是对账事故的高发源。

以后想升级 Seamless Wallet(单一钱包)要重接吗?

目录/会话/币种 5 个端点(sessions/create·terminate、games/list、providers/list、currencies)字段逐字相同,只是路径前缀不同(/v1/transfer/… → /v1/seamless/…),切换只改前缀。变化:撤掉 transfers / wallet 两组,改为实现 5 个钱包回调;records 两端点单一钱包面也有(/v1/seamless/records/list · get),字段同名,但 round_id、valid_bet 的含义不同、资金对账改用 transactions/list——见单一钱包文档 records 一节。签名、错误模型、幂等机制不变。

下载与资源

⬇ 转账产品 · OpenAPI 3.1(playrouter-transfer-wallet-v1.openapi.yaml) ⬇ 本页 Markdown 版 · for LLM(playrouter-transfer-docs.llm.md)

OpenAPI 文件可直接导入 Postman / Insomnia 生成请求集合,11 端点自包含(含签名 / 幂等 / 错误说明),可直接生成客户端 SDK 桩。Markdown 版与本页内容一致,专供投喂 AI 编码助手。