web-payments

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Web Payments Skill (Stripe)

网页支付技能(Stripe)

Load with: base.md + [framework].md
For integrating Stripe payments into web applications - one-time payments, subscriptions, and checkout flows.

加载方式:base.md + [framework].md
用于在网页应用中集成Stripe支付功能——涵盖一次性支付、订阅和结账流程。

Setup

设置步骤

1. Create Stripe Account

1. 创建Stripe账户

2. Environment Variables

2. 环境变量

bash
undefined
bash
undefined

.env

.env

STRIPE_SECRET_KEY=sk_test_xxx # Server-side only STRIPE_PUBLISHABLE_KEY=pk_test_xxx # Client-side safe STRIPE_WEBHOOK_SECRET=whsec_xxx # For webhook verification
STRIPE_SECRET_KEY=sk_test_xxx # 仅在服务端使用 STRIPE_PUBLISHABLE_KEY=pk_test_xxx # 可在客户端安全使用 STRIPE_WEBHOOK_SECRET=whsec_xxx # 用于Webhook验证

Production

生产环境

STRIPE_SECRET_KEY=sk_live_xxx STRIPE_PUBLISHABLE_KEY=pk_live_xxx
undefined
STRIPE_SECRET_KEY=sk_live_xxx STRIPE_PUBLISHABLE_KEY=pk_live_xxx
undefined

3. Install SDK

3. 安装SDK

bash
undefined
bash
undefined

Node.js

Node.js

npm install stripe @stripe/stripe-js
npm install stripe @stripe/stripe-js

Python

Python

pip install stripe

---
pip install stripe

---

Integration Options

集成选项

MethodBest ForComplexity
Checkout (Hosted)Quick setup, Stripe-hosted pageLow
Checkout (Embedded)Custom site, embedded formLow
Payment ElementFull customization, complex flowsMedium
Custom FormComplete control (rare)High
Recommendation: Start with Checkout, migrate to Payment Element if needed.

方法适用场景复杂度
Checkout(托管式)快速搭建,Stripe托管页面
Checkout(嵌入式)自定义网站,嵌入式表单
Payment Element完全自定义,复杂流程
自定义表单完全控制(罕见场景)
推荐方案:从Checkout开始,如需自定义再迁移到Payment Element。

Stripe Checkout (Recommended)

Stripe Checkout(推荐方案)

Server: Create Checkout Session

服务端:创建结账会话

Node.js / Next.js

Node.js / Next.js

typescript
// app/api/checkout/route.ts (Next.js App Router)
import Stripe from "stripe";
import { NextResponse } from "next/server";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(request: Request) {
  const { priceId, mode = "payment" } = await request.json();

  try {
    const session = await stripe.checkout.sessions.create({
      mode: mode as "payment" | "subscription",
      payment_method_types: ["card"],
      line_items: [
        {
          price: priceId,
          quantity: 1,
        },
      ],
      success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${process.env.NEXT_PUBLIC_URL}/canceled`,
      // Optional: Link to existing customer
      // customer: customerId,
      // Optional: Collect shipping
      // shipping_address_collection: { allowed_countries: ["US", "CA"] },
      // Optional: Add metadata for tracking
      metadata: {
        userId: "user_123",
        source: "pricing_page",
      },
    });

    return NextResponse.json({ sessionId: session.id, url: session.url });
  } catch (error) {
    console.error("Stripe error:", error);
    return NextResponse.json({ error: "Failed to create session" }, { status: 500 });
  }
}
typescript
// app/api/checkout/route.ts (Next.js App Router)
import Stripe from "stripe";
import { NextResponse } from "next/server";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(request: Request) {
  const { priceId, mode = "payment" } = await request.json();

  try {
    const session = await stripe.checkout.sessions.create({
      mode: mode as "payment" | "subscription",
      payment_method_types: ["card"],
      line_items: [
        {
          price: priceId,
          quantity: 1,
        },
      ],
      success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${process.env.NEXT_PUBLIC_URL}/canceled`,
      // 可选:关联已有客户
      // customer: customerId,
      // 可选:收集配送信息
      // shipping_address_collection: { allowed_countries: ["US", "CA"] },
      // 可选:添加元数据用于追踪
      metadata: {
        userId: "user_123",
        source: "pricing_page",
      },
    });

    return NextResponse.json({ sessionId: session.id, url: session.url });
  } catch (error) {
    console.error("Stripe错误:", error);
    return NextResponse.json({ error: "创建会话失败" }, { status: 500 });
  }
}

Python / FastAPI

Python / FastAPI

python
undefined
python
undefined

app/api/checkout.py

app/api/checkout.py

import stripe from fastapi import APIRouter, HTTPException from pydantic import BaseModel import os
stripe.api_key = os.environ["STRIPE_SECRET_KEY"] router = APIRouter()
class CheckoutRequest(BaseModel): price_id: str mode: str = "payment" # or "subscription"
@router.post("/api/checkout") async def create_checkout_session(request: CheckoutRequest): try: session = stripe.checkout.Session.create( mode=request.mode, payment_method_types=["card"], line_items=[{ "price": request.price_id, "quantity": 1, }], success_url=f"{os.environ['APP_URL']}/success?session_id={{CHECKOUT_SESSION_ID}}", cancel_url=f"{os.environ['APP_URL']}/canceled", metadata={ "user_id": "user_123", }, ) return {"session_id": session.id, "url": session.url} except stripe.error.StripeError as e: raise HTTPException(status_code=400, detail=str(e))
undefined
import stripe from fastapi import APIRouter, HTTPException from pydantic import BaseModel import os
stripe.api_key = os.environ["STRIPE_SECRET_KEY"] router = APIRouter()
class CheckoutRequest(BaseModel): price_id: str mode: str = "payment" # 或 "subscription"
@router.post("/api/checkout") async def create_checkout_session(request: CheckoutRequest): try: session = stripe.checkout.Session.create( mode=request.mode, payment_method_types=["card"], line_items=[{ "price": request.price_id, "quantity": 1, }], success_url=f"{os.environ['APP_URL']}/success?session_id={{CHECKOUT_SESSION_ID}}", cancel_url=f"{os.environ['APP_URL']}/canceled", metadata={ "user_id": "user_123", }, ) return {"session_id": session.id, "url": session.url} except stripe.error.StripeError as e: raise HTTPException(status_code=400, detail=str(e))
undefined

Client: Redirect to Checkout

客户端:跳转到结账页面

typescript
// components/CheckoutButton.tsx
"use client";

import { loadStripe } from "@stripe/stripe-js";

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

export function CheckoutButton({ priceId }: { priceId: string }) {
  const handleCheckout = async () => {
    const response = await fetch("/api/checkout", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ priceId }),
    });

    const { url } = await response.json();

    // Redirect to Stripe Checkout
    window.location.href = url;
  };

  return (
    <button onClick={handleCheckout}>
      Subscribe Now
    </button>
  );
}

typescript
// components/CheckoutButton.tsx
"use client";

import { loadStripe } from "@stripe/stripe-js";

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

export function CheckoutButton({ priceId }: { priceId: string }) {
  const handleCheckout = async () => {
    const response = await fetch("/api/checkout", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ priceId }),
    });

    const { url } = await response.json();

    // 跳转到Stripe Checkout
    window.location.href = url;
  };

  return (
    <button onClick={handleCheckout}>
      立即订阅
    </button>
  );
}

Embedded Checkout

嵌入式Checkout

For keeping users on your site:
typescript
// components/EmbeddedCheckout.tsx
"use client";

import { useEffect, useState } from "react";
import { loadStripe } from "@stripe/stripe-js";
import {
  EmbeddedCheckoutProvider,
  EmbeddedCheckout,
} from "@stripe/react-stripe-js";

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

export function EmbeddedCheckoutForm({ priceId }: { priceId: string }) {
  const [clientSecret, setClientSecret] = useState("");

  useEffect(() => {
    fetch("/api/checkout/embedded", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ priceId }),
    })
      .then((res) => res.json())
      .then((data) => setClientSecret(data.clientSecret));
  }, [priceId]);

  if (!clientSecret) return <div>Loading...</div>;

  return (
    <EmbeddedCheckoutProvider stripe={stripePromise} options={{ clientSecret }}>
      <EmbeddedCheckout />
    </EmbeddedCheckoutProvider>
  );
}
Server endpoint for embedded:
typescript
// app/api/checkout/embedded/route.ts
export async function POST(request: Request) {
  const { priceId } = await request.json();

  const session = await stripe.checkout.sessions.create({
    mode: "subscription",
    line_items: [{ price: priceId, quantity: 1 }],
    ui_mode: "embedded",
    return_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
  });

  return NextResponse.json({ clientSecret: session.client_secret });
}

用于让用户停留在你的网站内完成支付:
typescript
// components/EmbeddedCheckout.tsx
"use client";

import { useEffect, useState } from "react";
import { loadStripe } from "@stripe/stripe-js";
import {
  EmbeddedCheckoutProvider,
  EmbeddedCheckout,
} from "@stripe/react-stripe-js";

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

export function EmbeddedCheckoutForm({ priceId }: { priceId: string }) {
  const [clientSecret, setClientSecret] = useState("");

  useEffect(() => {
    fetch("/api/checkout/embedded", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ priceId }),
    })
      .then((res) => res.json())
      .then((data) => setClientSecret(data.clientSecret));
  }, [priceId]);

  if (!clientSecret) return <div>加载中...</div>;

  return (
    <EmbeddedCheckoutProvider stripe={stripePromise} options={{ clientSecret }}>
      <EmbeddedCheckout />
    </EmbeddedCheckoutProvider>
  );
}
嵌入式支付的服务端端点:
typescript
// app/api/checkout/embedded/route.ts
export async function POST(request: Request) {
  const { priceId } = await request.json();

  const session = await stripe.checkout.sessions.create({
    mode: "subscription",
    line_items: [{ price: priceId, quantity: 1 }],
    ui_mode: "embedded",
    return_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
  });

  return NextResponse.json({ clientSecret: session.client_secret });
}

Webhooks (Critical)

Webhook(关键功能)

Never trust client-side data. Always verify payments via webhooks.
绝不要信任客户端数据。始终通过Webhook验证支付状态。

Webhook Endpoint

Webhook端点

typescript
// app/api/webhooks/stripe/route.ts
import Stripe from "stripe";
import { headers } from "next/headers";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;

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

  let event: Stripe.Event;

  // Verify webhook signature
  try {
    event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
  } catch (err) {
    console.error("Webhook signature verification failed");
    return new Response("Invalid signature", { status: 400 });
  }

  // Handle events
  switch (event.type) {
    case "checkout.session.completed": {
      const session = event.data.object as Stripe.Checkout.Session;
      await handleCheckoutComplete(session);
      break;
    }
    case "customer.subscription.created":
    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 handleSubscriptionCanceled(subscription);
      break;
    }
    case "invoice.payment_failed": {
      const invoice = event.data.object as Stripe.Invoice;
      await handlePaymentFailed(invoice);
      break;
    }
    default:
      console.log(`Unhandled event type: ${event.type}`);
  }

  // Return 200 quickly - process async if needed
  return new Response("OK", { status: 200 });
}

async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
  const userId = session.metadata?.userId;
  const customerId = session.customer as string;
  const subscriptionId = session.subscription as string;

  // Update your database
  await db.user.update({
    where: { id: userId },
    data: {
      stripeCustomerId: customerId,
      stripeSubscriptionId: subscriptionId,
      subscriptionStatus: "active",
    },
  });
}
typescript
// app/api/webhooks/stripe/route.ts
import Stripe from "stripe";
import { headers } from "next/headers";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;

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

  let event: Stripe.Event;

  // 验证Webhook签名
  try {
    event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
  } catch (err) {
    console.error("Webhook签名验证失败");
    return new Response("无效签名", { 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.created":
    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 handleSubscriptionCanceled(subscription);
      break;
    }
    case "invoice.payment_failed": {
      const invoice = event.data.object as Stripe.Invoice;
      await handlePaymentFailed(invoice);
      break;
    }
    default:
      console.log(`未处理的事件类型: ${event.type}`);
  }

  // 快速返回200状态码——如需复杂处理可异步执行
  return new Response("OK", { status: 200 });
}

async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
  const userId = session.metadata?.userId;
  const customerId = session.customer as string;
  const subscriptionId = session.subscription as string;

  // 更新你的数据库
  await db.user.update({
    where: { id: userId },
    data: {
      stripeCustomerId: customerId,
      stripeSubscriptionId: subscriptionId,
      subscriptionStatus: "active",
    },
  });
}

Python Webhook

Python Webhook实现

python
undefined
python
undefined

app/api/webhooks.py

app/api/webhooks.py

import stripe from fastapi import APIRouter, Request, HTTPException
router = APIRouter()
@router.post("/api/webhooks/stripe") async def stripe_webhook(request: Request): payload = await request.body() sig_header = request.headers.get("stripe-signature")
try:
    event = stripe.Webhook.construct_event(
        payload, sig_header, os.environ["STRIPE_WEBHOOK_SECRET"]
    )
except ValueError:
    raise HTTPException(status_code=400, detail="Invalid payload")
except stripe.error.SignatureVerificationError:
    raise HTTPException(status_code=400, detail="Invalid signature")

# Handle events
if event["type"] == "checkout.session.completed":
    session = event["data"]["object"]
    await handle_checkout_complete(session)
elif event["type"] == "customer.subscription.deleted":
    subscription = event["data"]["object"]
    await handle_subscription_canceled(subscription)

return {"status": "success"}
undefined
import stripe from fastapi import APIRouter, Request, HTTPException
router = APIRouter()
@router.post("/api/webhooks/stripe") async def stripe_webhook(request: Request): payload = await request.body() sig_header = request.headers.get("stripe-signature")
try:
    event = stripe.Webhook.construct_event(
        payload, sig_header, os.environ["STRIPE_WEBHOOK_SECRET"]
    )
except ValueError:
    raise HTTPException(status_code=400, detail="无效负载")
except stripe.error.SignatureVerificationError:
    raise HTTPException(status_code=400, detail="无效签名")

# 处理事件
if event["type"] == "checkout.session.completed":
    session = event["data"]["object"]
    await handle_checkout_complete(session)
elif event["type"] == "customer.subscription.deleted":
    subscription = event["data"]["object"]
    await handle_subscription_canceled(subscription)

return {"status": "success"}
undefined

Key Webhook Events

关键Webhook事件

EventWhenAction
checkout.session.completed
Payment successfulProvision access
customer.subscription.created
New subscriptionStore subscription ID
customer.subscription.updated
Plan changeUpdate plan in DB
customer.subscription.deleted
CanceledRevoke access
invoice.payment_failed
Payment failedNotify user, retry
invoice.paid
Renewal successfulExtend access

事件触发时机处理动作
checkout.session.completed
支付成功开通用户权限
customer.subscription.created
新订阅创建存储订阅ID
customer.subscription.updated
套餐变更更新数据库中的套餐信息
customer.subscription.deleted
订阅取消收回用户权限
invoice.payment_failed
支付失败通知用户,重试支付
invoice.paid
续费成功延长用户权限

Products & Prices

产品与价格

Create via Dashboard (Recommended)

通过仪表盘创建(推荐)

  1. Go to https://dashboard.stripe.com/products
  2. Create product with name, description
  3. Add price(s) - one-time or recurring
  4. Copy Price ID (
    price_xxx
    )
  1. 访问https://dashboard.stripe.com/products
  2. 创建产品并填写名称、描述
  3. 添加价格——支持一次性或周期性
  4. 复制价格ID(
    price_xxx

Create via API

通过API创建

typescript
// One-time product
const product = await stripe.products.create({
  name: "Pro Plan",
  description: "Full access to all features",
});

const price = await stripe.prices.create({
  product: product.id,
  unit_amount: 2999, // $29.99 in cents
  currency: "usd",
});

// Subscription product
const subscriptionPrice = await stripe.prices.create({
  product: product.id,
  unit_amount: 999, // $9.99/month
  currency: "usd",
  recurring: {
    interval: "month",
  },
});

typescript
// 一次性支付产品
const product = await stripe.products.create({
  name: "专业版套餐",
  description: "全功能访问权限",
});

const price = await stripe.prices.create({
  product: product.id,
  unit_amount: 2999, // 29.99美元,以美分为单位
  currency: "usd",
});

// 订阅产品
const subscriptionPrice = await stripe.prices.create({
  product: product.id,
  unit_amount: 999, // 9.99美元/月
  currency: "usd",
  recurring: {
    interval: "month",
  },
});

Customer Portal

客户门户

Let users manage their subscriptions:
typescript
// app/api/portal/route.ts
export async function POST(request: Request) {
  const { customerId } = await request.json();

  const session = await stripe.billingPortal.sessions.create({
    customer: customerId,
    return_url: `${process.env.NEXT_PUBLIC_URL}/settings`,
  });

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

让用户自行管理订阅:
typescript
// app/api/portal/route.ts
export async function POST(request: Request) {
  const { customerId } = await request.json();

  const session = await stripe.billingPortal.sessions.create({
    customer: customerId,
    return_url: `${process.env.NEXT_PUBLIC_URL}/settings`,
  });

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

Subscriptions

订阅功能

Create Subscription with Trial

创建带试用期的订阅

typescript
const session = await stripe.checkout.sessions.create({
  mode: "subscription",
  line_items: [{ price: priceId, quantity: 1 }],
  subscription_data: {
    trial_period_days: 14,
    // Cancel if no payment method after trial
    trial_settings: {
      end_behavior: { missing_payment_method: "cancel" },
    },
  },
  success_url: successUrl,
  cancel_url: cancelUrl,
});
typescript
const session = await stripe.checkout.sessions.create({
  mode: "subscription",
  line_items: [{ price: priceId, quantity: 1 }],
  subscription_data: {
    trial_period_days: 14,
    // 试用期结束后若无支付方式则取消订阅
    trial_settings: {
      end_behavior: { missing_payment_method: "cancel" },
    },
  },
  success_url: successUrl,
  cancel_url: cancelUrl,
});

Check Subscription Status

检查订阅状态

typescript
// lib/subscription.ts
export async function getSubscriptionStatus(customerId: string) {
  const subscriptions = await stripe.subscriptions.list({
    customer: customerId,
    status: "all",
    limit: 1,
  });

  if (subscriptions.data.length === 0) {
    return { status: "none", plan: null };
  }

  const subscription = subscriptions.data[0];
  return {
    status: subscription.status,
    plan: subscription.items.data[0].price.id,
    currentPeriodEnd: new Date(subscription.current_period_end * 1000),
    cancelAtPeriodEnd: subscription.cancel_at_period_end,
  };
}

typescript
// lib/subscription.ts
export async function getSubscriptionStatus(customerId: string) {
  const subscriptions = await stripe.subscriptions.list({
    customer: customerId,
    status: "all",
    limit: 1,
  });

  if (subscriptions.data.length === 0) {
    return { status: "none", plan: null };
  }

  const subscription = subscriptions.data[0];
  return {
    status: subscription.status,
    plan: subscription.items.data[0].price.id,
    currentPeriodEnd: new Date(subscription.current_period_end * 1000),
    cancelAtPeriodEnd: subscription.cancel_at_period_end,
  };
}

Testing

测试

Test Cards

测试卡号

Card NumberScenario
4242424242424242
Success
4000000000000002
Declined
4000002500003155
Requires 3D Secure
4000000000009995
Insufficient funds
卡号测试场景
4242424242424242
支付成功
4000000000000002
支付被拒绝
4000002500003155
需要3D安全验证
4000000000009995
余额不足

Stripe CLI for Webhooks

使用Stripe CLI测试Webhook

bash
undefined
bash
undefined

Install CLI

安装CLI

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

Login

登录

stripe login
stripe login

Forward webhooks to local server

将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.deleted

---
stripe trigger checkout.session.completed stripe trigger customer.subscription.deleted

---

Project Structure

项目结构

project/
├── app/
│   ├── api/
│   │   ├── checkout/
│   │   │   └── route.ts          # Create checkout session
│   │   ├── portal/
│   │   │   └── route.ts          # Customer portal
│   │   └── webhooks/
│   │       └── stripe/
│   │           └── route.ts      # Webhook handler
│   ├── pricing/
│   │   └── page.tsx              # Pricing page
│   ├── success/
│   │   └── page.tsx              # Post-checkout success
│   └── settings/
│       └── page.tsx              # Manage subscription
├── lib/
│   ├── stripe.ts                 # Stripe client
│   └── subscription.ts           # Subscription helpers
└── .env.local

project/
├── app/
│   ├── api/
│   │   ├── checkout/
│   │   │   └── route.ts          # 创建结账会话
│   │   ├── portal/
│   │   │   └── route.ts          # 客户门户
│   │   └── webhooks/
│   │       └── stripe/
│   │           └── route.ts      # Webhook处理器
│   ├── pricing/
│   │   └── page.tsx              # 定价页面
│   ├── success/
│   │   └── page.tsx              # 结账成功页
│   └── settings/
│       └── page.tsx              # 订阅管理页
├── lib/
│   ├── stripe.ts                 # Stripe客户端
│   └── subscription.ts           # 订阅辅助函数
└── .env.local

Security Best Practices

安全最佳实践

Non-Negotiable Rules

必须遵守的规则

  1. Server-side only for secrets - Never expose
    STRIPE_SECRET_KEY
  2. Always verify webhooks - Check signature before processing
  3. Idempotency - Store webhook event IDs, skip duplicates
  4. Use metadata - Track user IDs, sources for debugging
  5. Handle all states - Success, failure, pending, canceled
  1. 密钥仅在服务端使用 - 绝不要暴露
    STRIPE_SECRET_KEY
  2. 始终验证Webhook - 处理前先检查签名
  3. 幂等性 - 存储Webhook事件ID,跳过重复事件
  4. 使用元数据 - 追踪用户ID、来源信息用于调试
  5. 处理所有状态 - 成功、失败、待处理、取消

Idempotent Webhook Handler

幂等Webhook处理器

typescript
const processedEvents = new Set<string>(); // Use Redis in production

export async function POST(request: Request) {
  // ... verify signature ...

  // Skip duplicate events
  if (processedEvents.has(event.id)) {
    return new Response("Already processed", { status: 200 });
  }
  processedEvents.add(event.id);

  // Process event...
}
typescript
const processedEvents = new Set<string>(); // 生产环境使用Redis

export async function POST(request: Request) {
  // ... 验证签名 ...

  // 跳过重复事件
  if (processedEvents.has(event.id)) {
    return new Response("已处理", { status: 200 });
  }
  processedEvents.add(event.id);

  // 处理事件...
}

Amount Handling

金额处理

typescript
// Always use cents (smallest currency unit)
const priceInCents = 2999; // $29.99

// Helper functions
const toCents = (dollars: number) => Math.round(dollars * 100);
const toDollars = (cents: number) => cents / 100;

// Display
const displayPrice = (cents: number) =>
  new Intl.NumberFormat("en-US", {
    style: "currency",
    currency: "USD",
  }).format(toDollars(cents));

typescript
// 始终使用美分(最小货币单位)
const priceInCents = 2999; // 29.99美元

// 辅助函数
const toCents = (dollars: number) => Math.round(dollars * 100);
const toDollars = (cents: number) => cents / 100;

// 显示金额
const displayPrice = (cents: number) =>
  new Intl.NumberFormat("en-US", {
    style: "currency",
    currency: "USD",
  }).format(toDollars(cents));

Common Patterns

常见实现模式

Pricing Page

定价页面

typescript
// app/pricing/page.tsx
const plans = [
  {
    name: "Starter",
    price: "$9/mo",
    priceId: "price_starter_monthly",
    features: ["Feature 1", "Feature 2"],
  },
  {
    name: "Pro",
    price: "$29/mo",
    priceId: "price_pro_monthly",
    features: ["Everything in Starter", "Feature 3", "Feature 4"],
    popular: true,
  },
];

export default function PricingPage() {
  return (
    <div className="grid md:grid-cols-2 gap-8">
      {plans.map((plan) => (
        <div key={plan.name} className={plan.popular ? "border-blue-500" : ""}>
          <h3>{plan.name}</h3>
          <p>{plan.price}</p>
          <ul>
            {plan.features.map((f) => <li key={f}>{f}</li>)}
          </ul>
          <CheckoutButton priceId={plan.priceId} />
        </div>
      ))}
    </div>
  );
}
typescript
// app/pricing/page.tsx
const plans = [
  {
    name: "入门版",
    price: "$9/月",
    priceId: "price_starter_monthly",
    features: ["功能1", "功能2"],
  },
  {
    name: "专业版",
    price: "$29/月",
    priceId: "price_pro_monthly",
    features: ["包含入门版所有功能", "功能3", "功能4"],
    popular: true,
  },
];

export default function PricingPage() {
  return (
    <div className="grid md:grid-cols-2 gap-8">
      {plans.map((plan) => (
        <div key={plan.name} className={plan.popular ? "border-blue-500" : ""}>
          <h3>{plan.name}</h3>
          <p>{plan.price}</p>
          <ul>
            {plan.features.map((f) => <li key={f}>{f}</li>)}
          </ul>
          <CheckoutButton priceId={plan.priceId} />
        </div>
      ))}
    </div>
  );
}

Protect Routes by Subscription

根据订阅状态保护路由

typescript
// middleware.ts
import { getSubscriptionStatus } from "@/lib/subscription";

export async function middleware(request: NextRequest) {
  const session = await getSession();

  if (request.nextUrl.pathname.startsWith("/pro")) {
    const { status } = await getSubscriptionStatus(session.stripeCustomerId);

    if (status !== "active" && status !== "trialing") {
      return NextResponse.redirect(new URL("/pricing", request.url));
    }
  }
}

typescript
// middleware.ts
import { getSubscriptionStatus } from "@/lib/subscription";

export async function middleware(request: NextRequest) {
  const session = await getSession();

  if (request.nextUrl.pathname.startsWith("/pro")) {
    const { status } = await getSubscriptionStatus(session.stripeCustomerId);

    if (status !== "active" && status !== "trialing") {
      return NextResponse.redirect(new URL("/pricing", request.url));
    }
  }
}

Anti-Patterns

反模式

  • Hardcoding API keys - Use environment variables
  • Client-side payment creation - Always create PaymentIntent/Session server-side
  • Skipping webhook verification - Always verify signatures
  • Processing duplicate webhooks - Implement idempotency
  • Floating-point currency math - Use integers (cents)
  • Trusting client data - Verify everything server-side
  • Ignoring failed payments - Handle
    invoice.payment_failed
  • No error handling - Catch and handle Stripe errors

  • 硬编码API密钥 - 使用环境变量
  • 客户端创建支付 - 始终在服务端创建PaymentIntent/会话
  • 跳过Webhook验证 - 始终验证签名
  • 处理重复Webhook - 实现幂等性
  • 使用浮点数处理货币 - 使用整数(美分)
  • 信任客户端数据 - 所有验证在服务端完成
  • 忽略支付失败 - 处理
    invoice.payment_failed
    事件
  • 无错误处理 - 捕获并处理Stripe错误

Quick Reference

快速参考

bash
undefined
bash
undefined

Install

安装依赖

npm install stripe @stripe/stripe-js @stripe/react-stripe-js
npm install stripe @stripe/stripe-js @stripe/react-stripe-js

Stripe CLI

Stripe CLI命令

stripe login stripe listen --forward-to localhost:3000/api/webhooks/stripe stripe trigger checkout.session.completed
stripe login stripe listen --forward-to localhost:3000/api/webhooks/stripe stripe trigger checkout.session.completed

Test mode prefix

测试环境密钥前缀

sk_test_xxx # Secret key pk_test_xxx # Publishable key
sk_test_xxx # 密钥 pk_test_xxx # 可发布密钥

Live mode prefix

生产环境密钥前缀

sk_live_xxx pk_live_xxx
undefined
sk_live_xxx pk_live_xxx
undefined

Key Endpoints

关键端点

EndpointPurpose
POST /api/checkout
Create checkout session
POST /api/portal
Customer billing portal
POST /api/webhooks/stripe
Handle Stripe events
端点用途
POST /api/checkout
创建结账会话
POST /api/portal
客户账单门户
POST /api/webhooks/stripe
处理Stripe事件

Environment Variables

环境变量

bash
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_PUBLISHABLE_KEY=pk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx
bash
STRIPE_SECRET_KEY=sk_test_xxx
STRIPE_PUBLISHABLE_KEY=pk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx