stripe

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Stripe Integration Helper

Stripe集成助手

Assist with Stripe payment gateway integration for SaaS applications.
为SaaS应用提供Stripe支付网关集成支持。

Quick Reference

快速参考

Installation

安装

bash
bun add stripe @stripe/stripe-js
bash
bun add stripe @stripe/stripe-js

Environment Variables

环境变量

bash
undefined
bash
undefined

Server-side (secret)

服务端(密钥)

STRIPE_SECRET_KEY="sk_live_..." STRIPE_WEBHOOK_SECRET="whsec_..."
STRIPE_SECRET_KEY="sk_live_..." STRIPE_WEBHOOK_SECRET="whsec_..."

Client-side (publishable)

客户端(可发布密钥)

NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_live_..."
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_live_..."

App URL for callbacks

回调用的应用URL

NEXT_PUBLIC_APP_URL="https://your-app.com"
undefined
NEXT_PUBLIC_APP_URL="https://your-app.com"
undefined

SDK Initialization

SDK初始化

Server-side:
typescript
// lib/stripe.ts
import Stripe from "stripe";

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2025-01-27.acacia",
  typescript: true,
});
Client-side:
typescript
// lib/stripe-client.ts
import { loadStripe } from "@stripe/stripe-js";

export const stripePromise = loadStripe(
  process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);
服务端:
typescript
// lib/stripe.ts
import Stripe from "stripe";

export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2025-01-27.acacia",
  typescript: true,
});
客户端:
typescript
// lib/stripe-client.ts
import { loadStripe } from "@stripe/stripe-js";

export const stripePromise = loadStripe(
  process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!
);

Common Tasks

常见任务

1. Create Checkout Session

1. 创建结账会话

typescript
// app/api/checkout/route.ts
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { stripe } from "@/lib/stripe";

export async function POST(request: NextRequest) {
  const { userId } = await auth();
  if (!userId) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const { priceId } = await request.json();

  const session = await stripe.checkout.sessions.create({
    mode: "subscription",
    payment_method_types: ["card"],
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
    metadata: { userId },
    customer_email: user.email, // Optional: pre-fill email
  });

  return NextResponse.json({ url: session.url });
}
typescript
// app/api/checkout/route.ts
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@clerk/nextjs/server";
import { stripe } from "@/lib/stripe";

export async function POST(request: NextRequest) {
  const { userId } = await auth();
  if (!userId) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const { priceId } = await request.json();

  const session = await stripe.checkout.sessions.create({
    mode: "subscription",
    payment_method_types: ["card"],
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: `${process.env.NEXT_PUBLIC_APP_URL}/billing/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
    metadata: { userId },
    customer_email: user.email, // 可选:预填邮箱
  });

  return NextResponse.json({ url: session.url });
}

2. Create Customer Portal Session

2. 创建客户门户会话

typescript
// app/api/billing/portal/route.ts
export async function POST(request: NextRequest) {
  const { userId } = await auth();
  if (!userId) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  // Get Stripe customer ID from your database
  const user = await db.query.users.findFirst({
    where: eq(users.id, userId),
  });

  if (!user?.stripeCustomerId) {
    return NextResponse.json({ error: "No subscription" }, { status: 400 });
  }

  const session = await stripe.billingPortal.sessions.create({
    customer: user.stripeCustomerId,
    return_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
  });

  return NextResponse.json({ url: session.url });
}
typescript
// app/api/billing/portal/route.ts
export async function POST(request: NextRequest) {
  const { userId } = await auth();
  if (!userId) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  // 从数据库获取Stripe客户ID
  const user = await db.query.users.findFirst({
    where: eq(users.id, userId),
  });

  if (!user?.stripeCustomerId) {
    return NextResponse.json({ error: "No subscription" }, { status: 400 });
  }

  const session = await stripe.billingPortal.sessions.create({
    customer: user.stripeCustomerId,
    return_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard`,
  });

  return NextResponse.json({ url: session.url });
}

3. Webhook Handler

3. Webhook处理器

typescript
// app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import Stripe from "stripe";

export async function POST(request: NextRequest) {
  const body = await request.text();
  const signature = request.headers.get("stripe-signature")!;

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    console.error("Webhook signature verification failed");
    return NextResponse.json({ error: "Invalid signature" }, { status: 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 subscription = event.data.object as Stripe.Subscription;
      await handleSubscriptionUpdate(subscription);
      break;
    }
    case "customer.subscription.deleted": {
      const subscription = event.data.object as Stripe.Subscription;
      await handleSubscriptionCancelled(subscription);
      break;
    }
    case "invoice.payment_failed": {
      const invoice = event.data.object as Stripe.Invoice;
      await handlePaymentFailed(invoice);
      break;
    }
  }

  return NextResponse.json({ received: true });
}
typescript
// app/api/webhooks/stripe/route.ts
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import Stripe from "stripe";

export async function POST(request: NextRequest) {
  const body = await request.text();
  const signature = request.headers.get("stripe-signature")!;

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    console.error("Webhook签名验证失败");
    return NextResponse.json({ error: "Invalid signature" }, { status: 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 subscription = event.data.object as Stripe.Subscription;
      await handleSubscriptionUpdate(subscription);
      break;
    }
    case "customer.subscription.deleted": {
      const subscription = event.data.object as Stripe.Subscription;
      await handleSubscriptionCancelled(subscription);
      break;
    }
    case "invoice.payment_failed": {
      const invoice = event.data.object as Stripe.Invoice;
      await handlePaymentFailed(invoice);
      break;
    }
  }

  return NextResponse.json({ received: true });
}

Webhook Events

Webhook事件

EventWhen to Handle
checkout.session.completed
User completes checkout
customer.subscription.created
New subscription starts
customer.subscription.updated
Plan change, renewal
customer.subscription.deleted
Subscription cancelled
invoice.payment_succeeded
Successful payment
invoice.payment_failed
Failed payment attempt
customer.updated
Customer info changed
事件处理时机
checkout.session.completed
用户完成结账时
customer.subscription.created
新订阅启动时
customer.subscription.updated
套餐变更、续订时
customer.subscription.deleted
订阅取消时
invoice.payment_succeeded
支付成功时
invoice.payment_failed
支付尝试失败时
customer.updated
客户信息变更时

Subscription Status Values

订阅状态值

StatusDescription
active
Subscription is current
past_due
Payment failed, retrying
canceled
Subscription ended
unpaid
All retry attempts failed
trialing
In trial period
incomplete
First payment pending
状态描述
active
订阅当前有效
past_due
支付失败,正在重试
canceled
订阅已终止
unpaid
所有重试尝试均失败
trialing
处于试用阶段
incomplete
首次支付待处理

Database Schema

数据库表结构

Users Table (add Stripe fields)

用户表(新增Stripe相关字段)

sql
stripeCustomerId    TEXT UNIQUE
stripeSubscriptionId TEXT
stripePriceId       TEXT
stripeCurrentPeriodEnd TIMESTAMP
sql
stripeCustomerId    TEXT UNIQUE
stripeSubscriptionId TEXT
stripePriceId       TEXT
stripeCurrentPeriodEnd TIMESTAMP

Subscription Sync Pattern

订阅同步模式

typescript
async function syncSubscription(
  userId: string,
  subscription: Stripe.Subscription
) {
  await db
    .update(users)
    .set({
      stripeSubscriptionId: subscription.id,
      stripePriceId: subscription.items.data[0].price.id,
      stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
    })
    .where(eq(users.id, userId));
}
typescript
async function syncSubscription(
  userId: string,
  subscription: Stripe.Subscription
) {
  await db
    .update(users)
    .set({
      stripeSubscriptionId: subscription.id,
      stripePriceId: subscription.items.data[0].price.id,
      stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000),
    })
    .where(eq(users.id, userId));
}

Feature Gating

功能权限控制

typescript
async function checkFeatureAccess(userId: string): Promise<boolean> {
  const user = await db.query.users.findFirst({
    where: eq(users.id, userId),
  });

  if (!user?.stripeSubscriptionId) return false;

  // Check if subscription is still valid
  const now = new Date();
  return user.stripeCurrentPeriodEnd > now;
}
typescript
async function checkFeatureAccess(userId: string): Promise<boolean> {
  const user = await db.query.users.findFirst({
    where: eq(users.id, userId),
  });

  if (!user?.stripeSubscriptionId) return false;

  // 检查订阅是否仍有效
  const now = new Date();
  return user.stripeCurrentPeriodEnd > now;
}

Testing

测试

Test Card Numbers

测试卡号

CardScenario
4242424242424242
Successful payment
4000000000000002
Card declined
4000002500003155
Requires 3D Secure
4000000000009995
Insufficient funds
卡号场景
4242424242424242
支付成功
4000000000000002
卡片被拒绝
4000002500003155
需要3D安全验证
4000000000009995
余额不足

Stripe CLI for Local Webhooks

使用Stripe CLI测试本地Webhook

bash
undefined
bash
undefined

Install Stripe CLI

安装Stripe CLI

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

Login

登录

stripe login
stripe login

Forward 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
undefined
stripe trigger checkout.session.completed
undefined

Pricing Page Pattern

定价页实现模式

typescript
// Get prices from Stripe
const prices = await stripe.prices.list({
  active: true,
  expand: ["data.product"],
});

// Display in component
{prices.data.map((price) => (
  <PriceCard
    key={price.id}
    name={(price.product as Stripe.Product).name}
    price={price.unit_amount! / 100}
    interval={price.recurring?.interval}
    priceId={price.id}
  />
))}
typescript
// 从Stripe获取价格
const prices = await stripe.prices.list({
  active: true,
  expand: ["data.product"],
});

// 在组件中展示
{prices.data.map((price) => (
  <PriceCard
    key={price.id}
    name={(price.product as Stripe.Product).name}
    price={price.unit_amount! / 100}
    interval={price.recurring?.interval}
    priceId={price.id}
  />
))}

Security Best Practices

安全最佳实践

  1. Never expose secret key - Use
    STRIPE_SECRET_KEY
    only server-side
  2. Verify webhook signatures - Always use
    stripe.webhooks.constructEvent
  3. Idempotency - Store event IDs to prevent duplicate processing
  4. Raw body for webhooks - Don't parse JSON before verification
  5. Use metadata - Store userId in checkout session metadata
  1. 切勿暴露密钥 - 仅在服务端使用
    STRIPE_SECRET_KEY
  2. 验证Webhook签名 - 始终使用
    stripe.webhooks.constructEvent
  3. 幂等性 - 存储事件ID以避免重复处理
  4. Webhook使用原始请求体 - 验证前不要解析JSON
  5. 使用元数据 - 在结账会话元数据中存储userId

Common Issues

常见问题

IssueSolution
Webhook signature invalidUse raw body, not parsed JSON
Customer not foundCreate customer before checkout
Subscription not syncingCheck webhook event registration
Test cards failingEnsure using test mode keys
Portal not loadingVerify customer has active subscription
问题解决方案
Webhook签名无效使用原始请求体,而非解析后的JSON
找不到客户在结账前创建客户
订阅未同步检查Webhook事件是否已注册
测试卡片支付失败确保使用测试模式密钥
客户门户无法加载验证客户是否有有效订阅

Useful Commands

常用命令

bash
undefined
bash
undefined

List products

列出产品

stripe products list
stripe products list

List prices

列出价格

stripe prices list
stripe prices list

Get subscription

获取订阅信息

stripe subscriptions retrieve sub_xxx
stripe subscriptions retrieve sub_xxx

Cancel subscription

取消订阅

stripe subscriptions cancel sub_xxx
undefined
stripe subscriptions cancel sub_xxx
undefined