Skip to content

本章是通过 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 会在付款人提交之后创建。若请求带 amountDepositOrder 会与 session 同步创建,托管页面直接显示付款信息。
付款人汇出资金的终端用户,与商户不同。付款人会与 payment_urlpayment_info 或托管的收银台页面交互。付款人不会对商户 API 做任何身份验证。
MATCHED 状态充值已被认定为已付款(一笔银行交易匹配上订单,或是渠道回报成功),但结算与账本提交尚未完成。并非终态。
Mock channel沙盒中内置的假渠道。用它可以端到端走完状态机,不需要动到任何真实 PSP。参见 Sandbox

两种充值模式

方面Direct API收银台会话
端点POST /api/v1/merchant/orders/depositPOST /api/v1/merchant/orders/deposit/cashier
由谁决定金额商户,于请求体中指定。默认由付款人在托管页面选择;若 cashier session 请求带 amount,则由商户预先指定。
由谁决定渠道网关的路由引擎,根据币种与金额决定。付款人提交后由网关的路由引擎决定;若 cashier session 请求带 amount,则在创建 session 时立即决定。
初始响应DepositPaymentInfo,含 payment_url 或银行信息。CashierCreateResponse,含 cashier_urlcashier_token
DepositOrder 列何时创建POST /deposit 当下立即创建。默认在付款人提交时创建。若 cashier 请求带 amount,会与 session 同步创建,托管页面直接显示付款信息。
商户要展示给付款人看的内容你自己的 UI;由你渲染银行信息或转址到 payment_url转址到 cashier_url。所有东西都由网关托管。
结果订单的 order_typeDIRECTCASHIER

模式选择指引

如果你已经在自家 UI 收集了金额、币种与付款人意图,使用 Direct API。你的应用程序仍然掌控结账的外观与流程;转址或银行信息页面由你自己处理。

如果你希望 Payment Gateway 托管给付款人看的页面,使用收银台会话。付款人需要自行选金额时,不带 amount;你已在自家流程收集金额、只需要托管页面显示付款信息时,带入 amount。你只要把单一的 cashier_url 交给付款人,就可以放手不管,直到收到回调。

两种模式最后会收敛到相同的 DepositOrder 实体与相同的回调握手。查询、回调验证与状态语义完全一致。

API 接口

MethodPath用途
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_nostring (1-64)你对这笔充值的唯一引用。每个商户内必须唯一。
amountdecimal必须大于 0。会做币种精度验证。
currency_codestring (3)ISO 4217 代码。必须是你的商户账号已启用的币种。
callback_urlstring (1-500)状态变更时我们会调用的 HTTPS URL。Direct API 必填。
extra_datastring不透明字符串,会在回调中原样回传。
user_idstring (≤64)你内部的用户标识符。用于分组与审计。

CashierDepositOrderCreate(收银台会话请求)

字段类型必填说明
order_typeenum默认为 cashier。此字段为前向兼容而保留。
merchant_order_nostring (1-64)唯一性规则与 Direct API 相同。
amountdecimal预填金额。必须大于 0,且需符合商户设置的 cashier 面额;若已启用自定义金额,则需落在设置的最小/最大值范围内。带入时后端会立即创建 DepositOrder,托管页面会跳过面额选择。
currency_codestring (3)ISO 4217 代码。
callback_urlstring (≤500)收银台模式可选。若省略,使用商户默认值。
extra_datastring会在回调中原样回传。
user_idstring (≤64)强烈建议带上。网关会对同一个 user_id 重用既有的 PENDING 会话,避免你的应用程序重试时意外创建重复会话。
payer_bank_cardslist 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_codebank_namebank_branchaccount_noaccount_holder账户订单;以及对于回传银行信息的渠道订单,为方便而镜像出来要展示给付款人看的银行信息。
remark仅账户订单付款人在银行转账备注栏必须填的字符串。对账器会用这个。

CashierCreateResponse(收银台响应,HTTP 201)

字段说明
cashier_url完整的转址 URL,付款人会被导到这里。包含已签名的 token。
cashier_token同一个 token,另外暴露出来方便后端查看。对商户来说并非必要。
session_idCashierSession.id。记录下来方便日志追踪。
expire_at会话超时。默认 15 分钟,可依商户设置调整。

完整的 schema 定义,包含 DepositOrderResponse 上的所有响应字段,请见 API reference

Direct API 流程

这张图描绘的是 happy path。网关在步骤 2-3 根据设置的路由规则决定订单路由:若有匹配的 PaymentChannel 就用渠道,否则用一个受管的银行 Account。订单列以 status=PENDINGexpire_at 写入,expire_at 取自商户的 deposit_expire_minutes 设置(默认 15 分钟)。

当付款人付款时,网关会把订单从 MATCHED(已认定付款)推进到 COMPLETED(账本已结算),然后触发回调。失败分支(FAILEDEXPIREDCANCELLED)详见状态转换

收银台会话流程

收银台模式的关键点:

  • 未带 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-TimestampX-Signature 标头。四个示例都用同一把 key 签名:

bash
#!/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
python
#!/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
<?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']);
typescript
#!/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 指定渠道。

bash
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"
  }'

响应(路由到渠道,已精简):

json
{
  "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

bash
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"
  }'

响应:

json
{
  "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 托管付款信息页时,使用此模式。金额会套用与托管面额选择相同的面额/自定义金额验证。

bash
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"
  }'

响应:

json
{
  "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 查询:

bash
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 查询:

bash
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,包含 statusamountactual_amountmatched_atcompleted_atcallback_status。用响应内容对齐你内部的记录。

示例 5:merchant_order_no 重复

如果你以一个对该商户已存在的 merchant_order_no 调用 POST /deposit(或 POST /deposit/cashier),后端会拒绝该请求:

json
HTTP/1.1 409 Conflict
Content-Type: application/json

{
  "success": false,
  "code": "CONFLICT",
  "message": "Duplicate merchant order number: DEP-20260520-0001",
  "data": null
}

建议的恢复方式:不要盲目重试。先查询。

bash
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_atactual_amount触发已签名回调。这是成功充值唯一的一次回调。
PENDING → EXPIREDexpire_at 已过且没有付款匹配上。后台任务会搬动该列。订单不会自动取消;它会明确转成 EXPIRED,并停止接受付款。默认不会为超时触发回调。通过查询对账。
PENDING → CANCELLED / MATCHED → CANCELLED运营人员或商户明确取消。对于账户订单,预留的充值额度会被释放。取消不会触发回调。通过查询或运营人员通知对账。
PENDING → FAILED / MATCHED → FAILED渠道回报终态失败(例如渠道订单被上游银行拒绝)。是否触发回调视渠道而定。通过查询对账。

MATCHED 不是终态

MATCHED 订单在少见情况下仍可能转到 CANCELLEDFAILED(匹配后的渠道错误、运营人员介入)。只把 COMPLETED 当成最终成功,把 EXPIRED / CANCELLED / FAILED 当成最终失败。

CashierSession 状态机

From → To触发条件商户观察到的现象
PENDING → SUBMITTED不带 amount 的 session:付款人在收银台页面选定面额(与银行卡,若有要求)。后端创建 DepositOrder 并以 deposit_order_id 串接。此步骤不会触发回调。充值生命周期从此开始。
创建时即为 SUBMITTEDcashier session 请求带 amount。后端会立即创建 DepositOrder、以 deposit_order_id 串接,托管页面跳过面额选择。此步骤不会触发回调。商户可立即查询对应的 DepositOrder
PENDING → EXPIRED会话的 expire_at 已过且付款人始终未提交。下一次触碰该会话的请求会把它标记为超时;运营人员明确动作也可以搬动。不会触发回调。会话无法续用;创建一笔新的。

不存在 SUBMITTED → EXPIRED 路径。一旦会话被提交,与之关联的 DepositOrder 会拥有自己的状态机;会话就形同已封存的记录。

陷阱

  1. 在不同的充值意图间重用 merchant_order_no 后端会以 HTTP 409 与标准错误信封中的 CONFLICT code 拒绝。现在传输层会正确使用 HTTP 409。为每一笔新的充值生成全新的 merchant_order_no

  2. 重试时换新的 merchant_order_no 与陷阱 1 相反。POST /deposit 过程中网络抖动时,若你用新的 merchant_order_no 重试,第一个请求可能其实已经到了,于是你会留下两笔订单。重试时永远使用相同的 merchant_order_no;重复检查会把冲突浮现出来,让你可以查询既有订单。参见 CONFLICTBUSINESS_ERROR

  3. 收银台会话在付款人完成前就超时。 会话无法续用。商户必须再调用一次 POST /deposit/cashier,并把付款人导向新的 cashier_url。在你的终端用户 UI 上把 expire_at 预期说清楚。

  4. 忘了验证回调签名。 每一次回调都使用与外发请求相同的 MD5 排序拼接签名方案。略过验证会让你暴露于伪造回调的风险,并破坏幂等假设。参见 回调INVALID_SIGNATURE

  5. 对「同一笔」订单混用 Direct API 与收银台模式。 Direct API 与收银台产生不同的 DepositOrderType 值(DIRECT vs CASHIER)。即使 merchant_order_no 相同,它们也不能互换。一个逻辑充值尝试只挑一种模式。

  6. payment_url 是否存在来判断收银台模式。 这样判断是错的。收银台会话回传 cashier_url,不是 payment_url。Direct API 渠道订单回传 payment_url。Direct API 账户订单两者都不会回传。要分支判断,依照你调用的是哪个端点,或依查询响应里的 order_type

  7. MATCHED 当成终态。 MATCHED 的意思是「付款已认定,账本尚未提交」。它仍可能转到 CANCELLED 或(少见地)FAILED。已签名的回调是在 COMPLETED 触发,不是在 MATCHED。参见状态转换

  8. 假设 amount 等于 requested_amount 对于使用金额浮动对账的币种与渠道,网关会调整应付金额,以区分同时汇到同一银行账户的多笔转账。展示指示时,要采用 amount(付款人实际付的)而非 requested_amount(你要求的)。这两个字段在 DepositOrderResponseDepositPaymentInfo 上都有清楚命名。

  9. 忽略 expire_at 超时订单会转到 DepositOrderStatus.EXPIRED 并停止接受付款。付款人在超时后才到账的转账不会自动对到该订单,且可能需要手动对账。对超时订单上的操作会得到怎样的响应,参见 BUSINESS_ERROR

  10. 在 UI 写死银行账户信息。 DepositPaymentInfo 上的 payment_infobank_codebank_nameaccount_noaccount_holder 字段会依订单而变——路由引擎可能根据负载、币种或浮动可用性挑选不同账户。响应给什么就显示什么;千万不要从你自己的数据库预填。

  11. 以轮询 /query/deposit/ 取代依赖回调。 轮询会浪费你的 API 配额并增加延迟。回调会在 COMPLETED 触发一次。GET /query/deposit/{order_no} 只应作为错过回调时的对账备援。重试语义参见 Callbacks

  12. 没处理信用额度设置账号的 INSUFFICIENT_MERCHANT_BALANCE 对充值来说罕见,但如果你的账号启用了预付手续费扣款,就有可能遇到。参见 INSUFFICIENT_MERCHANT_BALANCE

  13. 在提交前以 merchant_order_no 重新查询收银台会话。 GET /query/deposit?merchant_order_no=... 端点查的是 DepositOrder,不是 CashierSession。PENDING 的收银台会话此时还没有 DepositOrder,所以查询会回 404 NOT_FOUND。这不是 bug。若 cashier 请求已带 amount,订单会立即创建,此限制不适用。

  14. 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 — 相反方向;从你的商户余额付款出去。