SuperTerminal 文件

對客平台對接文檔

接入 SuperTerminal。只打本站下單;含可跑簽名範例與五條回調契約。公開頁面,可列印 PDF。

快速開始

SuperTerminal 是你的唯一下單對象。你用主控台核發的 API key + HMAC 呼叫本站;支付完成後 Terminal 會 POST 到你設定的 callback_url。

禁止直連:不要把訂單打到支付代理或 GoCash。只應呼叫 SuperTerminal。可選的憑證核對見下方 verify。

向營運索取

名稱位置型別必填範例
TERMINALenvorigin是https://your-terminal.example(無結尾斜線)
API_KEYenvstring是主控台客戶檔的 API key(X-Api-Key)
HMAC_SECRETenvstring是同一客戶檔的 HMAC secret(reveal 後複製)
callback_urlconsolehttps URL是https://pay.example.com/hooks/gocash

生產 TERMINAL 就是 SuperTerminal 主控台所在的公開主機,向營運索取。開發/UAT 也由營運提供。金鑰只出現在主控台客戶編輯頁(可揭示/複製),不要寫進前端瀏覽器。

amountMinor

所有金額都是最小貨幣單位的整數。兩位小數幣別(CNY / HKD / USD…)時 50000 = 500.00。不要傳小數、不要傳「元」。

關聯鏈(必須記住)

你的 clientOrderRef 必須唯一,對應 Terminal 訂單 ord_…。回調裡的 sessionId 永遠是你自己的 clientOrderRef。

sessionId:Terminal 打你的回調時,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-Api-Key。Terminal 打你的 webhook 只帶 X-Timestamp + X-Signature。不要用「呼叫 API 的三個 header」去驗回調;也不要因為缺 API key 就拒絕。

可執行的 discover 與回調驗簽見下方對應章節(Node / PHP / Python / curl)。

建議流程

對客平台
SuperTerminal
支付代理
POST /api/v1/client/channels/discover (HMAC)
通道與報價
POST /api/v1/client/checkout (clientOrderRef)
本站向通道取付款連結
付款連結 url
paymentUrl + expiresAt

玩家完成支付

通道回報付款完成
POST {callback_url}/api/internal/gocash/voucher-deposit (sessionId = clientOrderRef)
POST /api/v1/client/verify(可選,代驗憑證)
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 (可選,代驗憑證)
  1. POST /api/v1/client/channels/discover 取得通道與報價。
  2. 玩家選 channel 再選 cashier;可選打 deposit-quote 看加價。
  3. POST /api/v1/client/checkout 以 clientOrderRef 冪等建單,回 paymentUrl。連結由本站當下取得,不是預先註冊。
  4. 新分頁打開連結。過期後可能收到 deposit-expired;通道重開則 deposit-reopened。
  5. 收到 deposit-completed 後上分。可用 POST /api/v1/client/verify 代驗,或自行打 GoCash /api/v1/public/verify。
  6. 可靠性必做:即使有 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-Keyheaderstring是st_live_…
X-Timestampheaderunix seconds是1700000000
X-Signatureheaderhex HMAC-SHA256是a1b2…
typebody"deposit" | "withdraw"是deposit
countrybodystring是HK
currencybodystring否CNY
amountMinorbodyint64否50000
filters.tagsbodystring[]否["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[].channelIdcheckout 必填 channelId
channels[].cashiers[].cashierIdcheckout 必填 cashierId
reasons例如 COUNTRY_NOT_ALLOWED、CURRENCY_NOT_ALLOWED、NO_ELIGIBLE_CASHIER

錯誤

代碼HTTP訊息處理方式
INVALID_API_KEY401Unknown API key檢查主控台 API key 是否貼錯或已被輪換。
AUTH_BAD_SIGNATURE401Invalid HMACpath 只用 pathname(不含 query);raw body 與送出位元組必須一致。
AUTH_TIMESTAMP_OUT_OF_RANGE401Invalid HMAC伺服器時鐘與 UTC 差不可超過 ±300 秒。
CLIENT_DISABLED403Client is disabled客戶已停用,聯絡營運啟用。
VALIDATION_ERROR400invalid JSON / missing fields檢查 JSON 與必填欄位。
CLIENT_KEY_CORRUPT500Client secret cannot be decrypted聯絡 SuperTerminal 營運輪換金鑰。
CATALOG_UNAVAILABLE503GoCash catalog unavailable稍後重試,目錄會自動刷新。

注意事項

  • 空 channels 不一定是 HTTP 錯誤,先看 reasons。
  • 尚未獲指派任何夥伴支付方式時 channels 為空。
  • 簽名與送出必須用同一份 compact JSON;本頁範例已對齊。

POST /api/v1/client/deposit-quote

概覽

向選定或第一個合格通道詢價,回傳加價後金額。不建單。簽名方式與 discover 相同(改 path 與 body)。

請求參數

名稱位置型別必填範例
X-Api-Keyheaderstring是st_live_…
X-Timestampheaderunix seconds是1700000000
X-Signatureheaderhex HMAC-SHA256是a1b2…
amountMinorbodyint64是50000
currencybodystring是CNY
countrybodystring是HK
cashierIdbodyuuid否…
channelIdbodyuuid否…

呼叫範例

# 先向 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_KEY401Unknown API key檢查主控台 API key 是否貼錯或已被輪換。
AUTH_BAD_SIGNATURE401Invalid HMACpath 只用 pathname(不含 query);raw body 與送出位元組必須一致。
AUTH_TIMESTAMP_OUT_OF_RANGE401Invalid HMAC伺服器時鐘與 UTC 差不可超過 ±300 秒。
CLIENT_DISABLED403Client is disabled客戶已停用,聯絡營運啟用。
VALIDATION_ERROR400invalid JSON / missing fields檢查 JSON 與必填欄位。
CLIENT_KEY_CORRUPT500Client secret cannot be decrypted聯絡 SuperTerminal 營運輪換金鑰。
NO_ELIGIBLE_CASHIER409No cashier available for quote換通道或確認該支付方式已指派。
CASHIER_QUOTE_FAILED502Cashier quote failed通道詢價失敗。稍後或換通道重試。

注意事項

  • 金額單位一律最小貨幣單位。

POST /api/v1/client/withdraw-quote

概覽

出款詢價,欄位與存款詢價相同。

請求參數

名稱位置型別必填範例
X-Api-Keyheaderstring是st_live_…
X-Timestampheaderunix seconds是1700000000
X-Signatureheaderhex HMAC-SHA256是a1b2…
amountMinorbodyint64是50000
currencybodystring是CNY
countrybodystring是HK
cashierIdbodyuuid否…
channelIdbodyuuid否…

呼叫範例

# 先向 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_KEY401Unknown API key檢查主控台 API key 是否貼錯或已被輪換。
AUTH_BAD_SIGNATURE401Invalid HMACpath 只用 pathname(不含 query);raw body 與送出位元組必須一致。
AUTH_TIMESTAMP_OUT_OF_RANGE401Invalid HMAC伺服器時鐘與 UTC 差不可超過 ±300 秒。
CLIENT_DISABLED403Client is disabled客戶已停用,聯絡營運啟用。
VALIDATION_ERROR400invalid JSON / missing fields檢查 JSON 與必填欄位。
CLIENT_KEY_CORRUPT500Client secret cannot be decrypted聯絡 SuperTerminal 營運輪換金鑰。
NO_ELIGIBLE_CASHIER409No cashier available for quote換通道或確認該支付方式已指派。
CASHIER_QUOTE_FAILED502Cashier quote failed稍後或換櫃重試。

注意事項

  • 出款同樣走 checkout,type=withdraw。

POST /api/v1/client/checkout

概覽

以 clientOrderRef 冪等建單並由本站取付款連結。相同 ref 且 type/amountMinor/currency/country 一致時回舊單;這四項任一不同回 409 ORDER_REF_CONFLICT。

冪等比對不含 channel / cashier。同一 ref 換櫃重試會拿到舊單(含舊的 paymentUrl)。要換通道請換新的 clientOrderRef。

請求參數

名稱位置型別必填範例
X-Api-Keyheaderstring是st_live_…
X-Timestampheaderunix seconds是1700000000
X-Signatureheaderhex HMAC-SHA256是a1b2…
clientOrderRefbodystring是sess-uuid
typebody"deposit" | "withdraw"是deposit
amountMinorbodyint64是50000
currencybodystring是CNY
countrybodystring是HK
channelIdbodyuuid是discover 回傳
cashierIdbodyuuid是discover 回傳
userRefbodystring建議player-uuid
localebodystring否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"
}

回應欄位

名稱說明
orderIdTerminal 訂單號,用於查單/放棄/輪詢
paymentUrl給玩家開啟的付款頁。非預先註冊,由本站當下取得。
expiresAt連結過期時間
voucherNo完成後才有

錯誤

代碼HTTP訊息處理方式
INVALID_API_KEY401Unknown API key檢查主控台 API key 是否貼錯或已被輪換。
AUTH_BAD_SIGNATURE401Invalid HMACpath 只用 pathname(不含 query);raw body 與送出位元組必須一致。
AUTH_TIMESTAMP_OUT_OF_RANGE401Invalid HMAC伺服器時鐘與 UTC 差不可超過 ±300 秒。
CLIENT_DISABLED403Client is disabled客戶已停用,聯絡營運啟用。
VALIDATION_ERROR400invalid JSON / missing fields檢查 JSON 與必填欄位。
CLIENT_KEY_CORRUPT500Client secret cannot be decrypted聯絡 SuperTerminal 營運輪換金鑰。
NO_ELIGIBLE_CASHIER409Selected channel/cashier is not eligible重新 discover;該支付方式必須已指派。
ORDER_REF_CONFLICT409clientOrderRef already used with different parameters換新的 clientOrderRef。
CASHIER_DISPATCH_FAILED502Cashier did not return a payment link已嘗試 failover;請稍後或換通道。

注意事項

  • 同一客戶的 clientOrderRef 不可重用不同金額。
  • 不同客戶可以使用相同字串的 clientOrderRef,唯一鍵是 (client_id, client_order_ref)。

GET /api/v1/client/orders/:id

概覽

查自己的訂單。id 為 checkout 回傳的 orderId。

漏回調時的必做條款:以指數退避輪詢本端點,直到 status 進入終態(completed / failed / expired / abandoned / stale_completion)。不要只靠 webhook。GET 簽名時 path 為 /api/v1/client/orders/ord_…,body 為空字串,pathname 不含 query。

請求參數

名稱位置型別必填範例
X-Api-Keyheaderstring是st_live_…
X-Timestampheaderunix seconds是1700000000
X-Signatureheaderhex HMAC-SHA256是a1b2…
idpathstring是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_KEY401Unknown API key檢查主控台 API key 是否貼錯或已被輪換。
AUTH_BAD_SIGNATURE401Invalid HMACpath 只用 pathname(不含 query);raw body 與送出位元組必須一致。
AUTH_TIMESTAMP_OUT_OF_RANGE401Invalid HMAC伺服器時鐘與 UTC 差不可超過 ±300 秒。
CLIENT_DISABLED403Client is disabled客戶已停用,聯絡營運啟用。
VALIDATION_ERROR400invalid JSON / missing fields檢查 JSON 與必填欄位。
CLIENT_KEY_CORRUPT500Client secret cannot be decrypted聯絡 SuperTerminal 營運輪換金鑰。
NOT_FOUND404order not found確認 orderId 屬於此 API key。

注意事項

  • 不能查其他客戶的單。
  • paid_pending 時不會收到完成回調,繼續查單直到變成 completed(人工放行後會再打回調)。

POST /api/v1/client/orders/:id/abandon

概覽

放棄進行中訂單。已完成單原樣回傳。出款放棄會回調 withdraw-failed。

請求參數

名稱位置型別必填範例
X-Api-Keyheaderstring是st_live_…
X-Timestampheaderunix seconds是1700000000
X-Signatureheaderhex HMAC-SHA256是a1b2…
idpathstring是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。

回應欄位

名稱說明
statusabandoned 或原本的終態

錯誤

代碼HTTP訊息處理方式
INVALID_API_KEY401Unknown API key檢查主控台 API key 是否貼錯或已被輪換。
AUTH_BAD_SIGNATURE401Invalid HMACpath 只用 pathname(不含 query);raw body 與送出位元組必須一致。
AUTH_TIMESTAMP_OUT_OF_RANGE401Invalid HMAC伺服器時鐘與 UTC 差不可超過 ±300 秒。
CLIENT_DISABLED403Client is disabled客戶已停用,聯絡營運啟用。
VALIDATION_ERROR400invalid JSON / missing fields檢查 JSON 與必填欄位。
CLIENT_KEY_CORRUPT500Client secret cannot be decrypted聯絡 SuperTerminal 營運輪換金鑰。
NOT_FOUND404order not found確認 orderId。

注意事項

  • 本站會盡力取消付款頁。POST body 為空字串。

POST /api/v1/client/verify

概覽

由 Terminal 代打 GoCash public verify,並把結果寫回訂單。下單仍然只能打 Terminal;verify 是唯一允許碰到 GoCash 的可選步驟。

請求參數(打 Terminal)

名稱位置型別必填範例
X-Api-Keyheaderstring是st_live_…
X-Timestampheaderunix seconds是1700000000
X-Signatureheaderhex HMAC-SHA256是a1b2…
secretbodystring是回調 verifySecret
foreignTxnIdbodystring是clientOrderRef
namebodystring否客戶顯示名
orderIdbodystring否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" }
}

回應欄位

名稱說明
verifyStatusverified_via_terminal / verified_via_client / already_used / failed
resultGoCash 原始回應

自行打 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_KEY401Unknown API key檢查主控台 API key 是否貼錯或已被輪換。
AUTH_BAD_SIGNATURE401Invalid HMACpath 只用 pathname(不含 query);raw body 與送出位元組必須一致。
AUTH_TIMESTAMP_OUT_OF_RANGE401Invalid HMAC伺服器時鐘與 UTC 差不可超過 ±300 秒。
CLIENT_DISABLED403Client is disabled客戶已停用,聯絡營運啟用。
VALIDATION_ERROR400invalid JSON / missing fields檢查 JSON 與必填欄位。
CLIENT_KEY_CORRUPT500Client secret cannot be decrypted聯絡 SuperTerminal 營運輪換金鑰。
NOT_FOUND404order not foundorderId 不屬於此客戶時。

注意事項

  • foreignTxnId / foreign_txn_id 應對應你當初的 clientOrderRef。
  • 不要對 GoCash 下單、購券或 redeem;那不是你的金鑰範圍。

回調接收規格

概覽

主控台 callback_url 是你的 base URL,可含自訂網域與路徑前綴,例如 https://pay.example.com/hooks/gocash。Terminal 去掉尾斜線後追加下列固定子路徑。請在此前綴下實作全部 5 個 POST endpoint。

名稱位置型別必填範例
存款完成pathPOST是/api/internal/gocash/voucher-deposit
出款完成pathPOST是/api/internal/gocash/withdraw-complete
出款失敗pathPOST是/api/internal/gocash/withdraw-fail
存款過期pathPOST是/api/internal/gocash/deposit-expire
存款重開pathPOST是/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-Timestampheaderunix seconds是1700000000
X-Signatureheaderhex是…
X-Api-Keyheader—否(不會帶)不要要求此 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
agentTxIdTerminal 訂單號 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已被換通道取代的完成回報遲到,需人工對帳,不要自動上分