本章是通过 Payment Gateway 商户 API 接受充值的权威参考。内容涵盖两种充值模式(Direct API 与收银台会话)、它们的状态机、确切的请求与响应结构,以及在生产环境会出现的陷阱。
概念与术语
| 术语 | 含义 |
|---|---|
| 充值订单 | 一笔 DepositOrder 列,代表你的某位终端用户的一次付款意图。以 order_no(由网关指派)与 merchant_order_no(由你指派)识别。 |
| 渠道 | 上游付款供应商(例如第三方 PSP 或虚拟账号聚合商)。渠道订单会被路由到 PaymentChannel,并提供 payment_url 或渠道回传的 payment_info。 |
| 账户 | 由运营方持有的实体银行账户。账户订单会要求付款人把资金汇到该账户;对账是通过 expected_remark 或金额浮动完成。 |
payment_url | 部分渠道回传的转址 URL。把付款人导到这里完成付款。只有当订单被路由到使用托管付款页的渠道时才会出现。 |
payment_info | 渠道回传的 JSON 对象,包含银行信息、QR Code 数据,或任何给付款人看的指示。对于不使用转址 URL 的渠道订单会出现。 |
| 收银台会话 | 一笔 CashierSession 列,代表一个托管、给付款人看的页面。不带 amount 时,付款人在此页面上选金额,真正的 DepositOrder 会在付款人提交之后创建。若请求带 amount,DepositOrder 会与 session 同步创建,托管页面直接显示付款信息。 |
| 付款人 | 汇出资金的终端用户,与商户不同。付款人会与 payment_url、payment_info 或托管的收银台页面交互。付款人不会对商户 API 做任何身份验证。 |
| MATCHED 状态 | 充值已被认定为已付款(一笔银行交易匹配上订单,或是渠道回报成功),但结算与账本提交尚未完成。并非终态。 |
| Mock channel | 沙盒中内置的假渠道。用它可以端到端走完状态机,不需要动到任何真实 PSP。参见 Sandbox。 |
两种充值模式
| 方面 | Direct API | 收银台会话 |
|---|---|---|
| 端点 | POST /api/v1/merchant/orders/deposit | POST /api/v1/merchant/orders/deposit/cashier |
| 由谁决定金额 | 商户,于请求体中指定。 | 默认由付款人在托管页面选择;若 cashier session 请求带 amount,则由商户预先指定。 |
| 由谁决定渠道 | 网关的路由引擎,根据币种与金额决定。 | 付款人提交后由网关的路由引擎决定;若 cashier session 请求带 amount,则在创建 session 时立即决定。 |
| 初始响应 | DepositPaymentInfo,含 payment_url 或银行信息。 | CashierCreateResponse,含 cashier_url 与 cashier_token。 |
DepositOrder 列何时创建 | POST /deposit 当下立即创建。 | 默认在付款人提交时创建。若 cashier 请求带 amount,会与 session 同步创建,托管页面直接显示付款信息。 |
| 商户要展示给付款人看的内容 | 你自己的 UI;由你渲染银行信息或转址到 payment_url。 | 转址到 cashier_url。所有东西都由网关托管。 |
结果订单的 order_type | DIRECT | CASHIER |
模式选择指引
如果你已经在自家 UI 收集了金额、币种与付款人意图,使用 Direct API。你的应用程序仍然掌控结账的外观与流程;转址或银行信息页面由你自己处理。
如果你希望 Payment Gateway 托管给付款人看的页面,使用收银台会话。付款人需要自行选金额时,不带 amount;你已在自家流程收集金额、只需要托管页面显示付款信息时,带入 amount。你只要把单一的 cashier_url 交给付款人,就可以放手不管,直到收到回调。
两种模式最后会收敛到相同的 DepositOrder 实体与相同的回调握手。查询、回调验证与状态语义完全一致。
API 接口
| Method | Path | 用途 |
|---|---|---|
POST | /api/v1/merchant/orders/deposit | 创建 Direct API 充值订单。 |
POST | /api/v1/merchant/orders/deposit/cashier | 创建收银台会话。 |
GET | /api/v1/merchant/query/deposit/{order_no} | 以网关的 order_no 查询充值订单。 |
GET | /api/v1/merchant/query/deposit?merchant_order_no=... | 以你的 merchant_order_no 查询充值订单。 |
所有端点都需要标准的 MD5 签名标头(对排序后的参数拼接 api_key 后计算 MD5,输出小写 hex)。参见 认证与签名。
DepositOrderCreate(Direct API 请求)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
merchant_order_no | string (1-64) | 是 | 你对这笔充值的唯一引用。每个商户内必须唯一。 |
amount | decimal | 是 | 必须大于 0。会做币种精度验证。 |
currency_code | string (3) | 是 | ISO 4217 代码。必须是你的商户账号已启用的币种。 |
callback_url | string (1-500) | 是 | 状态变更时我们会调用的 HTTPS URL。Direct API 必填。 |
extra_data | string | 否 | 不透明字符串,会在回调中原样回传。 |
user_id | string (≤64) | 否 | 你内部的用户标识符。用于分组与审计。 |
CashierDepositOrderCreate(收银台会话请求)
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
order_type | enum | 否 | 默认为 cashier。此字段为前向兼容而保留。 |
merchant_order_no | string (1-64) | 是 | 唯一性规则与 Direct API 相同。 |
amount | decimal | 否 | 预填金额。必须大于 0,且需符合商户设置的 cashier 面额;若已启用自定义金额,则需落在设置的最小/最大值范围内。带入时后端会立即创建 DepositOrder,托管页面会跳过面额选择。 |
currency_code | string (3) | 是 | ISO 4217 代码。 |
callback_url | string (≤500) | 否 | 收银台模式可选。若省略,使用商户默认值。 |
extra_data | string | 否 | 会在回调中原样回传。 |
user_id | string (≤64) | 否 | 强烈建议带上。网关会对同一个 user_id 重用既有的 PENDING 会话,避免你的应用程序重试时意外创建重复会话。 |
payer_bank_cards | list of objects (≤10) | 否 | 预先绑定的付款人银行卡。提供时,收银台页面会强制付款人从中挑一张。 |
DepositPaymentInfo(Direct API 响应,HTTP 201)
| 字段 | 何时出现 | 说明 |
|---|---|---|
order_no | 总是 | 网关指派的标识符。查询时使用此值。 |
requested_amount | 总是 | 你提交的金额。可能会与 amount 不同。 |
amount | 总是 | 付款人实际需要汇出的金额。当路由引擎启用金额浮动进行对账时,会与 requested_amount 不同。 |
currency_code | 总是 | 从请求原样回传。 |
expire_at | 总是 | RFC 3339 时间戳。默认超时为 15 分钟;可依商户设置调整。 |
payment_url | 仅渠道订单 | 付款人的转址目标。 |
payment_info | 仅渠道订单 | 渠道回传的 JSON,结构依渠道而异。 |
bank_code、bank_name、bank_branch、account_no、account_holder | 账户订单;以及对于回传银行信息的渠道订单,为方便而镜像出来 | 要展示给付款人看的银行信息。 |
remark | 仅账户订单 | 付款人在银行转账备注栏必须填的字符串。对账器会用这个。 |
CashierCreateResponse(收银台响应,HTTP 201)
| 字段 | 说明 |
|---|---|
cashier_url | 完整的转址 URL,付款人会被导到这里。包含已签名的 token。 |
cashier_token | 同一个 token,另外暴露出来方便后端查看。对商户来说并非必要。 |
session_id | CashierSession.id。记录下来方便日志追踪。 |
expire_at | 会话超时。默认 15 分钟,可依商户设置调整。 |
完整的 schema 定义,包含 DepositOrderResponse 上的所有响应字段,请见 API reference。
Direct API 流程
这张图描绘的是 happy path。网关在步骤 2-3 根据设置的路由规则决定订单路由:若有匹配的 PaymentChannel 就用渠道,否则用一个受管的银行 Account。订单列以 status=PENDING 与 expire_at 写入,expire_at 取自商户的 deposit_expire_minutes 设置(默认 15 分钟)。
当付款人付款时,网关会把订单从 MATCHED(已认定付款)推进到 COMPLETED(账本已结算),然后触发回调。失败分支(FAILED、EXPIRED、CANCELLED)详见状态转换。
收银台会话流程
收银台模式的关键点:
- 未带
amount时,DepositOrder列在付款人提交之前不存在。 在那之前你手上只有CashierSession。对/api/v1/merchant/query/deposit/{order_no}发出查询会回 404,因为此时还没有order_no。 - 带
amount时,DepositOrder会立即创建。 cashier session 会以submitted状态返回,public cashier API 会显示has_deposit_order=true,托管页面会跳过面额选择。 cashier_url里的签名 token 是短效的,并且绑定到该会话。 它不是商户 API 的凭证。不要尝试拿它去调用商户 API 端点。- 会话重用以
(merchant_id, user_id)为键。 若同一用户已有 PENDING 会话,网关会用新的merchant_order_no、币种与 callback URL 更新它,并回传同一个 token。这可以避免重试时意外创建重复会话。 - 会话的
expire_at与最终订单的expire_at彼此独立。 付款人提交时,订单会根据商户设置取得新的超时时间。 - 收银台页面可能使用 WebSocket 推送状态更新给付款人浏览器,不需要轮询。对商户集成而言这是透明的:你还是依赖服务器对服务器的回调。
示例
下方片段假设你已依 Authentication 所述生成 X-Timestamp 与 X-Signature 标头。四个示例都用同一把 key 签名:
#!/usr/bin/env bash
# MD5 signature for Payment Gateway merchant API (bash reference).
#
# Usage:
# ./sign.sh --api-key <key> --params 'k1=v1&k2=v2&...'
set -euo pipefail
API_KEY=""
PARAMS=""
while [[ $# -gt 0 ]]; do
case "$1" in
--api-key) API_KEY="$2"; shift 2 ;;
--params) PARAMS="$2"; shift 2 ;;
*) echo "Unknown flag: $1" >&2; exit 2 ;;
esac
done
# Sort key=value pairs ASCII order (C locale), drop empty trailing field
SORTED=$(printf '%s' "$PARAMS" | tr '&' '\n' | LC_ALL=C sort | grep -v '^$' | tr '\n' '&' | sed 's/&$//')
SIGN_STRING="${SORTED}${API_KEY}"
# MD5: md5sum on Linux/CI, md5 on macOS
if command -v md5sum >/dev/null 2>&1; then
printf '%s' "$SIGN_STRING" | md5sum | cut -d' ' -f1
else
printf '%s' "$SIGN_STRING" | md5
fi#!/usr/bin/env python3
"""Reference implementation: MD5 signature for Payment Gateway merchant API.
Usage:
python sign.py --api-key <key> --params 'k1=v1&k2=v2&...'
Algorithm:
1. Parse params into a dict.
2. Sort keys alphabetically (ASCII order).
3. Build query string 'k=v&k=v', skipping None values.
4. Append api_key directly (no '&').
5. MD5 the UTF-8 bytes; output lowercase hex.
"""
import argparse
import hashlib
import sys
from urllib.parse import parse_qsl
def create_signature(params: dict[str, str], api_key: str) -> str:
sorted_items = sorted(params.items())
query_string = "&".join(f"{k}={v}" for k, v in sorted_items if v is not None)
return hashlib.md5((query_string + api_key).encode("utf-8")).hexdigest()
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--api-key", required=True)
parser.add_argument("--params", required=True)
args = parser.parse_args()
params = dict(parse_qsl(args.params, keep_blank_values=True))
print(create_signature(params, args.api_key))
return 0
if __name__ == "__main__":
sys.exit(main())<?php
/**
* MD5 signature for Payment Gateway merchant API (PHP reference).
*
* Usage:
* php sign.php --api-key=<key> --params='k1=v1&k2=v2&...'
*
* Note: getopt() requires --flag=value form (no space between flag and value).
*/
$opts = getopt('', ['api-key:', 'params:']);
if (!isset($opts['api-key']) || !isset($opts['params'])) {
fwrite(STDERR, "Usage: php sign.php --api-key=<key> --params='k=v&...'\n");
exit(2);
}
parse_str($opts['params'], $params);
ksort($params, SORT_STRING);
$parts = [];
foreach ($params as $k => $v) {
if ($v === null) continue;
$parts[] = "$k=$v";
}
echo md5(implode('&', $parts) . $opts['api-key']);#!/usr/bin/env node
/**
* MD5 signature for Payment Gateway merchant API (Node.js / TypeScript reference).
*
* Usage:
* tsx sign.ts --api-key <key> --params 'k1=v1&k2=v2&...'
*/
import { createHash } from "crypto";
function parseArgs(argv: string[]): { apiKey: string; params: string } {
let apiKey = "", params = "";
for (let i = 2; i < argv.length; i++) {
if (argv[i] === "--api-key") apiKey = argv[++i];
else if (argv[i] === "--params") params = argv[++i];
}
if (!apiKey || !params) {
process.stderr.write("Usage: sign.ts --api-key <key> --params 'k=v&...'\n");
process.exit(2);
}
return { apiKey, params };
}
function createSignature(paramsQuery: string, apiKey: string): string {
const parsed = new Map<string, string>();
for (const pair of paramsQuery.split("&")) {
if (!pair) continue;
const eq = pair.indexOf("=");
const k = eq >= 0 ? pair.slice(0, eq) : pair;
const v = eq >= 0 ? pair.slice(eq + 1) : "";
parsed.set(k, v);
}
const sorted = [...parsed.entries()].sort(([a], [b]) =>
a < b ? -1 : a > b ? 1 : 0
);
const query = sorted.map(([k, v]) => `${k}=${v}`).join("&");
return createHash("md5").update(query + apiKey, "utf8").digest("hex");
}
const { apiKey, params } = parseArgs(process.argv);
process.stdout.write(createSignature(params, apiKey));每个示例中,--data payload 就是你要签名的 JSON 请求体。签请求体,不要签 URL。
示例 1:Direct API 充值
创建一笔 direct 充值。渠道或账户由网关挑选;当前版本无法从商户 API 指定渠道。
curl -X POST https://pre-api.open-pay.co/api/v1/merchant/orders/deposit \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG" \
-d '{
"merchant_order_no": "DEP-20260520-0001",
"amount": "1000.00",
"currency_code": "THB",
"callback_url": "https://merchant.example.com/api/pg/callback",
"user_id": "user-42",
"extra_data": "checkout-cart-77"
}'响应(路由到渠道,已精简):
{
"order_no": "D2026052012345678",
"requested_amount": "1000.00",
"amount": "1000.00",
"currency_code": "THB",
"expire_at": "2026-05-20T08:15:00Z",
"payment_url": "https://pay.example-psp.com/pay/abc123",
"payment_info": {
"bank_code": "SCB",
"bank_name": "Siam Commercial Bank",
"account_no": "1234567890",
"account_name": "OPEN PAY CO LTD"
},
"bank_code": "SCB",
"bank_name": "Siam Commercial Bank",
"account_no": "1234567890",
"account_holder": "OPEN PAY CO LTD"
}把 order_no 与你的 merchant_order_no 对应后保存。后续查询与回调对账会用到。
示例 2:收银台会话
创建一笔收银台会话,并把付款人导到 cashier_url。
curl -X POST https://pre-api.open-pay.co/api/v1/merchant/orders/deposit/cashier \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG" \
-d '{
"merchant_order_no": "DEP-20260520-0002",
"currency_code": "THB",
"user_id": "user-42",
"callback_url": "https://merchant.example.com/api/pg/callback"
}'响应:
{
"cashier_url": "https://pre-cashier.open-pay.co/?token=eyJhbGciOi...",
"cashier_token": "eyJhbGciOi...",
"session_id": "0a3f5c2e-1c0b-4a3b-9c2d-1f4e7a8b9c0d",
"expire_at": "2026-05-20T08:15:00Z"
}把付款人的浏览器导向 cashier_url。不要把该页面嵌在隐藏的 iframe 里;收银台页面预期执行在顶层浏览器上下文中。
示例 3:带预填金额的收银台会话
当你的结账流程已经收集金额,但仍希望 Payment Gateway 托管付款信息页时,使用此模式。金额会套用与托管面额选择相同的面额/自定义金额验证。
curl -X POST https://pre-api.open-pay.co/api/v1/merchant/orders/deposit/cashier \
-H "Content-Type: application/json" \
-H "X-API-Key: $API_KEY" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG" \
-d '{
"merchant_order_no": "DEP-20260520-0003",
"amount": "1000.00",
"currency_code": "THB",
"user_id": "user-42",
"callback_url": "https://merchant.example.com/api/pg/callback"
}'响应:
{
"cashier_url": "https://pre-cashier.open-pay.co/?token=eyJhbGciOi...",
"cashier_token": "eyJhbGciOi...",
"session_id": "1d0f5c2e-2c0b-4a3b-9c2d-1f4e7a8b9c0d",
"expire_at": "2026-05-20T08:15:00Z"
}把付款人导向 cashier_url。托管页面会直接载入付款信息,不会显示面额选择。
示例 4:创建后查询充值订单
以网关的 order_no 查询:
curl https://pre-api.open-pay.co/api/v1/merchant/query/deposit/D2026052012345678 \
-H "X-API-Key: $API_KEY" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG"或以你的 merchant_order_no 查询:
curl "https://pre-api.open-pay.co/api/v1/merchant/query/deposit?merchant_order_no=DEP-20260520-0001" \
-H "X-API-Key: $API_KEY" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG"两者都会回传完整的 DepositOrderResponse schema,包含 status、amount、actual_amount、matched_at、completed_at 与 callback_status。用响应内容对齐你内部的记录。
示例 5:merchant_order_no 重复
如果你以一个对该商户已存在的 merchant_order_no 调用 POST /deposit(或 POST /deposit/cashier),后端会拒绝该请求:
HTTP/1.1 409 Conflict
Content-Type: application/json
{
"success": false,
"code": "CONFLICT",
"message": "Duplicate merchant order number: DEP-20260520-0001",
"data": null
}建议的恢复方式:不要盲目重试。先查询。
curl "https://pre-api.open-pay.co/api/v1/merchant/query/deposit?merchant_order_no=DEP-20260520-0001" \
-H "X-API-Key: $API_KEY" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG"如果现有订单已是 COMPLETED,就结束。如果仍是 PENDING 且尚未超时,把该订单的 payment_url 或银行信息回传给付款人,不要创建新订单。
状态转换
DepositOrder 状态机
| From → To | 触发条件 | 商户观察到的现象 |
|---|---|---|
PENDING → MATCHED | 银行 webhook 匹配上订单的 expected_remark 或浮动金额,或渠道发来已付款通知。后端设置 matched_at。 | 尚无回调。可通过查询看到状态。 |
MATCHED → COMPLETED | 账本把资金提交到商户余额。后端设置 completed_at、actual_amount。 | 触发已签名回调。这是成功充值唯一的一次回调。 |
PENDING → EXPIRED | expire_at 已过且没有付款匹配上。后台任务会搬动该列。订单不会自动取消;它会明确转成 EXPIRED,并停止接受付款。 | 默认不会为超时触发回调。通过查询对账。 |
PENDING → CANCELLED / MATCHED → CANCELLED | 运营人员或商户明确取消。对于账户订单,预留的充值额度会被释放。 | 取消不会触发回调。通过查询或运营人员通知对账。 |
PENDING → FAILED / MATCHED → FAILED | 渠道回报终态失败(例如渠道订单被上游银行拒绝)。 | 是否触发回调视渠道而定。通过查询对账。 |
MATCHED 不是终态
MATCHED 订单在少见情况下仍可能转到 CANCELLED 或 FAILED(匹配后的渠道错误、运营人员介入)。只把 COMPLETED 当成最终成功,把 EXPIRED / CANCELLED / FAILED 当成最终失败。
CashierSession 状态机
| From → To | 触发条件 | 商户观察到的现象 |
|---|---|---|
PENDING → SUBMITTED | 不带 amount 的 session:付款人在收银台页面选定面额(与银行卡,若有要求)。后端创建 DepositOrder 并以 deposit_order_id 串接。 | 此步骤不会触发回调。充值生命周期从此开始。 |
创建时即为 SUBMITTED | cashier session 请求带 amount。后端会立即创建 DepositOrder、以 deposit_order_id 串接,托管页面跳过面额选择。 | 此步骤不会触发回调。商户可立即查询对应的 DepositOrder。 |
PENDING → EXPIRED | 会话的 expire_at 已过且付款人始终未提交。下一次触碰该会话的请求会把它标记为超时;运营人员明确动作也可以搬动。 | 不会触发回调。会话无法续用;创建一笔新的。 |
不存在 SUBMITTED → EXPIRED 路径。一旦会话被提交,与之关联的 DepositOrder 会拥有自己的状态机;会话就形同已封存的记录。
陷阱
在不同的充值意图间重用
merchant_order_no。 后端会以 HTTP 409 与标准错误信封中的CONFLICTcode 拒绝。现在传输层会正确使用 HTTP 409。为每一笔新的充值生成全新的merchant_order_no。重试时换新的
merchant_order_no。 与陷阱 1 相反。POST /deposit过程中网络抖动时,若你用新的merchant_order_no重试,第一个请求可能其实已经到了,于是你会留下两笔订单。重试时永远使用相同的merchant_order_no;重复检查会把冲突浮现出来,让你可以查询既有订单。参见CONFLICT与BUSINESS_ERROR。收银台会话在付款人完成前就超时。 会话无法续用。商户必须再调用一次
POST /deposit/cashier,并把付款人导向新的cashier_url。在你的终端用户 UI 上把expire_at预期说清楚。忘了验证回调签名。 每一次回调都使用与外发请求相同的 MD5 排序拼接签名方案。略过验证会让你暴露于伪造回调的风险,并破坏幂等假设。参见 回调 与
INVALID_SIGNATURE。对「同一笔」订单混用 Direct API 与收银台模式。 Direct API 与收银台产生不同的
DepositOrderType值(DIRECTvsCASHIER)。即使merchant_order_no相同,它们也不能互换。一个逻辑充值尝试只挑一种模式。以
payment_url是否存在来判断收银台模式。 这样判断是错的。收银台会话回传cashier_url,不是payment_url。Direct API 渠道订单回传payment_url。Direct API 账户订单两者都不会回传。要分支判断,依照你调用的是哪个端点,或依查询响应里的order_type。把
MATCHED当成终态。MATCHED的意思是「付款已认定,账本尚未提交」。它仍可能转到CANCELLED或(少见地)FAILED。已签名的回调是在COMPLETED触发,不是在MATCHED。参见状态转换。假设
amount等于requested_amount。 对于使用金额浮动对账的币种与渠道,网关会调整应付金额,以区分同时汇到同一银行账户的多笔转账。展示指示时,要采用amount(付款人实际付的)而非requested_amount(你要求的)。这两个字段在DepositOrderResponse与DepositPaymentInfo上都有清楚命名。忽略
expire_at。 超时订单会转到DepositOrderStatus.EXPIRED并停止接受付款。付款人在超时后才到账的转账不会自动对到该订单,且可能需要手动对账。对超时订单上的操作会得到怎样的响应,参见BUSINESS_ERROR。在 UI 写死银行账户信息。
DepositPaymentInfo上的payment_info、bank_code、bank_name、account_no与account_holder字段会依订单而变——路由引擎可能根据负载、币种或浮动可用性挑选不同账户。响应给什么就显示什么;千万不要从你自己的数据库预填。以轮询
/query/deposit/取代依赖回调。 轮询会浪费你的 API 配额并增加延迟。回调会在COMPLETED触发一次。GET /query/deposit/{order_no}只应作为错过回调时的对账备援。重试语义参见 Callbacks。没处理信用额度设置账号的
INSUFFICIENT_MERCHANT_BALANCE。 对充值来说罕见,但如果你的账号启用了预付手续费扣款,就有可能遇到。参见INSUFFICIENT_MERCHANT_BALANCE。在提交前以
merchant_order_no重新查询收银台会话。GET /query/deposit?merchant_order_no=...端点查的是DepositOrder,不是CashierSession。PENDING 的收银台会话此时还没有DepositOrder,所以查询会回 404NOT_FOUND。这不是 bug。若 cashier 请求已带amount,订单会立即创建,此限制不适用。把
cashier_token当成长期凭证信任。 收银台 token 是短效、仅限该会话的已签名 JWS。会话更新时它会轮替。永远不要保存它;每次都以POST /deposit/cashier重新开始。
上线检查清单
把生产环境流量指向网关之前:
- [ ] 回调 URL 走 HTTPS、有监控,并在 5 秒内回传
200 OK(或响应体含success)。 - [ ] 已实现回调签名验证,并有单元测试。参见 Authentication。
- [ ] 你生产环境的 NAT 出口 IP 已加入 Cloudflare Access 允许清单。Cloudflare 边缘来源 IP 模型参见 Sandbox。
- [ ] 幂等策略已决定:
POST /deposit重试时使用相同的merchant_order_no,且你已有处理重复响应的代码路径。 - [ ]
currency_code的值会被验证为你商户账号已启用的币种。送不支持的币种会回BUSINESS_ERROR。 - [ ] Mock channel 沙盒测试针对 Direct API 与收银台两种模式都已通过,至少涵盖 happy path 与
EXPIRED分支。 - [ ] 生产环境的 API key 与签名 secret 存于你的 secret manager。它们不在源代码里、不在 CI 日志里、也不在客户端 bundle 里。
- [ ] 你已决定是否要把网关的
order_no暴露给终端用户。把它用于客服流程是安全的,但它并不是给付款人看的标识符。
交叉引用
- 认证与签名 — 每个请求与每个回调都使用的 MD5 排序拼接签名方案。
- Callbacks — 服务器对服务器的状态通知、重试策略、响应格式。
- Errors — 权威错误码参考。从陷阱章节交叉链接。
- API reference — 每个请求与响应字段的完整 schema 定义。
- Sandbox — PRE 环境、mock channel、Cloudflare 边缘 IP 允许清单模型。
- Quickstart — 创建第一笔充值的端到端示例。
- Withdrawals — 相反方向;从你的商户余额付款出去。