串接 payment service provider 最難的地方,不是把 HTTP request 送出去,而是 request 回來——或沒有回來——之後,我們的系統可以相信什麼。
我以前替跨國電商串接過多家當地 PSP。每一家使用的 endpoint 名稱、付款順序和回傳格式都不太一樣,但底層問題其實相同:發卡銀行是否同意付款、款項是否真的進入清算、這筆 hold 還能不能取消,以及最後應該相信哪一個系統的狀態。
以下用公開文件與簡化模型說明這些問題,不重現特定商家的內部介面、路由設定或交易資料。處理清單是整理後的設計建議,不代表當時系統已具備其中每一項控制。
PSP 與銀行到底是什麼關係
一筆常見的信用卡交易至少有這幾個角色:
- Merchant:擁有訂單與 checkout 的商家。
- PSP / gateway:提供商家串接的 API,並把不同付款方式包成較一致的介面。
- Acquirer:收單端,讓商家可以進入卡組織並接收後續結算。
- Card network:Visa、Mastercard 這類卡組織,負責路由訊息與制定 scheme rule。
- Issuer:消費者的發卡銀行,真正決定要不要核准交易。
同一家公司可能同時扮演多個角色。有的 PSP 也提供收單、風控、tokenization 與 settlement reporting。因此文件裡應該記錄對方實際負責的角色,不能只看廠商品牌就推定資料流。
Authorization 與 settlement 也不是同一件事。Authorization 是向 issuer 詢問「這筆交易能不能做」,通過後通常只是在額度或帳戶資金上保留一筆 hold。Capture 才把已核准的金額送進 clearing;settlement 又會更晚發生,扣除費用或其他調整後,淨額才往 merchant 方向移動。訂單顯示 authorized,不代表錢已經進入商家的銀行帳戶。
Pre-auth 並不是所有 PSP 都有的獨立階段
實務上常看到幾種表面不同的流程:
| PSP 顯示的流程 | 通常代表什麼 | 取消方式 |
|---|---|---|
authorize → capture |
先保留額度,之後才請款 | capture 前 void/cancel;capture 後 refund |
pre-auth → adjust/finalize → capture |
最終金額尚未確定,先保留估算金額,再調整或確認 | 取消 authorization,或依卡組織規則到期釋放 |
pre-auth → capture |
PSP 把延後請款的 authorization 稱為 pre-auth | capture 前 void;capture 後 refund |
sale/automatic capture |
PSP 把 authorization 與 capture 包成一次商家操作 | 通常走 refund;底層也可能在很短時間內做 reversal |
在 card network 的層次,pre-authorization 通常是 authorization 的一種用途或類型,不是每一家銀行都必定存在、而且一定發生在「正式 authorization」之前的獨立交易。它常用在一開始不知道最終金額的情境,例如飯店、租車、加油、押金或小費。有些 PSP 支援 incremental authorization 或 authorization adjustment;另一些只是把 manual capture 稱作 pre-auth。
所以我不會直接拿某一家 PSP 的名詞當內部 domain model,而是把每一家 provider 映射到同一套狀態:
- Pending:交易已建立,但消費者或 PSP 還有步驟沒完成。
- Authorized:issuer 已核准並保留額度,尚未 capture。
- Captured:已送入 clearing,接下來若要退回應走 refund。
- Voided / cancelled:尚未 capture 的 authorization 已釋放。
- Refund pending / refunded:capture 後的款項正在或已經退回。
- Failed / expired:PSP 已給出明確而且不可再前進的失敗結果。
Void 和 refund 不是同義詞。Void 是 capture 前釋放 hold;refund 是 capture 後產生另一筆反向資金流,可能需要時間、費用,也可能有自己的失敗狀態。再加上 partial capture、multiple capture 與 partial refund,付款資料模型不可能只靠一個 paid: true/false。
Authorization 也會過期。有效時間取決於卡組織、付款方式、merchant category 與 PSP。能拿到 provider 回傳的 expiry time 就應該保存,不能假設幾天後仍然一定能 capture。
HTTP timeout 的正確狀態是 unknown
每套 PSP integration 都應該明確表示這種同步結果不確定的情境:
- 後端把 authorization request 送給 PSP。
- PSP 已轉送,issuer 也成功核准。
- HTTP response 在路上遺失,或超過我們設定的 timeout 才抵達。
- 應用程式看到 exception,但消費者的卡片已出現 hold,甚至後續已成功請款。
如果系統立刻把訂單改成 failed,再用新的 reference 重試,就可能造成重複扣款。這時最安全的解讀不是 failed,而是 unknown。
第一次呼叫前就要建立穩定的 merchant payment reference 與 idempotency key,重試時沿用同一組。取得 PSP reference 後立刻保存。同步 response 不明確時,先用 merchant reference 或 PSP reference 查詢交易狀態,不要直接建立另一筆付款。
Webhook 很重要,但不是「最後收到的就一定最準」
Webhook 經常能補回原始 request 沒有帶回來的狀態,所以它是 payment integration 的必要部分。但 webhook 也可能延遲、重送或亂序。可靠的處理流程應該是:
- 透過 HTTPS 接收,依 provider 規格驗證 signature 或 HMAC;需要 raw body 時,在改寫內容之前完成驗簽。
- 驗證事件結構,再依事先定義的欄位白名單,持久化後續處理所需的事件資料;不把未知欄位直接寫入一般 log。
- 依 event ID,或 PSP 文件指定的 composite key 去重,並保存處理狀態。
- 確認事件已可靠入列或寫入 inbox table 後,依 provider 規範回應成功,再非同步執行業務處理。
- 每次狀態轉換都必須 idempotent。
- Event 互相矛盾或 context 太舊時,主動查詢 PSP payment API。
- 讓必要的交易識別、處理結果與查核證據能互相對應,並明確設定保存期限與查閱權限。
驗簽需要原始內容,不等於必須長期保存整包 payload。 例如 Stripe 的驗簽需要未修改的 request body;驗證完成後,持久化哪些資料則是另一個決策。若處理或爭議查核確實需要原始事件,應先確認可保存的內容,再放入受限、加密且有查閱紀錄的證據儲存區,依適用的保存要求管理;不是把所有欄位原樣丟進 log。Stripe:Webhook 驗簽
未知欄位可能帶來新的個資或敏感內容,應先確認用途與分類,再決定是否納入處理。對商家金流整合而言,CVV 這類敏感驗證資料在授權完成後不得保存,即使加密也一樣;「方便對帳」不是例外。PCI SSC:卡片驗證碼的保存限制
真正的 source of truth 是 PSP 的交易紀錄,最後還要以 financial reconciliation 收尾;不是「我們最後收到的那一支 webhook」。Webhook 的角色,是通知我們該把本地狀態與 PSP 現況重新對齊。
這也會影響前端文案。消費者完成 3DS 或 redirect 回來,但後端還沒得到 terminal state 時,顯示「付款處理中」通常比直接寫「付款失敗」誠實。
PSP 有卡片元件,就不要自己手刻 raw card form
多數 PSP 會提供 hosted checkout、iframe、hosted fields 或前端 component。卡號與 CVV 寫進 PSP 提供的欄位,由 PSP 的程式直接 tokenization 或加密;merchant backend 收到的是 token 或 encrypted payment method,而不是 raw card data。
這會降低 PCI DSS scope,但不代表 merchant 完全沒有責任。自己的頁面、redirect、前端 dependency、帳號權限與 integration configuration 還是要保護。PCI SSC 明確指出:若要符合 iframe 形式的 SAQ A 條件,所有負責收集卡片資料的元素都必須由合規第三方提供;只要 merchant 自己的元素碰到 PAN 或 CVV,責任範圍就會明顯增加。
實務原則很簡單:PSP 有持續維護的 card component,就優先使用。自己手刻相同表單,代表你要承擔儲存、log、JavaScript supply-chain attack、瀏覽器被植入惡意程式、key management、PCI 證據與 incident response。PAN 與 CVV 絕對不要進 log,也不要因為已加密就認為可以隨便保存。
「PSP 給了公私鑰」其實可能在做四件不同的事
我確實串接過需要 key 或 certificate 包裝 transaction 的 provider,但要先分清楚它的安全目的:
| 機制 | 用途 | 應該放在哪裡 |
|---|---|---|
| Client key/PSP-hosted encryption | 卡片資料離開瀏覽器前先加密或 tokenization | Browser,並限制 approved origin |
| Secret API key、OAuth 或 mTLS certificate | 驗證 merchant backend 有權呼叫 PSP | 只能放 server-side secret store |
| RSA request signature/payload encryption | 證明 request 完整性與來源,或保護 provider-specific payload | 後端 signing module,必須能 rotation |
| Webhook HMAC/public-key verification | 驗證 event 由 PSP 發出且未被修改 | Webhook ingress,處理前先驗證 |
它們不能互相取代。Signature 不會隱藏資料;encryption 本身不一定能證明 business intent;公開的 client key 也不是後端 secret。Key ID、algorithm、canonical field order、timestamp、nonce、certificate expiry 與 rotation procedure 都是 integration contract 的一部分。
現在我會問每一家 PSP 的問題
- Merchant reference 代表一次 attempt、一張 order,還是一次 capture?
- 哪些 operation 支援 idempotency、partial amount 與 multiple capture?
- Authorization 何時過期?能否 adjust 或 incremental authorize?
- Cancel 到底代表 void、reversal、refund,還是只代表「已接受請求」?
- 哪些狀態是 terminal?看起來 failed 的狀態會不會之後又成功?
- Webhook 是否重試、重送或亂序?唯一 event ID 是什麼?
- Webhook 如何簽章?換 key 時要如何同時接受新舊 key?
- 3-D Secure 是否非同步?應該相信 browser return、webhook 還是 status API?
- Amount 是否使用 minor unit integer?零小數貨幣怎麼處理?
- Fee、FX、settlement batch、dispute 與 chargeback 從哪一份報表取得?
- Sandbox 與 production 的 acquirer/issuer 行為有什麼差異?
- 哪份報表能證明
captured最後真的已經 settlement?
API call 只會開始一筆付款。狀態機、證據與對帳,才會把它變成一套 payment system。