支付代理對接文檔
接入 SuperTerminal 的支付代理規格。無 API key;必須實作詢價。公開頁面,可列印 PDF。
快速開始
這份規格給支付代理實作與 SuperTerminal 的對接。若只做 sessions + 回調、漏掉詢價 API,對客平台的 quote 會 502。
你沒有 X-Api-Key。Terminal 打你、你打 Terminal,都只有 X-Timestamp + X-Signature(sessions 另有 Idempotency-Key)。
禁止直連對客平台。不要打對客平台的 callback_url。對方也不該打你的下單 API。GoCash 只出現在:你用自己的 GoCash partner 金鑰購券/redeem。
環境
| 名稱 | 位置 | 型別 | 必填 | 範例 |
|---|---|---|---|---|
你的端點(每個支付方式一個) | gocash | origin | 是 | GoCash 主控台 自己 → 對接端點 登記,wechat / visa 可以是不同主機 |
TERMINAL | env | origin | 是 | SuperTerminal 公開主機,回調打這裡 |
CASHIER_HMAC | env | string | 是 | GoCash 自己 → 對接端點 產生的 Terminal HMAC(gocash-agent 為 SPARK_HMAC_SECRET) |
GoCash partner key | gocash | string | 是 | 只打 https://gocash.w8et.com |
amountMinor
最小貨幣單位整數。兩位小數幣別時 50000 = 500.00。
你必須實作的入站 API(Terminal → 你)
POST /api/internal/sessionsPOST /api/internal/sessions/cancelPOST /api/internal/deposit-quote(漏做 → 對客平台 deposit-quote 502)POST /api/internal/withdraw-quote
你必須打的出站回調(你 → SuperTerminal)
Host 是 TERMINAL,不是對客平台網域。路徑二選一,簽名 path 必須等於實際請求的 pathname:
- 建議:
/api/v1/cashier/callbacks/{kind} - 相容 pathname(歷史別名):
/api/internal/gocash/{kind}
簽 A 路徑、打 B 路徑會 401。
兩套金鑰,不要混用
| 名稱 | 說明 |
|---|---|
SuperTerminal 支付代理 HMAC | 在 GoCash 自己 → 對接端點 產生。用來驗 Terminal 打你的 sessions/quote,以及你回調 Terminal。沒有 API key。 |
GoCash partner key / secret | GoCash 目錄夥伴憑證。只打 gocash.w8et.com 購券、redeem。不能拿來簽 SuperTerminal。 |
上線檢查清單
discover 列出你,必須同時滿足:
- 已是 GoCash 目錄夥伴:在 Cashier 分頁登記 payment_methods(WeChat/Visa 等)、merchant/customer 金鑰、幣別與額度。未登記的方式,SuperTerminal 營運無法指派。
- 在 GoCash 自己 → 對接端點 登記每個支付方式的 URL,並產生 Terminal HMAC(密文只顯示一次,設定到你的服務;gocash-agent 為
SPARK_HMAC_SECRET)。SuperTerminal 每分鐘同步,不需在 SuperTerminal 登記任何 URL。 - 至少一個對客平台把你的特定支付方式指派到該接入方(不是整個代理一次全開)。
- 四條入站 API 與五條回調都已上線,且用支付代理 HMAC 互簽。
- 對客平台 discover 出現你的通道;deposit-quote 不再 502。
只接 sessions、不接 quote,資格看起來「半套」,對客平台詢價會失敗。
錯誤格式
SuperTerminal 失敗回應:
{
"error": {
"code": "AUTH_BAD_SIGNATURE",
"message": "Invalid HMAC"
}
}讀 error.code。你回給 Terminal 的 sessions/quote 也建議用同一形狀;非 2xx 或沒有有效 url 會觸發 failover。
HMAC 簽名規範
timestamp\\nMETHOD\\npath\\nsha256(rawBody),HMAC-SHA256 hex,±300 秒。
path = 實際 pathname,不含 query。Terminal 打你時 path 是 /api/internal/sessions、/api/internal/deposit-quote 等。你打 Terminal 時 path 必須是你請求的那一條(/api/v1/cashier/callbacks/voucher-deposit 或 /api/internal/gocash/voucher-deposit)。
raw body 位元組必須一致。沒有 X-Api-Key。
通道資格來源
Discover 是否列出你,取決於 GoCash 目錄同步的 payment_methods、currencies、額度、金鑰旗標、該方式是否有端點,以及對客平台是否被指派你的該支付方式。沒有端點的方式永遠不會出現。主控台 cashier_channels 表不影響 discover。請在 GoCash 營運的 Cashier 分頁登記方式,再到自己 → 對接端點 填 URL。
POST {endpoint[paymentMethod]}/api/internal/sessions
概覽
Terminal 在 checkout 時即時向你要付款連結。付款 URL 不是預先註冊,而是這次請求動態回傳。
請求參數
| 名稱 | 位置 | 型別 | 必填 | 範例 |
|---|---|---|---|---|
X-Timestamp | header | unix seconds | 是 | 1700000000 |
X-Signature | header | hex HMAC-SHA256 | 是 | a1b2… |
Idempotency-Key | header | string | 是 | att_…(attempt id) |
sparkSessionId | body | string | 是 | att_…(Terminal attempt id) |
sparkUserId | body | string | 是 | player 或 order id |
type | body | "deposit" | "withdraw" | 是 | deposit |
amountMinor | body | int64 | 是 | 50000 |
currency | body | string | 是 | CNY |
paymentMethod | body | string | 是 | |
locale | body | string | 否 | zh-Hant |
gocashPartnerId | body | string | 是 | GoCash partner id |
feeBearer | body | "user" | "client" | 是 | client |
platformClientCode | body | string | 是 | acme |
呼叫範例(你收到的 body)
{
"sparkSessionId": "att_…",
"sparkUserId": "player-or-order-ref",
"type": "deposit",
"amountMinor": 50000,
"currency": "CNY",
"paymentMethod": "wechat",
"locale": "zh-Hant",
"gocashPartnerId": "partner-id",
"feeBearer": "client",
"platformClientCode": "acme"
}成功回應
{ "id": "your-session-id", "url": "https://you.example/pay/…" }回應欄位
| 名稱 | 說明 |
|---|---|
url | 必須是可開啟的 http(s)。無效 URL 會觸發 failover。 |
id | 可選,Terminal 記為 cashier_tx_id |
錯誤
非 2xx 或沒有有效 url 時,Terminal 視此次 attempt 失敗並可能改派其他支付代理。
注意事項
- 同一 sparkSessionId(Terminal attempt id)重試必須回同一付款意圖(Idempotency-Key = attempt id)。
- 用 platformClientCode 區分不同對客平台,不要假設同一時間只有一個接入方。
POST {endpoint[paymentMethod]}/api/internal/sessions/cancel
概覽
對客平台放棄訂單或出款過期時,Terminal 盡力取消 session。
請求參數
| 名稱 | 位置 | 型別 | 必填 | 範例 |
|---|---|---|---|---|
X-Timestamp | header | unix seconds | 是 | 1700000000 |
X-Signature | header | hex HMAC-SHA256 | 是 | a1b2… |
sparkSessionId | body | string | 是 | att_… |
呼叫範例
{ "sparkSessionId": "att_…" }成功回應
{ "ok": true }回應欄位
| 名稱 | 說明 |
|---|---|
alreadyCompleted | 若已付款可回此旗標 |
錯誤
取消失敗只記事件,不會回滾對客平台的放棄。
注意事項
- 若玩家其實已付款,仍應走 voucher-deposit;Terminal 會依 attempt 是否仍為現行 attempt 判斷 stale。
POST {endpoint[paymentMethod]}/api/internal/deposit-quote
概覽
對客平台打 Terminal /api/v1/client/deposit-quote 時,Terminal 會立刻轉打你這支 API。未實作會讓對方看到 CASHIER_QUOTE_FAILED 502。不建單、不開付款頁。
請求參數
| 名稱 | 位置 | 型別 | 必填 | 範例 |
|---|---|---|---|---|
X-Timestamp | header | unix seconds | 是 | 1700000000 |
X-Signature | header | hex HMAC-SHA256 | 是 | a1b2… |
amountMinor | body | int64 | 是 | 50000 |
currency | body | string | 是 | CNY |
gocashPartnerId | body | string | 是 | 目錄 partner id |
feeBearer | body | "user" | "client" | 是 | client |
platformClientCode | body | string | 是 | acme |
呼叫範例(你收到的 body)
{
"amountMinor": 50000,
"currency": "CNY",
"gocashPartnerId": "partner-id",
"feeBearer": "client",
"platformClientCode": "acme"
}成功回應
{
"amountMinor": 50000,
"totalPriceMinor": 50400,
"handlingFeeMinor": 400,
"receiveMinor": 50000,
"currency": "CNY"
}回應欄位
| 名稱 | 說明 |
|---|---|
amountMinor | 券面額 |
totalPriceMinor | 玩家應付(含費) |
handlingFeeMinor | 手續費 |
receiveMinor | 到帳面額 |
錯誤
非 2xx 時 Terminal 回給對客平台 CASHIER_QUOTE_FAILED。建議用 { error: { code, message } }。
注意事項
- 用 gocashPartnerId 對應你的 GoCash 帳戶做試算,不要用支付代理 HMAC 打 GoCash。
- 驗簽 path 固定為
/api/internal/deposit-quote。
POST {endpoint[paymentMethod]}/api/internal/withdraw-quote
概覽
出款詢價。請求欄位與 deposit-quote 相同。取款也依 checkout 選中的支付方式走到 endpoint[paymentMethod]。
請求參數
| 名稱 | 位置 | 型別 | 必填 | 範例 |
|---|---|---|---|---|
X-Timestamp | header | unix seconds | 是 | 1700000000 |
X-Signature | header | hex HMAC-SHA256 | 是 | a1b2… |
amountMinor | body | int64 | 是 | 50000 |
currency | body | string | 是 | CNY |
gocashPartnerId | body | string | 是 | 目錄 partner id |
feeBearer | body | "user" | "client" | 是 | client |
platformClientCode | body | string | 是 | acme |
呼叫範例
{
"amountMinor": 50000,
"currency": "CNY",
"gocashPartnerId": "partner-id",
"feeBearer": "client",
"platformClientCode": "acme"
}成功回應
{
"amountMinor": 50000,
"handlingFeeMinor": 400,
"receiveMinor": 49600,
"currency": "CNY"
}回應欄位
| 名稱 | 說明 |
|---|---|
amountMinor | 申請出款面額 |
receiveMinor | 玩家實收;若手續費超過面額應回錯而不是負數 |
錯誤
例如手續費大於金額可回 FEE_EXCEEDS_AMOUNT。Terminal 仍會包成對客平台側的 quote 失敗。
注意事項
- 驗簽 path 為
/api/internal/withdraw-quote。
POST /api/v1/cashier/callbacks/voucher-deposit
概覽
玩家支付成功後,向 SuperTerminal 主機申請上分。相容 pathname(歷史別名):POST /api/internal/gocash/voucher-deposit。
請求參數
| 名稱 | 位置 | 型別 | 必填 | 範例 |
|---|---|---|---|---|
X-Timestamp | header | unix seconds | 是 | 1700000000 |
X-Signature | header | hex HMAC-SHA256 | 是 | a1b2… |
sessionId | body | string | 是 | att_…(即 sparkSessionId) |
userId | body | string | 否 | player |
amountMinor | body | int64 | 建議 | 50000 |
currency | body | string | 否 | CNY |
paymentMethod | body | string | 否 | |
voucherNo | body | string | 建議 | GC-… |
verifySecret | body | string | 建議 | XXXX-XXXX |
呼叫範例(可直接跑;path 與簽名一致)
下列打相容 pathname。若改打 v1 路徑,四個分頁裡的 path 都要一起改。
const crypto = require("crypto");
function sign({ secret, timestamp, method, path, rawBody }) {
const bodySha = crypto.createHash("sha256").update(rawBody ?? "", "utf8").digest("hex");
const stringToSign = [timestamp, method.toUpperCase(), path, bodySha].join("\n");
return crypto.createHmac("sha256", secret).update(stringToSign).digest("hex");
}
const TERMINAL = process.env.TERMINAL;
const CASHIER_HMAC = process.env.CASHIER_HMAC; // SuperTerminal 支付代理 HMAC,不要用 GoCash partner key
const path = "/api/internal/gocash/voucher-deposit";
const rawBody = "{\"sessionId\":\"att_EXAMPLE\",\"userId\":\"player-or-order-ref\",\"amountMinor\":50000,\"currency\":\"CNY\",\"paymentMethod\":\"wechat\",\"voucherNo\":\"GC-EXAMPLE\",\"verifySecret\":\"XXXX-XXXX-XXXX-XXXX\"}";
const timestamp = String(Math.floor(Date.now() / 1000));
const signature = sign({ secret: CASHIER_HMAC, timestamp, method: "POST", path, rawBody });
const res = await fetch(TERMINAL + path, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"X-Timestamp": timestamp,
"X-Signature": signature,
// 不要帶 X-Api-Key
},
body: rawBody,
});
console.log(await res.text());
v1 等價:/api/v1/cashier/callbacks/voucher-deposit,body 相同,簽名 path 改成該 pathname。
成功回應
{ "ok": true }回應欄位
| 名稱 | 說明 |
|---|---|
ok | 含重複完成、stale、hold 都可能回 ok |
錯誤
| 代碼 | HTTP | 訊息 | 處理方式 |
|---|---|---|---|
VALIDATION_ERROR | 400 | sessionId required | 帶上 sparkSessionId(Terminal attempt id)。 |
ATTEMPT_NOT_FOUND | 404 | Unknown attempt | sessionId 不是 att_… 或尚未建單。 |
AUTH_BAD_SIGNATURE | 401 | Invalid HMAC | 用支付代理 HMAC 簽實際 pathname;不要用 GoCash key。 |
注意事項
- 金額不符或 voucher 已被其他訂單使用時,Terminal 會攔停人工確認,不會立刻回調對客平台。
- 已被 failover 取代的舊 attempt 回報完成會標 stale_completion。
- 連結過期後若你重開並收款,請先打 deposit-reopen 再打 voucher-deposit。
其他回調
概覽
皆打 SuperTerminal。可用 /api/v1/cashier/callbacks/… 或相容 /api/internal/gocash/…。sessionId 一律是 att_…。
出款完成
POST /api/v1/cashier/callbacks/withdraw-complete
{
"sessionId": "att_…",
"agentTxId": "your-internal-tx-id",
"voucherNo": "GC-…"
}出款失敗
POST /api/v1/cashier/callbacks/withdraw-fail
{
"sessionId": "att_…",
"errorCode": "WITHDRAW_FAILED",
"errorMessage": "insufficient float"
}存款過期
POST /api/v1/cashier/callbacks/deposit-expire
{
"sessionId": "att_…",
"errorCode": "QUOTE_EXPIRED",
"errorMessage": "payment link expired"
}存款重開
POST /api/v1/cashier/callbacks/deposit-reopen
{
"sessionId": "att_…"
}成功回應
{ "ok": true }回應欄位
| 名稱 | 說明 |
|---|---|
ok | 已忽略的過期/重開也回 ok |
錯誤
| 代碼 | HTTP | 訊息 | 處理方式 |
|---|---|---|---|
ATTEMPT_NOT_FOUND | 404 | Unknown attempt | sessionId 必須是 attempt id。 |
CALLBACK_FAILED | 500 | callback failed | 查看 Terminal 日誌後重試。 |
注意事項
- deposit-reopen 會把訂單與最新 failed/cancelled attempt 一併復活,否則後續付款會被判 STALE。
- 相容路徑對應:withdraw-complete、withdraw-fail、deposit-expire、deposit-reopen。
多平台關聯
全域唯一鍵是 order_attempts.id(att_,即 sparkSessionId)。訂單唯一鍵是 (client_id, client_order_ref)。voucher_no 全站不可重複使用。你必須把 sparkSessionId 存到你自己的交易列,回調時原樣帶回。