How to Integrate Stripe Billing Into a SaaS Application

A practical guide to wiring Stripe Billing into a SaaS product — covering Checkout, subscriptions, webhooks, the customer portal, plan gating, and the billing edge cases that always catch teams off guard.

By SpiderHunts Technologies  ·  23 May 2026  ·  11 min read

TL;DR

  • Use Stripe Checkout for subscription sign-up — it handles PCI, 3DS, and card validation automatically
  • Sync subscription state to your own database via webhooks — never read it live from Stripe on every request
  • Handle webhooks idempotently — Stripe will send the same event multiple times
  • Use the Stripe Customer Portal for self-serve plan changes and cancellations
  • Test billing edge cases before launch: failed payments, plan upgrades, trial-to-paid, cancellations

The Stripe Billing Architecture

Stripe Billing has four core objects you need to understand before writing any code:

Stripe Object What It Represents In Your Database
Customer A paying entity (org or user) organisations.stripe_customer_id
Product Your plan (Starter, Growth, Pro) Reference only — set up in Stripe Dashboard
Price A specific billing interval + amount (£49/mo) subscriptions.stripe_price_id
Subscription The active recurring billing for a customer subscriptions.stripe_subscription_id

Step 1: Set Up Your Stripe Products and Prices

In the Stripe Dashboard, create your Products (plan tiers) and Prices (billing intervals). Copy the price IDs — you'll reference them throughout your application to identify which plan a customer is on.

# Store these as environment variables
STRIPE_PRICE_STARTER_MONTHLY = "price_xxx"
STRIPE_PRICE_GROWTH_MONTHLY = "price_yyy"
STRIPE_PRICE_PRO_MONTHLY = "price_zzz"
STRIPE_PRICE_GROWTH_ANNUAL = "price_aaa"
STRIPE_PRICE_PRO_ANNUAL = "price_bbb"

Step 2: Create a Stripe Customer on Sign-Up

When a new organisation signs up, immediately create a Stripe Customer and store the ID:

import stripe

stripe.api_key = settings.STRIPE_SECRET_KEY

def create_stripe_customer(organisation):
 customer = stripe.Customer.create(
 email=organisation.billing_email,
 name=organisation.name,
 metadata={"organisation_id": str(organisation.id)}
 )
 organisation.stripe_customer_id = customer.id
 organisation.save()
 return customer

Step 3: Stripe Checkout for Subscription Sign-Up

When a user selects a plan, create a Checkout Session and redirect them to Stripe's hosted page:

def create_checkout_session(organisation, price_id, trial_days=14):
 session = stripe.checkout.Session.create(
 customer=organisation.stripe_customer_id,
 mode="subscription",
 payment_method_types=["card"],
 line_items=[{"price": price_id, "quantity": 1}],
 subscription_data={
 "trial_period_days": trial_days,
 "metadata": {"organisation_id": str(organisation.id)}
 },
 success_url=f"{settings.BASE_URL}/billing/success?session_id={{CHECKOUT_SESSION_ID}}",
 cancel_url=f"{settings.BASE_URL}/billing/plans",
 )
 return session.url # redirect user here

Stripe Checkout handles card validation, 3D Secure authentication, and PCI DSS compliance automatically. Never collect card numbers on your own server unless you have PCI Level 1 compliance.

Step 4: Webhook Handler — Syncing Billing State

Your application must stay in sync with Stripe's billing state via webhooks. This is the most important — and most commonly broken — part of SaaS billing:

@app.post("/webhooks/stripe")
async def stripe_webhook(request: Request):
 payload = await request.body()
 sig_header = request.headers.get("stripe-signature")

 # Verify the webhook signature
 try:
 event = stripe.Webhook.construct_event(
 payload, sig_header, settings.STRIPE_WEBHOOK_SECRET
 )
 except stripe.error.SignatureVerificationError:
 raise HTTPException(status_code=400)

 # Idempotency check — has this event already been processed?
 if WebhookEvent.objects.filter(stripe_event_id=event["id"]).exists():
 return {"status": "already_processed"}

 WebhookEvent.objects.create(stripe_event_id=event["id"])

 event_type = event["type"]

 if event_type in [
 "customer.subscription.created",
 "customer.subscription.updated",
 "customer.subscription.deleted"
 ]:
 subscription = event["data"]["object"]
 sync_subscription_to_db(subscription)

 elif event_type == "invoice.payment_failed":
 handle_payment_failure(event["data"]["object"])

 return {"status": "ok"}

Critical: Always verify the webhook signature using your Stripe Webhook Secret. Never trust incoming webhooks without signature verification — anyone can POST to your webhook endpoint.

Step 5: Plan Gating — Feature Access by Subscription

Gate features based on the subscription synced to your database — never call the Stripe API on every request:

PLAN_FEATURES = {
 "price_starter": ["feature_a", "feature_b"],
 "price_growth": ["feature_a", "feature_b", "feature_c", "feature_d"],
 "price_pro": ["feature_a", "feature_b", "feature_c", "feature_d", "feature_e"],
}

def can_access_feature(organisation, feature_name: str) -> bool:
 sub = organisation.active_subscription
 if not sub or sub.status not in ("active", "trialing"):
 return False
 allowed = PLAN_FEATURES.get(sub.stripe_price_id, [])
 return feature_name in allowed

Step 6: Stripe Customer Portal for Self-Serve Billing

Enable the Stripe Customer Portal in your Dashboard, then generate a portal session when users click "Manage Billing":

def create_billing_portal_session(organisation):
 session = stripe.billing_portal.Session.create(
 customer=organisation.stripe_customer_id,
 return_url=f"{settings.BASE_URL}/settings/billing"
 )
 return session.url # redirect user here

The Customer Portal handles plan upgrades, downgrades, cancellations, payment method updates, and invoice history — all without any additional development on your side. Use it; don't build a custom billing UI from scratch.

Billing Edge Cases to Test Before Launch

Scenario What Must Happen
Trial expires, card charged Status changes from trialing → active; no feature interruption
Trial expires, no card Status → incomplete; redirect to payment on next login
Payment fails Status → past_due; Stripe retries; dunning emails sent
User upgrades plan Proration calculated; new features unlocked immediately
User downgrades plan Takes effect at period end; credit issued; restrict excess usage
User cancels subscription Access until period end; status → canceled at period end
Duplicate webhook received Idempotency check skips re-processing; returns 200

Need Stripe Billing Built Into Your SaaS?

We implement production-ready Stripe billing as part of every SaaS product we build — including subscriptions, webhooks, plan gating, and the customer portal. Fixed-price engagement.

Get a Fixed-Price Quote