← 所有文章2022.11.18 · 支付系統

HTTP 失敗,不代表付款失敗:我串接 PSP 後學到的交易生命週期

從 PSP 與銀行的關係、authorization、capture、void、refund,到代管卡片欄位、簽章與 webhook 狀態同步。

串接 payment service provider 最難的地方,不是把 HTTP request 送出去,而是 request 回來——或沒有回來——之後,我們的系統可以相信什麼。

我以前替跨國電商串接過多家當地 PSP。每一家使用的 endpoint 名稱、付款順序和回傳格式都不太一樣,但底層問題其實相同:發卡銀行是否同意付款、款項是否真的進入清算、這筆 hold 還能不能取消,以及最後應該相信哪一個系統的狀態。

以下用公開文件與簡化模型說明這些問題,不重現特定商家的內部介面、路由設定或交易資料。處理清單是整理後的設計建議,不代表當時系統已具備其中每一項控制。

PSP 與銀行到底是什麼關係

信用卡付款業務流程:消費者、商家、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 映射到同一套狀態:

PSP API 與付款狀態機

  • 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 都應該明確表示這種同步結果不確定的情境:

  1. 後端把 authorization request 送給 PSP。
  2. PSP 已轉送,issuer 也成功核准。
  3. HTTP response 在路上遺失,或超過我們設定的 timeout 才抵達。
  4. 應用程式看到 exception,但消費者的卡片已出現 hold,甚至後續已成功請款。

如果系統立刻把訂單改成 failed,再用新的 reference 重試,就可能造成重複扣款。這時最安全的解讀不是 failed,而是 unknown。

第一次呼叫前就要建立穩定的 merchant payment reference 與 idempotency key,重試時沿用同一組。取得 PSP reference 後立刻保存。同步 response 不明確時,先用 merchant reference 或 PSP reference 查詢交易狀態,不要直接建立另一筆付款。

Webhook 很重要,但不是「最後收到的就一定最準」

Webhook 經常能補回原始 request 沒有帶回來的狀態,所以它是 payment integration 的必要部分。但 webhook 也可能延遲、重送或亂序。可靠的處理流程應該是:

  1. 透過 HTTPS 接收,依 provider 規格驗證 signature 或 HMAC;需要 raw body 時,在改寫內容之前完成驗簽。
  2. 驗證事件結構,再依事先定義的欄位白名單,持久化後續處理所需的事件資料;不把未知欄位直接寫入一般 log。
  3. 依 event ID,或 PSP 文件指定的 composite key 去重,並保存處理狀態。
  4. 確認事件已可靠入列或寫入 inbox table 後,依 provider 規範回應成功,再非同步執行業務處理。
  5. 每次狀態轉換都必須 idempotent。
  6. Event 互相矛盾或 context 太舊時,主動查詢 PSP payment API。
  7. 讓必要的交易識別、處理結果與查核證據能互相對應,並明確設定保存期限與查閱權限。

驗簽需要原始內容,不等於必須長期保存整包 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。

參考資料