mirror of
https://github.com/wshobson/agents.git
synced 2026-03-18 09:37:15 +00:00
Remove references to non-existent resource files (references/, assets/, scripts/, examples/) from 115 skill SKILL.md files. These sections pointed to directories and files that were never created, causing confusion when users install skills. Also fix broken Code of Conduct links in issue templates to use absolute GitHub URLs instead of relative paths that 404.
493 lines
14 KiB
Markdown
493 lines
14 KiB
Markdown
---
|
|
name: stripe-integration
|
|
description: Implement Stripe payment processing for robust, PCI-compliant payment flows including checkout, subscriptions, and webhooks. Use when integrating Stripe payments, building subscription systems, or implementing secure checkout flows.
|
|
---
|
|
|
|
# Stripe Integration
|
|
|
|
Master Stripe payment processing integration for robust, PCI-compliant payment flows including checkout, subscriptions, webhooks, and refunds.
|
|
|
|
## When to Use This Skill
|
|
|
|
- Implementing payment processing in web/mobile applications
|
|
- Setting up subscription billing systems
|
|
- Handling one-time payments and recurring charges
|
|
- Processing refunds and disputes
|
|
- Managing customer payment methods
|
|
- Implementing SCA (Strong Customer Authentication) for European payments
|
|
- Building marketplace payment flows with Stripe Connect
|
|
|
|
## Core Concepts
|
|
|
|
### 1. Payment Flows
|
|
|
|
**Checkout Sessions**
|
|
|
|
- Recommended for most integrations
|
|
- Supports all UI paths:
|
|
- Stripe-hosted checkout page
|
|
- Embedded checkout form
|
|
- Custom UI with Elements (Payment Element, Express Checkout Element) using `ui_mode='custom'`
|
|
- Provides built-in checkout capabilities (line items, discounts, tax, shipping, address collection, saved payment methods, and checkout lifecycle events)
|
|
- Lower integration and maintenance burden than Payment Intents
|
|
|
|
**Payment Intents (Bespoke control)**
|
|
|
|
- You calculate the final amount with taxes, discounts, subscriptions, and currency conversion yourself.
|
|
- More complex implementation and long-term maintenance burden
|
|
- Requires Stripe.js for PCI compliance
|
|
|
|
**Setup Intents (Save Payment Methods)**
|
|
|
|
- Collect payment method without charging
|
|
- Used for subscriptions and future payments
|
|
- Requires customer confirmation
|
|
|
|
### 2. Webhooks
|
|
|
|
**Critical Events:**
|
|
|
|
- `payment_intent.succeeded`: Payment completed
|
|
- `payment_intent.payment_failed`: Payment failed
|
|
- `customer.subscription.updated`: Subscription changed
|
|
- `customer.subscription.deleted`: Subscription canceled
|
|
- `charge.refunded`: Refund processed
|
|
- `invoice.payment_succeeded`: Subscription payment successful
|
|
|
|
### 3. Subscriptions
|
|
|
|
**Components:**
|
|
|
|
- **Product**: What you're selling
|
|
- **Price**: How much and how often
|
|
- **Subscription**: Customer's recurring payment
|
|
- **Invoice**: Generated for each billing cycle
|
|
|
|
### 4. Customer Management
|
|
|
|
- Create and manage customer records
|
|
- Store multiple payment methods
|
|
- Track customer metadata
|
|
- Manage billing details
|
|
|
|
## Quick Start
|
|
|
|
```python
|
|
import stripe
|
|
|
|
stripe.api_key = "sk_test_..."
|
|
|
|
# Create a checkout session
|
|
session = stripe.checkout.Session.create(
|
|
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)
|
|
```
|
|
|
|
## Payment Implementation Patterns
|
|
|
|
### Pattern 1: One-Time Payment (Hosted Checkout)
|
|
|
|
```python
|
|
def create_checkout_session(amount, currency='usd'):
|
|
"""Create a one-time payment checkout session."""
|
|
try:
|
|
session = stripe.checkout.Session.create(
|
|
line_items=[{
|
|
'price_data': {
|
|
'currency': currency,
|
|
'product_data': {
|
|
'name': 'Blue T-shirt',
|
|
'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'
|
|
}
|
|
)
|
|
return session
|
|
except stripe.error.StripeError as e:
|
|
# Handle error
|
|
print(f"Stripe error: {e.user_message}")
|
|
raise
|
|
```
|
|
|
|
### Pattern 2: Elements with Checkout Sessions
|
|
|
|
```python
|
|
def create_checkout_session_for_elements(amount, currency='usd'):
|
|
"""Create a checkout session configured for Payment Element."""
|
|
session = stripe.checkout.Session.create(
|
|
mode='payment',
|
|
ui_mode='custom',
|
|
line_items=[{
|
|
'price_data': {
|
|
'currency': currency,
|
|
'product_data': {'name': 'Blue T-shirt'},
|
|
'unit_amount': amount,
|
|
},
|
|
'quantity': 1,
|
|
}],
|
|
return_url='https://yourdomain.com/complete?session_id={CHECKOUT_SESSION_ID}'
|
|
)
|
|
return session.client_secret # Send to frontend
|
|
```
|
|
|
|
```javascript
|
|
const stripe = Stripe("pk_test_...");
|
|
const appearance = { theme: "stripe" };
|
|
|
|
const checkout = stripe.initCheckout({
|
|
clientSecret,
|
|
elementsOptions: { appearance },
|
|
});
|
|
const loadActionsResult = await checkout.loadActions();
|
|
|
|
if (loadActionsResult.type === "success") {
|
|
const { actions } = loadActionsResult;
|
|
const session = actions.getSession();
|
|
|
|
const button = document.getElementById("pay-button");
|
|
const checkoutContainer = document.getElementById("checkout-container");
|
|
const emailInput = document.getElementById("email");
|
|
const emailErrors = document.getElementById("email-errors");
|
|
const errors = document.getElementById("confirm-errors");
|
|
|
|
// Display a formatted string representing the total amount
|
|
checkoutContainer.append(`Total: ${session.total.total.amount}`);
|
|
|
|
// Mount Payment Element
|
|
const paymentElement = checkout.createPaymentElement();
|
|
paymentElement.mount("#payment-element");
|
|
|
|
// Store email for submission
|
|
emailInput.addEventListener("blur", () => {
|
|
actions.updateEmail(emailInput.value).then((result) => {
|
|
if (result.error) emailErrors.textContent = result.error.message;
|
|
});
|
|
});
|
|
|
|
// Handle form submission
|
|
button.addEventListener("click", () => {
|
|
actions.confirm().then((result) => {
|
|
if (result.type === "error") errors.textContent = result.error.message;
|
|
});
|
|
});
|
|
}
|
|
```
|
|
|
|
### Pattern 3: Elements with Payment Intents
|
|
|
|
Pattern 2 (Elements with Checkout Sessions) is Stripe's recommended approach, but you can also use Payment Intents as an alternative.
|
|
|
|
```python
|
|
def create_payment_intent(amount, currency='usd', customer_id=None):
|
|
"""Create a payment intent for bespoke checkout UI with Payment Element."""
|
|
intent = stripe.PaymentIntent.create(
|
|
amount=amount,
|
|
currency=currency,
|
|
customer=customer_id,
|
|
automatic_payment_methods={
|
|
'enabled': True,
|
|
},
|
|
metadata={
|
|
'integration_check': 'accept_a_payment'
|
|
}
|
|
)
|
|
return intent.client_secret # Send to frontend
|
|
```
|
|
|
|
```javascript
|
|
// Mount Payment Element and confirm via Payment Intents
|
|
const stripe = Stripe("pk_test_...");
|
|
const appearance = { theme: "stripe" };
|
|
const elements = stripe.elements({ appearance, clientSecret });
|
|
|
|
const paymentElement = elements.create("payment");
|
|
paymentElement.mount("#payment-element");
|
|
|
|
document.getElementById("pay-button").addEventListener("click", async () => {
|
|
const { error } = await stripe.confirmPayment({
|
|
elements,
|
|
confirmParams: {
|
|
return_url: "https://yourdomain.com/complete",
|
|
},
|
|
});
|
|
|
|
if (error) {
|
|
document.getElementById("errors").textContent = error.message;
|
|
}
|
|
});
|
|
```
|
|
|
|
### Pattern 4: Subscription Creation
|
|
|
|
```python
|
|
def create_subscription(customer_id, price_id):
|
|
"""Create a subscription for a customer."""
|
|
try:
|
|
subscription = stripe.Subscription.create(
|
|
customer=customer_id,
|
|
items=[{'price': price_id}],
|
|
payment_behavior='default_incomplete',
|
|
payment_settings={'save_default_payment_method': 'on_subscription'},
|
|
expand=['latest_invoice.payment_intent'],
|
|
)
|
|
|
|
return {
|
|
'subscription_id': subscription.id,
|
|
'client_secret': subscription.latest_invoice.payment_intent.client_secret
|
|
}
|
|
except stripe.error.StripeError as e:
|
|
print(f"Subscription creation failed: {e}")
|
|
raise
|
|
```
|
|
|
|
### Pattern 5: Customer Portal
|
|
|
|
```python
|
|
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 Handling
|
|
|
|
### Secure Webhook Endpoint
|
|
|
|
```python
|
|
from flask import Flask, request
|
|
import stripe
|
|
|
|
app = Flask(__name__)
|
|
|
|
endpoint_secret = 'whsec_...'
|
|
|
|
@app.route('/webhook', methods=['POST'])
|
|
def webhook():
|
|
payload = request.data
|
|
sig_header = request.headers.get('Stripe-Signature')
|
|
|
|
try:
|
|
event = stripe.Webhook.construct_event(
|
|
payload, sig_header, endpoint_secret
|
|
)
|
|
except ValueError:
|
|
# Invalid payload
|
|
return 'Invalid payload', 400
|
|
except stripe.error.SignatureVerificationError:
|
|
# Invalid signature
|
|
return 'Invalid signature', 400
|
|
|
|
# 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']}")
|
|
```
|
|
|
|
### Webhook Best Practices
|
|
|
|
```python
|
|
import hashlib
|
|
import hmac
|
|
|
|
def verify_webhook_signature(payload, signature, secret):
|
|
"""Manually verify webhook signature."""
|
|
expected_sig = hmac.new(
|
|
secret.encode('utf-8'),
|
|
payload,
|
|
hashlib.sha256
|
|
).hexdigest()
|
|
|
|
return hmac.compare_digest(signature, expected_sig)
|
|
|
|
def handle_webhook_idempotently(event_id, handler):
|
|
"""Ensure webhook is processed exactly once."""
|
|
# Check if event already processed
|
|
if is_event_processed(event_id):
|
|
return
|
|
|
|
# Process event
|
|
try:
|
|
handler()
|
|
mark_event_processed(event_id)
|
|
except Exception as e:
|
|
log_error(e)
|
|
# Stripe will retry failed webhooks
|
|
raise
|
|
```
|
|
|
|
## Customer Management
|
|
|
|
```python
|
|
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
|
|
```
|
|
|
|
## Refund Handling
|
|
|
|
```python
|
|
def create_refund(payment_intent_id, amount=None, reason=None):
|
|
"""Create a refund."""
|
|
refund_params = {
|
|
'payment_intent': payment_intent_id
|
|
}
|
|
|
|
if amount:
|
|
refund_params['amount'] = amount # Partial refund
|
|
|
|
if reason:
|
|
refund_params['reason'] = reason # 'duplicate', 'fraudulent', 'requested_by_customer'
|
|
|
|
refund = stripe.Refund.create(**refund_params)
|
|
return refund
|
|
|
|
def handle_dispute(charge_id, evidence):
|
|
"""Update dispute with evidence."""
|
|
stripe.Dispute.modify(
|
|
charge_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'),
|
|
}
|
|
)
|
|
```
|
|
|
|
## Testing
|
|
|
|
```python
|
|
# Use test mode keys
|
|
stripe.api_key = "sk_test_..."
|
|
|
|
# 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="test@example.com"
|
|
)
|
|
|
|
# Create payment intent
|
|
intent = stripe.PaymentIntent.create(
|
|
amount=1000,
|
|
automatic_payment_methods={
|
|
'enabled': True
|
|
},
|
|
currency='usd',
|
|
customer=customer.id
|
|
)
|
|
|
|
# Confirm with test card
|
|
confirmed = stripe.PaymentIntent.confirm(
|
|
intent.id,
|
|
payment_method='pm_card_visa' # Test payment method
|
|
)
|
|
|
|
assert confirmed.status == 'succeeded'
|
|
```
|