Stripe Connect 是与普通 Stripe 不同的 Webhook 生态。签名数学相同。其余一切都不同。如果你一直在追查为什么你的 Connect 处理器悄悄忽略 account.application.deauthorized,这篇文章是为你写的。
Connect 改变你接收哪些事件
普通 Stripe Webhook 来自你的平台账户:charges、customers、subscriptions。Connect Webhook 来自关联账户以及平台上 Connect 特有的生命周期事件。列表大量重叠,但包含只有启用 Connect 才能看到的事件:
account.updated— 验证状态、capabilities、要求account.application.deauthorized— 关联账户撤销了你的访问capability.updated— payouts/transfers 激活状态person.created、person.updated— 用于 Custom 和 Express 账户payout.failed、payout.paid— 在关联账户上,而非你的平台
大多数团队在部署后才发现这点。本地测试反而在几分钟内就暴露问题。
Stripe-Account 头改变一切
当事件发生在关联账户上时,Stripe 附上带关联账户 ID(acct_xxx)的 Stripe-Account 头。你的处理器需要按那个头路由,而不是按 payload 里的内容。
const connectedAccountId = req.headers['stripe-account'];
const event = stripe.webhooks.constructEvent(
rawBody,
req.headers['stripe-signature'],
endpointSecret,
);
// Now process the event in the context of connectedAccountId
await handleConnectEvent(event, connectedAccountId);
如果你忘了读这个头,处理器就把每个 Connect 事件当作发生在你平台上的来处理。这种模式的 bug 通常表现为「payout 显示给了错误的商户」。
用 PortPreview 在本地测试 Connect
设置与普通 Stripe 本地测试相同,加一个额外步骤:
- 在本地运行你的平台应用。
- 启动隧道:
npx portpreview 3000。 - 在 Stripe 仪表盘里把隧道 URL 加为 Webhook 端点,并勾选 Events on Connected accounts 选项。这就是改变整个事件流的开关。
- 使用测试 Express 或 Custom 关联账户。Stripe 测试模式包含一个会触发真实事件的假 "Jenny Rosen" 账户创建器。
- 从 Connect 测试界面触发事件:完成入驻、请求 payout、解除授权一个账户。
解除授权流程是难点
当关联账户撤销你应用的访问时,Stripe 恰好触发一次 account.application.deauthorized。如果你的处理器崩溃、返回 5xx 或没及时确认事件,Stripe 会重试——但关联账户已经没了。你随后对该账户的 API 调用返回 401。
仔细测试 deauth 流程。用 Connect 测试模式解除测试账户的授权,捕获 webhook,并对捕获的 payload 运行你的清理逻辑。重放直到这条路径坚不可摧。
Express vs Standard vs Custom
账户类型改变你接收哪些 person/capability 事件。Express 和 Custom 账户发出 person.* 事件,因为你的平台帮助完成入驻。Standard 账户通过 Stripe 托管的界面处理自己的入驻,所以你看到的事件更少。如果你在项目中途切换账户类型——人们确实会——你的 webhook 处理器需要调整。
我们实际会怎么做
对于一个有平台级分析和按商户报表的真实市场,从第一天起就构建两条不同的处理器路径:一条用于平台事件(没有 Stripe-Account 头),一条用于关联账户事件。在函数顶部路由。这避免了后面 90% 的「这是给我们还是给商户的」bug。
Connect Webhook 共享 Stripe 的签名方案,所以验证机制与普通 Stripe Webhook 测试完全相同。关于跨提供商的签名数学背景,见签名验证指南。加入 PortPreview 等候名单,以内置捕获和重放测试 Connect。