你刷卡付款後,商店網站可能在幾秒內把訂單改成「已付款」;Git 平台收到新的 push 後,CI 服務也能立刻開始建置。兩個彼此獨立的系統,為什麼能這麼快知道另一邊剛剛發生了什麼?
一個常見答案就是 Webhook。
先從最直覺的方法開始:一直問
假設系統 A 想知道系統 B 的付款狀態。最直覺的方法是每隔幾秒呼叫一次 API:
GET /payments/123
如果回傳 pending,就過幾秒再問;直到它變成 paid。
這種做法叫 Polling。它不是錯誤技術,而且在某些情況非常合理,但如果事件很少發生,系統卻一直詢問,就會產生大量沒有新資訊的 request。
例如付款平均五分鐘才改變一次,但前端每五秒查一次,那麼絕大多數 request 得到的答案都只是:「還沒變。」
Webhook 把方向反過來:
不要一直問「有事情發生嗎?」;事情發生時,再由對方主動告訴你。
Webhook 本質上是什麼?
Webhook 並不是一種新的網路協定。大多數 Webhook 本質上就是一個 HTTP request。
假設你的服務提供一個網址:
POST https://example.com/webhooks/payment
你把這個 URL 登記到付款服務。當付款完成時,付款服務主動對這個 URL 發出 POST request,body 可能像這樣:
{
"event": "payment.completed",
"payment_id": "pay_123",
"amount": 500
}
這個接收網址通常稱為 Webhook Endpoint,而送來的資料稱為 Payload。
因此一個最基本的流程是:
這就是 Webhook 的核心。
Event:Webhook 通知的是「發生了什麼」
好的 Webhook 通常不是只傳一包模糊資料,而會標明 Event Type,例如:
payment.completed
payment.failed
user.created
repository.push
build.finished
接收端就能依照 event type 決定該做什麼。
例如收到 payment.completed 時更新訂單;收到 payment.failed 時則保留未付款狀態。
這裡有一個重要觀念:Webhook 通常是在描述「事件」,而不是讓接收端遠端操作來源服務。
API 常被用來要求系統做某件事或取得某個狀態;Webhook 則常用來告訴另一個系統:「剛剛發生了一件事。」
兩者不是競爭關係,實務上經常一起使用。
為什麼收到 HTTP 200 還不代表事情真的完成?
來源服務送出 Webhook 後,通常會期待接收端快速回覆成功狀態,例如 2xx。
但如果你的 endpoint 收到付款事件後,立刻開始做十幾件事:寫資料庫、寄信、產生發票、更新庫存、呼叫其他 API,整個 request 可能拖很久,中途任何一步失敗也可能讓來源端以為這次 Webhook 沒有成功。
因此 production 常採用另一種設計:
這也說明 Webhook 與 Message Queue 很常一起出現。Webhook 負責把外部事件送進系統;Queue 負責讓後續工作可靠地慢慢處理。
Webhook 可能送兩次,而且你必須預期它會送兩次
假設來源服務送出 Webhook,你成功更新了訂單,但回傳 200 的途中網路斷掉。
來源服務不知道你其實已經處理成功。從它的角度看,這次 delivery 失敗了,因此稍後可能 Retry,再送一次完全相同的事件。
如果你的程式把「payment.completed」理解成「每收到一次就加 500 元」,同一筆付款就可能被計算兩次。
所以 Webhook handler 必須考慮 Idempotency,也就是同一事件重複處理時,最終結果不應被錯誤累加。
常見做法是每個事件帶有唯一 event_id:
evt_8f31...
接收端先檢查這個 ID 是否已經處理過。若已處理,就直接回覆成功,而不是再次執行副作用。
這不是少見的例外處理,而是 Webhook 設計的基本要求之一。
Retry:失敗後不是立刻放棄
Webhook 經過網路傳輸,接收端也可能暫時維護、超時或回傳 500。因此可靠的來源服務通常有 Retry 機制。
最簡單的做法可能是失敗後固定每分鐘重試,但大量事件同時失敗時,這會讓服務恢復瞬間又遭到一波 request 攻擊。
因此常見策略是 Exponential Backoff:第一次稍後重試,之後逐漸拉長等待時間,並可能加入 Jitter,避免大量 retry 在完全相同的時間再次撞上服務。
接收 Webhook 的系統也應該知道:事件「現在才收到」不代表事件「現在才發生」。
Payload 最好包含事件發生時間,讓接收端能區分 event time 與 delivery time。
不能看到 POST 就相信:Webhook 也需要驗證
Webhook endpoint 通常是公開網址。只要有人知道 URL,就可能自己送出:
{
"event": "payment.completed"
}
如果你的伺服器看到這段 JSON 就直接把訂單改成已付款,任何人都可能偽造事件。
因此成熟的 Webhook 通常會附帶 Signature。
一種常見概念是來源服務與接收端共享一個 Secret。來源服務使用 secret 對原始 request body 計算 HMAC,再把結果放進 HTTP header。接收端收到 request 後,用同一個 secret 重新計算並比較。
若簽章吻合,代表內容很可能確實由持有 secret 的來源產生,而且傳輸內容沒有被任意修改。
注意:不要把「payload 裡寫 sender=trusted」當成驗證。攻擊者也可以自己寫這個欄位。
Secret 也不應直接寫進公開 repository,而應放在環境變數或 Secret 管理系統中。
為什麼簽章驗證常要求 Raw Body?
這是一個很容易踩到的細節。
假設來源端簽署的是收到前的原始 JSON bytes。你的 Web framework 先把 JSON parse 成 object,再重新 stringify,空白、欄位順序或編碼表示可能改變。
即使資料「看起來完全一樣」,bytes 已經不同,重新計算出的 signature 就可能不一致。
因此許多 Webhook provider 會要求你對 Raw Request Body 驗證簽章,而不是對 parse 後再產生的 JSON 驗證。
這也是為什麼「簽章一直驗證失敗」時,問題不一定是 secret 打錯,也可能是 body 在驗證前已經被 framework 處理過。
Timestamp 與 Replay Attack
即使一個 request 的 signature 是真的,攻擊者如果錄下這個合法 request,之後原封不動重送,簽章仍可能有效。
這稱為 Replay Attack 的一種形式。
因此簽章機制常會把 timestamp 一起納入簽署。接收端除了驗證 signature,也檢查 timestamp 是否落在合理時間窗口,例如只接受數分鐘內的 request。
再配合 event_id 去重,就能同時降低偽造與重播造成的風險。
事件不一定照順序到達
想像兩個事件:
- subscription.created
- subscription.cancelled
直覺會認為 created 一定先到、cancelled 一定後到。但在分散式系統裡,不同 request 可能經過不同 retry、queue 或網路延遲,第二個事件反而可能先被接收。
因此不要輕易假設 Webhook Delivery Order 等於 Event Order。
如果順序非常重要,可以利用事件版本號、sequence number 或 event timestamp,並在資料模型中定義哪些狀態轉移才合法。有些情況下,Webhook 只被當成「狀態可能改變了」的通知,接收端收到後再呼叫來源 API 查詢最新 authoritative state。
這個設計特別有用,因為 Webhook 本身不必承擔完整同步狀態的責任。
Webhook 和 Polling 到底誰比較好?
Webhook 的優點很明顯:事件發生時才傳輸,延遲通常低,也不需要大量無效查詢。
但它也帶來新的複雜度:你需要一個外部可連線的 endpoint,需要驗證來源,需要處理 retry、duplicate、out-of-order delivery,也需要觀察失敗事件。
Polling 則比較主動。只要你能呼叫 API,就能自行決定何時取得狀態,不必公開接收 endpoint;缺點是延遲與 request 數量通常較高。
所以真實系統常常不是二選一。
例如以 Webhook 作為即時通知,再用每天一次的 reconciliation job 主動查 API,比對是否有漏掉的事件。Webhook 提供速度,Polling 或批次同步提供最後一道一致性保險。
Webhook 與一般 API 的差異
可以把它們想成兩個方向。
一般 API:你的系統主動呼叫別人。
Webhook:別人的系統在事件發生時主動呼叫你。
因此 Webhook 有時也被形容成 Reverse API,但這只是幫助理解的說法,不代表它是一套獨立於 HTTP 的特殊協定。
更重要的是角色改變:使用 API 時,你控制 request 何時發生;接 Webhook 時,外部系統控制 request 何時抵達。
這代表你的 endpoint 必須能承受突發流量、重複 delivery 與暫時失敗。
一個比較完整的 Production Webhook 流程
把前面的概念合在一起,一個成熟流程可能是:
- 外部服務發生事件。
- 外部服務產生 event_id、timestamp 與 payload。
- 使用 secret 對原始內容產生 signature。
- 對你的 HTTPS endpoint 發出 POST request。
- 你的服務讀取 raw body 並驗證 signature 與 timestamp。
- 檢查 event_id 是否已處理。
- 將合法事件持久化或送進 queue。
- 快速回覆 2xx。
- Consumer 在背景執行真正工作。
- 處理成功後標記 event_id;失敗則依策略 retry。
- 透過 logs、metrics 或 dead-letter 機制追蹤無法處理的事件。
你會發現,真正可靠的 Webhook 系統並不是「開一個 POST endpoint」就結束。HTTP request 只是入口,後面還牽涉驗證、可靠性、非同步處理與可觀察性。
本機開發為什麼特別麻煩?
你的 localhost 通常無法被外部 SaaS 直接連線,因此 provider 沒辦法把 Webhook POST 到 http://localhost:3000。
開發時常見做法是使用 provider 提供的 CLI forwarding 工具,或使用安全 tunnel,把一個暫時的公開 HTTPS URL 轉送到本機。
但這些工具只是在解決「外部如何打進 localhost」,不會替你處理 signature、idempotency 或 retry。Production 上線前,這些邏輯仍必須正確實作。
最容易犯的五個錯誤
第一,收到 payload 就相信,完全沒有驗證 signature。
第二,把所有商業邏輯都塞進 webhook request 裡,導致 endpoint 很慢又容易 timeout。
第三,假設同一事件永遠只會送一次,沒有 event_id 去重或 idempotency。
第四,假設事件一定照順序抵達。
第五,把 Webhook 當成百分之百不會漏的神奇通知系統,沒有 logs、retry 狀態、reconciliation 或人工修復方法。
這五個問題的共同點是:它們都把網路與分散式系統想得太可靠。
結論:Webhook 是事件驅動系統的一扇門
Webhook 的核心概念其實很簡單:事件發生後,由來源系統主動透過 HTTP 通知接收端。
真正值得理解的不是 POST request 本身,而是它背後的工程條件:request 可能失敗、可能重複、可能延遲、可能亂序,也可能被偽造。
因此一個可靠的 Webhook handler 需要回答幾個問題:這真的是可信來源送來的嗎?同一事件來兩次會怎樣?處理失敗後怎麼重試?事件順序錯了怎麼辦?外部服務與自己的資料不一致時,誰才是 authoritative state?
當你開始回答這些問題,Webhook 就不再只是「某個服務打我的 API」,而會成為理解事件驅動架構、Message Queue、Idempotency、Retry 與分散式系統可靠性的入口。
把概念放回真正的系統流程裡看,通常比只背名詞更容易理解它。