本章是透過 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 — 相反方向;從你的商戶餘額付款出去。