stripe-integration について
Stripe の決済、サブスクリプション、返金、Webhook を実装する開発者向けスキルです。サーバー側の認可、SDK バージョンへの準拠、イベント配信と再試行の検証を重視します。
主な機能
- 01Checkout と Payment Intent のパターン
- 02サブスクリプションと顧客管理
- 03Webhook 署名と重複イベント処理
- 04返金の認可と再試行境界
- 05テストモードでの検証シナリオ
活用シーン
- 01単発または継続課金フローの構築
- 02サーバー側の権限確認を伴う返金処理
- 03Webhook の再試行と重複配信のテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ sickn33/ agentic-awesome-skills --skill stripe-integrationインストール後、Claude Code で/stripe-integrationを入力して読み込まれたか確認します。
詳細
Stripe 統合
サーバー側の明示的な認可とリトライ境界を備えた Stripe チェックアウト、サブスクリプション、Webhook、返金を実装および検証します。
このスキルを使用しない場合
- タスクが Stripe 統合と無関係である
- このスコープ外の別のドメインやツールが必要である
手順
- 目標、制約、必要な入力を明確にします。
- 関連するベストプラクティスを適用し、結果を検証します。
- 実行可能な手順と検証方法を提供します。
- インストールされている Stripe SDK と固定された API/Webhook バージョンを確認します。この単一ファイルのスキルには、バンドルされたプレイブックや本番用ラッパーはありません。
このスキルを使用する場合
- Web/モバイルアプリケーションでの決済処理を実装する
- サブスクリプション課金システムを構築する
- 単発決済と定期課金を処理する
- 返金と異議申し立てを処理する
- 顧客の支払い方法を管理する
- ヨーロッパの決済向けに SCA(Strong Customer Authentication)を実装する
- Stripe Connect を使ったマーケットプレイス決済フローを構築する
中核となる概念
1. 決済フロー
Checkout Session(ホスト型)
- Stripe がホストする決済ページ
- カードデータを直接扱う範囲を削減
- 最速の実装
- 単発決済と定期課金に対応
Payment Intents(カスタム UI)
- 決済 UI を完全に制御
- Stripe.js/Elements を使用して生のカードデータを直接扱うことを回避
- より複雑な実装
- より優れたカスタマイズオプション
Setup Intents(支払い方法の保存)
- 課金せずに支払い方法を収集
- サブスクリプションや将来の決済に使用
- 顧客による確認が必要
2. Webhook
重要なイベント:
payment_intent.succeeded: 決済が完了payment_intent.payment_failed: 決済が失敗customer.subscription.updated: サブスクリプションが変更customer.subscription.deleted: サブスクリプションがキャンセルcharge.refunded: 返金が処理済みinvoice.payment_succeeded: サブスクリプションの支払いが成功
3. サブスクリプション
構成要素:
- Product: 販売しているもの
- Price: いくらで、どのくらいの頻度か
- Subscription: 顧客の定期支払い
- Invoice: 各請求サイクルごとに生成される
4. 顧客管理
- 顧客レコードの作成と管理
- 複数の支払い方法の保存
- 顧客メタデータの追跡
- 請求詳細の管理
入力と安全境界
明示的に認可された Stripe テストアカウント/サンドボックス、サーバーが所有する注文および顧客レコード、インストールされている SDK/API バージョン、想定される Webhook タイプを使用します。金額、通貨、価格/顧客 ID、返金権限は、任意のクライアントパラメータではなく、認証済みのサーバーポリシーから取得する必要があります。Checkout/Elements はカードデータの露出を減らすことができますが、それ自体で PCI 準拠を確立するものではありません。
スニペットは統合のスケッチです。テスト用シークレットキーと Webhook 署名シークレットは別物です。プロジェクトのシークレット機構から読み込み、決して出力しないでください。このスキルを読んだだけでは、本番の決済、返金、顧客更新、アカウント設定のいずれも認可されません。
クイックスタート
import os
import stripe
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
# Create a checkout session
session = stripe.checkout.Session.create(
payment_method_types=['card'],
line_items=[{
'price_data': {
'currency': 'usd',
'product_data': {
'name': 'Premium Subscription',
},
'unit_amount': 2000, # $20.00
'recurring': {
'interval': 'month',
},
},
'quantity': 1,
}],
mode='subscription',
success_url='https://yourdomain.com/success?session_id={CHECKOUT_SESSION_ID}',
cancel_url='https://yourdomain.com/cancel',
)
# Redirect user to session.url
print(session.url)
決済実装パターン
パターン 1: 単発決済(ホスト型 Checkout)
def create_checkout_session(amount, order_attempt_id, currency='usd'):
"""Create a one-time payment checkout session."""
try:
session = stripe.checkout.Session.create(
payment_method_types=['card'],
line_items=[{
'price_data': {
'currency': currency,
'product_data': {
'name': 'Purchase',
'images': ['https://example.com/product.jpg'],
},
'unit_amount': amount, # Amount in cents
},
'quantity': 1,
}],
mode='payment',
success_url='https://yourdomain.com/success?session_id={CHECKOUT_SESSION_ID}',
cancel_url='https://yourdomain.com/cancel',
metadata={
'order_id': 'order_123',
'user_id': 'user_456'
},
idempotency_key=order_attempt_id
)
return session
except stripe.error.StripeError as e:
# Handle error
print(f"Stripe error: {e.user_message}")
raise
パターン 2: カスタム Payment Intent フロー
def create_payment_intent(amount, order_attempt_id, currency='usd', customer_id=None):
"""Create a payment intent for custom checkout UI."""
intent = stripe.PaymentIntent.create(
amount=amount,
currency=currency,
customer=customer_id,
automatic_payment_methods={
'enabled': True,
},
metadata={
'integration_check': 'accept_a_payment'
},
idempotency_key=order_attempt_id
)
return intent.client_secret # Only to the authenticated client for this order; never log it
# Frontend (JavaScript)
"""
const stripe = Stripe('pk_test_...');
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
const {error, paymentIntent} = await stripe.confirmCardPayment(
clientSecret,
{
payment_method: {
card: cardElement,
billing_details: {
name: 'Customer Name'
}
}
}
);
if (error) {
// Handle error
} else if (paymentIntent.status === 'succeeded') {
// Update display only; server fulfillment still verifies payment state
}
"""
パターン 3: サブスクリプション作成の契約
アカウントの固定された API バージョン向けに文書化されたフローを使用します。latest_invoice.payment_intent がすべてのバージョンに存在すると仮定したり、すべての請求書に即座に確認可能な支払いがあると仮定したりしないでください。認可された顧客と許可された価格を解決し、冪等性キーを使用して incomplete なサブスクリプションを作成し、返された確認状態をそのバージョンの API を通じて処理します。検証済みのサブスクリプション/請求書の状態からアクセスを許可し、トライアル、ゼロ金額の請求書、遅延支払い、キャンセル、リトライをテストします。
Stripe サブスクリプション統合を参照してください。以下のカスタマーポータルも、顧客 ID を受け入れる前に所有権チェックを必要とします。
パターン 4: カスタマーポータル
def create_customer_portal_session(customer_id):
"""Create a portal session for customers to manage subscriptions."""
session = stripe.billing_portal.Session.create(
customer=customer_id,
return_url='https://yourdomain.com/account',
)
return session.url # Redirect customer here
Webhook の処理
安全な Webhook エンドポイント
import os
from flask import Flask, request
import stripe
app = Flask(__name__)
endpoint_secret = os.environ["STRIPE_WEBHOOK_SECRET"]
@app.route('/webhook', methods=['POST'])
def webhook():
payload = request.get_data(cache=False) # Exact raw bytes; enforce an ingress body-size limit
sig_header = request.headers.get('Stripe-Signature')
if not sig_header:
return 'Missing signature', 400
try:
event = stripe.Webhook.construct_event(
payload, sig_header, endpoint_secret
)
except ValueError:
# Invalid payload
return 'Invalid payload', 400
except stripe.SignatureVerificationError:
# Invalid signature
return 'Invalid signature', 400
# Integration sketch: durable deduplication/queueing below must precede effects.
# Bind account, mode, order, amount/currency and expected current state first.
# Handle the event
if event['type'] == 'payment_intent.succeeded':
payment_intent = event['data']['object']
handle_successful_payment(payment_intent)
elif event['type'] == 'payment_intent.payment_failed':
payment_intent = event['data']['object']
handle_failed_payment(payment_intent)
elif event['type'] == 'customer.subscription.deleted':
subscription = event['data']['object']
handle_subscription_canceled(subscription)
return 'Success', 200
def handle_successful_payment(payment_intent):
"""Process successful payment."""
customer_id = payment_intent.get('customer')
amount = payment_intent['amount']
metadata = payment_intent.get('metadata', {})
# Update your database
# Send confirmation email
# Fulfill order
print(f"Payment succeeded: {payment_intent['id']}")
def handle_failed_payment(payment_intent):
"""Handle failed payment."""
error = payment_intent.get('last_payment_error', {})
print(f"Payment failed: {error.get('message')}")
# Notify customer
# Update order status
def handle_subscription_canceled(subscription):
"""Handle subscription cancellation."""
customer_id = subscription['customer']
# Update user access
# Send cancellation email
print(f"Subscription canceled: {subscription['id']}")
署名、重複、フルフィルメント
生のバイト列、完全な Stripe-Signature ヘッダー、正しいエンドポイントシークレットを指定して stripe.Webhook.construct_event を使用してください。ボディに対する単なる HMAC では、Stripe のタイムスタンプ付きヘッダー形式やリプレイ許容範囲を実装できません。SDK のタイムスタンプ検証を維持し、無効な署名と古い署名の両方を検証してください。詳細は Stripe webhook ドキュメント を参照してください。
Verify signature and event/account/mode before accepting the event.
Atomically insert event.id into a durable inbox with a unique constraint.
Return 2xx only after durable acceptance; retryable storage failure remains a failure.
A worker reconciles current payment/order state and applies a guarded transition.
Use a unique order/fulfillment key as well: different events can describe the same payment.
Commit the state change and an outbox entry together; downstream effects are idempotent.
Duplicate/concurrent delivery returns the recorded result without fulfilling twice.
チェックしてから処理し、マークを付けるという一連の流れはアトミックではなく、exactly-once の効果を提供しません。イベントは順不同で到着する可能性があります。上記の Flask ハンドラーのサンプルはプレースホルダーであり、完全な耐久性のあるプロセッサではありません。フルフィルメントとしてデプロイしないでください。チェックアウト成功時のリダイレクトは UI 上のシグナルであり、支払いの証明ではありません。フルフィルメントのガイダンス と リクエストの冪等性 を参照してください。
顧客管理
def create_customer(email, name, payment_method_id=None):
"""Create a Stripe customer."""
customer = stripe.Customer.create(
email=email,
name=name,
payment_method=payment_method_id,
invoice_settings={
'default_payment_method': payment_method_id
} if payment_method_id else None,
metadata={
'user_id': '12345'
}
)
return customer
def attach_payment_method(customer_id, payment_method_id):
"""Attach a payment method to a customer."""
stripe.PaymentMethod.attach(
payment_method_id,
customer=customer_id
)
# Set as default
stripe.Customer.modify(
customer_id,
invoice_settings={
'default_payment_method': payment_method_id
}
)
def list_customer_payment_methods(customer_id):
"""List all payment methods for a customer."""
payment_methods = stripe.PaymentMethod.list(
customer=customer_id,
type='card'
)
return payment_methods.data
返金処理
def create_refund(payment_intent_id, refund_attempt_id, amount=None, reason=None):
"""Create a refund."""
refund_params = {
'payment_intent': payment_intent_id
}
if amount is not None:
if type(amount) is not int or amount <= 0:
raise ValueError("Refund amount must be a positive minor-unit integer")
refund_params['amount'] = amount # Partial refund
if reason:
refund_params['reason'] = reason # 'duplicate', 'fraudulent', 'requested_by_customer'
refund = stripe.Refund.create(**refund_params, idempotency_key=refund_attempt_id)
return refund
def handle_dispute(dispute_id, evidence):
"""Update dispute with evidence."""
stripe.Dispute.modify(
dispute_id,
evidence={
'customer_name': evidence.get('customer_name'),
'customer_email_address': evidence.get('customer_email'),
'shipping_documentation': evidence.get('shipping_proof'),
'customer_communication': evidence.get('communication'),
}
)
テスト
# Use test mode keys
import os
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
# Test card numbers
TEST_CARDS = {
'success': '4242424242424242',
'declined': '4000000000000002',
'3d_secure': '4000002500003155',
'insufficient_funds': '4000000000009995'
}
def test_payment_flow():
"""Test complete payment flow."""
# Create test customer
customer = stripe.Customer.create(
email="[email protected]"
)
# Create payment intent
intent = stripe.PaymentIntent.create(
amount=1000,
currency='usd',
customer=customer.id,
payment_method_types=['card']
)
# Confirm with test card
confirmed = stripe.PaymentIntent.confirm(
intent.id,
payment_method='pm_card_visa' # Test payment method
)
assert confirmed.status == 'succeeded'
検証の実施例
承認されたテスト注文に対して、同じ有効な支払いイベントを 2 回、同時に配信し、その後、古い/無効な署名と、古い順不同の状態イベントを配信します。期待される結果: 耐久性のあるフルフィルメントが 1 回、無効な署名は拒否され、新しい状態がロールバックされないこと。インボックスへのコミット前にストレージ障害をシミュレートしてください。イベントを失う成功を返してはなりません。金額ゼロの返金が、黙って全額返金になるのではなく、拒否されることをアサートしてください。
これらはプロジェクトに実装する受け入れチェックです。このスキルは実際の取引結果を記録せず、隠れたクライアントラッパーや追加の参照ファイルも含みません。
ベストプラクティス
- 常に Webhook を使用する: クライアント側の確認だけに依存しないでください
- 冪等性: Webhook イベントを冪等に処理してください
- エラー処理: すべての Stripe エラーを適切に処理してください
- テストモード: 本番環境の前にテストキーで徹底的にテストしてください
- メタデータ: メタデータを使用して Stripe オブジェクトをデータベースにリンクしてください
- 監視: 支払いの成功率とエラーを追跡してください
- PCI コンプライアンス: サーバー上で生のカードデータを絶対に扱わないでください
- SCA 対応: 欧州の支払い向けに 3D セキュアを実装してください
よくある落とし穴
- Webhook を検証しない: 常に Webhook 署名を検証してください
- Webhook イベントの欠落: 関連するすべての Webhook イベントを処理してください
- 金額のハードコード: セント/最小通貨単位を使用してください
- リトライロジックがない: API 呼び出しにリトライを実装してください
- テストモードの無視: テストカードですべてのエッジケースをテストしてください
制限事項
- API バージョン、イベントペイロード、SDK の例外名前空間は異なります。異なるリリースの例を組み合わせるのではなく、インストールされているバージョンを確認してください。
- リクエストの冪等性キーは、同じ論理操作とパラメーターに紐付けられたままにしておく必要があります。リトライごとに新しいキーを使用すると、課金や返金が重複する可能性があります。
- Webhook の重複排除だけでは、異なるイベントによるビジネス上の重複した効果を防ぐことはできません。
- クライアントの成功、テストカードの成功、署名検証は、完全なフルフィルメント、税務、コンプライアンス、サブスクリプションの正確性を確立するものではありません。
- 顧客、支払い方法、紛争、返金の変更には、サーバー側の所有権/ロールのチェックと、明示的なタスク承認が必要です。