When This Rule Applies
- •Implementing checkout or payment flows
- •Setting up subscription billing
- •Handling Stripe webhooks
- •Managing payment methods or refunds
Critical Decision: Payment Intents vs Checkout Sessions
Use Checkout Sessions when:
- •Standard checkout experience is acceptable
- •You want Stripe-hosted payment page
- •Faster time-to-market is priority
Use Payment Intents when:
- •Custom checkout UI required
- •Complex conditional logic during payment
- •Need fine-grained control over flow
⚠️ The Charges API is legacy. Always use Payment Intents for new integrations.
Quick Start
bash
pip install stripe # Python npm install stripe # Node.js
python
import stripe
import os
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
# Create a Checkout Session
session = stripe.checkout.Session.create(
mode="subscription",
line_items=[{"price": "price_xxx", "quantity": 1}],
success_url="https://example.com/success?session_id={CHECKOUT_SESSION_ID}",
cancel_url="https://example.com/cancel",
)
Webhook Handling (CRITICAL)
40% of unprocessed payments come from missing/broken webhook handlers.
Essential Pattern
python
from fastapi import Request, HTTPException
import stripe
WEBHOOK_SECRET = os.environ["STRIPE_WEBHOOK_SECRET"]
@router.post("/webhooks/stripe")
async def stripe_webhook(request: Request):
payload = await request.body()
sig = request.headers.get("stripe-signature")
# 1. ALWAYS verify signature
try:
event = stripe.Webhook.construct_event(payload, sig, WEBHOOK_SECRET)
except stripe.error.SignatureVerificationError:
raise HTTPException(400, "Invalid signature")
# 2. Immediately acknowledge (< 5 seconds!)
# Process async in background
# 3. Handle idempotently (check event.id)
if await event_already_processed(event.id):
return {"received": True}
# 4. Process event
if event.type == "invoice.payment_succeeded":
await handle_payment_success(event.data.object)
elif event.type == "invoice.payment_failed":
await handle_payment_failure(event.data.object)
await mark_event_processed(event.id)
return {"received": True}
Must-Handle Events
- •
invoice.payment_succeeded- Provision access - •
invoice.payment_failed- Notify customer, schedule retry - •
customer.subscription.updated- Handle plan changes - •
customer.subscription.deleted- Revoke access
Common Gotchas
1. Test Keys in Production
python
# ALWAYS validate at startup
if os.environ.get("ENV") == "production":
if os.environ["STRIPE_SECRET_KEY"].startswith("sk_test_"):
raise Exception("Test key in production!")
2. Missing Idempotency Keys (causes duplicate charges!)
python
# ALWAYS use idempotency keys for mutations
import uuid
payment_intent = stripe.PaymentIntents.create(
amount=2000,
currency="usd",
idempotency_key=str(uuid.uuid4()), # Store this with order!
)
3. 5-Second Webhook Timeout
python
# BAD: Sync processing
@router.post("/webhook")
async def webhook(request: Request):
# ... verify ...
await slow_database_operation() # ❌ May timeout!
await send_email() # ❌ May timeout!
return {"received": True}
# GOOD: Async processing
@router.post("/webhook")
async def webhook(request: Request):
# ... verify ...
background_tasks.add_task(process_event, event) # ✅
return {"received": True} # Respond immediately
4. 3D Secure Authentication
When PaymentIntent.status == "requires_action":
- •Return
client_secretto frontend - •Call
stripe.confirmCardPayment(clientSecret) - •Wait for
payment_intent.succeededwebhook
5. Subscription Status Transitions
code
Payment succeeds → subscription: active, invoice: paid Payment fails → subscription: incomplete, invoice: open Needs 3DS → subscription: incomplete, requires_action
Testing with Stripe CLI
bash
# Listen for webhooks locally stripe listen --forward-to localhost:8080/webhooks/stripe # Trigger test events stripe trigger payment_intent.succeeded stripe trigger invoice.payment_failed
Test Card Numbers
- •
4242 4242 4242 4242- Always succeeds - •
4000 0000 0000 0002- Always declines - •
4000 0027 6000 3184- Requires 3D Secure
Security Checklist
- • Webhook signature verification enabled
- • HTTPS endpoints only
- • API keys in environment variables (never in code)
- • Service role key only on server
- • Idempotency keys for all mutations