本章是 Payment Gateway 商戶 API 的權威錯誤參考。用它把 HTTP 狀態碼與業務錯誤碼對應到商戶該採取的動作:重試、修正請求,或上報給操作員。
回應信封
每一個商戶 API 回應,無論成功或失敗,都使用相同的 JSON 信封。讀 code 欄位,不要只看 HTTP 狀態碼來決定動作。
{
"success": false,
"code": "INVALID_SIGNATURE",
"message": "X-Signature did not match expected value",
"data": null
}| 欄位 | 類型 | 說明 |
|---|---|---|
| success | boolean | 2xx 時為 true,其他情況為 false。 |
| code | string | 機器可讀。2xx 時為 SUCCESS,其他情況為下方列出的某個錯誤碼。 |
| message | string | 人類可讀。可安全寫入日誌。不要未翻譯就直接顯示給終端使用者。 |
| data | object | 成功時是該操作特定的 payload。錯誤時為 null。 |
部分錯誤回應也會包含 details 物件,附帶欄位層級的資訊(例如 Pydantic 驗證錯誤)。把 details 視為選用且僅供參考。
永遠用 code 做分支。不要用 message 做分支:message 的文字不屬於 API 合約,可能會改。
HTTP 狀態碼
商戶 API 使用以下 HTTP 狀態碼。「可重試?」欄位說明商戶客戶端是否可以在不需操作員介入的情況下,直接重試同一個請求。
| 狀態碼 | 意義 | 常見原因 | 可重試? |
|---|---|---|---|
| 200 | OK | 查詢成功。code 為 SUCCESS。 | 不需要。 |
| 201 | Created | 訂單或 session 已建立。code 為 SUCCESS。 | 不需要。 |
| 400 | Bad Request | 業務規則拒絕了該請求,或 JSON 請求主體無法解析。 | 否。修正請求或上報給操作員。 |
| 401 | Unauthorized | API key 缺失、無效、過期、簽章錯誤,或時間戳超出窗口範圍。 | 否。修正憑證或簽章邏輯。 |
| 403 | Forbidden | 商戶停用,或缺少 can_deposit / can_withdraw 權限。 | 否。聯絡平台操作員。 |
| 404 | Not Found | 訂單不存在,或訂單屬於不同商戶。 | 否。確認識別碼是否正確。 |
| 409 | Conflict | 資源衝突,例如重複的 merchant_order_no。 | 否。先解決衝突再重試。 |
| 422 | Unprocessable Entity | 請求形狀是合法的 JSON,但欄位層級的驗證失敗。 | 否。修正 payload。 |
| 429 | Too Many Requests | 超過速率限制。保留用途;目前並非所有端點都有強制執行。 | 是,使用指數退避。 |
| 500 | Internal Server Error | 未處理的伺服器錯誤。信封 code 永遠是 INTERNAL_ERROR。 | 是,使用退避策略。若持續發生請開立工單。 |
| 502 | Bad Gateway | 上游渠道或出款供應商回傳了無效的回應。 | 是,使用退避策略。 |
| 503 | Service Unavailable | 服務正在啟動、清空連線中,或暫時下線。 | 是,使用退避策略。 |
| 504 | Gateway Timeout | 上游渠道逾時。 | 是,使用退避策略並帶冪等鍵。 |
重試建議是針對「同一個原本送出的請求」。對 create 呼叫遇到 5xx 時,永遠使用冪等鍵(即 merchant_order_no),這樣重試才不會產生重複訂單。
業務錯誤碼
以下錯誤碼會出現在信封的 code 欄位。錯誤碼是穩定的。狀態碼配對反映目前後端的行為。
成功
| code | HTTP 狀態碼 | 意義 | 商戶應採取的動作 |
|---|---|---|---|
SUCCESS | 200、201 | 操作成功。 | 繼續。讀取 data 取得該操作的 payload。 |
認證與授權
| code | HTTP 狀態碼 | 意義 | 商戶應採取的動作 |
|---|---|---|---|
AUTHENTICATION_ERROR | 401 | 一般性認證失敗。 | 檢查 X-API-Key、簽章邏輯與時鐘。 |
INVALID_SIGNATURE | 401 | X-Signature 與伺服器計算出來的簽章不一致。 | 重新計算簽章。檢查參數排序與 api_key 附加方式。 |
INVALID_TIMESTAMP | 401 | X-Timestamp 缺失、格式錯誤,或超出 5 分鐘窗口。 | 用 NTP 同步你的時鐘。以當前的 Unix 時間戳重新送出。 |
AUTHORIZATION_ERROR | 403 | 已認證,但不被允許:商戶停用,或缺少該操作的權限。 | 聯絡平台操作員。不要重試。 |
認證依賴會在任何業務邏輯執行前,攔截錯誤的 key、錯誤的簽章與過期的時間戳。對當前請求而言,所有 401 回應都視為終局。
驗證
| code | HTTP 狀態碼 | 意義 | 商戶應採取的動作 |
|---|---|---|---|
VALIDATION_ERROR | 400、422 | 請求 payload 驗證失敗。422 用於 Pydantic 欄位錯誤,會帶 details.errors 陣列;400 用於 service 在業務邊界拒絕 payload 的情況。 | 修正 payload。沒修改就不要重試。 |
狀態與衝突
| code | HTTP 狀態碼 | 意義 | 商戶應採取的動作 |
|---|---|---|---|
INVALID_STATE | 400 | 目標資源目前的狀態不允許此操作,例如取消一筆已完成的訂單。 | 重新查詢訂單看當前的 status。不要盲目重試。 |
CONFLICT | 409 | 違反了唯一性約束,通常是重複的 merchant_order_no。 | 視為重複。決定是否用新的 merchant_order_no 重試前,先查詢既有訂單。 |
OPTIMISTIC_LOCK_ERROR | 400 | 在同一筆商戶餘額或訂單列上,與另一個同時進行的更新發生競爭。 | 在小範圍內(3 次)以短抖動重試同一請求。 |
OPTIMISTIC_LOCK_ERROR 是唯一一個業務層錯誤碼,可以在不修改請求的情況下安全重試。請限制重試次數並加入抖動,避免 thundering-herd。
餘額
| code | HTTP 狀態碼 | 意義 | 商戶應採取的動作 |
|---|---|---|---|
INSUFFICIENT_BALANCE | 400 | 來源帳本的一般性餘額不足。 | 充值或減少金額。不要原樣重試。 |
INSUFFICIENT_MERCHANT_BALANCE | 400 | 商戶可用餘額不足以涵蓋一筆提款。 | 等待充值完成入帳,然後用相同金額重試,或減少金額。 |
找不到
| code | HTTP 狀態碼 | 意義 | 商戶應採取的動作 |
|---|---|---|---|
NOT_FOUND | 404 | 訂單不存在,或訂單存在但屬於不同商戶。這兩種情況是刻意做成無法區分的。 | 確認你送出的識別碼是否正確。 |
業務與渠道
| code | HTTP 狀態碼 | 意義 | 商戶應採取的動作 |
|---|---|---|---|
BUSINESS_ERROR | 400 | 業務規則拒絕了該請求,例如「訂單已過期」或「無可用充值帳戶」。讀 message 看上下文。 | 修正請求或上報給操作員。不要重試同一個請求。 |
EXTERNAL_SERVICE_ERROR | 400、502 | 上游渠道或出款供應商回傳錯誤。 | 用退避策略重試,並使用相同的 merchant_order_no 維持冪等性。 |
MERCHANT_API_ERROR | 400 | 商戶 API 相關失敗的父類錯誤碼。通常你會看到更具體的子類碼,例如 INVALID_SIGNATURE。 | 視為終局,除非有更具體的錯誤碼另行說明。 |
系統
| code | HTTP 狀態碼 | 意義 | 商戶應採取的動作 |
|---|---|---|---|
INTERNAL_ERROR | 500 | 未處理的伺服器錯誤。在正式環境中 message 是通用的。 | 用退避策略重試。若持續發生,聯絡平台團隊。 |
重試策略
在你的客戶端決定如何處理一個回應時,使用以下這張矩陣。
| HTTP 狀態碼 | 可安全重試? | 策略 |
|---|---|---|
| 200、201 | N/A | 完成。把結果持久化。 |
| 400 | 否 | 中止。修正請求後再送。 |
| 401 | 否 | 中止。憑證或簽章錯誤。重試不會解決問題。 |
| 403 | 否 | 中止。需要操作員介入。 |
| 404 | 否 | 中止。識別碼錯誤或資源不存在。 |
| 409 | 否 | 中止。先解決衝突,例如查詢既有訂單。 |
| 422 | 否 | 中止。payload 格式錯誤。 |
| 429 | 是 | 帶抖動的指數退避。例如限制 60 秒內最多 5 次嘗試。 |
| 500 | 是 | 指數退避。永遠用相同的 merchant_order_no 維持冪等。 |
| 502 | 是 | 指數退避。上游渠道可能會恢復。 |
| 503 | 是 | 指數退避。服務可能正在重啟。 |
| 504 | 是 | 指數退避。使用相同的 merchant_order_no,因為訂單可能已經建立。 |
特殊情況:遇到 OPTIMISTIC_LOCK_ERROR(HTTP 400)時,即使狀態碼是 400,也以短抖動重試最多 3 次。
合理的預設退避策略是 min(2^attempt * 250ms + random_jitter, 8s),上限 5 次嘗試。
日誌建議
紀錄足以除錯的內容,但不要多到外洩資料。
每次錯誤都紀錄:
- HTTP 方法、路徑、狀態碼。
code,以及回應中若帶有request_id也一併紀錄。- 你自己的
merchant_order_no,以及任何內部追蹤 ID。 - 送出的
X-Timestamp,以及你觀察到的時間戳偏差。
不要紀錄:
X-API-Key的值。只紀錄短前綴或雜湊。X-Signature的值。只紀錄它驗證失敗的事實;值本身對除錯沒有用處。- 完整的銀行帳號、卡號,或終端使用者 PII。寫入遮罩過的值。
- 含有以上內容的請求主體。
對於 5xx 錯誤,從回應標頭(若有)擷取 request_id 與時間戳。聯絡平台支援時把兩者都附上。
把信封的 message 當成參考資訊。不要對它做模式比對來決定邏輯分支。永遠用 code 分支。
常見陷阱
以下錯誤是商戶整合中常見的錯誤。每一個都對應到 API 中真實存在的失敗情境。
把 HTTP 200 當成成功,不讀信封內容。 商戶 API 對某些查詢回應會回 HTTP 200,但
success仍然是false。永遠同時讀 HTTP 狀態與信封的code。重試 401。 401 意味著你的 key、簽章或時間戳錯了。重試同一個 payload 只會得到同一個 401。修正客戶端,再送出一個重新簽章後的請求。
遇到 429 不做退避就猛打。 當速率限制有強制執行時,沒有退避的反覆請求只會讓你持續被擋。使用帶抖動的指數退避。
訂單建立時忽略 5xx。 建立訂單時收到 5xx 並不代表訂單沒有建立。伺服器可能在失敗前已經 commit 了訂單。用相同的
merchant_order_no重試,然後以merchant_order_no查詢看真實狀態,再決定下一步。用
message文字做分支。message欄位不屬於 API 合約。新的措辭、翻譯或細節變更都可能默默地讓你的客戶端壞掉。只用code做分支。混淆 400 與 422。 400 表示業務層拒絕了一個結構上沒問題的請求(例如「訂單已過期」)。422 表示請求形狀本身的欄位驗證失敗。兩者的修法不同:400 可能是操作面的問題,422 則一定是客戶端 bug。
跨不同訂單重複使用
merchant_order_no。 這會產生 409CONFLICT。每一筆新訂單意圖都要產生一個全新的merchant_order_no。只有在重試「同一筆訂單」時才能重複使用同一個merchant_order_no。忽略
merchant_order_no的重試語意。 沒有穩定的冪等鍵,5xx 後的網路重試會建立重複訂單。伺服器無法替你去重。X-Timestamp的時鐘飄移。 伺服器接受 5 分鐘的X-Timestamp窗口。客戶端時鐘偏差超過 5 分鐘會產生INVALID_TIMESTAMP。使用 NTP。不要透過「把伺服器的時鐘回送過去」來「修」這個問題;用你自己當下的 Unix 時間簽章。不處理
OPTIMISTIC_LOCK_ERROR。 同一商戶餘額的併發更新可能會輸掉競爭。以短抖動重試最多 3 次。不要把第一次衝突直接上報給操作員。把 404 解讀成「絕對不存在」。 404 也可能代表該訂單屬於不同商戶。如果你確定自己有建立這筆訂單,先回頭檢查 API key 與 tenant,再認定訂單消失。
把 API key 或簽章寫進日誌。 這些是憑證。一份外洩的日誌檔就是一份外洩的憑證。只紀錄前綴或雜湊。