stripe について
Stripe SDK 連携をテストする開発者向けの、状態を保持する Stripe API エミュレーターです。決済関連データをメモリーに保存し、Webhook とホスト型チェックアウトを提供しますが、実際の決済は行いません。
主な機能
- 01Stripe API 呼び出しと応答のエミュレート
- 02顧客や決済データのメモリー保存
- 03状態変化に応じた Webhook の送信
- 04ホスト型チェックアウトページ
- 05シードデータの設定
活用シーン
- 01Stripe SDK 連携のローカルテスト
- 02チェックアウトと Payment Intent の検証
- 03Webhook 処理のテスト
- 04テスト用エンティティの事前登録
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ vercel-labs/ emulate --skill stripeインストール後、Claude Code で/stripeを入力して読み込まれたか確認します。
詳細
Stripe API エミュレーター
状態を完全に保持する Stripe API エミュレーションです。顧客、商品、価格、チェックアウトセッション、支払いインテント、請求、支払い方法はメモリ内に保存されます。状態が変化すると Webhook が発火します。ホスト型チェックアウト UI を使って、ブラウザーで支払いを完了できます。
実際の支払いは処理されません。Stripe SDK のすべての呼び出しはエミュレーターに送られ、実際に即したレスポンスが返されます。
起動
ローカルのリスナーは、デフォルトで 127.0.0.1 にバインドします。コンテナーやほかのマシンからアクセスするには、CLI に --host 0.0.0.0 を渡すか、createEmulator に hostname: '0.0.0.0' を指定します。それらのクライアントからアクセスできる URL を通知するには、--base-url または baseUrl を設定します。
# 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 用の環境変数を読み取りません。クライアントの構築時に渡す必要があります。
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 フィールドは、商品名を使って商品と紐付けます。すべての Webhook イベントを受信するには events: ['*'] を使用し、個別のイベントタイプを指定することもできます。
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'
支払いインテントフロー(チェックアウト UI なし)
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 エミュレーターのひな形を作成し、テストします。組み込みの { emulator: "stripe" } などのエントリーと並べて、emulate の defineConfig を使い、emulate.config.ts に登録します。npx emulate start --watch を実行すると、インポートが再読み込みされ、表示された /_emulate URL でカスタムの状態を確認できます。再読み込みが成功すると、実行状態はシードにリセットされます。既存のフラット形式のシード設定も引き続き使用できます。--config で明示的な設定ファイルを選択します。サードパーティ API エミュレーターの作成とテストについては、https://emulate.dev/docs/custom-emulators を参照してください。