要在本地测试 Stripe Webhook,需要把 Stripe 的事件投递经由稳定的公网 URL 路由,捕获每次请求以便检查,再把同一次投递转发到你本机的开发服务器。这样你能看到 Stripe 在代码处理之前发出的确切载荷,从而消除 Webhook 调试中大量猜测。
最快路径是:用 HookNexus 创建公网端点,把该 URL 填入 Stripe 控制台,触发测试事件,确认投递已落库,再用 CLI 把流量管道式送到本地路由。本指南逐步说明全流程,并包含正确校验 Stripe 签名所需的 Express.js 处理代码。
为什么本地 Stripe Webhook 测试比想象中难
Stripe 只能向公网可达的 URL 投递 Webhook。你的 localhost:3000 开发服务器不满足条件;若强行填写,Stripe 会静默失败或返回连接错误。仅此一条约束就会连带出一串让资深开发者也踩坑的问题。
首先,Stripe 用 Stripe-Signature 头中的 HMAC 对每次投递签名,签名基于原始请求体计算。若任何中间件在验证签名前解析或变换正文,校验会以令人困惑的 stripe.webhooks.constructEvent 错误失败。这是开发者最常遇到的问题,且与端点 URL 本身无关。
其次,Stripe 的测试模式与正式模式完全隔离,各有 API 密钥、Webhook 签名密钥与事件流。若在查看正式模式控制台时注册了端点,却在测试模式触发事件,端点永远收不到投递。控制台对这种错配提示得不够醒目。
第三,即便投递成功,改代码后仍需要重放完全相同的事件。重新触发 checkout.session.completed 意味着新建 Checkout Session、走完流程、再等 Stripe 发事件。在迭代调试中,这种开销会迅速累积。
一套合理的本地测试方案同时解决三件事:可接流量的公网 URL、为签名校验保留原始正文、以及可 按需重放 的捕获历史。
开始之前需要准备
开始前请确认:
- 拥有可进入测试模式的 Stripe 账户(无需正式模式即可完成本指南)。
- 已有 Stripe Webhook 签名密钥(以
whsec_开头),在注册端点后于 Stripe 控制台可见。 - 本机已安装 Node.js 18+(用于 Express 示例)。
- 已全局安装 HookNexus CLI:
npm install -g hooknexus
hooknexus login
- 本地有服务在已知端口监听(本指南假设为
http://localhost:3000/api/webhooks/stripe)。
若尚无 HookNexus 账户,免费额度足以跑通全流程。可通过 Webhook 调试器 注册。
分步本地测试流程
本节从创建端点到在应用代码中处理事件,覆盖完整链路。
第一步 —— 创建公网 Webhook 端点
在终端用 HookNexus CLI 新建端点:
hooknexus endpoints create
CLI 会返回形如 https://api.hooknexus.com/h/abc123 的 URL,复制它。这是 Stripe 将投递事件的稳定公网地址。命中该 URL 的每次请求都会被记录,便于稍后在 HookNexus 调试器 中查看。
也可在 Web 控制台创建端点,若你更习惯图形界面。
第二步 —— 在 Stripe 控制台注册端点
- 打开 Stripe 控制台,确认处于测试模式(右上角开关)。
- 进入 Developers > Webhooks。
- 点击 Add endpoint。
- 将 HookNexus 端点 URL 粘贴到 Endpoint URL。
- 在 Select events to listen to 中,选择应用实际需要的事件。结账类集成常见选项包括:
checkout.session.completedinvoice.paidinvoice.payment_failedcustomer.subscription.updatedcustomer.subscription.deleted
- 点击 Add endpoint 保存。
保存后进入端点详情页,在 Signing secret 下点击 Reveal,复制 whsec_...。应用校验签名时需要此值。
除非处理逻辑明确需要,否则避免选择「全部事件」。事件过多会增加调试噪音,难以聚焦关心的载荷。
第三步 —— 触发测试事件
保持在测试模式,执行能触发已选事件的操作。对 checkout.session.completed 而言,较简便的方式是:
- 用 Stripe API 或应用结账流程创建 Checkout Session。
- 使用 Stripe 测试卡(如
4242 4242 4242 4242)完成支付。 - 支付成功后,Stripe 会向已注册端点发送
checkout.session.completed。
也可在 Stripe 控制台端点详情页使用 Send test webhook,发送带示例数据的合成事件。适合验证连通性,但载荷结构可能与真实事件略有差异。
第四步 —— 查看已捕获的请求
打开 HookNexus 控制台,或用 CLI 查看端点请求历史:
hooknexus endpoints list
查看捕获的请求并确认:
- 状态码:HookNexus 向 Stripe 返回
200,表示投递成功。 - Headers:存在
Stripe-Signature(HMAC)、Content-Type: application/json、Stripe-Event-Id。 - Body:JSON 内含事件对象;
type与预期一致(如checkout.session.completed),data.object为预期资源。 - 时间:若同一事件有多条记录,可能是此前某次返回非 2xx,Stripe 在重试。
这一步很关键。若请求从未到达,问题在 Stripe 到端点的链路(URL 错误、模式错误、网络),而非本地代码。应先修复投递,再调试处理程序。
需要更完整的说明可参考 HookNexus Stripe 集成文档。
第五步 —— 转发到 localhost
确认投递正常后,将实时流量转发到本地服务器。高级选项见 CLI 转发文档。
hooknexus forward ENDPOINT_ID --to http://localhost:3000/api/webhooks/stripe
将 ENDPOINT_ID 换为第一步中的实际 ID。CLI 会保持长连接,把每次入站请求实时管道到本地路由,终端会打印转发的请求。
开发期间保持该终端会话运行,新的 Stripe 投递会自动转发。若本地服务未启动或返回错误,HookNexus 仍会保存原始请求,便于稍后 重放。
转发流程的背景说明见 将 Webhook 转发到 localhost 指南。
第六步 —— 在应用中处理 Webhook
下面是一个完整的 Express.js Webhook 处理示例:校验 Stripe 签名并处理事件。必须向 stripe.webhooks.constructEvent 传入原始请求体,而非解析后的 JSON。
const express = require("express");
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);
const app = express();
app.post(
"/api/webhooks/stripe",
express.raw({ type: "application/json" }),
(req, res) => {
const sig = req.headers["stripe-signature"];
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
} catch (err) {
console.error("Signature verification failed:", err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
switch (event.type) {
case "checkout.session.completed":
const session = event.data.object;
console.log("Checkout completed for session:", session.id);
// Fulfill the order, update database, send confirmation email
break;
case "invoice.paid":
const invoice = event.data.object;
console.log("Invoice paid:", invoice.id);
// Record successful payment, extend subscription
break;
case "invoice.payment_failed":
const failedInvoice = event.data.object;
console.log("Payment failed for invoice:", failedInvoice.id);
// Notify customer, flag account
break;
default:
console.log("Unhandled event type:", event.type);
}
res.status(200).json({ received: true });
}
);
app.listen(3000, () => {
console.log("Webhook handler listening on port 3000");
});
关键一行是 express.raw({ type: "application/json" }),它确保正文以原始 Buffer 进入处理函数。若全局使用 express.json(),会在 Webhook 路由之前解析正文,签名验证将始终失败。深入说明见 Stripe Webhook 签名验证失败排查。
若使用 Next.js App Router 而非 Express,对 Webhook 路由应禁用正文解析:
// app/api/webhooks/stripe/route.js
import Stripe from "stripe";
import { NextResponse } from "next/server";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export async function POST(request) {
const body = await request.text();
const sig = request.headers.get("stripe-signature");
let event;
try {
event = stripe.webhooks.constructEvent(
body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.error("Signature verification failed:", err.message);
return NextResponse.json(
{ error: `Webhook Error: ${err.message}` },
{ status: 400 }
);
}
switch (event.type) {
case "checkout.session.completed":
console.log("Checkout session completed:", event.data.object.id);
break;
case "invoice.paid":
console.log("Invoice paid:", event.data.object.id);
break;
default:
console.log("Unhandled event type:", event.type);
}
return NextResponse.json({ received: true });
}
Next.js App Router 在调用 request.text() 时不会自动解析正文,因此签名验证通常无需额外配置。
本地 Stripe 测试中的常见问题
签名验证失败
几乎总是因为正文解析中间件在 Webhook 路由之前运行。Stripe SDK 根据请求体原始字节计算预期签名;若 express.json() 等解析器把正文变成对象再序列化,字节表示变化,签名不再匹配。
修复:在 Webhook 路由上单独使用 express.raw({ type: "application/json" }),或确保全局正文解析不作用于 Webhook 路径。Next.js 中用 request.text() 而非 request.json()。
完整 walkthrough 见 Stripe Webhook 签名验证失败。
事件从未到达端点
按顺序检查:
- 模式不一致:控制台与触发操作须同为测试或同为正式。
- URL 笔误:Stripe 中的端点 URL 须与 HookNexus 端点 URL 完全一致。
- 事件筛选:确认你触发的事件类型包含在端点已选事件中。
- Stripe 投递日志:在控制台打开该端点,查看 Webhook attempts 标签中的状态与错误信息。
事件类型不对
若收到的事件 type 与预期不符,确认触发动作是否正确。例如仅创建 Checkout Session 会触发 checkout.session.created,而非 checkout.session.completed;completed 仅在客户完成支付后触发。
测试模式与正式模式混淆
Stripe 为测试与正式维护完全独立的 Webhook 端点列表。在正式模式注册的端点不会收到测试事件,反之亦然。务必确认控制台右上角模式与 API 密钥、生成的事件模式一致。
Webhook 签名密钥(whsec_...)也按模式区分。用正式密钥验证测试投递,即使正文完整也会失败。
何时用重放而非重新触发
捕获到一次 Webhook 投递后,不必每次改处理代码都回到 Stripe 里重做原始动作。可直接从 HookNexus 重放已捕获请求。重放会把相同的头与正文发到本地路由,相当于对原始投递的像素级复现。
重放特别适用于:
- 原始触发成本高或耗时长(完整 Checkout 流程)。
- 迭代错误处理,需要反复命中同一条失败路径。
- 测试幂等性:同一事件处理多次。
详细步骤见 在本地重放 Stripe Webhook 事件。
快速检查清单
- 已安装并登录 HookNexus CLI(
hooknexus login) - 已用
hooknexus endpoints create创建公网端点 - 已在 Stripe 控制台注册 URL(模式正确)
- 已选择具体事件类型(非「全部事件」)
- 已触发测试事件并在 HookNexus 确认投递
- 编写处理代码前已检查原始正文、头与事件类型
- 已用
hooknexus forward ENDPOINT_ID --to http://localhost:3000/api/webhooks/stripe启动转发 - Webhook 处理使用原始正文做签名校验(
constructEvent前不做 JSON 解析)
常见问题
只用 Stripe CLI 够本地测试 Webhook 吗?
Stripe CLI(stripe listen --forward-to)对纯 Stripe 场景很好用:临时隧道并直接转发事件。但会话结束后不持久保存请求历史,改代码后无法从历史重放,且只支持 Stripe。若应用对接多个提供商,或需要持久的检查与重放工作流,专用 Webhook 调试器 更灵活。
为什么不只在 Stripe 控制台里调试?
控制台展示投递尝试与响应码,有助于确认 Stripe 已发出事件。但它无法把请求转发到 localhost、在调试上下文中查看原始正文,也无法把同一次投递重发到开发机。控制台告诉你 Stripe 做了什么;本地调试告诉你 你的代码如何处理。
什么时候该把重放加入工作流?
一旦开始对 Webhook 处理做迭代修改,就应加入重放。首次投递确认事件结构与初始逻辑;之后每次代码变更都适合重放——跳过触发步骤,用完全相同载荷跑更新后的处理程序,把反馈周期从分钟级缩短到秒级。
本地测试需要单独的签名密钥吗?
不需要。无论请求直接来自 Stripe 还是经 HookNexus 转发,都使用 Stripe 控制台端点配置中的同一 whsec_...。HookNexus 保留原始头(含 Stripe-Signature),验证逻辑与生产一致。
下一步
你现已具备完整的本地 Stripe Webhook 测试链路:公网端点、事件捕获、检查、localhost 转发与签名验证。接下来可以:
- 配置 Stripe Webhook 事件重放,加速迭代调试。
- 若遇到 HMAC 错误,阅读 签名验证故障排查。
- 查阅 Stripe 集成文档 了解高级配置。
- 浏览更多 Stripe Webhook 指南,例如订阅生命周期与支付失败恢复等主题。