Stripe Integration for SaaS: The Definitive Guide
Stripe is the right choice for SaaS billing. Here's how to integrate it without the 2am webhook debugging sessions.
Key Takeaway
Stripe billing is more complex than it looks. The hard parts aren't the API calls— they're handling edge cases: failed payments, subscription changes, proration, and keeping your database in sync with Stripe. Use webhooks as your source of truth.
Billing Models for SaaS
1. Flat-Rate Subscriptions
Fixed monthly price per plan (e.g., $29/month Starter, $99/month Pro). Simple to understand, simple to implement.
Best for: Most SaaS products. Start here unless you have a specific reason not to.
2. Per-Seat Pricing
Charge per user (e.g., $10/user/month). Revenue scales with customer size.
Best for: Collaboration tools where value increases with team size (Slack, Notion, Linear).
3. Usage-Based Billing
Charge based on consumption (API calls, storage, compute). Complex to implement but aligns cost with value.
Best for: Infrastructure products, APIs, AI services where usage varies wildly.
Core Stripe Concepts
Customer
Represents a paying entity. Map this to your organization/tenant, not individual users. One customer = one billing relationship.
Product
What you're selling (e.g., "HiveForge Pro Plan"). Products have multiple prices (monthly, annual).
Price
A specific pricing configuration for a product. $99/month and $990/year are two different prices for the same product.
Subscription
The ongoing billing relationship. Has status (active, past_due, canceled), current period, and items (prices).
Webhook Handling
Webhooks are your source of truth. Don't rely on API responses—payments can fail, subscriptions can change, and Stripe is the authority on billing state.
Critical Webhooks to Handle
| Event | Your Action |
|---|---|
| customer.subscription.created | Provision access, update subscription record |
| customer.subscription.updated | Handle plan changes, update features |
| customer.subscription.deleted | Revoke access, downgrade to free |
| invoice.payment_succeeded | Record payment, send receipt |
| invoice.payment_failed | Notify customer, show warning in app |
# Always verify webhook signatures
import stripe
@app.post("/webhooks/stripe")
async def stripe_webhook(request: Request):
payload = await request.body()
sig_header = request.headers.get("stripe-signature")
try:
event = stripe.Webhook.construct_event(
payload, sig_header, STRIPE_WEBHOOK_SECRET
)
except ValueError:
raise HTTPException(400, "Invalid payload")
except stripe.error.SignatureVerificationError:
raise HTTPException(400, "Invalid signature")
# Handle the event
if event["type"] == "customer.subscription.updated":
subscription = event["data"]["object"]
await update_subscription_in_db(subscription)
return {"status": "success"}Common Pitfalls
Not handling webhook retries
Stripe retries failed webhooks. Make your handlers idempotent—processing the same event twice shouldn't break anything.
Trusting client-side success
Never provision access based on Checkout success redirect. Wait for the webhook—the payment might still fail.
Ignoring proration
When customers upgrade mid-cycle, Stripe prorates by default. Make sure your UI explains this or customers will be confused by charges.
Not using Customer Portal
Stripe's hosted Customer Portal handles plan changes, payment method updates, and invoice history. Use it instead of building your own.
Customer Portal
Stripe's Customer Portal is a hosted page where customers can:
- Update payment methods
- View and download invoices
- Change subscription plans
- Cancel subscriptions
It's free, PCI compliant, and saves you weeks of development. Create a portal session and redirect the user:
session = stripe.billing_portal.Session.create(
customer=customer_id,
return_url="https://yourapp.com/settings/billing"
)
# Redirect user to session.urlSkip the Stripe Integration Headaches
HiveForge includes production-ready Stripe integration: subscriptions, webhooks, customer portal, and usage-based billing—already tested and working.
Get Started Free