stripe-payments

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Stripe 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...UseComplexity
Accept a one-time paymentCheckout SessionsLow — Stripe hosts the payment page
Embed a payment form in your UIPayment Element + Payment IntentsMedium — you build the form, Stripe handles the card
Recurring billing / subscriptionsCheckout Sessions (subscription mode)Low-Medium
Save a card for laterSetup IntentsLow
Marketplace / platform paymentsStripe ConnectHigh
Let customers manage billingCustomer PortalLow — 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-js
bash
npm install stripe @stripe/stripe-js

API Keys

API密钥

bash
undefined
bash
undefined

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_...
undefined
STRIPE_SECRET_KEY=sk_test_... STRIPE_WEBHOOK_SECRET=whsec_... STRIPE_PUBLISHABLE_KEY=pk_test_...
undefined

Server-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
undefined
bash
undefined

Via 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

或通过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
undefined
bash
undefined

Local testing with Stripe CLI:

本地开发使用Stripe CLI转发:

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

undefined
undefined

Cloudflare Workers Gotcha

Cloudflare Workers注意事项

constructEvent
(synchronous) uses Node.js
crypto
which doesn't exist in Workers. Use
constructEventAsync
instead — it uses the Web Crypto API.
同步方法
constructEvent
依赖Node.js的
crypto
模块,在Workers环境中不可用。请使用
constructEventAsync
替代——它基于Web Crypto API实现。

Customer 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
undefined
bash
undefined

Install

安装

brew install stripe/stripe-cli/stripe
brew install stripe/stripe-cli/stripe

Login

登录

stripe login
stripe login

Listen for webhooks locally

本地监听Webhook事件

Trigger test events

触发测试事件

stripe trigger checkout.session.completed stripe trigger customer.subscription.created stripe trigger invoice.payment_failed
undefined
stripe trigger checkout.session.completed stripe trigger customer.subscription.created stripe trigger invoice.payment_failed
undefined

Common 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

常见问题与解决方案

GotchaFix
constructEvent
fails on Workers
Use
constructEventAsync
(Web Crypto API)
Webhook fires but handler not calledCheck the endpoint URL matches exactly (trailing slash matters)
Test mode payments not appearingMake sure you're using
sk_test_
key, not
sk_live_
Price amounts are in cents
2900
= $29.00. Always divide by 100 for display
Customer email doesn't match userUse
customer
(existing ID) not
customer_email
for returning users
Subscription status staleDon't cache — check via API or trust webhook events
Webhook retriesStripe retries failed webhooks for up to 3 days. Return 200 quickly.
CORS on checkout redirectCheckout URL is on stripe.com — use
window.location.href
, not fetch
问题解决方案
constructEvent
在Workers环境中失败
使用
constructEventAsync
(基于Web Crypto API)
Webhook事件已触发但处理器未执行检查端点URL是否完全匹配(末尾斜杠会影响匹配)
测试模式支付未显示确保使用的是
sk_test_
密钥,而非
sk_live_
价格金额以分为单位
2900
代表29.00美元/澳元,显示时需除以100
客户邮箱与自有用户不匹配对于老用户,使用
customer
(已有ID)而非
customer_email
订阅状态过期不要缓存订阅状态——通过API查询或信任Webhook事件
Webhook重试机制Stripe会对失败的Webhook重试最多3天,需快速返回200状态码
结账跳转时出现CORS问题结账URL属于stripe.com域名——使用
window.location.href
跳转,而非fetch请求