stripe-payments
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseStripe Payments
Stripe 支付集成
Add Stripe payments to a web app. Covers the common patterns — one-time payments, subscriptions, webhooks, customer portal — with working code. No MCP server needed.
为Web应用添加Stripe支付功能。涵盖常见使用场景——一次性支付、订阅、Webhook、客户门户——并提供可运行的代码。无需MCP服务器。
Which Stripe API Do I Need?
如何选择合适的Stripe API?
| You want to... | Use | Complexity |
|---|---|---|
| Accept a one-time payment | Checkout Sessions | Low — Stripe hosts the payment page |
| Embed a payment form in your UI | Payment Element + Payment Intents | Medium — you build the form, Stripe handles the card |
| Recurring billing / subscriptions | Checkout Sessions (subscription mode) | Low-Medium |
| Save a card for later | Setup Intents | Low |
| Marketplace / platform payments | Stripe Connect | High |
| Let customers manage billing | Customer Portal | Low — Stripe hosts it |
Default recommendation: Start with Checkout Sessions. It's the fastest path to accepting money. You can always add embedded forms later.
| 你想要... | 使用方案 | 复杂度 |
|---|---|---|
| 接受一次性支付 | Checkout Sessions | 低 — Stripe 托管支付页面 |
| 在UI中嵌入支付表单 | Payment Element + Payment Intents | 中 — 你负责构建表单,Stripe 处理卡片信息 |
| 定期账单 / 订阅 | Checkout Sessions(订阅模式) | 低-中 |
| 保存卡片以便后续使用 | Setup Intents | 低 |
| 市场/平台支付 | Stripe Connect | 高 |
| 让客户自行管理账单 | Customer Portal | 低 — Stripe 托管该服务 |
默认推荐:从Checkout Sessions开始。这是最快实现收款的方式,后续可随时添加嵌入式表单。
Setup
配置步骤
Install
安装依赖
bash
npm install stripe @stripe/stripe-jsbash
npm install stripe @stripe/stripe-jsAPI Keys
API密钥
bash
undefinedbash
undefinedGet keys from: https://dashboard.stripe.com/apikeys
从以下地址获取密钥:https://dashboard.stripe.com/apikeys
Test keys start with sk_test_ and pk_test_
测试密钥以 sk_test_ 和 pk_test_ 开头
Live keys start with sk_live_ and pk_live_
正式环境密钥以 sk_live_ 和 pk_live_ 开头
For Cloudflare Workers — store as secrets:
对于Cloudflare Workers — 存储为密钥:
npx wrangler secret put STRIPE_SECRET_KEY
npx wrangler secret put STRIPE_WEBHOOK_SECRET
npx wrangler secret put STRIPE_SECRET_KEY
npx wrangler secret put STRIPE_WEBHOOK_SECRET
For local dev — .dev.vars:
本地开发 — 在 .dev.vars 文件中配置:
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
undefinedSTRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
undefinedServer-Side Client
服务器端客户端初始化
typescript
import Stripe from 'stripe';
// Cloudflare Workers
const stripe = new Stripe(c.env.STRIPE_SECRET_KEY);
// Node.js
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);typescript
import Stripe from 'stripe';
// Cloudflare Workers 环境
const stripe = new Stripe(c.env.STRIPE_SECRET_KEY);
// Node.js 环境
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);One-Time Payment (Checkout Sessions)
一次性支付(Checkout Sessions)
The fastest way to accept payment. Stripe hosts the entire checkout page.
这是最快的收款方式,Stripe托管整个结账页面。
Create a Checkout Session (Server)
创建Checkout会话(服务器端)
typescript
app.post('/api/checkout', async (c) => {
const { priceId, successUrl, cancelUrl } = await c.req.json();
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [{ price: priceId, quantity: 1 }],
success_url: successUrl || `${new URL(c.req.url).origin}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: cancelUrl || `${new URL(c.req.url).origin}/pricing`,
});
return c.json({ url: session.url });
});typescript
app.post('/api/checkout', async (c) => {
const { priceId, successUrl, cancelUrl } = await c.req.json();
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [{ price: priceId, quantity: 1 }],
success_url: successUrl || `${new URL(c.req.url).origin}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: cancelUrl || `${new URL(c.req.url).origin}/pricing`,
});
return c.json({ url: session.url });
});Redirect to Checkout (Client)
跳转到结账页面(客户端)
typescript
async function handleCheckout(priceId: string) {
const res = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ priceId }),
});
const { url } = await res.json();
window.location.href = url;
}typescript
async function handleCheckout(priceId: string) {
const res = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ priceId }),
});
const { url } = await res.json();
window.location.href = url;
}Create Products and Prices
创建产品与价格
bash
undefinedbash
undefinedVia Stripe CLI (recommended for setup)
推荐使用Stripe CLI配置
stripe products create --name="Pro Plan" --description="Full access"
stripe prices create --product=prod_XXX --unit-amount=2900 --currency=aud --recurring[interval]=month
stripe products create --name="Pro Plan" --description="Full access"
stripe prices create --product=prod_XXX --unit-amount=2900 --currency=aud --recurring[interval]=month
Or via Dashboard: https://dashboard.stripe.com/products
或通过Stripe控制台创建:https://dashboard.stripe.com/products
**Hardcode price IDs** in your code (they don't change):
```typescript
const PRICES = {
pro_monthly: 'price_1234567890',
pro_yearly: 'price_0987654321',
} as const;
**在代码中硬编码价格ID**(价格ID不会变更):
```typescript
const PRICES = {
pro_monthly: 'price_1234567890',
pro_yearly: 'price_0987654321',
} as const;Subscriptions
订阅功能
Same as one-time but with :
mode: 'subscription'typescript
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: PRICES.pro_monthly, quantity: 1 }],
success_url: `${origin}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${origin}/pricing`,
// Link to existing customer if known:
customer: customerId, // or customer_email: 'user@example.com'
});与一次性支付类似,只需将设置为:
mode'subscription'typescript
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: PRICES.pro_monthly, quantity: 1 }],
success_url: `${origin}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${origin}/pricing`,
// 关联已有客户(如果已知):
customer: customerId, // 或 customer_email: 'user@example.com'
});Check Subscription Status
检查订阅状态
typescript
async function hasActiveSubscription(customerId: string): Promise<boolean> {
const subs = await stripe.subscriptions.list({
customer: customerId,
status: 'active',
limit: 1,
});
return subs.data.length > 0;
}typescript
async function hasActiveSubscription(customerId: string): Promise<boolean> {
const subs = await stripe.subscriptions.list({
customer: customerId,
status: 'active',
limit: 1,
});
return subs.data.length > 0;
}Webhooks
Webhook
Stripe sends events to your server when things happen (payment succeeded, subscription cancelled, etc.). You must verify the webhook signature.
当事件发生时(如支付成功、订阅取消等),Stripe会向你的服务器发送事件通知。必须验证Webhook签名。
Webhook Handler (Cloudflare Workers / Hono)
Webhook处理器(Cloudflare Workers / Hono)
typescript
app.post('/api/webhooks/stripe', async (c) => {
const body = await c.req.text();
const sig = c.req.header('stripe-signature')!;
let event: Stripe.Event;
try {
// Use constructEventAsync for Workers (no Node crypto)
event = await stripe.webhooks.constructEventAsync(
body,
sig,
c.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.error('Webhook signature verification failed:', err);
return c.json({ error: 'Invalid signature' }, 400);
}
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
// Fulfill the order — update database, send email, grant access
await handleCheckoutComplete(session);
break;
}
case 'customer.subscription.updated': {
const sub = event.data.object as Stripe.Subscription;
await handleSubscriptionChange(sub);
break;
}
case 'customer.subscription.deleted': {
const sub = event.data.object as Stripe.Subscription;
await handleSubscriptionCancelled(sub);
break;
}
case 'invoice.payment_failed': {
const invoice = event.data.object as Stripe.Invoice;
await handlePaymentFailed(invoice);
break;
}
}
return c.json({ received: true });
});typescript
app.post('/api/webhooks/stripe', async (c) => {
const body = await c.req.text();
const sig = c.req.header('stripe-signature')!;
let event: Stripe.Event;
try {
// 在Workers环境中使用constructEventAsync(无需Node.js crypto模块)
event = await stripe.webhooks.constructEventAsync(
body,
sig,
c.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.error('Webhook签名验证失败:', err);
return c.json({ error: 'Invalid signature' }, 400);
}
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
// 处理订单完成逻辑 — 更新数据库、发送邮件、开通权限
await handleCheckoutComplete(session);
break;
}
case 'customer.subscription.updated': {
const sub = event.data.object as Stripe.Subscription;
await handleSubscriptionChange(sub);
break;
}
case 'customer.subscription.deleted': {
const sub = event.data.object as Stripe.Subscription;
await handleSubscriptionCancelled(sub);
break;
}
case 'invoice.payment_failed': {
const invoice = event.data.object as Stripe.Invoice;
await handlePaymentFailed(invoice);
break;
}
}
return c.json({ received: true });
});Register Webhook
注册Webhook
bash
undefinedbash
undefinedLocal testing with Stripe CLI:
本地开发使用Stripe CLI转发:
stripe listen --forward-to http://localhost:8787/api/webhooks/stripe
stripe listen --forward-to http://localhost:8787/api/webhooks/stripe
Production — register via Dashboard:
生产环境 — 通过Stripe控制台注册:
Events: checkout.session.completed, customer.subscription.updated,
事件:checkout.session.completed, customer.subscription.updated,
customer.subscription.deleted, invoice.payment_failed
customer.subscription.deleted, invoice.payment_failed
undefinedundefinedCloudflare Workers Gotcha
Cloudflare Workers注意事项
constructEventcryptoconstructEventAsync同步方法依赖Node.js的模块,在Workers环境中不可用。请使用替代——它基于Web Crypto API实现。
constructEventcryptoconstructEventAsyncCustomer Portal
客户门户
Let customers manage their own subscriptions (upgrade, downgrade, cancel, update payment method):
typescript
app.post('/api/billing/portal', async (c) => {
const { customerId } = await c.req.json();
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${new URL(c.req.url).origin}/dashboard`,
});
return c.json({ url: session.url });
});Configure the portal in Dashboard: https://dashboard.stripe.com/settings/billing/portal
允许客户自行管理订阅(升级、降级、取消、更新支付方式):
typescript
app.post('/api/billing/portal', async (c) => {
const { customerId } = await c.req.json();
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${new URL(c.req.url).origin}/dashboard`,
});
return c.json({ url: session.url });
});在Stripe控制台配置客户门户:https://dashboard.stripe.com/settings/billing/portal
Pricing Page Pattern
定价页面实现方案
Generate a pricing page that reads from Stripe products:
typescript
// Server: fetch products and prices
app.get('/api/pricing', async (c) => {
const prices = await stripe.prices.list({
active: true,
expand: ['data.product'],
type: 'recurring',
});
return c.json(prices.data.map(price => ({
id: price.id,
name: (price.product as Stripe.Product).name,
description: (price.product as Stripe.Product).description,
amount: price.unit_amount,
currency: price.currency,
interval: price.recurring?.interval,
})));
});Or hardcode if you only have 2-3 plans — simpler and no API call on every page load.
从Stripe产品中动态获取数据生成定价页面:
typescript
// 服务器端:获取产品与价格数据
app.get('/api/pricing', async (c) => {
const prices = await stripe.prices.list({
active: true,
expand: ['data.product'],
type: 'recurring',
});
return c.json(prices.data.map(price => ({
id: price.id,
name: (price.product as Stripe.Product).name,
description: (price.product as Stripe.Product).description,
amount: price.unit_amount,
currency: price.currency,
interval: price.recurring?.interval,
})));
});如果只有2-3个定价方案,直接硬编码会更简单,无需每次页面加载都调用API。
Stripe CLI (Local Development)
Stripe CLI(本地开发)
bash
undefinedbash
undefinedInstall
安装
brew install stripe/stripe-cli/stripe
brew install stripe/stripe-cli/stripe
Login
登录
stripe login
stripe login
Listen for webhooks locally
本地监听Webhook事件
stripe listen --forward-to http://localhost:8787/api/webhooks/stripe
stripe listen --forward-to http://localhost:8787/api/webhooks/stripe
Trigger test events
触发测试事件
stripe trigger checkout.session.completed
stripe trigger customer.subscription.created
stripe trigger invoice.payment_failed
undefinedstripe trigger checkout.session.completed
stripe trigger customer.subscription.created
stripe trigger invoice.payment_failed
undefinedCommon Patterns
常见实现模式
Link Stripe Customer to Your User
关联Stripe客户与自有用户系统
typescript
// On first checkout, create or find customer:
const session = await stripe.checkout.sessions.create({
customer_email: user.email, // Creates new customer if none exists
// OR
customer: user.stripeCustomerId, // Use existing
metadata: { userId: user.id }, // Link back to your user
// ...
});
// In webhook, save the customer ID:
case 'checkout.session.completed': {
const session = event.data.object;
await db.update(users)
.set({ stripeCustomerId: session.customer as string })
.where(eq(users.id, session.metadata.userId));
}typescript
// 首次结账时,创建或查找客户:
const session = await stripe.checkout.sessions.create({
customer_email: user.email, // 若不存在则创建新客户
// 或
customer: user.stripeCustomerId, // 使用已有客户
metadata: { userId: user.id }, // 关联到自有用户ID
// ...
});
// 在Webhook中保存客户ID:
case 'checkout.session.completed': {
const session = event.data.object;
await db.update(users)
.set({ stripeCustomerId: session.customer as string })
.where(eq(users.id, session.metadata.userId));
}Free Trial
免费试用期
typescript
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: PRICES.pro_monthly, quantity: 1 }],
subscription_data: {
trial_period_days: 14,
},
// ...
});typescript
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: PRICES.pro_monthly, quantity: 1 }],
subscription_data: {
trial_period_days: 14,
},
// ...
});Australian Dollars
澳元支付配置
typescript
// Set currency when creating prices
const price = await stripe.prices.create({
product: 'prod_XXX',
unit_amount: 2900, // $29.00 in cents
currency: 'aud',
recurring: { interval: 'month' },
});typescript
// 创建价格时设置货币
const price = await stripe.prices.create({
product: 'prod_XXX',
unit_amount: 2900, // 29.00澳元(单位为分)
currency: 'aud',
recurring: { interval: 'month' },
});Gotchas
常见问题与解决方案
| Gotcha | Fix |
|---|---|
| Use |
| Webhook fires but handler not called | Check the endpoint URL matches exactly (trailing slash matters) |
| Test mode payments not appearing | Make sure you're using |
| Price amounts are in cents | |
| Customer email doesn't match user | Use |
| Subscription status stale | Don't cache — check via API or trust webhook events |
| Webhook retries | Stripe retries failed webhooks for up to 3 days. Return 200 quickly. |
| CORS on checkout redirect | Checkout URL is on stripe.com — use |
| 问题 | 解决方案 |
|---|---|
| 使用 |
| Webhook事件已触发但处理器未执行 | 检查端点URL是否完全匹配(末尾斜杠会影响匹配) |
| 测试模式支付未显示 | 确保使用的是 |
| 价格金额以分为单位 | |
| 客户邮箱与自有用户不匹配 | 对于老用户,使用 |
| 订阅状态过期 | 不要缓存订阅状态——通过API查询或信任Webhook事件 |
| Webhook重试机制 | Stripe会对失败的Webhook重试最多3天,需快速返回200状态码 |
| 结账跳转时出现CORS问题 | 结账URL属于stripe.com域名——使用 |