錯誤
所有 4xx/5xx 回應都帶有相同的 JSON 信封:
{
"code": 400,
"message": "invalid_input",
"details": [
{ "field": "items[0].unit_price", "message": "must be a positive decimal string" }
]
}code— 數值型的 HTTP status(400、401、404…)。用於通用的 HTTP 層處理很方便,但需要分支邏輯時請改用message—code無法區分如invalid_input與payment_method_not_supported(兩者都是 400)這類情境。message— lower-snake-case 的 sentinel 名稱,源自內部errors.go常數(例如INVALID_INPUT → "invalid_input")。跨版本穩定 — 以此分支。details— 僅在驗證錯誤時出現。{ field, message }物件陣列,讓 client 能將錯誤對應到輸入欄位。其他情境下會省略。
目前我們不會在 body 中回傳 trace_id 或 timestamp。本頁先前的草稿曾承諾這兩個欄位 — 那只是期望,並未實作。如果你需要將伺服器 log 與請求對應,請擷取回應的 Date 標頭以及 gateway 端的 rate-limit 標頭(X-RateLimit-*),並在 support ticket 中引用。
gateway 邊緣的拒絕使用不同的形態。 上面的信封是後端服務發出的。在抵達服務 之前 就被 gateway 拒絕的請求 —— /b2b/v1/* 呼叫上缺失 / 非法的 X-Signature、未知的 X-Client-ID,或過期的 X-Timestamp —— 會以 { "error": "...", "message": "..." } 的形態回傳,其中 error 是粗粒度 slug(unauthorized / bad_request / service_unavailable),message 攜帶具體資訊。沒有數值 code,也沒有 details。因此驗證方應先以 HTTP 狀態分支,再讀 message,只有當請求越過 gateway 之後才把 code / details 當作存在。gateway body 範例(401):
{ "error": "unauthorized", "message": "invalid signature" }HTTP 狀態 → 常見 code
| HTTP | 常見 code 值 | 意義 |
|---|---|---|
| 400 | INVALID_INPUT、MISSING_REQUIRED、INVALID_FORMAT、INVALID_LENGTH、INVALID_VALUE、INVALID_USER_STATUS、INVALID_USER_ROLE、PAYMENT_METHOD_NOT_SUPPORTED、AMOUNT_BELOW_MINIMUM | 請求有誤 — 請看 details |
| 401 | INVALID_CREDENTIALS、INVALID_TOKEN、TOKEN_EXPIRED、INVALID_SIGNATURE(B2B);INVALID_OTP、OTP_EXPIRED、SESSION_EXPIRED(僅儀表板 / JWT) | 驗證失敗 — 金鑰錯誤、timestamp 過期、簽章不符。OTP / SESSION_EXPIRED 這幾個 code 只出現在儀表板 JWT 路由(/payment/v1/*);純 B2B 整合不會看到它們。 |
| 402 | INSUFFICIENT_CREDIT | 商家預付餘額用盡 — 請先儲值再重試 |
| 403 | FORBIDDEN、IP_BLOCKED | 金鑰有效但缺少呼叫此 API 所需的 scope / IP 許可 |
| 404 | NOT_FOUND、RECORD_NOT_FOUND、USER_NOT_FOUND、SESSION_NOT_FOUND | 資源不存在(或對此商家不存在) |
| 409 | ALREADY_EXISTS、USER_ALREADY_EXISTS、SESSION_ALREADY_EXISTS | 不同 body 的冪等 replay,或狀態機拒絕該轉移 |
| 429 | TOO_MANY_REQUESTS、TOO_MANY_ATTEMPTS | 觸發速率限制;請退避後重試 |
| 500 | INTERNAL_SERVER_ERROR、DATABASE_CONNECTION_ERROR、DATABASE_QUERY_ERROR、DATABASE_TRANSACTION_ERROR、REDIS_CONNECTION_ERROR、REDIS_OPERATION_ERROR、EXTERNAL_SERVICE_ERROR | 我們的問題;可帶退避安全重試。(provider 專屬的 code 例如 TWILIO_SERVICE_ERROR / SENDGRID_SERVICE_ERROR 內部存在,但只在儀表板側的通知流程中出現,B2B endpoint 不會看到。) |
| 503 | SERVICE_UNAVAILABLE | 下游依賴出問題。請退避後重試 |
完整 code 參考
你可能看到的完整 code 值集合(對應
payment-service/pkg/errors/errors.go):
身分驗證與 session(401)
INVALID_CREDENTIALS— 帳號密碼或 API 金鑰組合被拒絕INVALID_TOKEN— JWT / session token 無法解析或被竄改TOKEN_EXPIRED— JWT 已超過expINVALID_OTP— OTP 不符OTP_EXPIRED— OTP 已超過容差視窗SESSION_EXPIRED— 儀表板 session 已過期INVALID_SIGNATURE— B2B / webhook 呼叫的 HMAC 簽章不符
授權(403)
FORBIDDEN— 已驗證但 role / scope / 商家邊界封鎖了該動作IP_BLOCKED— IP 在濫用清單上
找不到(404)
NOT_FOUND— 通用RECORD_NOT_FOUND— 找不到該 ID 對應的列USER_NOT_FOUND— 使用者查詢失敗SESSION_NOT_FOUND— 儀表板 session id 無法辨識
衝突(409)
ALREADY_EXISTS— 通用USER_ALREADY_EXISTS— 註冊撞到 unique 限制SESSION_ALREADY_EXISTS— 重複 session 插入
驗證(400)
INVALID_INPUT— 通用;請查detailsMISSING_REQUIRED— 缺少必要欄位INVALID_FORMAT— 值不符合預期格式(例如 UUID、URL、email)INVALID_LENGTH— 值過短或過長INVALID_VALUE— 值超出允許的 enum / 範圍INVALID_USER_STATUS— 使用者處於不允許該動作的狀態INVALID_USER_ROLE— Role 缺少執行該動作的權限PAYMENT_METHOD_NOT_SUPPORTED— 該 provider / 資產組合未對此商家啟用AMOUNT_BELOW_MINIMUM— 訂單金額低於該網路或環境的下限。回應的details.floor_usd攜帶目前設定的下限(USD),可直接取用顯示;message文字也會說明它。INSUFFICIENT_BALANCE— 買家的錢包持有的支付資產不足以支付此次轉帳。INSUFFICIENT_GAS— 買家的錢包缺少原生 gas 以廣播此次轉帳。
付款 / 計費(402)
INSUFFICIENT_CREDIT— 商家預付餘額不足以支付 gas / 平台費用。請從儀表板儲值後再重試
速率限制(429)
TOO_MANY_REQUESTS— gateway IP 速率限制TOO_MANY_ATTEMPTS— 對同一資源(例如 OTP)反覆失敗導致觸發 throttle
儲存 / 基礎設施(500)
DATABASE_CONNECTION_ERROR— 無法連線資料庫DATABASE_QUERY_ERROR— Query plan 在 runtime 失敗DATABASE_TRANSACTION_ERROR— Commit / rollback 失敗REDIS_CONNECTION_ERROR— 無法連線 RedisREDIS_OPERATION_ERROR— Redis 命令失敗EXTERNAL_SERVICE_ERROR— 通用第三方失敗(下方未分類的 provider)TWILIO_SERVICE_ERROR— Twilio SMS / Verify 呼叫失敗SENDGRID_SERVICE_ERROR— SendGrid 郵件寄送失敗INTERNAL_SERVER_ERROR— 非預期 fall-through;請擷取回應的Date標頭 +X-RateLimit-*並回報我們
可用性(503)
SERVICE_UNAVAILABLE— 一個關鍵下游回報不健康;請退避後重試
驗證錯誤(400)
當問題是請求 body 不正確時,details 是陣列,讓你可以把錯誤對應
回欄位:
{
"code": 400,
"message": "invalid_input",
"details": [
{ "field": "items[0].unit_price", "message": "must be a positive decimal string" },
{ "field": "success_url", "message": "must be a valid https URL" }
]
}身分驗證錯誤(401)
INVALID_SIGNATURE 涵蓋三種不同的失敗模式 — 區分它們的唯一方法
就是逐一排查:
- Secret key 錯誤(再次確認環境變數)
- Timestamp 漂移 > 5 分鐘(同步 NTP)
- Canonical string 錯誤(最常見:忘記
\n分隔符,或簽了一份 經過 parse-then-re-stringify 與實際送出不同的 body)
確切的簽章演算法請見身分驗證。
速率限制(429)
| 範圍 | 限制 |
|---|---|
所有 gateway 路由(含 /b2b/v1/*) | Gateway 以 IP 計算 — 500 req/min,共享 bucket。並非以商家為單位。 |
公開結帳(/checkout/*) | 更嚴格的子 bucket — 每 IP 20 req/min |
X-RateLimit-Limit 與 X-RateLimit-Remaining 在每一次經過
gateway 速率限制檢查的回應上都會發出(不只是成功時)。請把它們視為
你 IP 的即時預算。
被速率限制的回應會包含 Retry-After 標頭(距離視窗重置的秒數)—
請遵循它。作為備援,可以 jitter 退避 — 1s 基底 + 指數成長到 30s。
速率限制可能會調整。如果你是合法地撞到上限(例如對帳大量歷史 資料),請與我們聯繫 — 批次 endpoint 在路線圖上。
信用額度不足(402)
INSUFFICIENT_CREDIT(HTTP 402 Payment Required)代表商家的預付
餘額無法支付你嘗試的操作下一步的 gas 與平台費用 — 典型情境是結算
加密支付,或執行 gas 由平台代付的鏈上動作。請從商家儀表板
(Billing → Add credit)儲值後再重試該操作;處理中的工作會等待,
餘額恢復後會自動接續執行。
伺服器錯誤(5xx)
500 代表我們無法處理該請求。請以退避方式重試 — 你的
idempotency_key 確保即使原請求部分成功,也不會被重複收費。
若 1 分鐘內重試仍未恢復,請對買家顯示通用的「付款暫時無法使用」
訊息,並向 support 回報失敗的 endpoint、你的 merchant ID、回應的
Date 標頭與 X-RateLimit-* 的值,以及大約的請求時間 — 這些資訊
足以讓我們在 log 中定位到對應的請求。
Webhook 投遞錯誤
Webhook 投遞是獨立的失敗通道 — 它們不會以 API 錯誤的形式出現, 因為呼叫的不是你的伺服器。當投遞回非 2xx(或逾時)時,會以指數 退避排程重試:0s、1min、5min、15min、1h、6h(共 6 次嘗試 — 與 Webhooks → 總覽相同排程)。完整的每次 投遞歷史在儀表板的 Developers → Webhooks → [endpoint] → Delivery log 中。第 6 次嘗試失敗後,該投遞會進入死信 — 儀表板會顯示 「Failed」徽章,在你的伺服器恢復後可手動 replay。