integrating-stripe-webhooks
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseIntegrating Stripe Webhooks
集成Stripe Webhook
Overview
概述
Stripe webhooks require raw request bodies for signature verification. Most web frameworks parse JSON automatically, breaking verification. This skill provides framework-specific solutions for the raw body problem and documents common TypeScript type mismatches.
Stripe webhook需要原始请求体来进行签名验证。大多数Web框架会自动解析JSON,这会破坏验证流程。本技能提供针对不同框架的原始请求体问题解决方案,并记录了常见的TypeScript类型不匹配问题。
When to Use
使用场景
Use this skill when:
- Getting "Raw body not available" errors from Stripe webhooks
- Webhook signature verification fails with 400 errors
- Implementing new Stripe webhook endpoints
- Getting from subscription events
TypeError: Cannot read property 'current_period_start' - Webhooks return 404 (route registration issues)
Don't use for:
- General Stripe API integration (not webhooks)
- Frontend Stripe Elements implementation
- Stripe checkout session creation (use Stripe docs)
在以下场景使用本技能:
- 从Stripe webhook收到“Raw body not available”错误
- Webhook签名验证失败并返回400错误
- 实现新的Stripe webhook端点
- 从订阅事件中收到错误
TypeError: Cannot read property 'current_period_start' - Webhook返回404(路由注册问题)
请勿在以下场景使用:
- 通用Stripe API集成(非webhook相关)
- 前端Stripe Elements实现
- Stripe结账会话创建(请参考Stripe官方文档)
Quick Reference
快速参考
| Problem | Solution |
|---|---|
| Raw body not available | Configure custom body parser (see framework examples) |
| Signature verification fails | Use raw body bytes/buffer, not parsed JSON |
| 404 on webhook endpoint | Register webhook route inside API prefix |
| Access from |
| URI validation errors | URL-encode dynamic parameters with |
| 问题 | 解决方案 |
|---|---|
| 原始请求体不可用 | 配置自定义请求体解析器(查看框架示例) |
| 签名验证失败 | 使用原始请求体字节/缓冲区,而非解析后的JSON |
| Webhook端点返回404 | 在API前缀内注册webhook路由 |
| 从 |
| URI验证错误 | 使用 |
Critical: Raw Body Parsing
关键要点:原始请求体解析
THE PROBLEM: Stripe's requires the exact bytes received to verify the signature. JSON parsing modifies the body, breaking verification.
constructEvent()THE SOLUTION: Access raw body before any parsing middleware.
问题: Stripe的方法需要接收到的原始字节来验证签名。JSON解析会修改请求体,导致验证失败。
constructEvent()解决方案: 在任何解析中间件之前获取原始请求体。
Framework Examples
框架示例
Node.js - Fastify (most common for new projects):
typescript
// In main server file, BEFORE registering routes
server.addContentTypeParser('application/json',
{ parseAs: 'buffer' },
async (req: any, body: Buffer) => {
req.rawBody = body; // Store for webhooks
return JSON.parse(body.toString('utf8')); // Parse for other routes
}
);
// In webhook handler
const rawBody = (request as any).rawBody;
const event = stripe.webhooks.constructEvent(
rawBody, signature, webhookSecret
);Node.js - Express:
javascript
// Define webhook route BEFORE express.json() middleware
app.post('/webhooks/stripe',
express.raw({ type: 'application/json' }),
(req, res) => {
const event = stripe.webhooks.constructEvent(
req.body, // Already raw Buffer
req.headers['stripe-signature'],
webhookSecret
);
}
);
app.use(express.json()); // After webhook routePython - FastAPI:
python
@app.post('/webhooks/stripe')
async def stripe_webhook(request: Request):
payload = await request.body() # Use .body() not .json()
signature = request.headers.get('stripe-signature')
event = stripe.Webhook.construct_event(
payload, signature, webhook_secret
)General Pattern: Get raw bytes/buffer → verify signature → use parsed event from Stripe.
Node.js - Fastify(新项目最常用):
typescript
// In main server file, BEFORE registering routes
server.addContentTypeParser('application/json',
{ parseAs: 'buffer' },
async (req: any, body: Buffer) => {
req.rawBody = body; // Store for webhooks
return JSON.parse(body.toString('utf8')); // Parse for other routes
}
);
// In webhook handler
const rawBody = (request as any).rawBody;
const event = stripe.webhooks.constructEvent(
rawBody, signature, webhookSecret
);Node.js - Express:
javascript
// Define webhook route BEFORE express.json() middleware
app.post('/webhooks/stripe',
express.raw({ type: 'application/json' }),
(req, res) => {
const event = stripe.webhooks.constructEvent(
req.body, // Already raw Buffer
req.headers['stripe-signature'],
webhookSecret
);
}
);
app.use(express.json()); // After webhook routePython - FastAPI:
python
@app.post('/webhooks/stripe')
async def stripe_webhook(request: Request):
payload = await request.body() # Use .body() not .json()
signature = request.headers.get('stripe-signature')
event = stripe.Webhook.construct_event(
payload, signature, webhook_secret
)通用模式: 获取原始字节/缓冲区 → 验证签名 → 使用Stripe返回的解析后事件。
Common Mistakes
常见错误
1. Subscription Period Fields Missing
1. 订阅周期字段缺失
Error:
TypeError: Cannot read property 'current_period_start' of undefinedCause: Stripe returns period dates in , not at subscription root. TypeScript types don't include these fields on .
subscription.items.data[0]SubscriptionItemFix:
typescript
// ❌ WRONG - fields don't exist here
new Date(subscription.current_period_start * 1000)
// ✅ CORRECT - get from first subscription item
const firstItem = subscription.items.data[0] as any;
const periodStart = firstItem?.current_period_start || subscription.billing_cycle_anchor;
const periodEnd = firstItem?.current_period_end || subscription.billing_cycle_anchor;
await updateOrg({
start_date: new Date(periodStart * 1000),
end_date: new Date(periodEnd * 1000),
});错误:
TypeError: Cannot read property 'current_period_start' of undefined原因: Stripe在中返回周期日期,而非订阅根节点。TypeScript类型定义中不包含这些字段。
subscription.items.data[0]SubscriptionItem修复方案:
typescript
// ❌ 错误写法 - 这些字段不存在于此处
new Date(subscription.current_period_start * 1000)
// ✅ 正确写法 - 从第一个订阅项获取
const firstItem = subscription.items.data[0] as any;
const periodStart = firstItem?.current_period_start || subscription.billing_cycle_anchor;
const periodEnd = firstItem?.current_period_end || subscription.billing_cycle_anchor;
await updateOrg({
start_date: new Date(periodStart * 1000),
end_date: new Date(periodEnd * 1000),
});2. Route Not Found (404)
2. 路由未找到(404)
Cause: Webhook routes registered outside API prefix.
typescript
// ❌ WRONG - creates /webhooks/stripe instead of /api/v1/webhooks/stripe
export async function registerRoutes(server) {
server.register(async (api) => {
await api.register(subscriptionRoutes, { prefix: '/subscriptions' });
}, { prefix: '/api/v1' });
await server.register(webhookRoutes, { prefix: '/webhooks' }); // Outside!
}
// ✅ CORRECT - inside API prefix
export async function registerRoutes(server) {
server.register(async (api) => {
await api.register(subscriptionRoutes, { prefix: '/subscriptions' });
await api.register(webhookRoutes, { prefix: '/webhooks' }); // Inside
}, { prefix: '/api/v1' });
}原因: Webhook路由注册在API前缀之外。
typescript
// ❌ 错误写法 - 创建的是/webhooks/stripe而非/api/v1/webhooks/stripe
export async function registerRoutes(server) {
server.register(async (api) => {
await api.register(subscriptionRoutes, { prefix: '/subscriptions' });
}, { prefix: '/api/v1' });
await server.register(webhookRoutes, { prefix: '/webhooks' }); // 在外部!
}
// ✅ 正确写法 - 在API前缀内部
export async function registerRoutes(server) {
server.register(async (api) => {
await api.register(subscriptionRoutes, { prefix: '/subscriptions' });
await api.register(webhookRoutes, { prefix: '/webhooks' }); // 在内部
}, { prefix: '/api/v1' });
}3. URL Encoding in Checkout URLs
3. 结账URL中的URL编码问题
Error:
"body/successUrl must match format 'uri'"Cause: Organization names or parameters with spaces not URL-encoded.
typescript
// ❌ WRONG - "Broke Org" creates invalid URL
const successUrl = `${origin}/orgs?name=${orgName}&subscription=success`;
// ✅ CORRECT - encode dynamic parameters
const successUrl = `${origin}/orgs?name=${encodeURIComponent(orgName)}&subscription=success`;错误:
"body/successUrl must match format 'uri'"原因: 组织名称或包含空格的参数未进行URL编码。
typescript
// ❌ 错误写法 - "Broke Org"会创建无效URL
const successUrl = `${origin}/orgs?name=${orgName}&subscription=success`;
// ✅ 正确写法 - 对动态参数进行编码
const successUrl = `${origin}/orgs?name=${encodeURIComponent(orgName)}&subscription=success`;Implementation Checklist
实施检查清单
Server Setup:
- Configure raw body parser BEFORE routes
- Register webhook routes inside API prefix (if using one)
- Set environment variable
STRIPE_WEBHOOK_SECRET - Verify webhook secret is configured before processing
Webhook Handler:
- Validate header exists
stripe-signature - Access raw body (not parsed JSON)
- Use for verification
stripe.webhooks.constructEvent() - Handle separately
SignatureVerificationError - Return 200 for received events (even if processing fails)
- Log all events with ID and type
Subscription Events:
- Get period dates from
subscription.items.data[0] - Cast to to access TypeScript-missing fields
any - Fallback to if items missing
billing_cycle_anchor - Store in subscription metadata
org_id - Update verification status based on subscription status
Frontend:
- URL-encode all dynamic parameters
- URL-encode organization names in success/cancel URLs
- Handle checkout errors gracefully
- Poll for verification after checkout success
服务器设置:
- 在路由之前配置原始请求体解析器
- 在API前缀内注册webhook路由(如果使用前缀)
- 设置环境变量
STRIPE_WEBHOOK_SECRET - 在处理之前验证webhook密钥已配置
Webhook处理函数:
- 验证请求头存在
stripe-signature - 获取原始请求体(而非解析后的JSON)
- 使用进行验证
stripe.webhooks.constructEvent() - 单独处理错误
SignatureVerificationError - 对收到的事件返回200响应(即使处理失败)
- 记录所有事件的ID和类型
订阅事件:
- 从中获取周期日期
subscription.items.data[0] - 转换为类型以访问TypeScript未定义的字段
any - 如果订阅项缺失,回退到
billing_cycle_anchor - 在订阅元数据中存储
org_id - 根据订阅状态更新验证状态
前端:
- 对所有动态参数进行URL编码
- 对成功/取消URL中的组织名称进行URL编码
- 优雅处理结账错误
- 结账成功后轮询验证状态
Testing Locally
本地测试
bash
undefinedbash
undefinedInstall Stripe CLI
安装Stripe CLI
brew install stripe/stripe-cli/stripe
brew install stripe/stripe-cli/stripe
Forward webhooks to local server
将webhook转发到本地服务器
stripe listen --forward-to localhost:3000/api/v1/webhooks/stripe
stripe listen --forward-to localhost:3000/api/v1/webhooks/stripe
Trigger test events
触发测试事件
stripe trigger customer.subscription.created
stripe trigger customer.subscription.updated
stripe trigger invoice.paid
undefinedstripe trigger customer.subscription.created
stripe trigger customer.subscription.updated
stripe trigger invoice.paid
undefinedReal-World Impact
实际效果
Before applying these patterns:
- Webhooks fail with 400 "Invalid signature"
- Subscription updates crash with undefined property errors
- Hours debugging TypeScript type mismatches
- Checkout fails with URL validation errors
After applying:
- Webhooks verify successfully
- Subscription data extracts correctly
- Type-safe with explicit casting
- Checkout URLs work with any organization name
应用这些模式之前:
- Webhook因400“无效签名”失败
- 订阅更新因未定义属性错误崩溃
- 花费数小时调试TypeScript类型不匹配问题
- 结账因URL验证错误失败
应用之后:
- Webhook验证成功
- 订阅数据正确提取
- 通过显式转换实现类型安全
- 结账URL适用于任何组织名称
References
参考资料
- Stripe Webhook Signature Verification
- Stripe Subscription Object
- See framework documentation for body parsing middleware
- Stripe Webhook签名验证
- Stripe订阅对象
- 请查看各框架文档了解请求体解析中间件