ST Pay 商戶 APIST Pay 商戶 API
指南
API
錯誤碼
商戶後台
AI 協助對接
  • 繁體中文
  • 简体中文
  • English
指南
API
錯誤碼
商戶後台
AI 協助對接
  • 繁體中文
  • 简体中文
  • English
  • 開始

    • 概覽
    • 請求與回應格式
    • 身分驗證與 IP 白名單
    • 簽章
    • 商戶後台
    • AI 協助對接
  • API

    • 代收建單
    • 代付建單
    • 查單
    • Callback 通知
  • 參考

    • 訂單狀態與金額
    • 重複請求與結果確認
    • 錯誤碼
    • 銀行代碼
    • 上線前檢查清單

簽章

API 請求、API 回應與 Callback 使用同一套簽章規則(HMAC-SHA256)。

計算步驟

  1. 取本文中 sign 以外的所有欄位,排除值為 null 的欄位。空字串 "" 不排除。
  2. 依欄位名稱的字元碼(ASCII)由小到大排序。
  3. 每個欄位寫成 欄位名稱=欄位值,以 & 串接成一個字串(以下稱「待簽字串」)。欄位值照原樣放入,不做 URL 編碼,也不去除空白。
  4. 以 API 驗證金鑰為 key,對待簽字串的 UTF-8 位元組計算 HMAC-SHA256。
  5. 結果轉成小寫十六進位字串(64 個字元),放進 sign 欄位。

常見錯誤

  • 金鑰請直接以 UTF-8 字串當作 key,不要做 Base64 解碼(金鑰看起來像 Base64,但不需要解碼)。
  • 簽章用的值必須與 JSON 實際送出的值完全相同。例如簽 1000.00 卻送出 "1000",簽章就會不符。
  • 不要用 http_build_query、URLSearchParams、urlencode 之類的函式組字串,它們會做 URL 編碼。
  • 中文欄位值(例如 payer_name)請以 UTF-8 計算,不要用 Big5 或其他編碼。
  • 簽章涵蓋本文中所有欄位,包括本文件沒有列出的欄位。請不要在簽章之後再加入欄位。
  • callback_url 也要納入簽章,照原樣放入待簽字串;即使網址的 query 含 ?、&、= 也不要編碼。
  • timestamp 也要納入簽章。請在送出請求當下取時間再簽章,不要重用舊的請求;與 ST Pay 伺服器時間相差超過 5 分鐘會被拒絕(timestamp_expired)。

範例

以下使用範例憑證,您可以用它驗證自己的程式是否算出相同結果(timestamp 為示意值,實際呼叫請用當下時間):

項目範例值
api_identityqp_Xk3vT9aLm2QwE7rB1nZc
API 驗證金鑰Rk8vN2qXw5LpT9sJc3HmY7bF1dGz6AeU4oKi0Wn-QtE

要送出的欄位(簽章前):

{
  "api_identity": "qp_Xk3vT9aLm2QwE7rB1nZc",
  "merchant_order_no": "MO20261013-0001",
  "merchant_user_id": "U10086",
  "payer_name": "王小明",
  "amount": "1000.00",
  "channel_type": "fps",
  "response_mode": "url",
  "callback_url": "https://shop.example.com/qpay/callback",
  "timestamp": "2026-10-13 10:00:00"
}

排序並串接後的待簽字串:

amount=1000.00&api_identity=qp_Xk3vT9aLm2QwE7rB1nZc&callback_url=https://shop.example.com/qpay/callback&channel_type=fps&merchant_order_no=MO20261013-0001&merchant_user_id=U10086&payer_name=王小明&response_mode=url&timestamp=2026-10-13 10:00:00

HMAC-SHA256 結果(即 sign):

566fac06060d54d7450b41b6858b1ddf33df9d37117ab8a8c495ce748469f2b2

程式範例

以下範例都已用上面的範例憑證驗證過,算出的簽章與上方結果相同。驗證 Callback 時,請用收到的全部欄位重新計算,再以固定時間比對。

Python 3
import hmac, hashlib

def qpay_sign(fields: dict, secret: str) -> str:
    items = sorted((k, v) for k, v in fields.items() if k != "sign" and v is not None)
    text = "&".join(f"{k}={v}" for k, v in items)
    return hmac.new(secret.encode("utf-8"), text.encode("utf-8"), hashlib.sha256).hexdigest()

# 驗證 Callback
def qpay_verify(fields: dict, secret: str) -> bool:
    return hmac.compare_digest(qpay_sign(fields, secret), str(fields.get("sign", "")).lower())
Java 17+
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public final class QpaySigner {

    public static String sign(Map<String, String> fields, String secret) throws Exception {
        String text = new TreeMap<>(fields).entrySet().stream()
                .filter(e -> !"sign".equals(e.getKey()) && e.getValue() != null)
                .map(e -> e.getKey() + "=" + e.getValue())
                .collect(Collectors.joining("&"));
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        return HexFormat.of().formatHex(mac.doFinal(text.getBytes(StandardCharsets.UTF_8)));
    }

    /** 驗證 Callback */
    public static boolean verify(Map<String, String> fields, String secret) throws Exception {
        String sign = fields.getOrDefault("sign", "");
        return MessageDigest.isEqual(sign(fields, secret).getBytes(StandardCharsets.UTF_8),
                sign.toLowerCase().getBytes(StandardCharsets.UTF_8));
    }
}
PHP 7.4+
<?php
function qpay_sign(array $fields, string $secret): string
{
    unset($fields['sign']);
    $fields = array_filter($fields, function ($v) { return $v !== null; });
    ksort($fields, SORT_STRING);
    $parts = [];
    foreach ($fields as $k => $v) {
        $parts[] = $k . '=' . $v;   // 不要用 http_build_query,它會做 URL 編碼
    }
    return hash_hmac('sha256', implode('&', $parts), $secret);
}

// 驗證 Callback
function qpay_verify(array $fields, string $secret): bool
{
    return hash_equals(qpay_sign($fields, $secret), strtolower((string) ($fields['sign'] ?? '')));
}
Node.js
const crypto = require('crypto');

function qpaySign(fields, secret) {
  const text = Object.keys(fields)
    .filter((k) => k !== 'sign' && fields[k] !== null && fields[k] !== undefined)
    .sort()
    .map((k) => `${k}=${fields[k]}`)
    .join('&');
  return crypto.createHmac('sha256', secret).update(text, 'utf8').digest('hex');
}

// 驗證 Callback
function qpayVerify(fields, secret) {
  const expected = Buffer.from(qpaySign(fields, secret));
  const actual = Buffer.from(String(fields.sign || '').toLowerCase());
  return expected.length === actual.length && crypto.timingSafeEqual(expected, actual);
}
Go
import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"sort"
	"strings"
)

// 解析回應或 Callback 的 JSON 時也用 map[string]any:值為 null 的欄位會是 nil,不納入簽章
func QpaySign(fields map[string]any, secret string) string {
	keys := make([]string, 0, len(fields))
	for k, v := range fields {
		if k != "sign" && v != nil {
			keys = append(keys, k)
		}
	}
	sort.Strings(keys)
	parts := make([]string, len(keys))
	for i, k := range keys {
		parts[i] = k + "=" + fields[k].(string)
	}
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(strings.Join(parts, "&")))
	return hex.EncodeToString(mac.Sum(nil))
}

// 驗證 Callback
func QpayVerify(fields map[string]any, secret string) bool {
	sign, _ := fields["sign"].(string)
	return hmac.Equal([]byte(QpaySign(fields, secret)), []byte(strings.ToLower(sign)))
}

驗證回應

成功回應(code 為 OK)的 data 帶有 sign,ST Pay 以這次請求用的 API 驗證金鑰計算,規則同上:取 data 中 sign 以外、值不為 null 的所有欄位。

  • 請先驗章,通過後才使用回應內容,特別是代收建單回傳的 account_no(付款人要轉入的帳號)與 payment_url。
  • 驗不過代表回應可能在傳輸途中被竄改:不要把帳號或網址交給付款人,改以查單確認。
  • 驗章可直接用上面的驗證函式,傳入回應的 data。
  • 錯誤回應的 data 為 null,沒有簽章。

完整呼叫範例

curl -X POST 'https://merchant.vincoterra.com/api/v1/collect/orders' \
  -H 'Content-Type: application/json' \
  -d '{
    "api_identity": "qp_Xk3vT9aLm2QwE7rB1nZc",
    "merchant_order_no": "MO20261013-0001",
    "merchant_user_id": "U10086",
    "payer_name": "王小明",
    "amount": "1000.00",
    "channel_type": "fps",
    "response_mode": "url",
    "callback_url": "https://shop.example.com/qpay/callback",
    "timestamp": "2026-10-13 10:00:00",
    "sign": "566fac06060d54d7450b41b6858b1ddf33df9d37117ab8a8c495ce748469f2b2"
  }'
Prev
身分驗證與 IP 白名單
Next
商戶後台