关于 stripe
Stripe API 模拟器,为使用 Stripe SDK 的开发者提供有状态的本地测试环境。它在内存中保存支付相关实体,支持 Webhook 和托管结账页面,且不会处理真实付款。
主要功能
- 01模拟 Stripe API 调用与响应
- 02在内存中保存客户和支付实体
- 03状态变化时触发 Webhook
- 04提供托管结账页面
- 05支持种子数据配置
适用场景
- 01本地测试 Stripe SDK 集成
- 02验证结账与支付意图流程
- 03测试 Webhook 处理逻辑
- 04为测试预置确定性实体
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vercel-labs/ emulate --skill stripe安装后在 Claude Code 中输入/stripe确认已加载。
详细介绍
Stripe API 模拟器
完全有状态的 Stripe API 模拟。客户、产品、价格、结账会话、支付意图、扣款和支付方式都会保存在内存中。状态变更时会触发 Webhook。托管结账界面可让你在浏览器中完成支付。
不会处理真实支付。每次 Stripe SDK 调用都会发送到模拟器,并返回逼真的响应。
启动
本地监听器默认绑定到 127.0.0.1。若要从容器或其他机器访问,请向 CLI 传入 --host 0.0.0.0,或向 createEmulator 传入 hostname: '0.0.0.0'。配置 --base-url 或 baseUrl,以设置这些客户端可访问的对外 URL。
# Stripe only
npx emulate --service stripe
# Default port (when run alone)
# http://localhost:4000
也可以通过编程方式启动:
import { createEmulator } from 'emulate'
const stripe = await createEmulator({ service: 'stripe', port: 4000 })
// stripe.url === 'http://localhost:4000'
将应用指向模拟器
Stripe SDK
Stripe Node.js SDK 不会读取用于设置基础 URL 的环境变量。你必须在构造客户端时传入该 URL:
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-12-18.acacia',
host: 'localhost',
port: 4000,
protocol: 'http',
})
在 Next.js 中嵌入(adapter-next)
使用 @emulators/adapter-next 时,模拟器会在 Next.js 应用内的 /emulate/stripe 路径运行。SDK 需要指向 localhost,并通过代理路由将 /v1/* 请求转发到 /emulate/stripe/v1/*:
// next.config.ts
import { withEmulate } from '@emulators/adapter-next'
export default withEmulate({
env: {
STRIPE_SECRET_KEY: 'sk_test_emulated',
},
})
// lib/stripe.ts
import Stripe from 'stripe'
const port = process.env.PORT ?? '3000'
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-12-18.acacia',
host: 'localhost',
port: parseInt(port, 10),
protocol: 'http',
})
// app/emulate/[...path]/route.ts
import { createEmulateHandler } from '@emulators/adapter-next'
import * as stripe from '@emulators/stripe'
export const { GET, POST, PUT, PATCH, DELETE } = createEmulateHandler({
services: {
stripe: {
emulator: stripe,
seed: {
products: [
{ id: 'prod_widget', name: 'Widget', description: 'A useful widget' },
],
prices: [
{ id: 'price_widget', product_name: 'Widget', currency: 'usd', unit_amount: 1000 },
],
webhooks: [
{
url: `http://localhost:${process.env.PORT ?? '3000'}/api/webhooks/stripe`,
events: ['*'],
},
],
},
},
},
})
// app/v1/[...path]/route.ts (proxy for Stripe SDK)
const STRIPE_URL = `http://localhost:${process.env.PORT ?? '3000'}/emulate/stripe`
async function handler(req: Request, ctx: { params: Promise<{ path: string[] }> }) {
const { path } = await ctx.params
const url = new URL(req.url)
const target = `${STRIPE_URL}/v1/${path.join('/')}${url.search}`
const res = await fetch(target, {
method: req.method,
headers: req.headers,
body: req.body,
duplex: 'half',
} as any)
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: res.headers,
})
}
export { handler as GET, handler as POST, handler as PUT, handler as PATCH, handler as DELETE }
直接 fetch
curl http://localhost:4000/v1/customers \
-H "Authorization: Bearer sk_test_emulated"
种子配置
种子数据为可选项。所有实体都支持可选的 id 字段,以便使用可在服务重启后保持不变的确定性 ID。
stripe:
customers:
- id: cus_demo
email: [email protected]
name: Demo User
products:
- id: prod_tshirt
name: T-Shirt
description: A comfortable tee
prices:
- id: price_tshirt
product_name: T-Shirt
currency: usd
unit_amount: 2500
webhooks:
- url: http://localhost:3000/api/webhooks/stripe
events: ['*']
secret: whsec_test
价格中的 product_name 字段会根据名称关联到产品。使用 events: ['*'] 接收所有 Webhook 事件,或指定单个事件类型。
API 端点
客户
# Create customer
curl -X POST http://localhost:4000/v1/customers \
-d "[email protected]" -d "name=Jane Doe"
# Retrieve customer
curl http://localhost:4000/v1/customers/cus_xxx
# Update customer
curl -X POST http://localhost:4000/v1/customers/cus_xxx \
-d "name=Updated Name"
# Delete customer
curl -X DELETE http://localhost:4000/v1/customers/cus_xxx
# List customers
curl http://localhost:4000/v1/customers
产品
# Create product
curl -X POST http://localhost:4000/v1/products \
-d "name=Widget" -d "description=A useful widget"
# Retrieve product
curl http://localhost:4000/v1/products/prod_xxx
# List products
curl "http://localhost:4000/v1/products?active=true"
价格
# Create price
curl -X POST http://localhost:4000/v1/prices \
-d "product=prod_xxx" -d "currency=usd" -d "unit_amount=1000"
# Retrieve price
curl http://localhost:4000/v1/prices/price_xxx
# List prices
curl "http://localhost:4000/v1/prices?active=true"
结账会话
# Create checkout session
curl -X POST http://localhost:4000/v1/checkout/sessions \
-d "mode=payment" \
-d "line_items[0][price]=price_xxx" \
-d "line_items[0][quantity]=1" \
-d "success_url=http://localhost:3000/success?session_id={CHECKOUT_SESSION_ID}" \
-d "cancel_url=http://localhost:3000/cart"
# Retrieve session
curl http://localhost:4000/v1/checkout/sessions/cs_xxx
# List sessions
curl http://localhost:4000/v1/checkout/sessions
# Expire a session
curl -X POST http://localhost:4000/v1/checkout/sessions/cs_xxx/expire
会话的 url 字段指向 /checkout/cs_xxx 上的托管结账页面。在该页面点击“Pay”即可完成会话、触发 checkout.session.completed Webhook,并重定向到 success_url。success_url 中的 {CHECKOUT_SESSION_ID} 模板会替换为实际会话 ID。
支付意图
# Create payment intent
curl -X POST http://localhost:4000/v1/payment_intents \
-d "amount=2000" -d "currency=usd"
# Retrieve
curl http://localhost:4000/v1/payment_intents/pi_xxx
# Update
curl -X POST http://localhost:4000/v1/payment_intents/pi_xxx \
-d "amount=3000"
# Confirm (triggers payment_intent.succeeded + charge.succeeded webhooks)
curl -X POST http://localhost:4000/v1/payment_intents/pi_xxx/confirm
# Cancel
curl -X POST http://localhost:4000/v1/payment_intents/pi_xxx/cancel
# List
curl http://localhost:4000/v1/payment_intents
扣款
# Retrieve charge
curl http://localhost:4000/v1/charges/ch_xxx
# List charges
curl http://localhost:4000/v1/charges
确认支付意图时会自动创建扣款。
客户会话
# Create customer session
curl -X POST http://localhost:4000/v1/customer_sessions \
-d "customer=cus_xxx"
支付方式
# List payment methods
curl http://localhost:4000/v1/payment_methods
Webhook
状态变更时,模拟器会分发 Webhook 事件。可通过种子配置或编程方式注册 Webhook。
配置了 secret 的 Webhook 会包含 Stripe-Signature: t=<timestamp>,v1=<signature>。签名是对 <timestamp>.<raw body> 计算的 HMAC SHA-256。
分发的事件
| 事件 | 触发条件 |
|---|---|
customer.created | 创建客户 |
customer.updated | 更新客户 |
customer.deleted | 删除客户 |
product.created | 创建产品 |
price.created | 创建价格 |
payment_intent.created | 创建支付意图 |
payment_intent.succeeded | 确认支付意图 |
payment_intent.canceled | 取消支付意图 |
charge.succeeded | 确认支付意图(自动创建扣款) |
checkout.session.completed | 通过托管页面完成结账 |
checkout.session.expired | 结账会话过期 |
Webhook 处理程序示例
// app/api/webhooks/stripe/route.ts
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
const body = await request.json()
const event = body.type as string
const obj = body.data?.object
switch (event) {
case 'customer.created':
console.log('Customer created:', obj.id, obj.email)
break
case 'checkout.session.completed':
console.log('Checkout completed:', obj.id)
break
case 'payment_intent.succeeded':
console.log('Payment succeeded:', obj.id)
break
case 'charge.succeeded':
console.log('Charge succeeded:', obj.id)
break
}
return NextResponse.json({ received: true })
}
常见模式
结账流程(嵌入式 Next.js)
// Server action
const customer = await stripe.customers.create({
email: '[email protected]',
name: 'Demo Shopper',
})
const session = await stripe.checkout.sessions.create({
mode: 'payment',
customer: customer.id,
line_items: [{ price: 'price_widget', quantity: 2 }],
success_url: `${origin}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${origin}/cart`,
})
redirect(session.url!)
在成功页面获取会话
const session = await stripe.checkout.sessions.retrieve(session_id)
const customer = await stripe.customers.retrieve(session.customer as string)
console.log(session.payment_status) // 'paid'
console.log(customer.name) // 'Demo Shopper'
支付意图流程(无结账界面)
const pi = await stripe.paymentIntents.create({
amount: 5000,
currency: 'usd',
customer: 'cus_xxx',
})
// Confirm triggers payment_intent.succeeded + charge.succeeded webhooks
const confirmed = await stripe.paymentIntents.confirm(pi.id)
console.log(confirmed.status) // 'succeeded'
与内置模拟器并用的自定义模拟器
使用 npx emulate init --custom inventory 搭建第三方 API 模拟器并进行测试。在 emulate.config.ts 中使用 emulate 提供的 defineConfig 注册它,同时也可注册 { emulator: "stripe" } 等内置条目。运行 npx emulate start --watch 可重新加载导入项,并在打印出的 /_emulate URL 查看自定义状态。成功重新加载后,本次运行会重置为种子数据。现有的扁平种子配置仍可使用;--config 用于指定文件。有关编写和测试第三方 API 模拟器的信息,请参阅 https://emulate.dev/docs/custom-emulators。