對客平台對接文檔
接入 SuperTerminal。只打本站下單;含可跑簽名範例與五條回調契約。公開頁面,可列印 PDF。
快速開始
SuperTerminal 是你的唯一下單對象。你用主控台核發的 API key + HMAC 呼叫本站;支付完成後 Terminal 會 POST 到你設定的 callback_url。
向營運索取
| 名稱 | 位置 | 型別 | 必填 | 範例 |
|---|---|---|---|---|
TERMINAL | env | origin | 是 | https://your-terminal.example(無結尾斜線) |
API_KEY | env | string | 是 | 主控台客戶檔的 API key(X-Api-Key) |
HMAC_SECRET | env | string | 是 | 同一客戶檔的 HMAC secret(reveal 後複製) |
callback_url | console | https URL | 是 | https://pay.example.com/hooks/gocash |
生產 TERMINAL 就是 SuperTerminal 主控台所在的公開主機,向營運索取。開發/UAT 也由營運提供。金鑰只出現在主控台客戶編輯頁(可揭示/複製),不要寫進前端瀏覽器。
amountMinor
所有金額都是最小貨幣單位的整數。兩位小數幣別(CNY / HKD / USD…)時 50000 = 500.00。不要傳小數、不要傳「元」。
關聯鏈(必須記住)
你的 clientOrderRef 必須唯一,對應 Terminal 訂單 ord_…。回調裡的 sessionId 永遠是你自己的 clientOrderRef。
sessionId = 你的 clientOrderRef,不是本站訂單號 ord_…。錯誤格式
失敗回應一律是:
{
"error": {
"code": "AUTH_BAD_SIGNATURE",
"message": "Invalid HMAC"
}
}請讀 error.code 做分支,不要只看 HTTP 狀態文字。message 是給人看的補充。
HMAC 簽名規範
簽名字串四行以換行連接:
timestamp + "\n" + METHOD + "\n" + path + "\n" + sha256_hex(rawBody)
再用主控台 HMAC secret 做 HMAC-SHA256,輸出小寫 hex。GET 的 body 視為空字串。時鐘偏移 ±300 秒。
path 只含 pathname,不含 query。驗簽用的是伺服器上的 req.nextUrl.pathname(例如 /api/v1/client/orders/ord_xxx)。把 ?foo=1 簽進去會 401。
raw body 必須與送出的位元組完全一致。先 pretty-print 再對另一份壓縮 JSON 簽名、或 JSON.stringify 兩次、或讓 HTTP 客戶端改寫 body,都會驗簽失敗。請對「即將寫入 socket 的那一份字串」簽名,並用 --data-binary/同等 API 原樣送出。
你呼叫 SuperTerminal 時帶三個 header:X-Api-Key、X-Timestamp、X-Signature。
X-Timestamp + X-Signature。不要用「呼叫 API 的三個 header」去驗回調;也不要因為缺 API key 就拒絕。可執行的 discover 與回調驗簽見下方對應章節(Node / PHP / Python / curl)。
建議流程
玩家完成支付
Mermaid 原始碼(可貼到 mermaid.live)
sequenceDiagram
participant C as 對客平台
participant T as SuperTerminal
participant A as 支付代理
C->>T: POST /api/v1/client/channels/discover (HMAC)
T-->>C: 通道與報價(依指派/幣別/國家過濾)
C->>T: POST /api/v1/client/checkout (clientOrderRef)
T->>A: 本站向通道取付款連結
A-->>T: 付款連結 url
T-->>C: paymentUrl + expiresAt
Note over A: 玩家完成支付
A->>T: 通道回報付款完成
T->>C: POST {callback_url}/api/internal/gocash/voucher-deposit (sessionId=clientOrderRef)
C->>T: POST /api/v1/client/verify (可選,代驗憑證)POST /api/v1/client/channels/discover取得通道與報價。- 玩家選 channel 再選 cashier;可選打
deposit-quote看加價。 POST /api/v1/client/checkout以clientOrderRef冪等建單,回paymentUrl。連結由本站當下取得,不是預先註冊。- 新分頁打開連結。過期後可能收到
deposit-expired;通道重開則deposit-reopened。 - 收到
deposit-completed後上分。可用POST /api/v1/client/verify代驗,或自行打 GoCash/api/v1/public/verify。 - 可靠性必做:即使有 webhook,也要輪詢
GET /api/v1/client/orders/:id直到終態。漏回調或paid_pending人工確認時,只等 webhook 會以為單死了。
clientOrderRef 每筆交易必須唯一且不可重用。不同金額/幣別/國家重用同一 ref 會回 ORDER_REF_CONFLICT。建議必帶 userRef 以便對帳。
POST /api/v1/client/channels/discover
概覽
依營運指派給你的夥伴 × 支付方式、允許幣別/國家、GoCash 目錄,回傳可用通道與櫃台。同一個代理登記了 WeChat 與 Visa,營運可以只把 WeChat 指派給你,discover 就不會出現 Visa。資格是「已指派方法 ∩ 目錄 payment_methods」,不是主控台 cashier_channels 表。
請求參數
| 名稱 | 位置 | 型別 | 必填 | 範例 |
|---|---|---|---|---|
X-Api-Key | header | string | 是 | st_live_… |
X-Timestamp | header | unix seconds | 是 | 1700000000 |
X-Signature | header | hex HMAC-SHA256 | 是 | a1b2… |
type | body | "deposit" | "withdraw" | 是 | deposit |
country | body | string | 是 | HK |
currency | body | string | 否 | CNY |
amountMinor | body | int64 | 否 | 50000 |
filters.tags | body | string[] | 否 | ["gocash"] |
呼叫範例(可直接跑)
設好 TERMINAL、API_KEY、HMAC_SECRET 後,下列範例會算 timestamp、簽 {"type":"deposit","country":"HK","currency":"CNY"}、帶三個 header 打 discover。
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; // 例如 https://terminal.example.com
const API_KEY = process.env.API_KEY;
const HMAC_SECRET = process.env.HMAC_SECRET;
const method = "POST";
const path = "/api/v1/client/channels/discover";
const rawBody = "{\"type\":\"deposit\",\"country\":\"HK\",\"currency\":\"CNY\"}"; // 必須與送出的位元組完全一致
const timestamp = String(Math.floor(Date.now() / 1000));
const signature = sign({ secret: HMAC_SECRET, timestamp, method, path, rawBody });
const res = await fetch(TERMINAL + path, {
method,
headers: {
Accept: "application/json",
"X-Api-Key": API_KEY,
"X-Timestamp": timestamp,
"X-Signature": signature,
"Content-Type": "application/json",
},
body: rawBody,
});
const text = await res.text();
if (!res.ok) {
const err = JSON.parse(text);
throw new Error(err.error?.code + ": " + err.error?.message);
}
console.log(JSON.parse(text));
成功回應
{
"channels": [
{
"channelId": "uuid",
"code": "wechat",
"label": "WeChat Pay",
"logo": "https://…/brand/channel-wechat.png",
"cashiers": [
{
"cashierId": "uuid",
"name": "Desk A",
"currencies": ["CNY","HKD"],
"minAmountMinor": 1000,
"maxAmountMinor": 5000000,
"commissionRateBps": 80,
"feeBearer": "client",
"speedMinutes": 8,
"rating": 4.8
}
]
}
],
"reasons": []
}回應欄位
| 名稱 | 說明 |
|---|---|
channels[].channelId | checkout 必填 channelId |
channels[].cashiers[].cashierId | checkout 必填 cashierId |
reasons | 例如 COUNTRY_NOT_ALLOWED、CURRENCY_NOT_ALLOWED、NO_ELIGIBLE_CASHIER |
錯誤
| 代碼 | HTTP | 訊息 | 處理方式 |
|---|---|---|---|
INVALID_API_KEY | 401 | Unknown API key | 檢查主控台 API key 是否貼錯或已被輪換。 |
AUTH_BAD_SIGNATURE | 401 | Invalid HMAC | path 只用 pathname(不含 query);raw body 與送出位元組必須一致。 |
AUTH_TIMESTAMP_OUT_OF_RANGE | 401 | Invalid HMAC | 伺服器時鐘與 UTC 差不可超過 ±300 秒。 |
CLIENT_DISABLED | 403 | Client is disabled | 客戶已停用,聯絡營運啟用。 |
VALIDATION_ERROR | 400 | invalid JSON / missing fields | 檢查 JSON 與必填欄位。 |
CLIENT_KEY_CORRUPT | 500 | Client secret cannot be decrypted | 聯絡 SuperTerminal 營運輪換金鑰。 |
CATALOG_UNAVAILABLE | 503 | GoCash catalog unavailable | 稍後重試,目錄會自動刷新。 |
注意事項
- 空 channels 不一定是 HTTP 錯誤,先看 reasons。
- 尚未獲指派任何夥伴支付方式時 channels 為空。
- 簽名與送出必須用同一份 compact JSON;本頁範例已對齊。
POST /api/v1/client/deposit-quote
概覽
向選定或第一個合格通道詢價,回傳加價後金額。不建單。簽名方式與 discover 相同(改 path 與 body)。
請求參數
| 名稱 | 位置 | 型別 | 必填 | 範例 |
|---|---|---|---|---|
X-Api-Key | header | string | 是 | st_live_… |
X-Timestamp | header | unix seconds | 是 | 1700000000 |
X-Signature | header | hex HMAC-SHA256 | 是 | a1b2… |
amountMinor | body | int64 | 是 | 50000 |
currency | body | string | 是 | CNY |
country | body | string | 是 | HK |
cashierId | body | uuid | 否 | … |
channelId | body | uuid | 否 | … |
呼叫範例
# 先向 SuperTerminal 營運取得 TERMINAL / API_KEY / HMAC_SECRET
TERMINAL="${TERMINAL:?missing TERMINAL — SuperTerminal origin, no trailing slash}"
API_KEY="${API_KEY:?missing API_KEY}"
HMAC_SECRET="${HMAC_SECRET:?missing HMAC_SECRET}"
PATHNAME="/api/v1/client/deposit-quote"
METHOD="POST"
BODY='{"amountMinor":50000,"currency":"CNY","country":"HK"}'
TS="$(date +%s)"
body_sha() { printf '%s' "$1" | openssl dgst -sha256 | awk '{print $NF}'; }
hmac_hex() { printf '%s' "$2" | openssl dgst -sha256 -hmac "$1" | awk '{print $NF}'; }
BODY_SHA="$(body_sha "$BODY")"
STS="$(printf '%s\n%s\n%s\n%s' "$TS" "$METHOD" "$PATHNAME" "$BODY_SHA")"
SIG="$(hmac_hex "$HMAC_SECRET" "$STS")"
curl -sS -X POST "$TERMINAL$PATHNAME" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
--data-binary "$BODY" \
-H "X-Api-Key: $API_KEY" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG"成功回應
{
"amountMinor": 50000,
"totalPriceMinor": 50400,
"handlingFeeMinor": 400,
"receiveMinor": 50000,
"currency": "CNY"
}回應欄位
| 名稱 | 說明 |
|---|---|
amountMinor | 券面額(最小單位) |
totalPriceMinor | 玩家應付(含費時) |
handlingFeeMinor | 手續費 |
receiveMinor | 到帳面額 |
錯誤
| 代碼 | HTTP | 訊息 | 處理方式 |
|---|---|---|---|
INVALID_API_KEY | 401 | Unknown API key | 檢查主控台 API key 是否貼錯或已被輪換。 |
AUTH_BAD_SIGNATURE | 401 | Invalid HMAC | path 只用 pathname(不含 query);raw body 與送出位元組必須一致。 |
AUTH_TIMESTAMP_OUT_OF_RANGE | 401 | Invalid HMAC | 伺服器時鐘與 UTC 差不可超過 ±300 秒。 |
CLIENT_DISABLED | 403 | Client is disabled | 客戶已停用,聯絡營運啟用。 |
VALIDATION_ERROR | 400 | invalid JSON / missing fields | 檢查 JSON 與必填欄位。 |
CLIENT_KEY_CORRUPT | 500 | Client secret cannot be decrypted | 聯絡 SuperTerminal 營運輪換金鑰。 |
NO_ELIGIBLE_CASHIER | 409 | No cashier available for quote | 換通道或確認該支付方式已指派。 |
CASHIER_QUOTE_FAILED | 502 | Cashier quote failed | 通道詢價失敗。稍後或換通道重試。 |
注意事項
- 金額單位一律最小貨幣單位。
POST /api/v1/client/withdraw-quote
概覽
出款詢價,欄位與存款詢價相同。
請求參數
| 名稱 | 位置 | 型別 | 必填 | 範例 |
|---|---|---|---|---|
X-Api-Key | header | string | 是 | st_live_… |
X-Timestamp | header | unix seconds | 是 | 1700000000 |
X-Signature | header | hex HMAC-SHA256 | 是 | a1b2… |
amountMinor | body | int64 | 是 | 50000 |
currency | body | string | 是 | CNY |
country | body | string | 是 | HK |
cashierId | body | uuid | 否 | … |
channelId | body | uuid | 否 | … |
呼叫範例
# 先向 SuperTerminal 營運取得 TERMINAL / API_KEY / HMAC_SECRET
TERMINAL="${TERMINAL:?missing TERMINAL — SuperTerminal origin, no trailing slash}"
API_KEY="${API_KEY:?missing API_KEY}"
HMAC_SECRET="${HMAC_SECRET:?missing HMAC_SECRET}"
PATHNAME="/api/v1/client/withdraw-quote"
METHOD="POST"
BODY='{"amountMinor":50000,"currency":"CNY","country":"HK"}'
TS="$(date +%s)"
body_sha() { printf '%s' "$1" | openssl dgst -sha256 | awk '{print $NF}'; }
hmac_hex() { printf '%s' "$2" | openssl dgst -sha256 -hmac "$1" | awk '{print $NF}'; }
BODY_SHA="$(body_sha "$BODY")"
STS="$(printf '%s\n%s\n%s\n%s' "$TS" "$METHOD" "$PATHNAME" "$BODY_SHA")"
SIG="$(hmac_hex "$HMAC_SECRET" "$STS")"
curl -sS -X POST "$TERMINAL$PATHNAME" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
--data-binary "$BODY" \
-H "X-Api-Key: $API_KEY" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG"成功回應
{
"amountMinor": 50000,
"handlingFeeMinor": 400,
"receiveMinor": 49600,
"currency": "CNY"
}回應欄位
| 名稱 | 說明 |
|---|---|
amountMinor | 申請出款面額 |
receiveMinor | 玩家實收 |
錯誤
| 代碼 | HTTP | 訊息 | 處理方式 |
|---|---|---|---|
INVALID_API_KEY | 401 | Unknown API key | 檢查主控台 API key 是否貼錯或已被輪換。 |
AUTH_BAD_SIGNATURE | 401 | Invalid HMAC | path 只用 pathname(不含 query);raw body 與送出位元組必須一致。 |
AUTH_TIMESTAMP_OUT_OF_RANGE | 401 | Invalid HMAC | 伺服器時鐘與 UTC 差不可超過 ±300 秒。 |
CLIENT_DISABLED | 403 | Client is disabled | 客戶已停用,聯絡營運啟用。 |
VALIDATION_ERROR | 400 | invalid JSON / missing fields | 檢查 JSON 與必填欄位。 |
CLIENT_KEY_CORRUPT | 500 | Client secret cannot be decrypted | 聯絡 SuperTerminal 營運輪換金鑰。 |
NO_ELIGIBLE_CASHIER | 409 | No cashier available for quote | 換通道或確認該支付方式已指派。 |
CASHIER_QUOTE_FAILED | 502 | Cashier quote failed | 稍後或換櫃重試。 |
注意事項
- 出款同樣走 checkout,type=withdraw。
POST /api/v1/client/checkout
概覽
以 clientOrderRef 冪等建單並由本站取付款連結。相同 ref 且 type/amountMinor/currency/country 一致時回舊單;這四項任一不同回 409 ORDER_REF_CONFLICT。
clientOrderRef。請求參數
| 名稱 | 位置 | 型別 | 必填 | 範例 |
|---|---|---|---|---|
X-Api-Key | header | string | 是 | st_live_… |
X-Timestamp | header | unix seconds | 是 | 1700000000 |
X-Signature | header | hex HMAC-SHA256 | 是 | a1b2… |
clientOrderRef | body | string | 是 | sess-uuid |
type | body | "deposit" | "withdraw" | 是 | deposit |
amountMinor | body | int64 | 是 | 50000 |
currency | body | string | 是 | CNY |
country | body | string | 是 | HK |
channelId | body | uuid | 是 | discover 回傳 |
cashierId | body | uuid | 是 | discover 回傳 |
userRef | body | string | 建議 | player-uuid |
locale | body | string | 否 | zh-Hant |
呼叫範例
# 先向 SuperTerminal 營運取得 TERMINAL / API_KEY / HMAC_SECRET
TERMINAL="${TERMINAL:?missing TERMINAL — SuperTerminal origin, no trailing slash}"
API_KEY="${API_KEY:?missing API_KEY}"
HMAC_SECRET="${HMAC_SECRET:?missing HMAC_SECRET}"
PATHNAME="/api/v1/client/checkout"
METHOD="POST"
BODY='{"clientOrderRef":"sess-uuid","type":"deposit","amountMinor":50000,"currency":"CNY","country":"HK","channelId":"<channel-uuid>","cashierId":"<cashier-uuid>","userRef":"<player-uuid>"}'
TS="$(date +%s)"
body_sha() { printf '%s' "$1" | openssl dgst -sha256 | awk '{print $NF}'; }
hmac_hex() { printf '%s' "$2" | openssl dgst -sha256 -hmac "$1" | awk '{print $NF}'; }
BODY_SHA="$(body_sha "$BODY")"
STS="$(printf '%s\n%s\n%s\n%s' "$TS" "$METHOD" "$PATHNAME" "$BODY_SHA")"
SIG="$(hmac_hex "$HMAC_SECRET" "$STS")"
curl -sS -X POST "$TERMINAL$PATHNAME" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
--data-binary "$BODY" \
-H "X-Api-Key: $API_KEY" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG"成功回應
{
"orderId": "ord_…",
"clientOrderRef": "sess-uuid",
"type": "deposit",
"status": "link_ready",
"channelId": "…",
"channelCode": "wechat",
"cashierId": "…",
"amountMinor": 50000,
"currency": "CNY",
"country": "HK",
"paymentUrl": "https://agent.example/pay/…",
"expiresAt": "2026-09-02T03:00:00.000Z"
}回應欄位
| 名稱 | 說明 |
|---|---|
orderId | Terminal 訂單號,用於查單/放棄/輪詢 |
paymentUrl | 給玩家開啟的付款頁。非預先註冊,由本站當下取得。 |
expiresAt | 連結過期時間 |
voucherNo | 完成後才有 |
錯誤
| 代碼 | HTTP | 訊息 | 處理方式 |
|---|---|---|---|
INVALID_API_KEY | 401 | Unknown API key | 檢查主控台 API key 是否貼錯或已被輪換。 |
AUTH_BAD_SIGNATURE | 401 | Invalid HMAC | path 只用 pathname(不含 query);raw body 與送出位元組必須一致。 |
AUTH_TIMESTAMP_OUT_OF_RANGE | 401 | Invalid HMAC | 伺服器時鐘與 UTC 差不可超過 ±300 秒。 |
CLIENT_DISABLED | 403 | Client is disabled | 客戶已停用,聯絡營運啟用。 |
VALIDATION_ERROR | 400 | invalid JSON / missing fields | 檢查 JSON 與必填欄位。 |
CLIENT_KEY_CORRUPT | 500 | Client secret cannot be decrypted | 聯絡 SuperTerminal 營運輪換金鑰。 |
NO_ELIGIBLE_CASHIER | 409 | Selected channel/cashier is not eligible | 重新 discover;該支付方式必須已指派。 |
ORDER_REF_CONFLICT | 409 | clientOrderRef already used with different parameters | 換新的 clientOrderRef。 |
CASHIER_DISPATCH_FAILED | 502 | Cashier did not return a payment link | 已嘗試 failover;請稍後或換通道。 |
注意事項
- 同一客戶的 clientOrderRef 不可重用不同金額。
- 不同客戶可以使用相同字串的 clientOrderRef,唯一鍵是 (client_id, client_order_ref)。
GET /api/v1/client/orders/:id
概覽
查自己的訂單。id 為 checkout 回傳的 orderId。
completed / failed / expired / abandoned / stale_completion)。不要只靠 webhook。GET 簽名時 path 為 /api/v1/client/orders/ord_…,body 為空字串,pathname 不含 query。請求參數
| 名稱 | 位置 | 型別 | 必填 | 範例 |
|---|---|---|---|---|
X-Api-Key | header | string | 是 | st_live_… |
X-Timestamp | header | unix seconds | 是 | 1700000000 |
X-Signature | header | hex HMAC-SHA256 | 是 | a1b2… |
id | path | string | 是 | ord_… |
呼叫範例
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; // 例如 https://terminal.example.com
const API_KEY = process.env.API_KEY;
const HMAC_SECRET = process.env.HMAC_SECRET;
const method = "GET";
const path = "/api/v1/client/orders/ord_EXAMPLE";
const rawBody = ""; // 必須與送出的位元組完全一致
const timestamp = String(Math.floor(Date.now() / 1000));
const signature = sign({ secret: HMAC_SECRET, timestamp, method, path, rawBody });
const res = await fetch(TERMINAL + path, {
method,
headers: {
Accept: "application/json",
"X-Api-Key": API_KEY,
"X-Timestamp": timestamp,
"X-Signature": signature,
},
});
const text = await res.text();
if (!res.ok) {
const err = JSON.parse(text);
throw new Error(err.error?.code + ": " + err.error?.message);
}
console.log(JSON.parse(text));
成功回應
形狀與 checkout 的 publicOrder 相同。
回應欄位
| 名稱 | 說明 |
|---|---|
status | 見下方狀態機。paid_pending = 營運對帳中,不是失敗。 |
錯誤
| 代碼 | HTTP | 訊息 | 處理方式 |
|---|---|---|---|
INVALID_API_KEY | 401 | Unknown API key | 檢查主控台 API key 是否貼錯或已被輪換。 |
AUTH_BAD_SIGNATURE | 401 | Invalid HMAC | path 只用 pathname(不含 query);raw body 與送出位元組必須一致。 |
AUTH_TIMESTAMP_OUT_OF_RANGE | 401 | Invalid HMAC | 伺服器時鐘與 UTC 差不可超過 ±300 秒。 |
CLIENT_DISABLED | 403 | Client is disabled | 客戶已停用,聯絡營運啟用。 |
VALIDATION_ERROR | 400 | invalid JSON / missing fields | 檢查 JSON 與必填欄位。 |
CLIENT_KEY_CORRUPT | 500 | Client secret cannot be decrypted | 聯絡 SuperTerminal 營運輪換金鑰。 |
NOT_FOUND | 404 | order not found | 確認 orderId 屬於此 API key。 |
注意事項
- 不能查其他客戶的單。
paid_pending時不會收到完成回調,繼續查單直到變成completed(人工放行後會再打回調)。
POST /api/v1/client/orders/:id/abandon
概覽
放棄進行中訂單。已完成單原樣回傳。出款放棄會回調 withdraw-failed。
請求參數
| 名稱 | 位置 | 型別 | 必填 | 範例 |
|---|---|---|---|---|
X-Api-Key | header | string | 是 | st_live_… |
X-Timestamp | header | unix seconds | 是 | 1700000000 |
X-Signature | header | hex HMAC-SHA256 | 是 | a1b2… |
id | path | string | 是 | ord_… |
呼叫範例
# 先向 SuperTerminal 營運取得 TERMINAL / API_KEY / HMAC_SECRET
TERMINAL="${TERMINAL:?missing TERMINAL — SuperTerminal origin, no trailing slash}"
API_KEY="${API_KEY:?missing API_KEY}"
HMAC_SECRET="${HMAC_SECRET:?missing HMAC_SECRET}"
PATHNAME="/api/v1/client/orders/ord_EXAMPLE/abandon"
METHOD="POST"
BODY=''
TS="$(date +%s)"
body_sha() { printf '%s' "$1" | openssl dgst -sha256 | awk '{print $NF}'; }
hmac_hex() { printf '%s' "$2" | openssl dgst -sha256 -hmac "$1" | awk '{print $NF}'; }
BODY_SHA="$(body_sha "$BODY")"
STS="$(printf '%s\n%s\n%s\n%s' "$TS" "$METHOD" "$PATHNAME" "$BODY_SHA")"
SIG="$(hmac_hex "$HMAC_SECRET" "$STS")"
curl -sS -X POST "$TERMINAL$PATHNAME" \
-H "Accept: application/json" \
-H "X-Api-Key: $API_KEY" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG"成功回應
publicOrder,status 多為 abandoned。
回應欄位
| 名稱 | 說明 |
|---|---|
status | abandoned 或原本的終態 |
錯誤
| 代碼 | HTTP | 訊息 | 處理方式 |
|---|---|---|---|
INVALID_API_KEY | 401 | Unknown API key | 檢查主控台 API key 是否貼錯或已被輪換。 |
AUTH_BAD_SIGNATURE | 401 | Invalid HMAC | path 只用 pathname(不含 query);raw body 與送出位元組必須一致。 |
AUTH_TIMESTAMP_OUT_OF_RANGE | 401 | Invalid HMAC | 伺服器時鐘與 UTC 差不可超過 ±300 秒。 |
CLIENT_DISABLED | 403 | Client is disabled | 客戶已停用,聯絡營運啟用。 |
VALIDATION_ERROR | 400 | invalid JSON / missing fields | 檢查 JSON 與必填欄位。 |
CLIENT_KEY_CORRUPT | 500 | Client secret cannot be decrypted | 聯絡 SuperTerminal 營運輪換金鑰。 |
NOT_FOUND | 404 | order not found | 確認 orderId。 |
注意事項
- 本站會盡力取消付款頁。POST body 為空字串。
POST /api/v1/client/verify
概覽
由 Terminal 代打 GoCash public verify,並把結果寫回訂單。下單仍然只能打 Terminal;verify 是唯一允許碰到 GoCash 的可選步驟。
請求參數(打 Terminal)
| 名稱 | 位置 | 型別 | 必填 | 範例 |
|---|---|---|---|---|
X-Api-Key | header | string | 是 | st_live_… |
X-Timestamp | header | unix seconds | 是 | 1700000000 |
X-Signature | header | hex HMAC-SHA256 | 是 | a1b2… |
secret | body | string | 是 | 回調 verifySecret |
foreignTxnId | body | string | 是 | clientOrderRef |
name | body | string | 否 | 客戶顯示名 |
orderId | body | string | 否 | ord_… |
呼叫範例(Terminal 代驗)
# 先向 SuperTerminal 營運取得 TERMINAL / API_KEY / HMAC_SECRET
TERMINAL="${TERMINAL:?missing TERMINAL — SuperTerminal origin, no trailing slash}"
API_KEY="${API_KEY:?missing API_KEY}"
HMAC_SECRET="${HMAC_SECRET:?missing HMAC_SECRET}"
PATHNAME="/api/v1/client/verify"
METHOD="POST"
BODY='{"secret":"XXXX-XXXX","foreignTxnId":"sess-uuid"}'
TS="$(date +%s)"
body_sha() { printf '%s' "$1" | openssl dgst -sha256 | awk '{print $NF}'; }
hmac_hex() { printf '%s' "$2" | openssl dgst -sha256 -hmac "$1" | awk '{print $NF}'; }
BODY_SHA="$(body_sha "$BODY")"
STS="$(printf '%s\n%s\n%s\n%s' "$TS" "$METHOD" "$PATHNAME" "$BODY_SHA")"
SIG="$(hmac_hex "$HMAC_SECRET" "$STS")"
curl -sS -X POST "$TERMINAL$PATHNAME" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
--data-binary "$BODY" \
-H "X-Api-Key: $API_KEY" \
-H "X-Timestamp: $TS" \
-H "X-Signature: $SIG"成功回應
{
"status": 200,
"verifyStatus": "verified_via_terminal",
"result": { "result": "success" }
}回應欄位
| 名稱 | 說明 |
|---|---|
verifyStatus | verified_via_terminal / verified_via_client / already_used / failed |
result | GoCash 原始回應 |
自行打 GoCash(可選,無 HMAC)
POST https://gocash.w8et.com/api/v1/public/verify,JSON 欄位是 secret / name / foreign_txn_id(底線,不是 camelCase)。沒有 API key。
await fetch("https://gocash.w8et.com/api/v1/public/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
secret: "XXXX-XXXX-XXXX-XXXX",
name: "Your Shop",
foreign_txn_id: "sess-uuid",
}),
});錯誤
| 代碼 | HTTP | 訊息 | 處理方式 |
|---|---|---|---|
INVALID_API_KEY | 401 | Unknown API key | 檢查主控台 API key 是否貼錯或已被輪換。 |
AUTH_BAD_SIGNATURE | 401 | Invalid HMAC | path 只用 pathname(不含 query);raw body 與送出位元組必須一致。 |
AUTH_TIMESTAMP_OUT_OF_RANGE | 401 | Invalid HMAC | 伺服器時鐘與 UTC 差不可超過 ±300 秒。 |
CLIENT_DISABLED | 403 | Client is disabled | 客戶已停用,聯絡營運啟用。 |
VALIDATION_ERROR | 400 | invalid JSON / missing fields | 檢查 JSON 與必填欄位。 |
CLIENT_KEY_CORRUPT | 500 | Client secret cannot be decrypted | 聯絡 SuperTerminal 營運輪換金鑰。 |
NOT_FOUND | 404 | order not found | orderId 不屬於此客戶時。 |
注意事項
- foreignTxnId / foreign_txn_id 應對應你當初的 clientOrderRef。
- 不要對 GoCash 下單、購券或 redeem;那不是你的金鑰範圍。
回調接收規格
概覽
主控台 callback_url 是你的 base URL,可含自訂網域與路徑前綴,例如 https://pay.example.com/hooks/gocash。Terminal 去掉尾斜線後追加下列固定子路徑。請在此前綴下實作全部 5 個 POST endpoint。
| 名稱 | 位置 | 型別 | 必填 | 範例 |
|---|---|---|---|---|
存款完成 | path | POST | 是 | /api/internal/gocash/voucher-deposit |
出款完成 | path | POST | 是 | /api/internal/gocash/withdraw-complete |
出款失敗 | path | POST | 是 | /api/internal/gocash/withdraw-fail |
存款過期 | path | POST | 是 | /api/internal/gocash/deposit-expire |
存款重開 | path | POST | 是 | /api/internal/gocash/deposit-reopen |
實際 URL 例:https://pay.example.com/hooks/gocash/api/internal/gocash/voucher-deposit
驗簽必須用 canonical 固定子路徑(例如 /api/internal/gocash/voucher-deposit),不是你伺服器上的完整路徑。有前綴的客戶若用完整 path 驗簽會失敗。
回調 headers 只有 X-Timestamp 與 X-Signature,沒有 X-Api-Key。用呼叫 API 的三 header 規則去驗,一定錯。
請求參數(Terminal → 你,通用)
| 名稱 | 位置 | 型別 | 必填 | 範例 |
|---|---|---|---|---|
X-Timestamp | header | unix seconds | 是 | 1700000000 |
X-Signature | header | hex | 是 | … |
X-Api-Key | header | — | 否(不會帶) | 不要要求此 header |
1. 存款完成 — 上分
路徑:/api/internal/gocash/voucher-deposit。玩家已付款且 Terminal 接受。你應對 sessionId(= clientOrderRef)上分,並以同一 ref 做冪等,避免重試重複加款。
{
"sessionId": "sess-uuid",
"userId": "player-uuid",
"amountMinor": 50000,
"currency": "CNY",
"paymentMethod": "wechat",
"voucherNo": "GC-260902-123456",
"verifySecret": "XXXX-XXXX-XXXX-XXXX"
}| 名稱 | 說明 |
|---|---|
sessionId | 你的 clientOrderRef |
userId | 你當初的 userRef(可能為空) |
amountMinor | 下單金額(最小單位) |
voucherNo / verifySecret | 可選再打 Terminal 或 GoCash verify |
2. 出款完成 — 關單扣款
路徑:/api/internal/gocash/withdraw-complete。出款已完成。你應關閉出款單並確認已扣玩家餘額(若下單時已預扣,此處核銷即可)。
{
"sessionId": "sess-uuid",
"agentTxId": "ord_…",
"voucherNo": "GC-260902-654321"
}| 名稱 | 說明 |
|---|---|
sessionId | 你的 clientOrderRef |
agentTxId | Terminal 訂單號 order.id(ord_…) |
voucherNo | 出款憑證,可能為 null |
3. 出款失敗 — 關單並退回預扣
路徑:/api/internal/gocash/withdraw-fail。你應關單;若下單時已預扣,在此 退回玩家餘額。同一 sessionId 重送不得退兩次。
{
"sessionId": "sess-uuid",
"errorCode": "WITHDRAW_FAILED",
"errorMessage": "cashier reported withdraw failure"
}4. 存款過期 — 關單,允許玩家重開
路徑:/api/internal/gocash/deposit-expire。連結過期。你應把該筆標為過期/待重試,不要上分。稍後可能收到重開回調。
{
"sessionId": "sess-uuid",
"errorCode": "QUOTE_EXPIRED",
"errorMessage": "payment link expired"
}5. 存款重開 — 允許同一單繼續付
路徑:/api/internal/gocash/deposit-reopen。過期/失敗單被重開。你應保持同一 clientOrderRef 未上分,讓玩家用新連結付款;之後仍可能收到存款完成。
{
"sessionId": "sess-uuid"
}驗簽範例(回調,無 API key)
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");
}
function verifyCallback({ secret, timestamp, canonicalPath, rawBody, signature }) {
const expected = sign({ secret, timestamp, method: "POST", path: canonicalPath, rawBody });
const a = Buffer.from(expected, "hex");
const b = Buffer.from(String(signature || "").toLowerCase(), "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express / Fastify:用 raw body parser,不要 JSON.parse 後再 JSON.stringify
app.post("/hooks/gocash/api/internal/gocash/voucher-deposit", (req, res) => {
const rawBody = req.rawBody; // Buffer 或 utf8 字串,必須是網路上收到的原樣
const ok = verifyCallback({
secret: process.env.HMAC_SECRET,
timestamp: req.get("X-Timestamp"),
canonicalPath: "/api/internal/gocash/voucher-deposit",
rawBody: Buffer.isBuffer(rawBody) ? rawBody.toString("utf8") : rawBody,
signature: req.get("X-Signature"),
});
if (!ok) return res.status(401).json({ error: { code: "AUTH_BAD_SIGNATURE", message: "Invalid HMAC" } });
const payload = JSON.parse(rawBody);
// payload.sessionId === 你的 clientOrderRef;此處上分,必須冪等
return res.status(200).json({ ok: true });
});
// 回調只有 X-Timestamp + X-Signature,沒有 X-Api-Key。
// 驗簽 path 是固定子路徑 /api/internal/gocash/voucher-deposit,
// 不是 https://pay.example.com/hooks/gocash/... 的完整 pathname。
成功回應
你必須在約 10 秒內回 HTTP 2xx。非 2xx 會以 2s→60s 退避、每分鐘 cron 重試,最多 10 次後標記 failed。
回應欄位
無強制 JSON 形狀,2xx 即可。
錯誤與可靠性
你回非 2xx 時 Terminal 會重送同一 payload。你的 endpoint 必須冪等。
paid_pending 不會打回調。金額不符或 voucher 與其他訂單重複時,Terminal 會把單停在人工確認。這不是失敗,不要對玩家顯示「支付失敗」或自動退款。請輪詢查單;營運放行後會再送存款完成回調。
漏回調、回調耗盡 10 次、或處於 paid_pending:一律 GET /api/v1/client/orders/:id 直到終態。
- callback_url 不得含 query string,必須是 http(s)。
- 客戶停用後,已建立訂單的回調仍會送出。
訂單狀態機
created → dispatching → link_ready → paid_pending → completed。也可能 expired、failed、abandoned、stale_completion。
| 名稱 | 說明 |
|---|---|
dispatching | 正在取付款連結,可能換通道多次 |
link_ready | 已有 paymentUrl,等玩家付款 |
paid_pending | 通道回報付款,但金額不符或憑證重複,營運對帳中。此時不回調、不是失敗。繼續查單。 |
completed | 已完成並嘗試回調(上分/核銷出款) |
expired | 連結過期;之後可能重開回到 link_ready |
failed / abandoned | 失敗或客戶放棄。出款失敗應退回預扣。 |
stale_completion | 已被換通道取代的完成回報遲到,需人工對帳,不要自動上分 |