Stripe 如何傳遞 Webhook

Stripe 沒有公布逾時,實務上抓 20 秒。重試持續 3 天,之後 Stripe 會停用端點。附經驗證的簽章格式與 Node 驗證程式。

傳遞行為

取自服務商自己的文件,最後檢查於 2026-08-06。服務商沒寫數字的地方,這張表也不填。

回應時限服務商未公開

Stripe 沒有公布數字,文件只說要在任何耗時的邏輯之前回傳 2xx。社群一般以 20 秒估算

重試正式環境以指數退避重試,最長 3 天。沙箱環境:數小時內嘗試 3 次
放棄持續失敗後停用端點,停用前會先寄信通知。事件在 Events API 中保存 30 天
出處https://docs.stripe.com/webhooks

簽章

用一個經測試套件證明可運作的驗證程式核對過,不是照文件抄來的。最後驗證於 2026-08-20。

演算法HMAC-SHA256
簽章內容{timestamp}.{body}
編碼hex
標頭stripe-signature = t=X,v1=hex
容許時間差5 分鐘

驗證簽章

要用原始請求主體(raw body),逐位元組原封不動,並在任何 JSON 解析之前取得。框架只要重新序列化 payload,這頁上的每一種機制就會立刻失效。

const crypto = require("node:crypto");

// stripe-signature: "t=1700000000,v1=abc..." — HMAC-SHA256 over "{t}.{body}"
const pairs = Object.fromEntries(
  req.headers["stripe-signature"].split(",").map((p) => p.split("=")),
);
const expected = crypto
  .createHmac("sha256", endpointSecret) // whsec_... from the endpoint's page
  .update(`${pairs.t}.${rawBody}`)
  .digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(pairs.t)) < 300;
const received = Buffer.from(pairs.v1 ?? "");
const wanted = Buffer.from(expected);
// timingSafeEqual throws on a length mismatch, so check that first.
const valid =
  fresh && received.length === wanted.length && crypto.timingSafeEqual(received, wanted);

或交給 SDK:stripe.webhooks.constructEvent(rawBody, sig, secret)。它需要原始請求主體,所以要在任何 JSON 解析器之前,把 express.raw() 掛到 Webhook 路由上。

容易踩的坑

Stripe 從不公布回應期限。文件只說要在任何耗時的邏輯之前回傳 2xx;社群實務上以 20 秒為準,而同步寫入慢速資料庫的處理程式,每個事件都在逼近這條線。

失敗 3 天後,事件會悄悄離開重試佇列。持續失敗會讓端點被停用,停用前會先寄信通知;事件在 30 天內仍可從 Events API 拉取。

延伸閱讀

AnyHook 架在接收 Stripe Webhook 的端點前面:大約 1 秒就回應,伺服器當機時依自己的排程重試,並讓每個事件都能重播。 換一個網址,程式碼不用動;要接受重試與重播,改為檢查 AnyHook-Signature,因為 Stripe 自己的簽章在 5 分鐘後就會過期。

運作方式 →