最近更新:—
近 7 天订单走势
订单(柱高)
已支付(柱内绿段)
柱顶数字 = 当天订单数;金额按「已支付 / 未支付」分列(不相加);鼠标悬停 / 手指按住柱子看当天明细
口径:已支付 = 已提取到卡密;出卡 = 卡密库在该时段新增的卡数(按代理分域,优先 cards_inventory,回退 cards);
金额单位为元(数据库按分存储,页面上按 amount_fen/100 显示);
已支付金额(amount_paid_fen)与 未支付金额(amount_unpaid_fen)分开展示、绝不相加;
可用池 = 未取用且未过期的支付串;冻结中 = 取用后 300 秒内不再给第二个客户;
已失效 = 状态 dead 或超过有效期。
—
最近 10 条订单
| 时间 | 代理 | 后台单号 | 抖音订单号 | 金额 | 卡号 | 卡密 | 状态 / 复制 |
代理管理
脚本里填「代理显示名」即按名字归属;填「代理账号/密码」按账号归属
开新代理
设备
—
按最后在线倒序。sid_tt 过长会截断(悬停看全文、点 📋 复制);最近错误只显示前 24 字(悬停看全文)。
代理只能看到自己名下的设备;行尾【删除】只删这条设备记录,它名下的订单与卡密一条不动。
| 设备名称 | 设备 ID | sid_tt | 状态 | 接收时间 |
凭证刷新时间 | 订单同步时间 | 最近错误 | 操作 |
| 正在读取… |
可用池
取用后冻结 5 分钟(倒计时到点自动回池,冻结期内不会给第二个客户);自动刷新只重画表格,你正在输入时自动跳过这一轮。
| 时间 | 代理 | 设备 | 金额(元) | 订单号 | 支付串 | 收银台地址 |
状态(冻结倒计时) | 取用次数 | 操作 |
设置
这里的改动全站生效
管理员密码
数据修复
把「已收到但没解析入库」的历史响应再解析一遍(幂等,重复点不会重复入库)。
对接密钥
把这两段发给对接方:私钥给他签请求,公钥给他验我们的回调
● 读取中…
·
签名 —
·
推送 —
·
待推送 — 条
点右上「在线试调」→ 用开放 key 真调一次 open_ping,返回的 JSON 会显示在这里
取用流水
取用 — · 已拉起 — · 已支付 — · 已放回 —
—
| 时间 |
消费方 |
金额 |
状态 |
拉起时间 |
出卡时间 |
链接 |
支付串 |
| 读取中… |
流水是 只读 的,只追加不删除:每次 open_pool_take 记一条,
拉起支付 / 出卡 / 放回都记在 同一条 上(换新链接也不删历史)。
对接文档(可直接给对方看)
对方只需要 6 个接口。默认只展开「接口速查」,其余按需展开;
文档里的接口地址已换成上面「开放接口」里配置的真实地址。
① 接口速查 —— 6 个接口一览(默认展开)
你只需要 6 个接口:拿支付链接、取一条、放回、拉订单状态、查单笔状态、探活。
所有接口都在 https://域名/api.php,用 action=open_xxx 区分。
| action | 作用 | 鉴权 |
| open_ping | 探活(接完先调这个) | key;开了强制签名再加 ts / sign |
| open_pool_list | 可用池列表(只看,不冻结) | key;开了签名加 ts / sign |
| open_pool_take | 取一条(★ 取用即冻结) | key;开了签名加 ts / sign |
| open_pool_release | 放回池子(会换一条新链接) | key;开了签名加 ts / sign |
| open_orders | 增量拉订单状态事件(带上一次的 cursor) | key;开了签名加 ts / sign |
| open_order_get | 单笔实时状态 | key;开了签名加 ts / sign |
完整字段见下面各节。
一分钟看懂
┌─────────────────────── 你的系统 ───────────────────────┐
│ │
① open_pool_list ──┤ 看看池子里现在有哪些可用支付链接(含金额、剩余有效期) │
② open_pool_take ──┤ 取一条(★ 取用即冻结 5 分钟,保证不会再发给别人) │
│ → 拿到 cashier_url / pay_raw,拿去给客户付款 │
③ 客户付款… │ │
④ open_order_get ──┤ 查这笔单现在是什么状态(待支付/已拉起/已支付出卡/已过期) │
⑤ open_orders ──┤ 增量拉状态事件(带上一次的 cursor,只拿新的,不会漏也不会重) │
或 webhook ──┤ ★ 也可以让我们的后台【主动 POST】到你的地址,实时推送 │
⑥ open_pool_release┤ 客户不要了 / 超时了 → 把链接放回池子(会换一条新链接) │
└────────────────────────────────────────────────────────┘
订单状态只有 4 个值:pending(待支付)、launched(已拉起支付)、
paid(已支付 = 已出卡;只给 has_card=1,不含卡密)、expired(已过期)。
接入准备
- 让后台管理员打开:后台 → 设置 → 开放接口 → 打开「开启开放接口」,把 key 发给你。
- 如果开了「强制 HMAC 签名」,再要一个 secret(同一页面里)。
- 把你的接收地址填到「回调地址」(要 webhook 推送的话)。
请求地址:https://域名/api.php
请求方式:POST(application/x-www-form-urlencoded)或 GET 都可以,
参数放 query 或 body 都行。
返回格式:一律 JSON,{"code":200,"ok":true,...}。
② 鉴权与签名(key 必填 · HMAC_SHA256 · ts ±600 秒)
每个请求都要带 key:
https://域名/api.php?action=open_ping&key=<你的key>
如果后台开了「强制 HMAC 签名」,还要再带 ts 和 sign:
ts = 当前秒级时间戳(允许与服务器差 ±600 秒)
sign = HMAC_SHA256(secret, key + "\n" + ts + "\n" + action)
↑ 十六进制小写;action 就是 open_xxx 这个名字
也就是签名原文是这三行拼起来(\n 是换行符,不是字面量):
签名可以放 header 更干净:X-Open-Key: <key>(替代 key 参数)。
签名示例代码
四种语言任选一种,直接复制到你的项目里改三个变量即可。
PHP
$key='你的key'; $secret='你的secret'; $action='open_pool_list';
$ts = time();
$sign = hash_hmac('sha256', $key."\n".$ts."\n".$action, $secret);
$url = "https://域名/api.php?".http_build_query([
'action'=>$action, 'key'=>$key, 'ts'=>$ts, 'sign'=>$sign,
]);
$resp = json_decode(file_get_contents($url), true);
Python
import hmac, hashlib, time, urllib.parse, urllib.request, json
key, secret, action = '你的key', '你的secret', 'open_pool_list'
ts = int(time.time())
sign = hmac.new(secret.encode(), f"{key}\n{ts}\n{action}".encode(), hashlib.sha256).hexdigest()
q = urllib.parse.urlencode({'action': action, 'key': key, 'ts': ts, 'sign': sign})
print(json.loads(urllib.request.urlopen(f"https://域名/api.php?{q}", timeout=20).read()))
Node.js
const crypto = require('crypto');
const key = '你的key', secret = '你的secret', action = 'open_pool_list';
const ts = Math.floor(Date.now() / 1000);
const sign = crypto.createHmac('sha256', secret).update(`${key}\n${ts}\n${action}`).digest('hex');
const url = `https://域名/api.php?${new URLSearchParams({action, key, ts, sign})}`;
const r = await (await fetch(url)).json();
curl
KEY=你的key; SECRET=你的secret; ACT=open_ping; TS=$(date +%s)
SIGN=$(printf '%s\n%s\n%s' "$KEY" "$TS" "$ACT" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')
curl -s "https://域名/api.php?action=$ACT&key=$KEY&ts=$TS&sign=$SIGN"
③ open_ping —— 探活(接完先调这个)
参数
| 参数 | 必填 | 说明 |
| — | — | 除 action / key(+ts/sign) 外无参数 |
返回字段
| 字段 | 类型 | 说明 |
| code | int | 200 = 成功 |
| ok | bool | true = 通 |
| server_time | int | 服务器当前秒级时间戳(对时钟用) |
| pool_available | int | 当前可用池里有多少条可取的链接 |
| sign_required | int | 1 = 后台开了强制 HMAC 签名 |
| push_enabled | int | 1 = 后台开了 webhook 推送 |
示例
{
"code": 200, "ok": true,
"server_time": 1792230123,
"pool_available": 12,
"sign_required": 1,
"push_enabled": 1
}
pool_available = 当前可用池里有多少条可取的链接。
④ open_pool_list —— 可用池列表
参数
| 参数 | 必填 | 默认 | 说明 |
| page | 否 | 1 | 页码 |
| size | 否 | 20 | 每页条数,最大 200 |
| amount_min_fen | 否 | — | 金额下限(单位:分) |
| amount_max_fen | 否 | — | 金额上限(分) |
返回字段(响应级)
| 字段 | 类型 | 说明 |
| code / ok | int / bool | 200 / true = 成功 |
| total | int | 符合条件的总条数 |
| page / size | int | 当前页码 / 每页条数(服务端已纠正上限 200) |
| count | int | 本次返回的条数 |
| items | array | 条目数组(字段见下表) |
| server_time | int | 服务器当前秒级时间戳 |
返回字段(items[] 每一条)
| 字段 | 类型 | 说明 |
| id | int | 链接唯一 id(幂等去重用它) |
| order_no | string | 订单号 |
| amount | float | 金额(元) |
| amount_str | string | 金额("1.00") |
| pay_raw | string | 支付串原文(见下) |
| cashier_url | string | 收银台链接(见下) |
| expire_in | int | 这条支付串本身还剩多少秒彻底失效 |
示例
{
"code": 200, "ok": true, "total": 12, "page": 1, "size": 20, "count": 12,
"items": [
{
"id": 135,
"biz_no": "202610170000123",
"order_no": "20261017123456U85370194",
"amount_fen": 100,
"amount": 1.0,
"amount_str": "1.00",
"subject": "商品名",
"device_name": "设备A",
"agent_id": 7,
"pay_raw": "alipay_sdk=alipay-sdk-php-20200401&app_id=...&sign=...",
"cashier_url": "https://域名/pay/cashier.php?t=6ad23607...",
"expire_in": 1520,
"created_at": 1792229000
}
],
"server_time": 1792230123
}
关键字段(★ 必读)
| 字段 | 说明 |
| pay_raw | 支付串原文。能直接拿去唤醒支付(支付宝客户端深链 / SDK 串) |
| cashier_url | 收银台链接。直接给客户打开(内含本条的专属令牌,打开才开始 5 分钟计时) |
| expire_in | 这条支付串本身还剩多少秒彻底失效(过了就别再用了) |
| amount_fen / amount_str | 金额(分 / "1.00" 元) |
⚠️ open_pool_list 只是看,不会冻结。真正要发给客户请用 open_pool_take。
⑤ open_pool_take —— 取一条(★ 核心)
参数
| 参数 | 必填 | 说明 |
| id | 否 | 指定某一条;不填则自动取最早的一条 |
| consumer | 否 | 你的系统名(≤40 字,如 客户A)。会记进后台的「取用流水」,方便对账 |
返回字段
| 字段 | 类型 | 说明 |
| code / ok | int / bool | 200 / true = 取到了 |
| taken | int | 1 = 成功取到;0 + ok:false = 没取到(看 msg) |
| frozen_sec | int | 冻结秒数(默认 300 = 5 分钟) |
| expire_at | int | 冻结到期时间(秒级时间戳) |
| item | object | 取到的那一条,结构同 open_pool_list 的条目 |
| server_time | int | 服务器当前秒级时间戳 |
示例
{
"code": 200, "ok": true, "taken": 1,
"frozen_sec": 300,
"expire_at": 1792230423,
"item": { "...": "同 open_pool_list 的条目结构" },
"server_time": 1792230123
}
语义(★ 很重要,务必读懂)
- 取用即冻结,计时从这一刻开始:接口返回成功的那一瞬间,这条链接就已经被冻结
frozen_sec 秒
(默认 300 = 5 分钟)并开始倒计时 —— 不是等客户打开才开始算。
期间其他人(包括你自己)再取都拿不到它,绝不会同一条发给两个客户。
⇒ 所以 take 之后请尽快把链接给客户,越晚给,客户可用的时间越少。
- 每一次取用都会记一条流水(谁取的
consumer、什么时候、金额、链接、用的哪把 key、冻结到几时),
拉起支付 / 出卡也会记进同一条流水;这条流水只追加不删除 —— 就算链接被回收换新,历史记录也一直在
(后台「API 对接」页可查)。
- 冻结到期还没付款 → 后台会自动把它放回池子(并换一条新链接)。
- 同一条被并发取用时,只有一个成功:另一个会收到
{"ok":false,"msg":"手慢了,这条刚被别的请求取走,请重试"} —— 重试即可。
- 没有可用的了:
{"ok":false,"taken":0,"msg":"当前没有可用链接"}。
⑥ open_pool_release —— 放回池子
参数
返回字段
| 字段 | 类型 | 说明 |
| code / ok | int / bool | 200 / true = 放回成功 |
| msg | string | 结果说明(失败时看这里) |
示例
{"code":200,"ok":true,"msg":"已放回可用池(下次取用会是新链接)"}
- 已经出卡(已支付)的不能放回:
{"ok":false,"msg":"这条已经出卡(已支付),不能放回"}。
⑦ open_orders —— 增量拉订单状态事件(★ 实时性靠它)
参数
| 参数 | 必填 | 默认 | 说明 |
| since | 否 | 0 | 上一次返回的 cursor;第一次传 0 |
| limit | 否 | 50 | 每次最多取多少条(≤200) |
| order_no | 否 | — | 只看某一笔单 |
返回字段
| 字段 | 类型 | 说明 |
| cursor | int | 本次拉到的位置,存起来下次当 since 传 |
| count | int | 本次返回的事件数 |
| has_more | int | 1 = 还有,立刻用新 cursor 再拉一次 |
| events | array | 事件数组(字段见下表) |
| server_time | int | 服务器当前秒级时间戳 |
返回字段(events[] 每一条)
| 字段 | 类型 | 说明 |
| event_id | int | 事件唯一 id(单调递增,去重用它) |
| kind | string | 事件类型,见下表 |
| order_no | string | 订单号 |
| status | string | 当前状态 |
| at | int | 事件发生时间(秒) |
| data | object | 订单完整数据(字段见 ⑧ 末尾「附:字段对照」) |
示例
{
"code": 200, "ok": true,
"cursor": 57,
"count": 2,
"has_more": 0,
"events": [
{
"event_id": 56,
"kind": "order_paid",
"order_no": "20261017123456U85370194",
"status": "paid",
"at": 1792230500,
"data": {
"order_no": "20261017123456U85370194",
"platform_order_no": "20261017123456U85370194",
"biz_no": "202610170000123",
"status": "paid",
"status_text": "已支付(已出卡)",
"amount_fen": 100, "amount": 1.0, "amount_str": "1.00",
"subject": "商品名", "device_name": "设备A", "agent_id": 7, "agent_name": "代理01",
"has_card": 1,
"created_at": 1792229000
}
}
],
"server_time": 1792230523
}
事件类型 kind
| kind | 什么时候产生 | 含义 |
| order_new | 后台收到一笔新单 | 有新订单了(状态 pending) |
| order_paid | 取到卡密入库(客户已付款) | 已支付,data.has_card = 1(不含卡密) |
| order_updated | 订单信息被更新(非首次出卡) | 状态以 data.status 为准 |
用法:把 cursor 存起来,下次带 since=<cursor>;不会漏、不会重复。
has_more=1 说明还有,立刻再拉一次(用新 cursor)。
🔒 接口永远不返回卡号 / 卡密:支付成功只多一个 has_card=1 标记,
未支付时是空字符串 —— 未付款的单不会泄露卡密。
⑧ open_order_get —— 单笔实时状态
参数
| 参数 | 必填 | 说明 |
| order_no | 是 | 订单号(平台单号或你的单号都行,能匹配上即可) |
返回字段
| 字段 | 类型 | 说明 |
| code / ok | int / bool | 200 / true = 查到了;ok:false 时看 msg |
| order | object | 订单数据(结构同 open_orders 的 data) |
| server_time | int | 服务器当前秒级时间戳 |
示例
{"code":200,"ok":true,"order":{ "...": "同上面 data 的结构" },"server_time":1792230523}
没有这个单:{"code":200,"ok":false,"msg":"没有这个订单"}。
附:字段对照(data / order 里的字段)
| 字段 | 类型 | 说明 |
| order_no | string | 订单号(后台归一后的) |
| status | string | pending / launched / paid / expired |
| status_text | string | 中文状态 |
| amount | float | 金额(元) |
| amount_str | string | 金额("1.00") |
| agent_id / agent_name | int / string | 归属代理 |
| has_card | int | 1 = 已出卡;卡号卡密不返回 |
⑨ 事件与 webhook(让后台主动推给你,最实时)
在「设置 → 开放接口」里填「回调地址」,并打开「开启 webhook 实时推送」。
之后每次状态变化(新单 / 已支付出卡 / 订单更新)我们都会 POST 一次。
请求头
POST <你填的回调地址>
Content-Type: application/json; charset=utf-8
User-Agent: VTL-OpenAPI/1.0
X-Open-Event: 56
X-Open-Timestamp: 1792230500
X-Open-Signature: <十六进制小写>
Body
{
"event_id": 56,
"kind": "order_paid",
"order_no": "20261017123456U85370194",
"status": "paid",
"sent_at": 1792230500,
"data": { "...": "和 open_orders 里 data 完全一致" }
}
验签(务必校验,否则谁都能伪造推送给你)
sign = HMAC_SHA256(secret, X-Open-Timestamp + "." + 原始 body)
注意是原始 body 字符串参与签名,不要先解析再序列化(字段顺序会变,签名就对不上了)。
PHP 验签
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_OPEN_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_OPEN_SIGNATURE'] ?? '';
$want = hash_hmac('sha256', $ts.'.'.$raw, '你的secret');
if (!hash_equals($want, $sig)) { http_response_code(401); exit('bad sign'); }
$j = json_decode($raw, true);
// $j['kind']='order_paid' 时 $j['data']['card_no'] 就是卡号
echo 'ok';
Python(Flask)验签
import hmac, hashlib
@app.post('/vtl/callback')
def cb():
raw = request.get_data() # 原始 body,别用 request.get_json()
ts = request.headers.get('X-Open-Timestamp', '')
sig = request.headers.get('X-Open-Signature', '')
want = hmac.new(SECRET.encode(), f'{ts}.'.encode() + raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(want, sig):
return 'bad sign', 401
j = json.loads(raw)
return 'ok', 200
你要做的 / 失败重试
你要做的:返回 HTTP 2xx 就算成功。
失败重试:后台会自动重试,最多 5 次(每轮由定时任务补推);也可以让管理员在后台点「立刻补推」。
⑩ 错误码
| code | 含义 | 怎么办 |
| 200 | 成功(业务失败看 ok 字段和 msg) | ok=false 时读 msg |
| 401 | key 不对 / 缺 ts / sign 不对 / 时间偏差 >600 秒 | 检查 key 和签名算法;对一下服务器时间 |
| 403 | 开放接口没开启 | 让管理员在后台打开 |
| 400 | 缺必填参数 | 看 msg |
| 404 | action 写错了 | 对照本文档 |
| 500 | 服务端异常 | 把 msg 发给我们 |
⑪ 推荐对接流程
每 10~30 秒: open_pool_list(看有没有货) ← 可选,也可以直接 take
客户来了: open_pool_take → 拿到 cashier_url(或 pay_raw)
↓ 把 cashier_url 发给客户(或你自己打开 pay_raw 去支付)
客户付款后: 什么都不用做 —— 我们会在取到卡的第一时间
· 推 webhook 给你(kind=order_paid,带 has_card=1;不含卡密)
· 或者你轮询 open_orders?since=<cursor>
超时不要了: open_pool_release(把链接放回池子)
对不上时: open_order_get?order_no=...(查单笔实时状态)
幂等:open_pool_take 的 item.id 就是链接唯一 id,按它去重即可。
并发:同一 id 只会成功一次;失败重试即可(不要死循环)。
⑫ 常见问题(FAQ)
| 问题 | 原因 / 解决 |
open_pool_take 返回"当前没有可用链接" | 池子是空的(都在冻结中或已出卡)→ 稍后重试,或让后台补链接 |
拿到的 cashier_url 客户打开显示"链接已过期" | 客户打开得太晚(支付串 expire_in 已到),或这条已被回收 → 重新 take 一条 |
| 客户打开收银台后没付,5 分钟后这条链接失效了 | 正常:取用即冻结 5 分钟(从 take 那一刻算),没付就回收换新链接(这是防"一条链接同时给多人"的机制) |
take 之后客户过几分钟才打开,倒计时只剩一点点 | 正常:5 分钟窗口从 take 开始算。取到就尽快发给客户 |
| 后台怎么看誰取走了、有没有拉起、有没有出卡 | 后台 →「API 对接」页 →「取用流水」(只读,只追加不删;可按状态/订单号筛) |
| webhook 收不到 | 看后台「设置 → 开放接口」里的「待推送 N 条」;确认地址可从公网访问、返回 2xx;用「立刻补推」试 |
| 同一个事件收到两次 | 用 event_id 去重(我们保证 event_id 单调递增) |
| 签名总是不对 | ①\n 是真换行 ②签名原文用原始 body ③secret 别搞成 key ④时钟偏差 |
本页内容与项目根目录「API对接文档.md」一致,可直接把该文件发给对接方。