--- url: /index.md --- # 概覽 本文件僅供已簽約商戶使用,請勿公開轉載。 ## 交易類型 ST Pay 提供兩種交易,都由您的伺服器呼叫 API 建立: | 交易 | 資金方向 | 流程 | | --- | --- | --- | | 代收 | 付款人 → ST Pay 收款帳戶 → 您的 ST Pay 餘額 | 您建立代收單,取得收款帳號與**訂單金額**,請付款人轉帳。ST Pay 確認入帳後,扣除手續費的結算金額加入您的餘額,並以 Callback 通知您。 | | 代付 | 您的 ST Pay 餘額 → 收款人 | 您建立代付單並指定收款帳號。ST Pay 出款後,從您的餘額扣除代付金額加手續費,並以 Callback 通知您。 | 支援兩種通道:`fps`(轉數快)與 `bank_transfer`(銀行轉帳)。 ## API 一覽 | API | 用途 | 方法與路徑 | | --- | --- | --- | | [代收建單](./api/collect.md) | 建立代收單,取得付款資訊 | `POST /api/v1/collect/orders` | | [代付建單](./api/payout.md) | 建立代付單 | `POST /api/v1/payout/orders` | | [代收查單](./api/query.md#代收查單) | 查詢代收單目前的狀態與金額 | `POST /api/v1/collect/orders/query` | | [代付查單](./api/query.md#代付查單) | 查詢代付單目前的狀態與金額 | `POST /api/v1/payout/orders/query` | | [Callback](./api/callback.md) | 訂單有結果時,ST Pay 主動通知您 | ST Pay `POST` 到您建單時帶的 `callback_url` | ## 對接流程 1. **提供資料給 ST Pay** * 伺服器出口 IP:呼叫 API 的所有來源 IP,每個都須是單一 IPv4 或 IPv6 位址,不支援網段。 * 後台登入 IP:登入[商戶後台](./guide/backoffice.md)的來源 IP,格式同上。與 API 的 IP 白名單分開設定。 2. **取得 API 憑證與後台帳號**:ST Pay 會交付 `api_identity`(API 身分)與 **API 驗證金鑰**。金鑰只在建立時顯示一次,之後 ST Pay 不再顯示,請立即妥善保存。ST Pay 也會建立商戶主管的後台帳號並交付初始密碼,第一次登入時必須更改。 3. **在測試環境完成串接**:依本文件完成四支 API 與 Callback,並以[上線前檢查清單](./reference/checklist.md)逐項確認。 4. **上線正式環境**:正式環境的網域、憑證、IP 白名單與測試環境完全獨立,需另外申請。 API 與後台登入的 IP 白名單、憑證都由 ST Pay 設定。需要變更時,請聯繫您的 ST Pay 對接窗口;IP 白名單變更後約 1 分鐘生效。 Callback 網址不需要提供給 ST Pay:請在每次建單時以 `callback_url` 帶入,規則見 [Callback 網址](./api/callback.md#callback-網址)。 ## 環境 | 環境 | API 網域 | 商戶後台 | 說明 | | --- | --- | --- | --- | | 測試環境 | 由 ST Pay 對接窗口提供 | 由 ST Pay 對接窗口提供 | 不產生真實金流;使用測試環境專用的憑證 | | 正式環境 | 測試環境完成串接後另行提供 | 測試環境完成串接後另行提供 | 真實金流;使用正式環境專用的憑證 | 完整網址為「網域 + 路徑」,例如 `https://merchant.vincoterra.com/api/v1/collect/orders`。商戶後台與 API 使用同一個網域(只是路徑不同),付款頁則在 `pay.vincoterra.com`。本文件範例一律使用正式環境的實際網域:`merchant.vincoterra.com`(API 與商戶後台)、`pay.vincoterra.com`(付款頁);測試環境的網域由 ST Pay 對接窗口另行提供。 --- --- url: /api/callback.md --- # Callback 通知 訂單有結果時,ST Pay 會 `POST` 到該單建單時帶的 `callback_url`。 ## 什麼時候發送 | 事件 | `status` | 是否發送 | | --- | --- | --- | | 代收成功 | `success` | 發送 | | 代收被拒絕(ST Pay 人工拒絕,或 30 分鐘到期被系統拒絕) | `rejected` | 發送 | | 代收逾時入帳 | `late_credited` | **不發送**,請用查單確認,見[訂單狀態](../reference/status.md#代收單) | | 代付已出款 | `paid` | 發送 | | 代付被拒絕 | `rejected` | 發送 | ## Callback 網址 * 每次建單([代收](./collect.md)、[代付](./payout.md))時以 `callback_url` 帶入,不需要事先提供給 ST Pay。每張單可以不同,例如測試與正式環境各用自己的網址。 * **建單後不能更改**。自動發送與 ST Pay 手動重發都送往該單建單時的網址。若您之後更換網址,舊單的通知仍送往舊網址;舊網址收不到時,請以[查單](./query.md)確認舊單的結果。 * 最多 500 字元,照原樣保存,不做任何轉換。 * `http` 與 `https` 都可以,建議用 `https`:`http` 會以明文傳送訂單號與金額。不論哪一種,都**務必驗證 `sign`**,才能防止偽造或竄改的通知。 * 使用 `https` 時,網站的 HTTPS 憑證(SSL/TLS 憑證)必須由公開的憑證機構簽發、在有效期限內、名稱與網址上的主機名稱相符,而且伺服器要送出完整的憑證鏈(含中繼憑證)。自簽憑證、過期、名稱不符或缺少中繼憑證都視為送達失敗,**不會重試**。 * 不能含帳號密碼(`user:pass@`)、`#`、空白或非 ASCII 字元;中文網域請轉成 punycode(`xn--` 開頭)。可以有 path 與 query。 * 主機名稱只能有英數字、`.`、`-`(不能有底線 `_`),或以 `[ ]` 包住的 IPv6;有寫埠號時須為 1 至 65535(不能只寫 `:`)。 * path 與 query 裡不能直接出現 `|`、`{`、`}`、`^`、`"`、`<`、`>`、`\`、反引號,`%` 後面必須是兩位十六進位數字;這些字元請先做 URL 編碼(例如 `%` 寫成 `%25`)。 * 沒送、空字串或全是空白回 `missing_parameter`。 * 不能是本機或內網位址,測試與正式環境都一樣: * `localhost`,以及 `.localhost`、`.local`、`.localdomain`、`.internal`、`.intranet`、`.lan`、`.corp`、`.home.arpa`、`.private` 結尾的網域,或不含「.」的主機名稱。 * 私有、迴圈、鏈路本地、CGNAT、保留與文件用途、多播的 IP,例如 `10.x.x.x`、`172.16.x.x`–`172.31.x.x`、`192.168.x.x`、`127.x.x.x`、`169.254.x.x`、`100.64.x.x`;IPv6 只接受全球單播位址(`2000::/3`),其中 Teredo `2001::/32`、文件用途 `2001:db8::/32`、6to4 `2002::/16` 也不接受。 * 純數字、十六進位、八進位或省略段數的 IP 寫法,例如 `2130706433`、`0x7f.0.0.1`、`0177.0.0.1`、`127.1`。 * 公網 IP 可以直接寫。 * 不符合以上規則時,建單回 `invalid_callback_url`(格式錯誤、超過 500 字元則回 `invalid_parameter`),不會建單。 * ST Pay **每次發送前**(包括手動重發)會重新解析網域;解析失敗,或解析出的任何一個位址不是公網位址,這次就不發送,視為送達失敗。請確認網域可從公網解析,而且只指向公網 IP。 ## 請求格式 * 方法 `POST`,`Content-Type: application/json`,本文是 JSON 物件,所有值都是字串。 * 包含 `sign`,[簽章](../guide/signature.md)規則與 API 請求相同,使用您的 API 驗證金鑰(見[更換金鑰](../guide/security.md#更換金鑰))。Callback 不含 `api_identity` 與 `timestamp`。 * ST Pay 等待連線最多 5 秒、等待回應最多 10 秒,且**不跟隨重新導向(3xx)**:收到 3xx 視為送達失敗。請直接填最終網址,例如 `http` 轉 `https`、少了結尾的 `/` 都可能造成轉址。 * Callback 內容不含 `callback_url`。 * 請求標頭固定帶 `User-Agent: QPay-Callback/1.0` 與 `Accept: */*`。Callback 網址在 Cloudflare、WAF 或其他防火牆後面時,請放行這個 User-Agent 與 `POST` 請求,否則可能被擋成 403 而送達失敗。 ## 您必須回應的內容 回應 HTTP **2xx**,且回應本文是純文字 `ok`(前後空白會被忽略)。其他任何回應,包括逾時、非 2xx、本文不是 `ok`,都視為**送達失敗**。 ```http HTTP/1.1 200 OK Content-Type: text/plain ok ``` ::: warning Callback 不會自動重試 每張單只會自動發送一次。送達失敗時,只有 ST Pay 人員能手動重發,重發一律送往該單建單時帶的 `callback_url`。因此: * 請不要只依賴 Callback。對於長時間沒有結果的訂單,請定期查單確認。 * 同一張單可能收到不只一次 Callback(手動重發),請以 `order_no` 加 `status` 做冪等處理,已處理過的通知直接回 `ok`。 * Callback 送達失敗不影響交易結果,訂單狀態與餘額不會因此回復。 ::: ## 欄位 | 欄位 | 代收成功 | 代收拒絕 | 代付已出款 | 代付拒絕 | 說明 | | --- | --- | --- | --- | --- | --- | | `order_no` | ✓ | ✓ | ✓ | ✓ | ST Pay 訂單號 | | `merchant_order_no` | ✓ | ✓ | ✓ | ✓ | 商戶單號 | | `status` | `success` | `rejected` | `paid` | `rejected` | 訂單狀態 | | `received_amount` | ✓ | | | | 實收金額 | | `fee` | ✓ | | ✓ | | 手續費 | | `settlement_amount` | ✓ | | | | 結算金額 | | `payable_amount` | | ✓ | | | 訂單金額(含尾數) | | `amount` | | | ✓ | ✓ | 代付金額 | | `finished_at` | ✓ | ✓ | ✓ | ✓ | 完成時間 | | `sign` | ✓ | ✓ | ✓ | ✓ | 簽章 | Callback 不帶拒絕原因,需要時請用查單取得。 代收與代付若帶同一個 `callback_url`,兩者的 `status` 都可能是 `rejected`,而且代收單與代付單的商戶單號可以相同。請以建單時取得的 `order_no` 對應訂單;如果要用商戶單號對應,請讓代收與代付使用不會重複的單號,例如加上不同前綴。 ## 處理建議 1. 解析 JSON,用**收到的全部欄位**(`sign` 以外)重新計算簽章並比對。不要只挑本文件列出的欄位,日後可能新增欄位。 2. 簽章不符就丟棄,不要更新訂單。 3. 以 `order_no` 找到您的訂單;已處理過相同結果時直接回 `ok`。 4. 更新訂單後回 `ok`。耗時的後續處理(出貨、通知會員等)請放到背景執行,避免超過 10 秒。 5. 代收成功請以 `received_amount`(實收金額,或扣除手續費後的 `settlement_amount`)入帳,不要用建單時的金額。實收金額與您預期的金額不同時,請轉人工確認,不要自動入帳。 6. 訂單已是終態(`success`、`paid`、`rejected`)後,又收到較晚到、狀態不同的通知時,不要回退訂單狀態;請記錄下來轉人工,有疑義以[查單](./query.md)結果為準。 ## 範例 以下簽章都是以[範例金鑰](../guide/signature.md#範例)計算的實際值。 ::: code-tabs @tab 代收成功 ```json { "order_no": "C202610131000051230000", "merchant_order_no": "MO20261013-0001", "status": "success", "received_amount": "1000.37", "fee": "25.00", "settlement_amount": "975.37", "finished_at": "2026-10-13 10:12:30", "sign": "49d9d35c3b3a97a55fe60976a331d36913b0d3cd9b82e840e0315c445653d207" } ``` @tab 代收拒絕 ```json { "order_no": "C202610131001178820000", "merchant_order_no": "MO20261013-0002", "status": "rejected", "payable_amount": "2000.58", "finished_at": "2026-10-13 10:31:17", "sign": "cd85bf732776bb2897f6e767dda873ce580330f979be3f812c390ec09af0d2f4" } ``` @tab 代付已出款 ```json { "order_no": "P202610131005224410000", "merchant_order_no": "PO20261013-0001", "status": "paid", "amount": "500.00", "fee": "7.50", "finished_at": "2026-10-13 10:20:41", "sign": "64e3b813fb086b98c1a4e68c2689423ff347336219f182293e5344a3f6853562" } ``` @tab 代付拒絕 ```json { "order_no": "P202610131006403520000", "merchant_order_no": "PO20261013-0002", "status": "rejected", "amount": "3000.00", "finished_at": "2026-10-13 10:25:09", "sign": "bdf0a8031ac0beff801a4b2591793ea6d6edbfd27dbdadce24ab871602f99094" } ``` ::: 代收成功範例的待簽字串: ```text fee=25.00&finished_at=2026-10-13 10:12:30&merchant_order_no=MO20261013-0001&order_no=C202610131000051230000&received_amount=1000.37&settlement_amount=975.37&status=success ``` --- --- url: /api/collect.md --- # 代收建單 ```text POST /api/v1/collect/orders ``` 建立一張代收單,取得要給付款人的付款資訊。 ::: tip 付款人必須轉入「訂單金額」payable_amount,而不是您送出的原始金額 ST Pay 會在原始金額後加上 0.01 至 0.99 的辨識尾數,例如原始金額 1000.00,訂單金額可能是 1000.37。ST Pay 依這個精確金額辨識是哪一張訂單的入帳。如果您用 json 模式自行呈現付款畫面,請清楚顯示訂單金額,並提醒付款人照這個金額轉帳。 ::: * 訂單在建單後 **30 分鐘**失效(`expires_at`)。到期仍未入帳會被系統拒絕,並發送失敗 Callback。 * `response_mode` 決定回傳內容: * `url`:回傳 ST Pay 付款頁網址 `payment_url`,將付款人導向此頁即可。 * `json`:回傳收款帳號資料,由您自行呈現付款畫面。 * 同一會員最多同時有 **10 張**待審批(`pending`)的代收單,超過會回 `user_active_orders_limit`。 ## 請求欄位 | 欄位 | 必填 | 格式 | 說明 | | --- | --- | --- | --- | | `api_identity` | 是 | 字串 | ST Pay 核發的 API 身分 | | `merchant_order_no` | 是 | 1–200 字元,英數字、`_`、`-` | 您的訂單號。在您的代收單中不能重複,不論原單是什麼狀態 | | `merchant_user_id` | 二擇一 | 1–200 字元,英數字、`_`、`-` | 付款會員在您系統中的唯一識別。有送時,以它判斷是否為同一會員 | | `payer_name` | 二擇一 | 1–200 字元,只能有中文、英文字母、空白 | 付款人真實姓名。沒送 `merchant_user_id` 時,以姓名判斷是否為同一會員(不分大小寫,忽略所有空白)。不能含數字或標點,例如 `Chan Tai-man` 的連字號會被拒絕 | | `amount` | 是 | 金額字串 | 原始金額。單筆上下限以這個金額判斷(不含尾數) | | `channel_type` | 是 | `fps` 或 `bank_transfer` | 通道類別 | | `response_mode` | 是 | `url` 或 `json` | 回傳模式,說明見上方 | | `language` | 否 | `zh-TW`、`zh-CN` 或 `en` | 付款頁的語言:繁體中文、簡體中文、英文。沒送或空字串為 `zh-TW`。只影響 `url` 模式的付款頁 | | `callback_url` | 是 | 網址,最多 500 字元 | 這張單的 [Callback](./callback.md) 送往這個網址,建單後不能更改。`http` 或 `https` 都可以,不能是本機或內網位址,見 [Callback 網址](./callback.md#callback-網址) | | `timestamp` | 是 | `yyyy-MM-dd HH:mm:ss` | 送出請求當下的時間(UTC+8),與 ST Pay 伺服器時間相差超過 5 分鐘會被拒絕,見[請求與回應格式](../guide/conventions.md#請求) | | `sign` | 是 | 64 字元小寫十六進位 | 見[簽章](../guide/signature.md) | `merchant_user_id` 與 `payer_name` 至少要送一個;空字串或全是空白都視為沒送。兩個都沒送會回 `missing_member_identity`。兩個都送時,以 `merchant_user_id` 判斷會員,姓名只作顯示用。 ## 請求範例 ```json { "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" } ``` ## 回應欄位 | 欄位 | 出現時機 | 說明 | | --- | --- | --- | | `order_no` | 一律 | ST Pay 訂單號 | | `merchant_order_no` | 一律 | 您送出的商戶單號 | | `status` | 一律 | 固定為 `pending` | | `payment_url` | `url` 模式 | 付款頁網址,見[付款頁](#付款頁) | | `channel_type` | `json` 模式 | `fps` 或 `bank_transfer` | | `bank_name` | `json` 模式且為 `bank_transfer` | 收款銀行名稱 | | `bank_code` | `json` 模式且為 `bank_transfer` | 收款銀行代碼(字串,保留開頭的 0) | | `account_no` | `json` 模式 | 完整收款帳號 | | `account_name` | `json` 模式 | 收款戶名 | | `payable_amount` | 一律 | 訂單金額,等於原始金額加辨識尾數。付款人須轉入這個金額 | | `expires_at` | 一律 | 失效時間,等於建單時間加 30 分鐘 | | `sign` | 一律 | 回應簽章,以這次請求用的金鑰計算,見[驗證回應](../guide/signature.md#驗證回應)。驗章通過才使用 `account_no`、`payment_url` | ### 回應範例:url 模式 ```json { "code": "OK", "message": "成功", "data": { "order_no": "C202610131000051230000", "merchant_order_no": "MO20261013-0001", "status": "pending", "payment_url": "https://pay.vincoterra.com/pay/QzIwMjYxMDEzMTAwMDA1MTIzMDAwMA.v3Kd9QeLr7Zp1sWm4XyT8bN2cF6hJ0aG5uE-iOkRtYw", "payable_amount": "1000.37", "expires_at": "2026-10-13 10:30:05", "sign": "7e876d9780d033c6224f52015eb15e1d1a9bbda9b75764fff0cdbf3d36b183ee" } } ``` ### 回應範例:json 模式、銀行轉帳 請求為 `"merchant_order_no": "MO20261013-0002"`、`"payer_name": "Chan Tai Man"`、`"amount": "2000.00"`、`"channel_type": "bank_transfer"`、`"response_mode": "json"`、`"callback_url": "https://shop.example.com/qpay/callback"`、`"timestamp": "2026-10-13 10:01:17"`,簽章為 `e20f45942090246543ab7bd95a1fecd52011699382a93637529c2aa987767848`。 ```json { "code": "OK", "message": "成功", "data": { "order_no": "C202610131001178820000", "merchant_order_no": "MO20261013-0002", "status": "pending", "channel_type": "bank_transfer", "bank_name": "The Hongkong and Shanghai Banking Corporation Limited", "bank_code": "004", "account_no": "123-456789-001", "account_name": "WONG KA YAN", "payable_amount": "2000.58", "expires_at": "2026-10-13 10:31:17", "sign": "4cd29b8f52d256516363af07eccfb821caaef52a10777cdc5495b809447aa175" } } ``` json 模式且通道為 `fps` 時,沒有 `bank_name` 與 `bank_code`,`account_no` 為 FPS 收款帳號。 ## 付款頁 使用 `url` 模式時: * 頁面顯示訂單金額、通道類別、收款帳號與戶名、失效時間;銀行轉帳另外顯示銀行名稱與代碼。 * 訂單金額、銀行代碼、收款帳號與戶名旁有「複製」鍵。 * 訂單一旦結束(成功、拒絕、逾時),頁面只會顯示「訂單已結束」,不再顯示收款資訊。付款人停留在頁面上時會自動換成這個畫面,不必重新整理。 * 頁面語言依建單時的 `language` 顯示,付款人無法切換。 * 網址本身就是存取憑證,持有網址就能看到完整收款帳號。請只提供給該筆訂單的付款人,不要寫進公開的日誌、分析工具或分享連結。 * 請完整使用回傳的網址,不要修改或自行組合,任何改動都會讓頁面顯示「找不到頁面」。 ## 可能的錯誤 `invalid_signature`、`timestamp_expired`、`ip_not_allowed`、`merchant_disabled`、`missing_parameter`、`invalid_parameter`、`invalid_amount`、`missing_member_identity`、`invalid_callback_url`、`amount_out_of_range`、`merchant_order_no_duplicate`、`user_active_orders_limit`、`channel_full`、`no_available_card`、`internal_error`。說明見[錯誤碼](../reference/errors.md)。 --- --- url: /api/payout.md --- # 代付建單 ```text POST /api/v1/payout/orders ``` 建立一張代付單,由 ST Pay 從您的餘額付款給指定收款人。 * 建單時會檢查**可用餘額 ≥ 代付金額 + 手續費**,不足就回 `insufficient_balance`,不會建單。 * 建單**不會凍結或預留**餘額。ST Pay 實際出款時才扣款,並會再檢查一次餘額。若您同時建立多張代付單,合計金額可能超過餘額;出款時餘額不足的單會維持待審批,並可能被拒絕。 * 手續費率在建單當下鎖定,之後費率調整不影響這張單。建單回應不包含手續費,出款後可從 Callback 或查單取得。 ## 請求欄位 | 欄位 | 必填 | 格式 | 說明 | | --- | --- | --- | --- | | `api_identity` | 是 | 字串 | ST Pay 核發的 API 身分 | | `merchant_order_no` | 是 | 1–200 字元,英數字、`_`、`-` | 您的訂單號。在您的代付單中不能重複,代收與代付各自獨立計算 | | `amount` | 是 | 金額字串 | 代付金額,也是手續費的計算基礎 | | `channel_type` | 是 | `fps` 或 `bank_transfer` | 通道類別 | | `account_no` | 是 | 依通道,見[收款帳號格式](#收款帳號格式) | 收款帳號 | | `account_name` | 是 | 1–200 字元 | 收款人姓名或戶名 | | `bank_name` | 銀行轉帳必填 | 1–100 字元 | 收款銀行名稱。FPS 不可帶 | | `bank_code` | 銀行轉帳必填 | 字串 | 收款銀行代碼,須為[銀行代碼](../reference/banks.md)清單中的值,保留開頭的 0。FPS 不可帶 | | `callback_url` | 是 | 網址,最多 500 字元 | 這張單的 [Callback](./callback.md) 送往這個網址,建單後不能更改。`http` 或 `https` 都可以,不能是本機或內網位址,見 [Callback 網址](./callback.md#callback-網址) | | `timestamp` | 是 | `yyyy-MM-dd HH:mm:ss` | 送出請求當下的時間(UTC+8),與 ST Pay 伺服器時間相差超過 5 分鐘會被拒絕,見[請求與回應格式](../guide/conventions.md#請求) | | `sign` | 是 | 64 字元小寫十六進位 | 見[簽章](../guide/signature.md) | ### 收款帳號格式 | 通道 | `account_no` 接受的格式 | | --- | --- | | `fps` | 以下三種擇一,放在同一個欄位:・手機號碼:8 位數字,例如 `91234567`・Email:最長 200 字元,只能包含英數字與 `@ . _ -`;恰好一個 `@` 且前後都有字元,`@` 後面要有 `.`,例如 `chan.taiman@example.com`・FPS 識別碼:1 至 50 位數字 | | `bank_transfer` | 數字與連字號 `-`,1 至 50 字元,例如 `012-345678-001` | FPS 單帶了 `bank_name` 或 `bank_code` 會回 `invalid_parameter`;銀行轉帳單缺少其中任何一個會回 `missing_parameter`。 ## 請求範例 ::: code-tabs @tab FPS ```json { "api_identity": "qp_Xk3vT9aLm2QwE7rB1nZc", "merchant_order_no": "PO20261013-0001", "amount": "500.00", "channel_type": "fps", "account_no": "91234567", "account_name": "陳大文", "callback_url": "https://shop.example.com/qpay/callback", "timestamp": "2026-10-13 10:05:22", "sign": "693c80aaf437abcd6dbf60f8aed9e4eda09deed052977fa63cb9b1768150044f" } ``` @tab 銀行轉帳 ```json { "api_identity": "qp_Xk3vT9aLm2QwE7rB1nZc", "merchant_order_no": "PO20261013-0002", "amount": "3000.00", "channel_type": "bank_transfer", "account_no": "012-345678-001", "account_name": "CHAN TAI MAN", "bank_name": "Bank of China (Hong Kong) Limited", "bank_code": "012", "callback_url": "https://shop.example.com/qpay/callback", "timestamp": "2026-10-13 10:06:40", "sign": "44aadf0a2be821c13c1aef71d8cc7cfc6cf012d3b15f543d2650cdf762002a2b" } ``` ::: ## 回應欄位 | 欄位 | 說明 | | --- | --- | | `order_no` | ST Pay 訂單號 | | `merchant_order_no` | 您送出的商戶單號 | | `status` | 固定為 `pending` | | `amount` | 代付金額 | | `sign` | 回應簽章,以這次請求用的金鑰計算,見[驗證回應](../guide/signature.md#驗證回應) | ```json { "code": "OK", "message": "成功", "data": { "order_no": "P202610131005224410000", "merchant_order_no": "PO20261013-0001", "status": "pending", "amount": "500.00", "sign": "e2fe8e5d430ba4bf8a1003b0d7fc8eabcc5d7d974ceda5135783d1aecfa44c8a" } } ``` ## 可能的錯誤 `invalid_signature`、`timestamp_expired`、`ip_not_allowed`、`merchant_disabled`、`missing_parameter`、`invalid_parameter`、`invalid_amount`、`invalid_callback_url`、`amount_out_of_range`、`merchant_order_no_duplicate`、`insufficient_balance`、`internal_error`。說明見[錯誤碼](../reference/errors.md)。 --- --- url: /api/query.md --- # 查單 查詢訂單目前的狀態與金額。以下情況請用查單確認結果: * 沒收到 Callback。 * 代收單被[逾時入帳](../reference/status.md#代收單)(不發 Callback)。 * 建單請求逾時,或收到 `internal_error`。待付款的單會一併回傳付款資訊,可以繼續讓付款人付這張單(見下方)。 * 建單回 `merchant_order_no_duplicate`,想知道原單狀態。 查單是唯讀操作,可以重複呼叫。 ## 請求欄位 代收查單與代付查單的請求欄位相同: | 欄位 | 必填 | 說明 | | --- | --- | --- | | `api_identity` | 是 | ST Pay 核發的 API 身分 | | `order_no` | 二擇一 | ST Pay 訂單號 | | `merchant_order_no` | 二擇一 | 您的商戶單號 | | `timestamp` | 是 | 送出請求當下的時間(UTC+8,`yyyy-MM-dd HH:mm:ss`),與 ST Pay 伺服器時間相差超過 5 分鐘會被拒絕,見[請求與回應格式](../guide/conventions.md#請求) | | `sign` | 是 | 見[簽章](../guide/signature.md) | * 兩個都沒送:回 `missing_parameter`。 * 兩個都送:必須指向同一張單,指向不同單回 `query_conflict`;其中一個查不到則回 `order_not_found`。 * 只能查到您自己的單。訂單不存在、屬於其他商戶,或已超過 6 個月被刪除,一律回 `order_not_found`。 可能的錯誤:`invalid_signature`、`timestamp_expired`、`ip_not_allowed`、`missing_parameter`、`invalid_parameter`、`query_conflict`、`order_not_found`、`internal_error`。 ## 代收查單 ```text POST /api/v1/collect/orders/query ``` ### 請求範例 ```json { "api_identity": "qp_Xk3vT9aLm2QwE7rB1nZc", "merchant_order_no": "MO20261013-0001", "timestamp": "2026-10-13 10:35:00", "sign": "c29089b08a6fdcd3d9c17a404cae90a4aaa0a79210778e53997e4f9f43bb6462" } ``` ### 回應欄位 | 欄位 | 說明 | | --- | --- | | `order_no` | ST Pay 訂單號 | | `merchant_order_no` | 商戶單號 | | `channel_type` | `fps` 或 `bank_transfer` | | `status` | `pending`、`success`、`rejected`、`late_credited`,見[訂單狀態](../reference/status.md) | | `amount` | 原始金額 | | `payable_amount` | 訂單金額(原始金額加尾數) | | `received_amount` | 實收金額,`success`、`late_credited` 才有值,其他狀態為 `null` | | `fee` | 手續費,`success`、`late_credited` 有值;`rejected` 為 `"0.00"`;`pending` 為 `null` | | `settlement_amount` | 結算金額(加入您餘額的金額),`success`、`late_credited` 才有值,其他狀態為 `null` | | `created_at` | 建單時間 | | `finished_at` | 完成時間,`pending` 為 `null` | | `reject_reason` | `rejected` 時的拒絕原因文字:ST Pay 人工拒絕時為所選原因(可能未選,為 `null`),系統逾時拒絕時為「訂單已逾時」。這是給人看的文字,不是代碼。其他狀態請忽略這個欄位 | | `payment_url` | 付款頁網址,與建單回應相同。只有**待付款且還沒到失效時間**、建單時用 `url` 模式的單才有 | | `bank_name`、`bank_code` | 銀行名稱與代碼。只有待付款且還沒到失效時間、建單時用 `json` 模式且通道為 `bank_transfer` 的單才有 | | `account_no`、`account_name` | 完整收款帳號與戶名(或收款姓名)。只有待付款且還沒到失效時間、建單時用 `json` 模式的單才有 | | `expires_at` | 失效時間。只有待付款且還沒到失效時間的單才有 | | `sign` | 回應簽章,以這次請求用的金鑰計算,見[驗證回應](../guide/signature.md#驗證回應) | 建單請求逾時拿不到回應時,用商戶單號查單:查到 `pending` 且帶有上述付款資訊,就可以把付款人導向 `payment_url`(或顯示收款帳號),繼續付這張單,不必換新的商戶單號重建。失效時間照建單時的 `expires_at`,不會延長;過了失效時間或單已結束,就不再回傳付款資訊。請先驗章,通過才使用這些資訊。 ### 回應範例 ::: code-tabs @tab 待付款(url 模式) ```json { "code": "OK", "message": "成功", "data": { "order_no": "C202610131000051230000", "merchant_order_no": "MO20261013-0001", "channel_type": "fps", "status": "pending", "amount": "1000.00", "payable_amount": "1000.37", "received_amount": null, "fee": null, "settlement_amount": null, "created_at": "2026-10-13 10:00:05", "finished_at": null, "reject_reason": null, "payment_url": "https://pay.vincoterra.com/pay/QzIwMjYxMDEzMTAwMDA1MTIzMDAwMA.v3Kd9QeLr7Zp1sWm4XyT8bN2cF6hJ0aG5uE-iOkRtYw", "expires_at": "2026-10-13 10:30:05", "sign": "8cba06866fc16ea3b585309cd13d851d7bda49bf2e95605275f2d99047620487" } } ``` @tab 成功 ```json { "code": "OK", "message": "成功", "data": { "order_no": "C202610131000051230000", "merchant_order_no": "MO20261013-0001", "channel_type": "fps", "status": "success", "amount": "1000.00", "payable_amount": "1000.37", "received_amount": "1000.37", "fee": "25.00", "settlement_amount": "975.37", "created_at": "2026-10-13 10:00:05", "finished_at": "2026-10-13 10:12:30", "reject_reason": null, "sign": "889c767037b6748de1d9fa399129b97012c85f3599a0cce9aff289cf79477626" } } ``` @tab 系統逾時拒絕 ```json { "code": "OK", "message": "成功", "data": { "order_no": "C202610131001178820000", "merchant_order_no": "MO20261013-0002", "channel_type": "bank_transfer", "status": "rejected", "amount": "2000.00", "payable_amount": "2000.58", "received_amount": null, "fee": "0.00", "settlement_amount": null, "created_at": "2026-10-13 10:01:17", "finished_at": "2026-10-13 10:31:17", "reject_reason": "訂單已逾時", "sign": "6a99a7a5085900260830aee441271fb2a0e94d88d33e49b15bfb5a46d6a6e05e" } } ``` @tab 查無此單 ```json { "code": "order_not_found", "message": "查無此單", "data": null } ``` ::: ## 代付查單 ```text POST /api/v1/payout/orders/query ``` ### 請求範例 ```json { "api_identity": "qp_Xk3vT9aLm2QwE7rB1nZc", "order_no": "P202610131005224410000", "timestamp": "2026-10-13 10:40:00", "sign": "adae0ba479bd688b63425e34dcb77cbf5114b34bee68e266ba045ea69748f2b9" } ``` ### 回應欄位 | 欄位 | 說明 | | --- | --- | | `order_no` | ST Pay 訂單號 | | `merchant_order_no` | 商戶單號 | | `channel_type` | `fps` 或 `bank_transfer` | | `status` | `pending`、`paid`、`rejected`,見[訂單狀態](../reference/status.md) | | `amount` | 代付金額 | | `fee` | 手續費,`paid` 有值;`rejected` 為 `"0.00"`;`pending` 為 `null` | | `total_deducted` | 實扣總額(代付金額加手續費),`paid` 才有值,其他狀態為 `null` | | `created_at` | 建單時間 | | `finished_at` | 完成時間,`pending` 為 `null` | | `reject_reason` | `rejected` 時的拒絕原因文字(可能未選,為 `null`)。其他狀態請忽略這個欄位 | | `sign` | 回應簽章,以這次請求用的金鑰計算,見[驗證回應](../guide/signature.md#驗證回應) | ### 回應範例 ```json { "code": "OK", "message": "成功", "data": { "order_no": "P202610131005224410000", "merchant_order_no": "PO20261013-0001", "channel_type": "fps", "status": "paid", "amount": "500.00", "fee": "7.50", "total_deducted": "507.50", "created_at": "2026-10-13 10:05:22", "finished_at": "2026-10-13 10:20:41", "reject_reason": null, "sign": "67d2dfa30d5df0d5456f706400e22faaf5acbb22b164885d5e08f1102e8538a7" } } ``` --- --- url: /guide/backoffice.md --- # 商戶後台 除了 API,ST Pay 也提供商戶後台,讓您查詢訂單、核對餘額與管理員工帳號。後台不能建單,也不能導出資料;建單一律透過 API。 ## 登入 | 項目 | 說明 | | --- | --- | | 網址 | 由 ST Pay 對接窗口提供(與 API 的網域不同);測試環境與正式環境的網域不同,正式環境的網域在測試環境完成串接後另行提供。兩個環境的帳號各自獨立。 | | 登入資料 | 帳號、密碼、4 位數圖形驗證碼。看不清楚驗證碼時,點圖片換一張。 | | 來源 IP | 只能從 ST Pay 為您設定的**後台登入白名單** IP 登入(與 API 白名單是兩份不同的清單)。白名單是空的就無法登入。需要新增或變更時,請聯繫您的 ST Pay 對接窗口。 | | 第一個帳號 | 由 ST Pay 建立商戶主管帳號並交付初始密碼。 | | 密碼 | 至少 6 字元,須包含英文字母與數字,不得與帳號相同。初始密碼與被重設的密碼,第一次登入時必須先更改,不能略過,更改前無法使用其他功能;新密碼不得與目前的密碼相同。之後可從右上角的帳號選單「更改密碼」。 | | 鎖定 | 密碼連續錯誤 5 次,帳號會被鎖定(驗證碼錯誤不計次;更改密碼時舊密碼輸入錯誤也計入)。商戶員工由商戶主管解除,商戶主管請聯繫 ST Pay。 | | 忘記密碼 | 沒有自助重設。商戶員工由商戶主管重設,商戶主管請聯繫 ST Pay。 | | 自動登出 | 閒置 30 分鐘自動登出;登入滿 12 小時須重新登入;帳號被停用時立即登出;來源 IP 被移出後台登入白名單(或換到白名單外的 IP)時也立即登出。 | | 語言 | 右上角可切換繁體中文、简体中文、English。 | 商戶被 ST Pay 停用時,只是不能再建單;仍可登入後台,也可以用 API 查單。 ## 角色與頁面 | 頁面 | 商戶主管 | 商戶員工 | 內容 | | --- | --- | --- | --- | | 訂單查詢 | 可用 | 可用 | 查詢自己商戶的代收單與代付單 | | 餘額流水 | 可用 | 不可用 | 目前可用餘額與每筆餘額異動 | | 員工帳號 | 可用 | 不可用 | 建立與管理商戶員工帳號 | 商戶主管可以再關閉個別員工的頁面或「顯示完整帳號」權限(見[員工帳號](#員工帳號))。 ## 訂單查詢 * 分「代收單」「代付單」兩個頁籤,最新的單在最前面,一次最多列出最新的 500 筆。 * 可依狀態、建立時間、訂單號(完整)、商戶單號、通道類別篩選;建立時間預設為當日(00:00:00 至 23:59:59),可自行調整或清空,按「重設」回到當日。畫面不會自動刷新,請按「重新整理」或「查詢」。 * 只列出保存期限(6 個月)內的單。 * 狀態的顯示文字與 API 的 `status` 對應如下(另見[訂單狀態](../reference/status.md)): | `status` | 後台顯示 | | --- | --- | | `pending` | 待審批 | | `success` | 成功 | | `rejected` | 已拒絕 | | `late_credited` | 逾時入帳 | | `paid` | 已代付 | * 欄位名稱與 API 相同:訂單金額 `payable_amount`、原始金額 `amount`、實收金額 `received_amount`、手續費 `fee`、結算金額 `settlement_amount`、代付金額 `amount`、實扣總額 `total_deducted`。 * **付款人姓名只在後台顯示**,查單 API 與 Callback 都不會回傳。 * 帳號預設遮罩(只顯示末 4 碼)。代收單的收款帳號可以點兩下顯示完整帳號,一次只顯示一筆,且 ST Pay 會留下紀錄;代付收款人帳號在商戶後台只顯示遮罩。 * 點「詳情」可看到拒絕原因與 **回調發送紀錄**:每次發送的時間、結果(成功或失敗)與觸發方式(自動或手動)。送達失敗不會自動重試,需要重發時請聯繫 ST Pay(見 [Callback](../api/callback.md))。 ## 餘額流水 頂部顯示目前的可用餘額,下方依時間由新到舊列出每筆異動,一次最多列出最新的 1000 筆;可依時間與異動類型篩選,時間預設為當日。流水不能修改或刪除。 | 異動類型 | 何時產生 | 方向 | | --- | --- | --- | | 代收入帳 | 代收單入帳成功(`success`),金額為結算金額 | 增加 | | 逾時入帳 | 代收單轉為 `late_credited`,金額為結算金額。**這種情況不發 Callback**,請以此核對 | 增加 | | 代付出款 | 代付單出款(`paid`),金額為實扣總額 | 扣減 | | 人工增加、人工扣減 | ST Pay 人工調整餘額;「原因/備註」欄是 ST Pay 填寫的說明 | 增加或扣減 | 每筆都有調整前餘額、調整後餘額與關聯訂單號,可用來與您的帳務逐筆核對。 ## 員工帳號 商戶主管可以管理自己商戶的商戶員工帳號: | 操作 | 說明 | | --- | --- | | 建立帳號 | 輸入帳號與初始密碼(規則同上),員工第一次登入時必須更改密碼。帳號只能用英文字母、數字與 `.` `_` `-`,以英數開頭,最多 64 字元。 | | 停用 | 停用後無法登入,已登入的連線立即中斷,**不能再改回正常**。 | | 解除鎖定 | 員工密碼連續錯誤 5 次被鎖定時使用。 | | 重設密碼 | 輸入新的初始密碼,原密碼立即失效,員工下次登入時必須更改。 | | 權限 | 可關閉該員工的「訂單查詢」頁面,或「顯示完整帳號」操作,儲存後立即生效。 | 商戶主管帳號由 ST Pay 建立、停用、解除鎖定與重設密碼。由 ST Pay 建立的帳號,清單的建立人顯示「ST Pay」。 --- --- url: /guide/conventions.md --- # 請求與回應格式 ## 請求 * 一律使用 HTTPS 與 `POST`,`Content-Type: application/json`,本文以 UTF-8 編碼。 * 本文是一個 JSON 物件。**所有欄位值都必須是 JSON 字串**,金額也一樣,例如 `"amount": "1000.00"`;送數字 `1000.00` 會失敗(回 `invalid_signature` 或 `invalid_parameter`)。 * 每個請求都必須帶 `api_identity`、`timestamp` 與 `sign`(見[簽章](./signature.md))。 * `timestamp` 是送出請求當下的時間,格式同下方的[時間](#資料格式)(UTC+8,`yyyy-MM-dd HH:mm:ss`)。與 ST Pay 伺服器時間相差超過 5 分鐘(過去或未來都算)的請求會被拒絕,回 `timestamp_expired`。請讓伺服器開啟 NTP 時間同步。 * 選填欄位送空字串 `""`,視同沒送。 ## 回應 所有 API 都回傳相同結構: | 欄位 | 說明 | | --- | --- | | `code` | 成功為 `OK`,失敗為英文錯誤碼(見[錯誤碼](../reference/errors.md))。**程式請以此欄位判斷結果。** | | `message` | 繁體中文說明,給人閱讀用,內容可能調整,請勿用來判斷。 | | `data` | 成功時為回傳資料,失敗時為 `null`。 | HTTP 狀態碼:成功為 `200`,失敗則依錯誤種類回傳 4xx 或 5xx(見[錯誤碼](../reference/errors.md))。 ```json { "code": "merchant_order_no_duplicate", "message": "商戶單號重複", "data": null } ``` ## 資料格式 | 類型 | 格式 | | --- | --- | | 金額 | 字串。請求:大於 0、最多 2 位小數(照字面算,`"1000.500"` 是 3 位)、不超過 `9999999999999999.99`,只能有數字與小數點,例如 `"1000"`、`"1000.5"`、`"1000.50"`。`"1,000.00"`、`"1000.001"`、`"-5"`、`"0"` 都不合法。回應與 Callback 的金額一律是 2 位小數,例如 `"1000.50"`。不帶幣別欄位。 | | 時間 | 字串,UTC+8,格式 `yyyy-MM-dd HH:mm:ss`,例如 `"2026-10-13 10:30:05"`。 | | 訂單號 `order_no` | ST Pay 產生的字串。請當作不透明字串保存,不要解析內容或依賴格式。 | | 商戶單號 `merchant_order_no` | 由您產生,1 至 200 字元,只能包含英數字、底線 `_`、連字號 `-`。 | ## 資料保存期限 訂單自建立起保存 6 個月,之後會被刪除。刪除後查單會回 `order_not_found`,該商戶單號也可以再次使用。 --- --- url: /guide/security.md --- # 身分驗證與 IP 白名單 ## 檢查順序 每個請求依下列順序檢查,任一項不通過就回傳錯誤: 1. **簽章**:`api_identity` 必須是狀態為正常的憑證,且 `sign` 正確。不通過回 `invalid_signature`(HTTP 401)。 2. **時間戳**:`timestamp` 必填,格式 `yyyy-MM-dd HH:mm:ss`(UTC+8),與 ST Pay 伺服器時間相差不得超過 5 分鐘。缺少回 `missing_parameter`,格式不符回 `invalid_parameter`,相差超過 5 分鐘回 `timestamp_expired`(HTTP 400)。簽章通過才檢查時間戳,所以 `timestamp` 被竄改時回 `invalid_signature`。 3. **來源 IP**:必須在該商戶的 API 白名單內。白名單是空的就全部拒絕。不通過回 `ip_not_allowed`(HTTP 403)。 4. **商戶狀態**(只有建單會檢查):商戶被停用時不能建單,回 `merchant_disabled`(HTTP 403)。停用期間仍可查單。 ## 保管金鑰 * 金鑰只能放在您的伺服器端,不要放進網頁、App 或任何會發給終端使用者的程式。 * 金鑰外洩或遺失時,請立即聯繫 ST Pay 建立新憑證並停用舊憑證。 ## 更換金鑰 同一商戶可以同時有多組狀態為正常的憑證,每組都可以用來呼叫 API。Callback 一律以**最新建立且狀態為正常**的那組金鑰簽章,因此建議依以下順序更換金鑰: 1. 先讓您的 Callback 驗章程式能同時接受兩把金鑰(新金鑰在第 2 步之後才拿得到,可先準備好設定位置)。 2. 請 ST Pay 建立新憑證,並把新金鑰加入 Callback 驗章。新憑證建立後,Callback 會立即改用新金鑰簽章;新金鑰加入驗章之前收到的 Callback 會驗章失敗,這些單請以[查單](../api/query.md)確認結果,或請 ST Pay 重發。 3. 呼叫 API 改用新的 `api_identity` 與金鑰。 4. 確認沒有請求再使用舊憑證後,請 ST Pay 停用舊憑證,並從 Callback 驗章移除舊金鑰。 --- --- url: /guide/signature.md --- # 簽章 API 請求、API 回應與 Callback 使用同一套簽章規則(HMAC-SHA256)。 ## 計算步驟 1. 取本文中 `sign` 以外的**所有**欄位,排除值為 `null` 的欄位。空字串 `""` 不排除。 2. 依**欄位名稱**的字元碼(ASCII)由小到大排序。 3. 每個欄位寫成 `欄位名稱=欄位值`,以 `&` 串接成一個字串(以下稱「待簽字串」)。欄位值照原樣放入,**不做 URL 編碼**,也不去除空白。 4. 以 API 驗證金鑰為 key,對待簽字串的 UTF-8 位元組計算 HMAC-SHA256。 5. 結果轉成**小寫**十六進位字串(64 個字元),放進 `sign` 欄位。 ::: warning 常見錯誤 * 金鑰請直接以 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_identity` | `qp_Xk3vT9aLm2QwE7rB1nZc` | | API 驗證金鑰 | `Rk8vN2qXw5LpT9sJc3HmY7bF1dGz6AeU4oKi0Wn-QtE` | 要送出的欄位(簽章前): ```json { "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" } ``` 排序並串接後的待簽字串: ```text 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×tamp=2026-10-13 10:00:00 ``` HMAC-SHA256 結果(即 `sign`): ```text 566fac06060d54d7450b41b6858b1ddf33df9d37117ab8a8c495ce748469f2b2 ``` ## 程式範例 以下範例都已用上面的範例憑證驗證過,算出的簽章與上方結果相同。驗證 Callback 時,請用收到的全部欄位重新計算,再以固定時間比對。 ::: code-tabs @tab Python 3 ```python 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()) ``` @tab Java 17+ ```java 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 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 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)); } } ``` @tab PHP 7.4+ ```php $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'] ?? ''))); } ``` @tab Node.js ```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); } ``` @tab Go ```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`。 * 驗不過代表回應可能在傳輸途中被竄改:不要把帳號或網址交給付款人,改以[查單](../api/query.md)確認。 * 驗章可直接用上面的驗證函式,傳入回應的 `data`。 * 錯誤回應的 `data` 為 `null`,沒有簽章。 ## 完整呼叫範例 ```bash 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" }' ``` --- --- url: /reference/banks.md --- # 銀行代碼 代付銀行轉帳的 `bank_code` 必須是下列其中之一。清單由 ST Pay 維護,如有異動會另行通知。 | `bank_code` | 銀行名稱 | | --- | --- | | `003` | Standard Chartered Bank (Hong Kong) Limited | | `004` | The Hongkong and Shanghai Banking Corporation Limited | | `009` | China Construction Bank (Asia) Corporation Limited | | `012` | Bank of China (Hong Kong) Limited | | `015` | The Bank of East Asia, Limited | | `016` | DBS Bank (Hong Kong) Limited | | `018` | China CITIC Bank International Limited | | `020` | CMB Wing Lung Bank Limited | | `024` | Hang Seng Bank Limited | | `025` | Shanghai Commercial Bank Limited | | `027` | Bank of Communications Co., Ltd. Hong Kong Branch | | `028` | Public Bank (Hong Kong) Limited | | `035` | OCBC Bank (Hong Kong) Limited | | `039` | Chiyu Banking Corporation Limited | | `040` | Dah Sing Bank, Limited | | `041` | Chong Hing Bank Limited | | `043` | Nanyang Commercial Bank, Limited | | `072` | Industrial and Commercial Bank of China (Asia) Limited | | `128` | Fubon Bank (Hong Kong) Limited | | `250` | Citibank (Hong Kong) Limited | | `382` | Bank of Communications (Hong Kong) Limited | | `387` | ZA Bank Limited | | `388` | Livi Bank Limited | | `389` | Mox Bank Limited | | `390` | WeLab Bank Limited | | `391` | Fusion Bank Limited | | `392` | PAObank Limited | | `393` | Ant Bank (Hong Kong) Limited | | `395` | Airstar Bank Limited | --- --- url: /reference/checklist.md --- # 上線前檢查清單 1. 用[範例憑證](../guide/signature.md#範例)算出的簽章與文件相同。 2. 所有欄位值都以字串送出,金額不是數字型別。 3. 代收建單 url 與 json 兩種模式(如有使用)、fps 與 bank_transfer 兩種通道都測過。 4. 付款畫面顯示的是 `payable_amount`,不是原始金額。 5. 代付建單 FPS 的三種帳號格式(如有使用)與銀行轉帳都測過。 6. Callback 端點:驗章正確、簽章不符會丟棄、重複通知能冪等處理、10 秒內回應 `ok`。 7. Callback 以 `order_no` 對應訂單,能區分代收與代付。 8. 有定期查單的機制,處理沒收到 Callback 與 `late_credited` 的情況。 9. 建單逾時與 `internal_error` 會以商戶單號查單確認,不會直接判定失敗。 10. 程式以 `code` 判斷結果,不依賴 `message` 文字。 11. 正式環境的憑證、網域與 IP 白名單已另外申請,且金鑰只存在伺服器端。 12. 伺服器時間已校準(建議開啟 NTP 時間同步),每個請求的 `timestamp` 都是送出當下的時間。 13. 每個建單請求都帶 `callback_url`;網址是合法的 `http` 或 `https` 網址(兩者都接受),網域可從公網解析且只指向公網 IP,不會轉址(3xx),伺服器回 HTTP 2xx。 14. 代收成功以 `received_amount`(或 `settlement_amount`)入帳;與預期金額不同時轉人工,不自動入帳。 15. 已是終態的訂單不會因後到、狀態不同的 Callback 被回退;有疑義以[查單](../api/query.md)結果為準。 --- --- url: /reference/errors.md --- # 錯誤碼 建單時,ST Pay 依以下順序檢查,同時違反多項時只回傳第一個: 簽章 → 時間戳 → 來源 IP → 商戶狀態 → 必填欄位與格式 → `callback_url` 的環境與位址 → 單筆上下限 → 商戶單號重複 →(代收)同一會員待審批單上限 →(代收)收款帳戶與尾數分配 /(代付)餘額。 在這之前:本文不是 JSON 物件或 `Content-Type` 不是 `application/json`,直接回 `invalid_parameter`;沒送 `api_identity` 或 `sign` 回 `invalid_signature`。 | `code` | HTTP | `message` | 原因與處理方式 | | --- | --- | --- | --- | | `invalid_signature` | 401 | 簽章驗證失敗 | `api_identity` 錯誤、憑證已停用,或簽章不符。請依[簽章](../guide/signature.md#範例)的範例核對程式,並確認使用對應環境的憑證。欄位值送成數字或布林會回這個或 `invalid_parameter`,所有值請以字串送出;沒送 `api_identity` 或 `sign` 也是。 | | `timestamp_expired` | 400 | 請求時間與伺服器時間相差超過 5 分鐘,請校正您的伺服器時間(伺服器目前時間:yyyy-MM-dd HH:mm:ss) | `timestamp` 與 ST Pay 伺服器時間相差超過 5 分鐘(過去或未來都算)。訊息附上伺服器目前時間,可據此看出差距;請開啟 NTP 校正伺服器時間,並在送出當下才取時間簽章。 | | `ip_not_allowed` | 403 | 來源 IP 不在白名單 | 請確認伺服器實際的出口 IP,並請 ST Pay 加入白名單;加入後約 1 分鐘生效。來源 IP 不在任何白名單時,請求在閘道就被擋下:回 HTTP 403,本文是 HTML 而不是 JSON,處理方式相同。 | | `merchant_disabled` | 403 | 商戶已停用 | 商戶停用期間不能建單,請聯繫 ST Pay。 | | `permission_denied` | 403 | 無此操作權限 | 通常是 `/api/v1/` 底下的路徑拼錯。用 `GET`,或路徑不在 `/api/v1/` 底下時,閘道直接回 HTTP 404(本文不是 JSON)。 | | `missing_parameter` | 400 | 缺少必填欄位 | 必填欄位沒送、為 `null` 或空字串;`callback_url` 全是空白;銀行轉帳代付缺少銀行欄位;查單沒送任何單號。 | | `invalid_parameter` | 400 | 欄位值不合法 | 欄位值的格式、長度、字元或列舉值不合法;欄位值不是字串;本文不是合法的 JSON 物件,或 `Content-Type` 不是 `application/json`;`callback_url` 格式不合(規則見 [Callback 網址](../api/callback.md#callback-網址));FPS 代付帶了銀行欄位;銀行代碼不在清單內。 | | `invalid_amount` | 400 | 金額格式不正確 | 金額為 0、負數、超過 2 位小數(照字面算,`1000.500` 也算 3 位)、大於 `9999999999999999.99`,或不是數字。 | | `missing_member_identity` | 400 | 請提供會員唯一識別或真實姓名 | 代收建單時 `merchant_user_id` 與 `payer_name` 都沒送。 | | `invalid_callback_url` | 400 | Callback 網址不符合規定:不可為本機或內網位址 | 建單的 `callback_url` 主機是本機、內網位址、內部網域、非標準的 IP 寫法。見 [Callback 網址](../api/callback.md#callback-網址)。格式錯誤(不是網址、超過 500 字元、含空白或其他格式規則,見同一節)回 `invalid_parameter`。 | | `amount_out_of_range` | 422 | 金額超出限額 | 原始金額或代付金額超出單筆上下限。 | | `merchant_order_no_duplicate` | 409 | 商戶單號重複 | 見[重複請求與結果確認](./idempotency.md)。 | | `user_active_orders_limit` | 422 | 有效訂單已達上限 | 同一會員已有 10 張待審批的代收單。請等其中的單完成或到期後再建立。 | | `channel_full` | 422 | 通道已滿,請輸入其他金額再試 | 同一通道、同一原始金額的辨識尾數已全部被待審批單占用。請付款人換一個金額再試。 | | `no_available_card` | 422 | 通道已滿,請輸入其他金額再試 | 該通道目前沒有可用的收款帳戶。可稍後再試或改用另一個通道;持續發生請聯繫 ST Pay。 | | `insufficient_balance` | 422 | 商戶餘額不足 | 代付建單時,可用餘額小於代付金額加手續費。 | | `order_not_found` | 404 | 查無此單 | 訂單不存在、不屬於您,或已超過保存期限被刪除。 | | `query_conflict` | 400 | 訂單號與商戶單號指向不同訂單 | 查單時同時送了兩個單號,但它們不是同一張單。 | | `internal_error` | 500 | 系統錯誤,請以商戶單號查單確認結果 | ST Pay 內部錯誤,訂單可能已建立也可能沒有。**不要直接當作失敗**,請以商戶單號查單確認。 | `channel_full` 與 `no_available_card` 的 `message` 相同,請以 `code` 區分。 收到 `invalid_parameter`、`invalid_signature` 等錯誤又找不出原因時,請提供 `api_identity`、商戶單號(查單則提供查詢的單號)與發生時間(UTC+8),請 ST Pay 對接窗口查詢 API 錯誤紀錄,可查到是哪一欄有問題或簽章失敗的原因。ST Pay 只記錄 `api_identity` 正確的請求;`api_identity` 本身錯誤時查不到,請先核對。 --- --- url: /reference/idempotency.md --- # 重複請求與結果確認 | 情況 | 結果與建議做法 | | --- | --- | | 用已經使用過的商戶單號再建單 | 回 `merchant_order_no_duplicate`,不論原單狀態,也不比對內容(金額不同也一樣)。不會產生第二張單。回應不附原單資料,請用商戶單號[查單](../api/query.md)。 | | 建單請求逾時、連線中斷,或收到 `internal_error` | **不要直接當作失敗**。用同一個商戶單號查單:查得到就以查單結果為準(待付款且還沒到失效時間的單,查單會附上付款網址或收款帳號,可以繼續讓付款人付這張單,見[查單](../api/query.md));回 `order_not_found` 表示 ST Pay 沒有建立這張單,可以用同一個商戶單號重新建單。 | | 訂單被拒絕後要重新發起 | 必須使用新的商戶單號。 | | 商戶單號的唯一性範圍 | 同一商戶內,代收單與代付單各自不能重複;訂單超過 6 個月被刪除後,該單號可以再次使用。 | | 查單重複呼叫 | 唯讀,每次回傳目前狀態,不會產生任何變動。 | | 近乎同時送出多個相同商戶單號的建單 | 依序處理,只有第一個成功,其餘回 `merchant_order_no_duplicate`。 | | 同一張單收到多次 Callback | 以 `order_no` 加 `status` 判斷是否處理過,重複的直接回 `ok`。 | --- --- url: /reference/status.md --- # 訂單狀態與金額 「後台顯示」是[商戶後台](../guide/backoffice.md)顯示的狀態文字。 ## 代收單 ```text pending ──┬── 確認入帳 ───────────────→ success (發 Callback) ├── ST Pay 人工拒絕 ─────────→ rejected (發 Callback,終態) └── 30 分鐘到期,系統拒絕 ──→ rejected (發 Callback) │ └── 之後確認收到款項 ──→ late_credited (不發 Callback) ``` | `status` | 後台顯示 | 意義 | 是否為最終結果 | | --- | --- | --- | --- | | `pending` | 待審批 | 等待付款人轉帳與 ST Pay 確認 | 否 | | `success` | 成功 | 入帳成功,結算金額已加入您的餘額 | 是 | | `rejected` | 已拒絕 | 不入帳、不收手續費 | 人工拒絕是最終結果;**系統逾時拒絕之後仍可能變成 `late_credited`** | | `late_credited` | 逾時入帳 | 訂單已被系統逾時拒絕,但之後確認付款人確實有付款,結算金額已加入您的餘額 | 是 | ::: warning 逾時入帳不發 Callback 您會先收到一次 `rejected` 的 Callback,之後同一張單若轉為 `late_credited`,不會再收到通知,但餘額已經增加。 建議:付款人反映已付款但訂單顯示拒絕時,請用查單確認;並定期以查單或[商戶後台的餘額流水](../guide/backoffice.md#餘額流水)(異動類型「逾時入帳」)對帳,比對 `reject_reason` 為「訂單已逾時」的單是否轉為 `late_credited`。 ::: ## 代付單 ```text pending ──┬── ST Pay 出款 ──→ paid (扣款並發 Callback) └── ST Pay 拒絕 ──→ rejected (不扣款並發 Callback) ``` | `status` | 後台顯示 | 意義 | 是否為最終結果 | | --- | --- | --- | --- | | `pending` | 待審批 | 等待 ST Pay 出款 | 否 | | `paid` | 已代付 | 已出款,已從餘額扣除代付金額加手續費 | 是 | | `rejected` | 已拒絕 | 不扣款、不收手續費 | 是 | 代付單沒有自動逾時,會維持 `pending` 直到 ST Pay 處理。 ## 金額與手續費 | 項目 | 計算方式 | | --- | --- | | 代收訂單金額 `payable_amount` | 原始金額 + 辨識尾數(0.01–0.99) | | 代收手續費 `fee` | 實收金額 × 代收費率,小數第 3 位起無條件捨去 | | 代收結算金額 `settlement_amount` | 實收金額 − 手續費 | | 代付手續費 `fee` | 代付金額 × 代付費率,小數第 3 位起無條件捨去 | | 代付實扣總額 `total_deducted` | 代付金額 + 手續費 | * 費率在**建單當下鎖定**,之後費率調整只影響新建立的單。 * 實收金額 `received_amount` 是 ST Pay 實際確認入帳的金額,以它計算手續費與結算金額。若付款人轉帳金額與訂單金額不同,實收金額可能和 `payable_amount` 不一樣。 * 被拒絕的單手續費為 `0.00`。 * 每筆單的原始金額或代付金額須在 ST Pay 設定的單筆上下限內(含上下限本身)。上下限依「代收或代付」與「通道類別」分別設定,請向 ST Pay 確認目前的數值。 範例:代收費率 2.5%,實收 1000.37,手續費為 1000.37 × 2.5% = 25.00925,捨去後是 25.00,結算金額為 975.37。