mcpField
S
stripe

stripe

github.com/vercel-labsテスト・品質2.0k273 回インストール1 日前に更新
リポジトリ

stripe について

Stripe SDK 連携をテストする開発者向けの、状態を保持する Stripe API エミュレーターです。決済関連データをメモリーに保存し、Webhook とホスト型チェックアウトを提供しますが、実際の決済は行いません。

主な機能

  1. 01Stripe API 呼び出しと応答のエミュレート
  2. 02顧客や決済データのメモリー保存
  3. 03状態変化に応じた Webhook の送信
  4. 04ホスト型チェックアウトページ
  5. 05シードデータの設定

活用シーン

  1. 01Stripe SDK 連携のローカルテスト
  2. 02チェックアウトと Payment Intent の検証
  3. 03Webhook 処理のテスト
  4. 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 を参照してください。

コメント