Webhook 總覽
Webhook 是權威訊號。瀏覽器回呼 (onSuccess) 與儀表板畫面僅是
輔助資訊;Webhook 才是真相之源。
投遞保證
- 至少一次。 若你的伺服器未在逾時內回應 2xx,單一事件最多會被
投遞 6 次。處理函式請保持冪等 — 以
X-Delivery去重。 - 每個 HTTP 請求僅一個事件。 不做批次。
- 每個端點獨立。 如果你註冊了多個端點,各自擁有獨立的投遞與 重試軌道。某個慢的商家 URL 不會拖累其他端點 — 每台主機都有自己的 斷路器。
- 已簽章。 每個酬載都帶
X-Signature標頭(於密鑰輪替後的 24 小時 視窗期內,還會附帶X-Signature-Prev)。處理 body 前請先驗證。 詳見簽章驗證。
可訂閱的事件類型
| 事件 | 觸發條件 |
|---|---|
payment.settled | 鏈上轉帳達到該鏈的確認數。用此事件標記訂單為已付款。 |
payment.failed | 法幣付款被 provider 明確拒絕(目前:Stripe webhook 通報失敗)。不會因為加密貨幣超時而觸發 — 那些情境會以 checkout.expired 呈現;而短付的加密貨幣付款會以 payment.underpaid 呈現。 |
payment.underpaid | 資金已抵達但不足訂單總額(典型範例:穩定幣轉帳手續費從金額中扣除)。 |
payment.overpaid | 資金超過訂單總額。多出的部分被記錄,但不會自動退款。 |
order.created | 新訂單建立 — 透過你的 B2B API 呼叫或結帳會話轉換。 |
order.canceled | 訂單轉為取消。酬載中的 data.reason 區分手動取消與 payment_timeout(worker 清掃的過期未付訂單)。 |
order.resolved | 一個 PARTIAL_PAID 訂單被解析為 PAID — 商家接受短缺。 |
order.reopened | 先前自動取消(canceled_reason=payment_timeout)的訂單被商家重新開啟。 |
checkout.created | 買家開啟了訂單的結帳。 |
checkout.completed | 買家端流程完成(不代表鏈上結算 — 那是 payment.settled 的事)。 |
checkout.expired | 買家放棄,會話 TTL 已過。 |
payment.refund.requested | 退款紀錄被建立 — 來自商家發起的 API 呼叫,或客戶提交的退款申請表單。 |
payment.refund.approved | 待審退款通過你的核准流程。 |
payment.refund.rejected | 待審退款被駁回。 |
payment.refund.executed | 退款的鏈上轉帳已確認,紀錄進入終態 executed。 |
refund_request.created | Refund-request token 被鑄造。data.source 為 b2b / dashboard / renewal。訂閱與否為選擇性 — 對於追蹤每個訂單目前哪個 token 是有效的稽核管線很有用。 |
refund_request.renewal_requested | 買家在 token 過期後點了「申請新連結」。強烈建議訂閱 — 這是商家收到續期 widget 有新項目需要處理的訊號。 |
refund_request.renewed | 續期通過核准,新的 token 取代舊的。data.old_token / data.new_token 形成稽核鏈。 |
refund_request.canceled | 商家從儀表板把一個 token 翻為 CANCELED(例如駁回續期請求、終止仍有效的連結)。冪等 — 只有第一次轉換會發出。data.reason 是商家選填的備註。 |
儀表板從 GET /v1/webhooks/event-types 取得此列表,所以端點建立與
編輯表單永遠匹配平台實際發出的事件。訂閱平台不發出的事件會在建立
時被拒絕,並附帶清楚的錯誤訊息。
測試事件無法訂閱。 儀表板上的逐端點 Send Test 按鈕會把一個
webhook.test.ping 信封同步 POST 到該端點(繞過重試管線);而舊版的
商家層級「發送測試事件」管道則無論過濾器為何,都會把 webhook.test
信封散發到每個活躍端點。兩者都不會出現在上面的目錄中 — 你是因為註冊了
端點才會收到它們,而不是靠訂閱。
只訂閱你會處理的事件。每個端點有獨立的事件過濾器;萬用字元 "*"
代表「所有事件,包含未來新增的」。訂閱較少事件可讓處理函式更整潔,
並且減少我們需要重試的範圍。
酬載 + 標頭
HTTP body 直接是事件專屬的 data 物件。沒有 Stripe 風格的外層
信封 — 事件類型、投遞 ID、發出時間等欄位改放在標頭裡。對於
payment.settled,body 看起來像這樣:
{
"receipt_id": "rcp_…",
"order_id": "ord_…",
"payment_intent_id": "pin_…",
"checkout_session_id": "cst_…",
"merchant_id": "mer_…",
"customer_id": "cus_…",
"total": "49.00",
"currency": "USD",
"payment_method": "crypto",
"token": "USDC",
"network": "polygon",
"tx_hash": "0x…",
"deposit_address": "0x…",
"treasury_address": "0x…",
"amount_received": "49.00",
"confirmations": 5,
"metadata": { /* 每事件 */ }
}其他事件帶各自的欄位集合 — 在逐事件文件發佈之前,規範形狀請見
payment-service/internal/domain/events.go 的 publisher struct。
欄位名稱穩定(lower snake_case);鏈上 tx hash 永遠是 tx_hash
(不是 transaction_hash)。
入站請求的標頭
Content-Type: application/json
X-Event: payment.settled
X-Delivery: 7c9e6679-7425-40de-944b-e07fc1f90ae7
Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7
X-Timestamp: 1729536000
X-Signature: sha256=9a8b7c…
X-Signature-Prev: sha256=fa31b2… (僅在輪替寬限視窗期內出現)| 標頭 | 內容 |
|---|---|
X-Event | 事件類型(例如 payment.settled)。如想跳過 JSON 解析,可在代理層以此路由。 |
X-Delivery | UUID,識別一筆投遞列。在同一 (event, endpoint) pair 的所有重試間穩定 — 請以它作為冪等鍵。 |
Idempotency-Key | 與 X-Delivery 對應(同值)。每次投遞都會帶 — 沿用 Stripe / GitHub 的慣例。 |
X-Timestamp | 嘗試送出時的 Unix 秒。簽入酬載,使被捕獲的 (body, X-Signature) 對無法被無限重放 — 對超出容差視窗的投遞請拒絕。 |
X-Signature | HMAC-SHA256(secret, X-Timestamp + "." + raw_body) 的 sha256=<hex>。詳見簽章驗證。 |
X-Signature-Prev | 相同演算法但使用前一個金鑰。僅在輪替後的 24 小時視窗期內出現 — 讓使用任一金鑰的驗證器在切換期間都能繼續接受投遞。視窗關閉後此標頭便不再發送。 |
重試時程
若你的端點未在逾時內回應 2xx,我們依下列時程重試(時間戳相對首次
嘗試):
| 嘗試 | 延遲 | 累計 |
|---|---|---|
| 1 | 0s | 0s |
| 2 | +1 分 | 1m |
| 3 | +5 分 | 6m |
| 4 | +15 分 | 21m |
| 5 | +1 小時 | 1h 21m |
| 6 | +6 小時 | 7h 21m |
第 6 次嘗試失敗後,投遞進入死信,商家帳號信箱會收到通知。死信
事件可從儀表板的 Developers → Webhooks → Delivery history 面板
重放,或直接透過 POST /v1/webhooks/deliveries/:id/replay 觸發。
每次重放會建立一筆新的投遞列、擁有自己的 X-Delivery — 稽核鏈
透過 parent_delivery_id 連回原始事件,讓 replay 的重試不會掩蓋
原始事件。
註冊端點
在商家儀表板 中:
- Developers → Webhooks → + Add endpoint
- 貼上你的 URL — 僅限
https://…(純 HTTP 會被拒絕;建立表單也會 阻擋localhost、私有 IP 範圍、以及攜帶使用者資訊的 URL) - 選擇要訂閱的事件(或
*代表全部) - 選擇環境 — test 或 live(各自擁有獨立金鑰,二者永不交叉)
- 儲存 → 儀表板僅顯示一次簽章金鑰(
whsec_…)。請保存到伺服端; 下面兩個功能會用到它。
每個商家每個環境最多可註冊 10 個端點(例如:一個用於正式履約, 一個用於預備鏡像,一個用於 Slack 通知)。每個端點維護獨立的重試 狀態、金鑰以及主機級斷路器。
每個端點的生命週期操作
每張端點卡片的 ⋮ 選單提供:
- Edit — 變更 URL、描述或訂閱列表。新 URL 會以與建立時相同的
https:///SSRF 規則重新驗證。 - Send Test — 以你目前的金鑰同步 POST 一個
webhook.test.ping信封。儀表板顯示 HTTP 狀態、延遲與你回應的前 512 位元組。繞過 RMQ 管線,結果立刻返回。 - Rotate Secret — 產生新金鑰。前一個金鑰仍維持 24 小時 有效
(期間投遞同時帶
X-Signature與X-Signature-Prev,讓使用任一 金鑰的驗證器在你重新部署期間都能繼續接受事件)。 - Reveal Secret — 重新顯示目前金鑰。受新的 2FA 驗證控管並記錄 到稽核日誌;僅在你遺失副本且無法接受輪替時使用。
- Enable / Disable — 不遺失投遞歷史的情況下切換
is_active。 停用的端點仍保留在儀表板,但不再接收新投遞。 - Delete — 永久刪除。若可能再次啟用,請使用 Disable。
處理函式小技巧
- 儘速回應 2xx。 在做重活前先以
200 OK確認 — 把履約丟到 背景佇列。每次嘗試的逾時為 10 秒;回應保持超過此時間會觸發 重試。此逾時為平台端設定,商家無法調整 — 若你的 handler 確實 需要更多時間,請聯絡 support。 - 以
X-Delivery去重(或同值的Idempotency-Key)。即使你 回應了 2xx,上游代理可能斷線並觸發重試;投遞 ID 在同一筆投遞 列的每次重試之間穩定,因此是正確的鍵。 - 容忍未知事件類型。 可能會出現新事件;請回 200 + no-op, 而非 4xx,否則重試佇列會被塞滿。
- 在業務邏輯旁邊記錄
X-Delivery。 出問題時,這就是我們這邊 與你那邊的串接鍵。