AnyHook 文件

Webhook 中繼服務(webhook relay)。把發送端指向 AnyHook,就有自動重試、完整的事件紀錄、統一的簽章驗證和一鍵重播。不需要任何 SDK。

快速入門

AnyHook 不需要 SDK,也不需要安裝套件:把發送端儀表板裡的 Webhook 網址換掉,你的處理程式就會繼續收到同樣的請求。有一件事要事先規劃:重試和重播帶的是發送端原始的時間戳記,所以發送端的簽章會過期時(Stripe、Slack,以及 /providers 上標有容許時間差的其他服務商),就改為檢查 AnyHook-Signature 來接受這些請求。

最快的方式(不需帳號):執行 curl -X POST https://anyhook.net/api/v1/quickstart,立刻取得一個可用的接收網址(inbound URL)和 API 金鑰,適合 AI agent 和快速測試。這個端點如果 7 天內沒有透過回傳的 claim_url 認領,就會到期。把發送端指向回傳的 inbound_url,再用 GET /api/v1/events 和 GET /api/v1/events/{id} 讀回事件;回應中的 next_steps 列出了每個指令。

  1. 1
    建立帳號
    前往 anyhook.net/sign-up 建立免費帳號。不需信用卡。
  2. 2
    建立 App
    一個 App 就是一個接收網址,對應一個 Webhook 來源。新帳號一開始就會看到建立表單:替它取個名字(例如「Stripe Production」),也可以選填你自己的伺服器網址,AnyHook 收到每個事件後會轉送到那裡。留空的話,App 會記錄所有事件,但不轉送。目的地隨時都能新增,Free 方案每個 App 最多 3 個。
  3. 3
    把發送端指向 AnyHook
    App 頁面會顯示你的接收網址,以及常見發送端要在哪裡填入它(例如 Stripe → Developers → Webhooks)。把它貼到那裡,取代原本的 Webhook 網址:
    https://in.anyhook.net/{user-slug}/{app-slug}
    這個頁面會持續監看:第一個請求幾秒內就會出現在上面。還沒有東西可以送?在同一頁按下 Send a test event(傳送測試事件)。

接收網址格式

每個 App 都有一個專屬的接收網址:

https://in.anyhook.net/{user-slug}/{app-slug}
  • user-slug:你的帳號識別碼,可以在儀表板的設定中看到。
  • app-slug:每個 App 各不相同,依 App 名稱自動產生,也可以修改。

接收端點接受任何 POST 請求,也會回應設定時的驗證交握:帶有 hub.mode=subscribe 的 GET(Meta 的 hub.challenge 檢查)會自動原樣回傳,即使 App 還沒設定好也一樣。其他方法一律回 405。網頁和一般的 HTML 表單也能 POST 到這裡,詳見程式碼處理程式(code handler)。事件一寫入持久佇列,AnyHook 就回傳 HTTP 200,大約 1 秒,不受目的地端點的速度影響,所以發送端完全不必等它。

內容類型。 文字格式的請求主體,例如 application/json、application/x-www-form-urlencoded、XML 或純文字,AnyHook 會逐位元組原樣保存,並帶著原本的 Content-Type 轉送。請求主體以文字形式儲存,所以二進位 payload 或 multipart 表單裡的檔案,無法完整送達。

支援的來源

AnyHook 會從傳入的標頭自動判斷發送端,並在邊緣節點驗證 payload 的簽章。要啟用驗證,建立 App 時選擇你的 Webhook 服務商,並貼上服務商的 Webhook 簽章密鑰(例如 Stripe 的 whsec_...)。

強烈建議。 沒有簽章密鑰,AnyHook 就無法確認傳入的請求真的來自你的服務商。未驗證的請求照樣會轉送;加上密鑰後,AnyHook 就能在偽造的請求進入你的處理流程之前,先在邊緣節點拒收。
來源簽章標頭演算法
StripeStripe-SignatureHMAC-SHA256 + 時間戳記
GitHubX-Hub-Signature-256HMAC-SHA256
Meta (Instagram / Facebook / Threads)X-Hub-Signature-256HMAC-SHA256
ShopifyX-Shopify-Hmac-Sha256HMAC-SHA256 (Base64)
DiscordX-Signature-Ed25519Ed25519
SlackX-Slack-SignatureHMAC-SHA256 + 時間戳記
LINEx-line-signatureHMAC-SHA256 (Base64)
Xerox-xero-signatureHMAC-SHA256 (Base64)
LinearLinear-SignatureHMAC-SHA256
SentrySentry-Hook-SignatureHMAC-SHA256
Vercelx-vercel-signatureHMAC-SHA1
PaddlePaddle-SignatureHMAC-SHA256 + 時間戳記
SendGridX-Twilio-Email-Event-Webhook-SignatureECDSA P-256
TwilioX-Twilio-SignatureHMAC-SHA1
IntercomX-Hub-SignatureHMAC-SHA1
HubSpotX-HubSpot-Signature-v3HMAC-SHA256 (v3) / SHA-256 (v1/v2)
Standard Webhooks (Svix, OpenAI, Replicate)webhook-signature / Svix-SignatureHMAC-SHA256 + 時間戳記
ClerkSvix-SignatureHMAC-SHA256(透過 Svix)
ResendSvix-SignatureHMAC-SHA256(透過 Svix)
Lemon SqueezyX-SignatureHMAC-SHA256
WooCommerceX-WC-Webhook-SignatureHMAC-SHA256 (Base64)
PayPalPAYPAL-TRANSMISSION-SIGRSA-SHA256
Generic/自訂無不驗證

沒通過檢查的請求會在邊緣節點拒收,不會抵達你的端點。App 頁面會計算數量,每天的前 5 個會以 rejected 狀態連同標頭和主體保存在事件紀錄裡,讓你看得到原因。

Meta:Instagram、Facebook、Threads

這三者共用同一套 Graph API Webhook 規格,所以一個 App 就能涵蓋。Meta 用 X-Hub-Signature-256 簽章,和 GitHub 用的標頭相同;AnyHook 靠 X-Github-Event 區分兩者,GitHub 一定會送這個標頭,Meta 從來不送。

Meta 要等 callback 網址回應帶有 hub.mode=subscribe 的 GET,把 hub.challenge 原樣當作整個回應主體回傳,才會儲存這個網址。AnyHook 會替你回應,所以接收網址第一次就能儲存成功。

在 AnyHook在 Meta 的 App Dashboard
Source:選 MetaApp settings → Basic
App Secret從這裡複製到 AnyHook
Verify Token(選填)Webhooks → Verify token:輸入同一個字串
你的接收網址Webhooks → Callback URL:貼上它
Verify Token 的方向相反。 這裡其他的密鑰都是從服務商那裡複製出來的;這一個是你自己訂的,再輸入到 Meta。它不是 App Secret,也不該用同一個字串:它會出現在設定交握的查詢字串裡,而簽章密鑰不該出現在網址中。留空的話,訂閱照樣能完成;設定之後,AnyHook 會先檢查它再回應。

Meta 會在 36 小時內以逐漸降低的頻率重試失敗的傳遞,之後就放棄。因為 AnyHook 會先儲存事件,再嘗試傳遞到你的端點,所以你這邊部署或服務中斷時,不會用掉這段時間。

傳遞的運作方式

AnyHook 採用以佇列為基礎的非同步架構,讓 Webhook 發送端和你的後端彼此解耦:

  1. 邊緣節點接收:Cloudflare Workers 接收 POST、驗證簽章,事件持久儲存之後就回 200,大約 1 秒。
  2. 佇列:事件發布到持久的訊息佇列(Upstash QStash)。這時發送端的 HTTP 連線已經關閉。
  3. 轉送:QStash 把事件交給 AnyHook 的轉送程式,再由它把原始 payload POST 到你的目的地端點。
  4. 記錄:每個步驟都會記下來,包括接收的標頭和主體、傳遞狀態、端點回應的狀態碼和主體,以及延遲時間。
這樣做的好處。 你的發送端(Stripe、GitHub 等)大約 1 秒就會收到 200。你的後端接著最多可以花方案的傳遞逾時時間回應(Free 方案 60 秒、Pro 方案 120 秒、Scale 方案 5 分鐘),發送端不會重試,因為它已經收到 200。等待超過第一秒的部分,會用掉運算額度。
payload 大小上限。 每個事件的 payload 上限:Free 方案 512 KB、Pro 方案 2 MB、Scale 方案 4 MB、Enterprise 方案客製。超過上限的請求會在邊緣節點以 413 Payload Too Large 拒收,絕不會無聲無息地丟棄,也絕不計費。App 頁面會計算數量。
傳遞模式。 預設情況下,事件一進來,AnyHook 就會轉送,沒有延遲,也沒有並行上限。Pro 方案以上,目的地可以設定每秒的速率限制,以及第一次嘗試前的延遲。如果你的端點回傳 429,AnyHook 會退避後重試。

傳遞保證

直接把約定講清楚,因為對保證含糊其辭的中繼服務,會把這份含糊推給你的程式碼:

  1. 至少一次,不是恰好一次。重試就是 AnyHook 的本業,而回應遺失之後的重試,和一次全新的傳遞根本無法區分。偶爾你會收到同一個事件兩次;沒有任何中繼服務能保證不會這樣,宣稱做得到的,說的只是一切順利時的情況。你的處理程式必須冪等,去重的鍵就是我們在 X-AnyHook-Delivery 轉送的事件 ID。
  2. 不保證順序。因為有重試和並行傳遞,較晚的事件可能比還在重試中的較早事件先送達。大多數發送端在我們的上游也不承諾順序。依 payload 裡的欄位排序(版本號、updated_at),絕不要依抵達時間。
  3. 不要用簽章去重。AnyHook-Signature 每次嘗試都會以新的時間戳記重新簽章,這是刻意的,讓重試永遠不會超出你的容許時間差。同一個事件的兩次傳遞會帶著不同的簽章;不變的是事件的 id。
  4. 收下就代表已持久儲存。邊緣節點要等事件儲存之後才回 200,而且在改回 503 之前,佇列後面還有兩個備援儲存。接收網址回 503,代表什麼都沒儲存,這時發送端自己的重試就是安全網;這正是我們寧可回 503、也不假裝成功的原因。
實務上的做法。 把處理程式寫成同一個事件處理兩次也無害,並把抵達順序當成雜訊。我們把這些做法依保護程度排序,寫在「同一個 Webhook 收到兩次:這是約定好的行為,不是 bug」這篇文章裡。

重試排程

目的地回傳可重試的錯誤或逾時時,AnyHook 會以指數退避自動重試:

第幾次嘗試延遲方案
1立即所有方案
25 分鐘後所有方案
330 分鐘後所有方案
43 小時後Pro、Scale、Enterprise
518 小時後Pro、Scale、Enterprise
6-10每次間隔 24 小時Scale、Enterprise
傳遞逾時。 AnyHook 等你的端點回應的時間上限:Free 方案 60 秒、Pro 方案 120 秒、Scale 以上方案 5 分鐘。如果處理程式需要更久,例如多步驟的 AI agent,就立即回傳 HTTP 202 Accepted,再非同步處理。AnyHook 收到 202 就會把這次傳遞標為成功。

哪些回應會觸發重試?

不是所有錯誤都可以重試。AnyHook 會分類端點的回應,避免把嘗試次數或運算額度浪費在再試也解決不了的錯誤上:

狀態碼處理方式原因
2xx成功:傳遞完成你的端點已接受這個事件。
3xx不再重試:立即標為失敗不會跟隨重新導向;把目的地設成最終網址。
408 Request Timeout退避後重試暫時性逾時,下次嘗試可能成功。
429 Too Many Requests退避後重試你的端點正在限制速率,給它一點時間。
4xx(其他:400、401、403、404、422…)不再重試:立即標為失敗用戶端錯誤:請求格式有誤或未經授權。用同一個 payload 重試也沒有用。
5xx退避後重試伺服器錯誤,可能是暫時的。
網路錯誤/逾時退避後重試無法連上你的端點。
4xx 不重試的原因。 400 Bad Request 或 422 Unprocessable Entity 代表你的處理程式拒絕了這個 payload,重試也不會改變結果。立即標為失敗,你能更快收到通知,重試不會用掉運算額度,修好處理程式後還能重播這個事件。

每次嘗試都會記錄完整的回應主體和狀態碼。不可重試的 4xx 失敗和可重試的失敗都會計入斷路器,任何類型連續失敗 20 次,斷路器就會跳脫。

失敗通知

AnyHook 只在特定門檻寄出 Email 警示,而不是每個事件都寄,讓你的收件匣不會被淹沒:

  • 第 1 次失敗:立即發出警示,附上狀態碼和目的地。
  • 連續第 5 次失敗:發出升級警示,附上處理建議。
  • 連續第 20 次失敗:斷路器跳脫(見下方),並寄出斷路器警示。

上面每個門檻各寄一封 Email,之後斷路器的每個階段(每次暫停和停用)也各寄一封,絕不會每個事件各寄一封。只要有一次傳遞成功,計數就會歸零。

斷路器

如果 AnyHook 偵測到同一個目的地連續 20 次傳遞失敗(包括不可重試的 4xx),斷路器就會跳脫,並開始逐步升級:

第幾次跳脫暫停時間結果
110 分鐘暫停目的地,寄出 Email
230 分鐘暫停目的地,寄出 Email
32 小時暫停目的地,寄出 Email
424 小時暫停目的地,寄出 Email
5永久停用目的地,寄出 Email

運作方式

  • 照樣接收事件。接收網址始終有效,Stripe、GitHub 等來源的事件照樣接收並儲存。暫停的只有傳遞到目的地這一段。
  • 每次暫停結束後,AnyHook 會送出半開啟探測:用下一個事件測試你的目的地。成功的話,斷路器關閉,一切重設。
  • 只要成功一次,一切重設:連續失敗次數和跳脫次數都歸零。
  • 暫停期間收到的事件會以 circuit_open 狀態儲存,目的地恢復後即可重播。
  • 每升級一級,你都會收到 Email 通知,內容包括目的地網址、失敗次數和暫停時間。
  • 如果目的地已停用(第 5 次跳脫),必須從儀表板手動重新啟用。

這種逐步升級的做法,讓你的伺服器有時間恢復,也替運算額度省下當機的端點會耗掉的那些重試。

事件紀錄

每個抵達接收網址的事件都會被記錄,不論傳遞結果如何。事件紀錄從你把服務商指向 AnyHook 的那一刻開始:在那之前失敗的事件只存在於服務商那裡(Stripe 的情況見如何重播 Webhook)。每筆紀錄包含:

  • 完整的接收標頭和主體(和發送端送來的一模一樣)
  • 傳遞狀態:queued → success / retrying / failed / rejected / circuit_open / quota_exceeded。rejected 代表 AnyHook 拒收了這個事件(簽章驗證失敗,或目的地網址被封鎖),從未嘗試傳遞,也無法重播。
  • 目的地回應的 HTTP 狀態碼和主體(每次嘗試)
  • 延遲時間(毫秒)
  • 嘗試次數和下次重試時間
  • 事件是否為重播(is_replay: true)

事件保存多久取決於方案。Free 方案:3 天,期間內都能重播,之後刪除。Pro 方案:1 年,前 30 天可以重播,之後轉為唯讀的加密封存。Scale 方案:7 年,前 90 天可以重播,之後轉為唯讀的加密封存。Enterprise 方案:彈性。

暫停中的 App 什麼都不保存。 App 暫停期間(is_active: false),它的接收網址會回 202,但什麼都不儲存:服務商不會因此停用端點,事件卻就此消失。需要的流量進來之前,先取消暫停。

在儀表板重播

事件紀錄裡的任何事件,都能在儀表板上一鍵重播。重播會把儲存的 payload 逐位元組原樣送到事件當初的目的地;如果之後你改過那個目的地的網址,就送到新網址。

  • 前往 Dashboard → Events
  • 找到該事件,按一下 Replay(重播)
  • 系統會建立一個帶有 is_replay: true 的新事件,並連結到原始事件
配額。 重播不計入每月事件配額,只有被保留(quota_exceeded)的事件例外,重播時會像新事件一樣算一次。重播到 App 的程式碼時,會算一次程式碼執行。

API 重播(單一事件)

POST/api/v1/events/{id}/replay依 ID 重播單一事件

把原始事件的 payload 發布到 QStash 重新傳遞。新事件透過 replay_of 連結到原始事件。

curl -X POST https://anyhook.net/api/v1/events/{event_id}/replay \
  -H "Authorization: Bearer {api_key}"

成功時回傳 200 和新事件的 ID。事件超過可重播期間時,回傳 410 Gone。

未送達的事件

GET/api/v1/apps/{slug}/events/undelivered列出傳遞失敗的事件

回傳狀態為 failed、circuit_open 或 quota_exceeded 的事件。

參數類型預設值說明
sinceISO 860124 小時前只列出這個時間戳記之後建立的事件
limitinteger100回傳的事件數上限(1–200)
curl "https://anyhook.net/api/v1/apps/stripe-prod/events/undelivered?since=2026-04-11T00:00:00Z&limit=50" \
  -H "Authorization: Bearer {api_key}"
{
  "count": 3,
  "events": [
    { "id": "evt_abc", "status": "failed", "created_at": "..." },
    ...
  ]
}

批次重播

POST/api/v1/apps/{slug}/replay-failed重播某個 App 失敗的事件

重播符合條件的失敗事件,最舊的先重播,每次呼叫不超過上限。已經重播過,或超過可重播期間的事件會略過。

請求主體參數類型預設值說明
sinceISO 860124 小時前只重播這個時間戳記之後建立的事件
limitinteger50重播的事件數上限(1–100)
curl -X POST https://anyhook.net/api/v1/apps/stripe-prod/replay-failed \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{"since": "2026-04-11T00:00:00Z", "limit": 100}'
{
  "replayed": 3,
  "events": [
    { "original_id": "evt_abc", "new_id": "evt_xyz" },
    ...
  ]
}
每日重播上限。 Free 方案每天 100 次,Pro 方案每天 500 次,Scale 方案每天 1,000 次,Enterprise 方案不限。

本機開發

AnyHook 直接就能搭配本機開發,不需要特殊模式或 CLI。有兩種做法:

1. 透過隧道即時轉送

把 App 的目的地設成指向你本機的隧道網址。任何隧道服務都可以:

# ngrok
ngrok http 3000
# → https://abc123.ngrok.io

# cloudflared
cloudflared tunnel --url http://localhost:3000

# localhost.run
ssh -R 80:localhost:3000 nokey@localhost.run

在儀表板把隧道網址填進 App 的目的地。AnyHook 會像轉送到其他端點一樣把 Webhook 轉送過去,重試、紀錄和簽章標頭全都照常運作。

提示。 開發期間,讓服務商繼續指向正式環境的接收網址(https://in.anyhook.net/you/app)。真實事件會記錄在事件紀錄裡,你也能在隧道和正式環境之間切換目的地,完全不用動 Stripe。

2. 先收集,再重播到 localhost

讓正式環境的真實事件累積在 AnyHook 裡,需要時再把任何一個重播到本機環境。不必手動觸發測試事件,直接用真實的 payload 測試,只要不超過每日重播上限,想測幾次都可以。

  • 把目的地切換成隧道網址
  • 開啟 Dashboard → Events,在任一事件上按一下 Replay
  • 或使用 API:POST /api/v1/events/{id}/replay
  • 原始 payload 會逐位元組原樣送到目前的目的地

這在為邊界情況除錯時特別有用:在正式環境的紀錄裡找到出問題的事件,在本機重播,反覆修改直到處理程式正確運作。

重播不計入配額。 重播不計入每月事件配額。重播會用到運算額度,並受每日重播上限限制(Free 方案 100 次、Pro 方案 500 次、Scale 方案 1,000 次)。

靜態資料加密

所有 Webhook payload 資料在寫入資料庫之前,都會以 AES-256-GCM 加密。範圍包括接收標頭、請求主體,以及每個目的地的簽章密鑰。

加密使用專屬的金鑰(AES_SECRET_KEY),和資料庫憑證分開存放。資料庫外洩時,沒有加密金鑰就只拿得到密文:沒有明文的 Webhook 資料、沒有簽章密鑰,也沒有客戶資訊。

加密的內容

  • Webhook 請求標頭和主體(每個事件)
  • 服務商的簽章密鑰(Stripe Webhook 密鑰等)
  • 每個目的地的 AnyHook 簽章密鑰
資料庫裡沒有任何明文密鑰。 每個密鑰和每個 Webhook payload 在儲存前都以 AES-256-GCM 加密。即使攻擊者取得資料庫的完整存取權,看到的也只有密文。

邊緣驗證

AnyHook 在 Cloudflare 的邊緣節點驗證 Webhook 簽章,在事件進入佇列之前完成。App 有了簽章密鑰後,缺少簽章或簽章無效的請求會以 401 拒收,不會用掉你的配額;沒有密鑰時,請求不經驗證就轉送。

AnyHook 依標頭自動判斷簽章類型。建立 App 並選擇來源類型時,AnyHook 會儲存簽章密鑰(儲存時已加密),並針對該服務商使用正確的演算法。

無效的請求。 簽章驗證失敗的請求會以 HTTP 401 拒收,不會抵達你的目的地。App 頁面會計算數量,每天的前 5 個會連同標頭和主體保存在事件紀錄裡。

不支援或自訂的來源

如果你的 Webhook 來源不在支援清單上,建立 App 時選擇 Generic 來源。AnyHook 會:

  • 接收每個傳入的 POST 並放進佇列,不檢查傳入的簽章。
  • 把請求主體逐位元組原樣連同服務商的原始標頭轉送到你的目的地,每次嘗試另外附上新的 AnyHook-Signature。在我們邊緣節點之前由基礎設施加上的標頭(cf-* 之類)會被移除。
  • 附上 AnyHook-Signature 標頭,讓你的端點仍能驗證請求確實經過 AnyHook。

Generic App 不論設了什麼密鑰,都不檢查傳入的簽章。AnyHook-Signature 證明請求經過 AnyHook,卻無法證明是誰送給 AnyHook;如果你需要這一點,就繼續自己檢查發送端的原始標頭(原封不動轉送)。如果發送端用 Standard Webhooks 簽章(webhook-id、webhook-timestamp、webhook-signature,OpenAI 和 Replicate 都是這樣),就改選那個來源:它會在邊緣節點驗證。

AnyHook-Signature

每個轉送的請求都帶有 AnyHook-Signature 標頭,這是用目的地的簽章密鑰產生、附時間戳記的 HMAC-SHA256 簽章。

AnyHook-Signature: t=1712934567,v1=5257a869...

在你的伺服器上驗證:

// Node.js / TypeScript
import crypto from "crypto";

function verifyAnyHook(header: string, body: string, secret: string): boolean {
  const [tPart, vPart] = header.split(",");
  const timestamp = Number(tPart?.replace("t=", ""));
  // Re-signed on every attempt, so a genuine signature is never older than this.
  if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
  const signature = Buffer.from(vPart?.replace("v1=", "") ?? "", "hex");

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${body}`)
    .digest();

  // timingSafeEqual throws on a length mismatch, so check that first.
  return signature.length === expected.length && crypto.timingSafeEqual(signature, expected);
}

簽章密鑰依目的地個別產生,可以在儀表板的 App → Destinations → Signing secret 找到。你隨時都能透過 API 輪替它。

自訂標頭

你可以依目的地,為對外的請求加上自訂標頭。常見用途:

  • Authorization: Bearer ...:向你的 API 驗證身分
  • x-api-key: ...:API 金鑰驗證
  • 你的端點需要的任何標頭

自訂標頭的值會加密儲存,也不會出現在事件紀錄裡。在 App → Destinations → Custom headers 設定。

原始標頭原封不動。 服務商的原始標頭(Stripe-Signature 等)會逐位元組原樣轉送,和你的自訂標頭及 AnyHook-Signature 一起送出,所以既有的 SDK 檢查在第一次傳遞時就能通過。重試和重播帶的是服務商原始的時間戳記,而 Stripe 的 SDK 預設超過 5 分鐘就會拒絕:這時改為檢查 AnyHook-Signature,它每次嘗試都會重新簽章。

傳遞控制

每個目的地都有三個選用設定,Pro 以上方案皆可使用。在 App → Destinations → Delivery controls 設定,或用 PATCH /api/v1/apps/{slug} 設定目的地物件。Free 帳號會保有付費期間設定的控制,但無法更改。

{
  "destinations": [{
    "url": "https://api.example.com/hooks/stripe",
    "event_types": ["invoice.*", "checkout.session.completed"],
    "rate_limit": 20,
    "delay_seconds": 5
  }]
}

事件類型篩選

event_types 是一組比對模式;目的地只會收到類型符合其中之一的事件。* 比對任意長度的字元,所以 invoice.* 涵蓋所有 invoice 事件,checkout.session.completed 則只比對這一種。比對不分大小寫,且針對整個類型名稱。有兩個目的地時,就能依類型分送:帳務事件送往一個服務,其他事件送往另一個。

  • 在邊緣節點判斷,時間點在簽章驗證之後、配額計數之前:被篩掉的事件不會儲存、不會傳遞,也不計入配額。
  • 發送端會收到 200 {"filtered":true,"event_type":"…"},Stripe 和 Shopify 會在它們自己的 Webhook 紀錄裡顯示這個回應。
  • 類型從哪裡讀取,依服務商而定:GitHub、Shopify、WooCommerce 和 Lemon Squeezy 讀標頭;Stripe、Clerk、Resend、Vercel 和 Svix 類型的發送端讀請求主體的 type;Paddle 和 PayPal 讀 event_type。這個值和事件紀錄 Type(類型)欄顯示的相同。
無法辨識類型的事件一律傳遞。 AnyHook 讀不出類型的事件(沒有 type 欄位的一般請求主體、服務商的 ping),不論篩選條件如何,都會傳遞到每個目的地。中繼服務的承諾是不會遺失任何事件;篩選如果吃掉未知類型的事件,就打破了這個承諾。

速率限制

rate_limit 把送往目的地的傳遞限制在每秒這個次數(1 到 1,000)。超過上限的事件會在佇列中等候,有餘裕時依序送出;不會遺失任何事件,重試也計入同一個上限。發送端的突發流量可能超過你的伺服器能消化的速度時,就用它:Shopify 大量更新商品、Stripe 月初的一波訂閱續約、GitHub monorepo 一次 push 觸發大量 CI 事件。

批次重播也遵守這個上限,而這正是它最重要的時候:以每秒 20 次重播 500 個失敗事件,就是 25 秒的穩定流量,而不是一波再次失敗的尖峰。

延遲傳遞

delay_seconds 會延後第一次傳遞嘗試(1 秒到 1 天)。常見的原因是發送端的 Webhook 和它自己的 API 互相賽跑:Stripe 的 checkout.session.completed 可能在你的結帳成功頁面寫完訂單之前就送到,延遲五秒,這場競態就不再是問題。重播一律不延遲;第一次嘗試之後的重試排程不變。

Slack / Discord 警示

在 Pro 以上方案,AnyHook 寄出的每封 Email 警示,也會發布到你在 Settings → Notifications 設定一次的 Incoming Webhook:傳遞失敗(第 1 次和連續第 5 次)、斷路器跳脫與停用、拒收量暴增與恢復正常,以及 50% / 90% / 100% 的配額通知。儲存網址時會發布一則測試訊息,讓你確認有送達。Slack、Discord、Mattermost、Rocket.Chat 和 Teams 的 incoming-webhook 連接器都能使用;payload 是 {"text": …}(Discord 則是 content)。關閉「Email on failure」(失敗時寄送 Email)只會停掉 Email,頻道仍會繼續收到警示。

程式碼處理程式

在 Pro 和 Scale 方案中,App 可以在沙箱裡,對收到的每個事件執行你自己的程式碼,不需要自己的伺服器。儲存程式碼後,App 會多一個執行它的目的地,因此重試、重播、斷路器和事件紀錄都和其他目的地一樣運作,每次執行的主控台輸出和回傳值,就是該次傳遞的回應。可以在 App 的 Code(程式碼)區塊撰寫,或使用下方的端點。常見操作的範本在 /code-templates.md,從 Pipedream 搬過來也有專門的指南。

export default async function (event, { env, store }) {
  // event: { method, headers, body, raw_body, delivery_id, attempt }
  console.log(event.body);
}
  • 事件。body 是解析後的 JSON、表單欄位(urlencoded,或 multipart 表單的文字欄位)或原始文字,raw_body 則是收到時的原始文字。method 一律是 POST;查詢字串、路徑和呼叫端的 IP 都不會保存。delivery_id 是事件的 X-AnyHook-Delivery ID,attempt 從 1 開始計數,測試執行時為 0。
  • 失敗。拋出錯誤會讓這次傳遞失敗,並依方案的嘗試次數重試(重試排程)。某個事件的執行已經成功時,它的重試不會再執行一次;但如果執行是在某個呼叫已經送出後才失敗,重試就會再執行一次,所以在 API 允許的地方,用 delivery_id 當這類呼叫的冪等鍵。
  • Pipedream 步驟。export default defineComponent({ async run({ steps, $ }) {} }) 也能執行:steps.trigger.event 就是上面的事件,$.export 和 $.flow.exit 都能用,每個 data_store prop 都是 App 的儲存區(store),app prop 的 $auth.field 會讀取密鑰 APP_FIELD,其他 prop 則讀取以它命名的密鑰(channelId 讀取 CHANNEL_ID)。哪些無法沿用,列在 Pipedream 指南裡。
  • npm 套件。程式碼 import 的套件會在儲存時安裝,固定版本的寫法和 Pipedream 相同(import axios from "axios@1.7.9"),最多 20 個套件。需要安裝新套件的儲存可能要花幾分鐘;安裝失敗就什麼都不會儲存。
  • 密鑰。以 env.NAME 或 process.env.NAME 讀取,最多 20 個,名稱只能用英文字母、數字和底線,且不能以數字開頭。PATH, HOME, NODE_OPTIONS, NODE_PATH, PWD 和以 ANYHOOK_ 開頭的名稱由執行環境專用,每個值最多 4,000 個字元。值絕不會離開伺服器。
  • 儲存區。store.get、set({ ttl } 以秒為單位)、has、delete、keys、entries 和 clear 能在事件之間保存狀態,每個 App 一個儲存區:最多 1,000 個鍵,每個鍵最長 256 個字元,每個值以 JSON 計最多 64,000 位元組。匯入時接受 {"entries": {key: value}},會覆寫已存在的鍵,並且不設 TTL。
  • 排程。schedule 是五個 cron 欄位,除非加上 CRON_TZ=Area/City 前綴,否則以 UTC 計。每次觸發都會把一個 anyhook.schedule 事件 POST 到 App 自己的網址,所以同時算一次執行和一個事件,而且只在程式碼開啟時觸發。App 的來源必須是 generic 或 auto,且不設簽章密鑰:替排程用的程式碼另開一個 App。
  • Email。每個 App 也能在 {user-slug}.{app-slug}@anyhook.net 收信,信件會變成一個 email.received 事件,主體包含 from、to、subject、message_id、date、size,以及放在 raw 的 MIME 信件。和排程一樣,需要來源為 generic 或 auto、且不設簽章密鑰的 App;超過方案 payload 上限的信件會被退回。
  • 網頁和表單。網頁可以用 fetch() 呼叫接收網址:邊緣節點會回應 CORS 預檢請求,每個回應都帶有 Access-Control-Allow-Origin: *。一般的 HTML 表單需要 method="post"。在表單的 action 加上 ?redirect=https://your-site.com/thanks,事件儲存後就會以 303 把訪客導向那裡,前提是這個網址和送出表單的網站是同一個網站;否則訪客會看到一個寫著「Received」(已收到)的簡短頁面。文字欄位會出現在 event.body;上傳的檔案則不會留下。
  • 上限。程式碼最多 64,000 位元組,每次執行在 Pro 方案最多 105 秒、Scale 方案最多 285 秒。每次執行保存主控台輸出的最後 16,000 個字元。每月執行次數列在方案表裡,每次執行都計入,包括重試、測試執行、重播和排程觸發。沒有超額計費:超過當月執行次數後,送往程式碼的傳遞會回 429,並被保留(circuit_open),不重試,等到下個月或方案升級後即可重播。
  • 測試,以及開啟與關閉。POST .../code/test 會用 App 最新儲存的事件、指定的 event_id,或你傳入的 body 執行已儲存的程式碼,並回傳 log、回傳值和執行時間。它算一次執行,不會傳遞任何東西,但程式碼自己發出的請求都會真的送出。只有程式碼開啟時,事件才會執行它。儀表板儲存的新程式碼預設為關閉,而 PUT .../code 儲存的第一個版本會直接上線,除非你傳入 "enabled": false;之後的儲存則維持目前的狀態。如果還有另一個目的地在處理同樣的事件(例如遷移期間),就用 "enabled": false 儲存,移除那個目的地之後再開啟程式碼。這兩種變更最多需要一分鐘,才會傳到每個邊緣節點。
GET/api/v1/apps/{slug}/code程式碼、密鑰名稱、排程和套件、事件是否會執行它,以及本月已用的執行次數
PUT/api/v1/apps/{slug}/code儲存程式碼、密鑰(合併;null 會移除該項)、排程(null 會移除)和 enabled
DELETE/api/v1/apps/{slug}/code移除程式碼、它的密鑰和排程
POST/api/v1/apps/{slug}/code/test立即用已儲存的事件或你傳入的 body,執行已儲存的程式碼
GET/api/v1/apps/{slug}/storeApp 儲存區中所有未過期的鍵
POST/api/v1/apps/{slug}/store匯入鍵:{"entries": {key: value}}
DELETE/api/v1/apps/{slug}/store用 ?key= 移除一個鍵,或移除所有鍵

驗證

所有 API 請求都需要在 Authorization 標頭帶上 Bearer token:

Authorization: Bearer ahk_live_xxxxxxxxxxxxxxxx

在 Dashboard → Settings → API Keys 產生 API 金鑰。金鑰的範圍是你的帳號,可以是完整存取或唯讀(建立時選擇),而且隨時都能撤銷。透過 MCP 登入(anyhook.net/mcp)連線的用戶端,可以在你核准的權限內,用它的 OAuth 存取權杖呼叫同樣的端點。

速率限制。 API 和 MCP 呼叫的上限是每個帳號每個 UTC 日:Free 方案 300 次、Pro 方案 1,000 次、Scale 方案 3,000 次。超過上限時,會收到 429 Too Many Requests,並附上 Retry-After 標頭,直到 UTC 00:00。

AI agent(MCP)

AnyHook 是位於 https://anyhook.net/mcp 的 MCP 伺服器,agent 可以替你建立 App、讀取事件紀錄、重播失敗的事件,在 Pro 和 Scale 方案還能部署和測試 App 的程式碼(anyhook_code_deploy、anyhook_code_test、anyhook_code_get)。Claude Code、claude.ai、Codex和VS Code 只要網址加上瀏覽器登入就能連線,不必複製金鑰:各用戶端的步驟。

Cursor、Windsurf 和 Claude Desktop 使用金鑰。在 Dashboard → Settings → API Keys 建立金鑰後,畫面會顯示下面這段設定,你的金鑰已經填在裡面:

{
  "mcpServers": {
    "anyhook": {
      "command": "npx",
      "args": [
        "-y",
        "anyhook-mcp"
      ],
      "env": {
        "ANYHOOK_API_KEY": "ahk_live_xxxxxxxxxxxxxxxx"
      }
    }
  }
}

Cursor:Settings → MCP → Add new MCP server,或 ~/.cursor/mcp.json。Windsurf:~/.codeium/windsurf/mcp_config.json。Claude Desktop:Settings → Developer → Edit Config。程式碼工具需要 anyhook-mcp 0.4.0 以上版本(npx -y 會自動取得),或改用 https://anyhook.net/mcp 的伺服器。

接著對它說:「幫我的 Stripe Webhook 建立一個 AnyHook App,並給我它的網址。」這些內容,agent 也都能在 /llms.txt 讀到。

App

GET/api/v1/apps列出所有 App
POST/api/v1/apps建立新的 App
GET/api/v1/apps/{slug}取得 App 詳細資料
PATCH/api/v1/apps/{slug}更新 App
DELETE/api/v1/apps/{slug}刪除 App

建立 App:

curl -X POST https://anyhook.net/api/v1/apps \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Stripe Production",
    "source": "stripe",
    "destination_url": "https://api.example.com/webhooks"
  }'
{
  "id": "app_xxxxx",
  "name": "Stripe Production",
  "slug": "stripe-production",
  "inbound_url": "https://in.anyhook.net/you/stripe-production",
  "destinations": [
    { "url": "https://api.example.com/webhooks", "enabled": true }
  ]
}

連接 Stripe 或 GitHub

POST/api/v1/connect用服務商自己的金鑰,在服務商那裡設定 App 的網址

提供 Stripe 金鑰或 GitHub token,AnyHook 就會在服務商那裡把 App 的接收網址新增為 Webhook,保存服務商產生的簽章密鑰,並從第一個事件開始檢查簽章。服務商由金鑰判斷。金鑰只用於這幾次呼叫,不會儲存。沒有指定 app 時會建立新的 App;再執行一次會取代原本的設定,而不是新增第二個。

最小權限就夠了:具備 Webhook Endpoints: Write 權限的 Stripe 受限金鑰(restricted key),或對該儲存庫具備 Webhooks: Read and write 權限的 GitHub fine-grained token。

# Stripe: a restricted key, connecting an existing app
curl -X POST https://anyhook.net/api/v1/connect \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{ "key": "rk_live_...", "app": "payments" }'

# GitHub: name the repository (or an organization); no app, so one is created
curl -X POST https://anyhook.net/api/v1/connect \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{ "key": "github_pat_...", "target": "acme/api" }'

Webhook 一建立,GitHub 就會送出一個簽章過的 ping,所以幾秒內就會看到第一個事件。其他服務商:用 POST /api/v1/apps 建立 App,再把它的接收網址貼到服務商的 Webhook 設定裡。

事件

GET/api/v1/events列出事件摘要,最新的在前(不含 payload)
GET/api/v1/events/{id}取得事件詳細資料,包含完整 payload

列出事件時可用的查詢參數:

參數類型說明
app_slugstring依 App slug 篩選
statusstring依狀態篩選(queued、success、retrying、failed、rejected、circuit_open、quota_exceeded)
limitinteger結果數上限(預設 50,最多 200)

重播(API)

POST/api/v1/events/{id}/replay重播單一事件

完整說明和範例見上方的API 重播。

未送達(API)

GET/api/v1/apps/{slug}/events/undelivered列出失敗/未送達的事件

完整說明和範例見上方的未送達的事件。

批次重播(API)

POST/api/v1/apps/{slug}/replay-failed重播某個 App 所有失敗的事件

完整說明和範例見上方的批次重播。

方案上限

FreePro($20/月)Scale($90/月)Enterprise
每月事件數10,00050,000250,000彈性
每日上限1,000無無無
App 數11050彈性
每個 App 的目的地數32050彈性
傳遞嘗試次數3510彈性
保存期限3 天1 年7 年彈性
可重播期間3 天30 天90 天最多 365 天
傳遞逾時60 秒120 秒5 分鐘彈性
payload 上限512 KB2 MB4 MB客製
超額計費無$0.15/1,000 個$0.17/1,000 個依合約
每日重播上限1005001,000不限
每月程式碼執行次數無3,00012,000彈性
程式碼執行時間無105 秒285 秒彈性
運算額度每月 $0.15每月 $3.00每月 $11.00彈性
API 和 MCP 呼叫每天 300 次每天 1,000 次每天 3,000 次彈性

查看完整定價 →

用量與配額

AnyHook 以自然月計算用量(例如「2026-04」)。計數器在每個日曆月開始時歸零。

  • 每次傳遞算一個:送往兩個目的地的事件算兩個,大型事件則依主體加標頭的大小,每 64 KB 算一個(未滿也算一個)。沒有目的地的 App 收到的事件算一個。被篩選掉的事件和簽章驗證失敗的事件不計。Free 方案的每日上限比較單純:邊緣節點接受的每個請求算一次,不論送往幾個目的地。
  • 只算第一次嘗試:重試不計入配額。
  • 重播不計:is_replay: true 的事件不列入計費,只有超過配額而被保留(已儲存、不傳遞)的事件,重播時會像新事件一樣算一次。重播會用到運算額度和方案的每日重播上限。
  • 運算額度:AnyHook 在每個事件第一次嘗試之外,為你的帳號花的成本,按成本計算:重試、重播、程式碼執行、套件安裝、排程觸發、當機演練,以及等待目的地超過第一秒的時間(重試一個 1 KB 的事件約用掉 $0.00002,執行幾秒鐘的程式碼約 $0.0008)。額度用完後,這些都會停止,直到下個月 1 日(UTC):失敗的傳遞不再重試,重播和程式碼執行會被拒絕,排程暫停,目的地只有一秒可以回應。事件照樣會抵達,也會得到第一次嘗試。
  • API 和 MCP 呼叫:用 API 金鑰或 MCP 用戶端發出的每次呼叫算一次,回應超過 32 KB 時,每 32 KB 算一次(未滿也算一次);儀表板的操作不計。超過當天上限後,API 會回 429 並附上 Retry-After,直到 UTC 00:00。GET /api/v1/usage(或透過 MCP 呼叫 anyhook_usage)會列出每項上限和已用掉的量。
  • 保留,不會丟棄:超過方案內含的事件數(如果設了支出上限,則是超過該上限)之後,每個事件都會以 quota_exceeded 狀態記錄下來,但不傳遞。升級或設定支出上限後,就能在儀表板一鍵重播。在支出上限買得到的量之外,最多再保留相當於配額 20% 的事件;超過之後,邊緣節點會以 429 拒收,什麼都不儲存,為失控的來源設下界限。
Free 方案的上限。 超過每日上限(1,000)的事件會在邊緣節點拒收。超過每月上限(10,000)的事件會被保留,最多再保留每月上限的 20%:它們會以 quota_exceeded 狀態出現在事件紀錄裡,升級後即可重播。超過這個量的事件,和超過每日上限一樣拒收。升級到 Pro 方案就能使用超額計費。
付費方案的超額計費。 Pro 方案的超額用量每 1,000 個事件收 $0.15,Scale 方案每 1,000 個收 $0.17,金額無條件進位到分。我們絕不會在你不知情時丟棄付費流量。

超額計費

每月配額用完時,超額計費讓事件繼續傳遞,而不是被保留。只有 Pro 和 Scale 方案提供。

運作方式

  1. 在 Settings(設定)頁面設定每月支出上限(可選 $5 / $10 / $20 / $50,或選 Off 關閉)。
  2. 方案的事件配額用完之後,AnyHook 會檢查超額計費是否已啟用,以及支出上限是否還沒用完。
  3. 如果允許,事件就照常傳遞,並標記為超額用量。
  4. 下個月初,超額用量會以單一項目列在你的 Stripe 帳單上:Pro 方案每 1,000 個事件 $0.15,Scale 方案每 1,000 個事件 $0.17(按事件計算,無條件進位到分),金額不超過你的支出上限。

個別 App 開關

每個 App 都有超額計費設定:Inherit default(沿用預設)、Enabled(啟用)或 Disabled(停用)。App 預設沿用帳號層級的設定。要個別覆寫,就到 App Settings → Overage Billing 設定。

帳號預設值

在 Settings → Overage Billing,只要支出上限高於 Off,所有設為 Inherit default 的 App(包括既有的 App)都會開啟超額計費;設為 Disabled 的 App 則維持關閉。

支出上限是硬性上限。 超額費用達到上限後,之後的事件就不再傳遞:每個事件都以 quota_exceeded 儲存,等你提高上限或進入下個月後即可重播。超過上限的部分不會計費。超過上限後,最多再保留相當於配額 20% 的事件;其餘的由邊緣節點以 429 拒收,什麼都不儲存。

API 金鑰

API 金鑰用來驗證你對 AnyHook REST API 的請求。在 Dashboard → Settings → API Keys 管理。

  • 正式環境的金鑰以 ahk_live_ 開頭。
  • 你可以建立多個金鑰並加上標籤(例如「CI/CD」、「Agent」)。
  • 金鑰可以立即撤銷,撤銷後的金鑰下次使用時會回傳 401。
  • 金鑰只在建立時顯示一次。務必妥善保存。
GET/api/v1/api-keys列出所有 API 金鑰(金鑰值已遮蔽)
POST/api/v1/api-keys建立新的 API 金鑰
PATCH/api/v1/api-keys/{id}/revoke撤銷 API 金鑰

需要協助?

寫信到 hello@anyhook.net,每封信我們都會回覆。服務的即時狀態可以到 status.anyhook.net 查看。