stripe-integration
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseStripe Integration for SaaS
面向SaaS的Stripe集成
Setup
配置
Environment Variables
环境变量
env
STRIPE_SECRET_KEY=sk_...
STRIPE_PUBLISHABLE_KEY=pk_...
STRIPE_WEBHOOK_SECRET=whsec_...env
STRIPE_SECRET_KEY=sk_...
STRIPE_PUBLISHABLE_KEY=pk_...
STRIPE_WEBHOOK_SECRET=whsec_...Install
安装
bash
pnpm add stripe @stripe/stripe-jsbash
pnpm add stripe @stripe/stripe-jsServer Client
服务端客户端
typescript
// lib/stripe.ts
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-11-20.acacia',
typescript: true,
});typescript
// lib/stripe.ts
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-11-20.acacia',
typescript: true,
});Checkout Session
结账会话
typescript
// app/api/checkout/route.ts
import { stripe } from '@/lib/stripe';
import { auth } from '@/lib/auth';
export async function POST(request: Request) {
const user = await auth();
if (!user) return new Response('Unauthorized', { status: 401 });
const { priceId } = await request.json();
// Validate price ID against allowed list
const allowedPrices = ['price_xxx', 'price_yyy'];
if (!allowedPrices.includes(priceId)) {
return new Response('Invalid price', { status: 400 });
}
const session = await stripe.checkout.sessions.create({
customer: user.stripeCustomerId,
mode: 'subscription',
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_URL}/dashboard?success=true`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
subscription_data: {
metadata: { userId: user.id },
},
});
return Response.json({ url: session.url });
}typescript
// app/api/checkout/route.ts
import { stripe } from '@/lib/stripe';
import { auth } from '@/lib/auth';
export async function POST(request: Request) {
const user = await auth();
if (!user) return new Response('Unauthorized', { status: 401 });
const { priceId } = await request.json();
// Validate price ID against allowed list
const allowedPrices = ['price_xxx', 'price_yyy'];
if (!allowedPrices.includes(priceId)) {
return new Response('Invalid price', { status: 400 });
}
const session = await stripe.checkout.sessions.create({
customer: user.stripeCustomerId,
mode: 'subscription',
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_URL}/dashboard?success=true`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
subscription_data: {
metadata: { userId: user.id },
},
});
return Response.json({ url: session.url });
}Webhook Handler (CRITICAL)
Webhook处理程序(关键)
See templates/webhook_handler.ts for complete implementation.
完整实现可查看templates/webhook_handler.ts文件。
Security Requirements
安全要求
- ✅ Verify signature with
stripe.webhooks.constructEvent() - ✅ Use raw body (not parsed JSON)
- ✅ Return 200 quickly, process async
- ✅ Handle idempotency (check if already processed)
- ✅ Log webhook events for debugging
- ✅ 使用验证签名
stripe.webhooks.constructEvent() - ✅ 使用原始请求体(而非解析后的JSON)
- ✅ 快速返回200状态码,异步处理业务逻辑
- ✅ 处理幂等性(检查事件是否已被处理过)
- ✅ 记录Webhook事件便于调试
Event Types to Handle
需要处理的事件类型
- - Initial purchase
checkout.session.completed - - New subscription
customer.subscription.created - - Plan change
customer.subscription.updated - - Cancellation
customer.subscription.deleted - - Failed payment
invoice.payment_failed - - Successful payment
invoice.paid
- - 首次购买
checkout.session.completed - - 新订阅创建
customer.subscription.created - - 套餐变更
customer.subscription.updated - - 订阅取消
customer.subscription.deleted - - 支付失败
invoice.payment_failed - - 支付成功
invoice.paid
Testing
测试
bash
undefinedbash
undefinedForward webhooks to local
将Webhook转发到本地环境
stripe listen --forward-to localhost:3000/api/webhooks/stripe
stripe listen --forward-to localhost:3000/api/webhooks/stripe
Trigger test events
触发测试事件
stripe trigger checkout.session.completed
stripe trigger customer.subscription.updated
stripe trigger invoice.payment_failed
undefinedstripe trigger checkout.session.completed
stripe trigger customer.subscription.updated
stripe trigger invoice.payment_failed
undefinedCommon Errors
常见错误
"No signatures found matching"
"未找到匹配的签名"
- Check STRIPE_WEBHOOK_SECRET is correct
- Ensure using raw body:
await request.text()
- 检查STRIPE_WEBHOOK_SECRET是否正确
- 确保使用原始请求体:
await request.text()
"Webhook timeout"
"Webhook超时"
- Process heavy work async
- Return 200 immediately, use queue for processing
- 异步处理耗时任务
- 立即返回200状态码,使用队列处理后续逻辑