Stripe 如何投递 Webhook

Stripe 没有公布 Webhook 超时;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 分钟

验证签名

要用原始请求体,逐字节一致,并且在任何 JSON 解析之前。框架一旦重新序列化请求体,本页的每一种方案都会失效。

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)。它需要原始请求体,所以要在 Webhook 路由上、任何 JSON 解析器之前挂载 express.raw()。

容易踩的坑

Stripe 从不公布响应时限。它的文档只说要在任何耗时逻辑之前返回 2xx;社区按 20 秒来做,而同步写入慢数据库的处理程序,每个事件都在逼近这条线。

连续失败 3 天后,事件会悄无声息地退出重试队列。持续失败会让端点被停用(停用前会先发邮件),事件在 30 天内仍可以从 Events API 拉取。

延伸阅读

AnyHook 挡在接收 Stripe Webhook 的端点前面:约 1 秒应答,你的服务器宕机时按自己的计划重试,并且每个事件都可以重放。 只改一个 URL,代码不用动;要接受重试和重放,就验证 AnyHook-Signature 请求头,因为 Stripe 自己的签名 5 分钟后就会过期。

工作原理 →