Background
Archive
Journal Entry

Stripe Integration Beyond Checkout: Subscriptions, Connect & Billing

Documented
Capacity
10 MIN READ
Domain
E-commerce & SaaS

Most Stripe tutorials stop at checkout. Click a button, redirect to a payment page, money arrives. Job done.

But if you’re building anything more sophisticated than a donation button, like a SaaS product, a subscription service, or a marketplace platform, you need to go deeper. You need webhooks that don’t fail silently, subscription billing that handles upgrades without leaking revenue, and customer portals that let users manage their own accounts without opening support tickets.

We’ve built this infrastructure for clients who need it. CardDeckr, our largest custom build, handles both one-off product sales and recurring subscriptions through a single Stripe integration, backed by over 6,900 automated tests. This article shares the patterns we use when Stripe becomes core platform infrastructure, not just a payment widget.

Why Stripe is the Default Choice for Custom Commerce

When we scope e-commerce builds or SaaS platforms, Stripe is almost always the answer. Not because it’s the only payment processor, but because it’s the only one that scales gracefully from basic checkout to complex billing logic without requiring a platform migration.

Stripe handles complexity you’d otherwise build yourself.

  • Subscription billing with automatic renewals, proration, and dunning
  • Customer portals for self-service subscription management
  • Webhook infrastructure that retries failed events and maintains idempotency
  • PCI compliance handled at the platform level
  • International payments with multi-currency support and local payment methods
  • Connect for marketplace platforms that need to split payments across multiple sellers

You pay for this in fees (1.5% + 20p per transaction in the UK), but you avoid building and maintaining payment infrastructure yourself. For most custom builds, that trade-off is obvious.

Webhook Architecture: Handling Billing Events Reliably

Stripe Checkout works without webhooks. A user clicks “Buy”, completes payment, and gets redirected to a success URL. You can detect that redirect and show a confirmation message.

But that’s not reliable enough for production systems.

What happens if the user closes the browser before the redirect completes? What if the payment succeeds but your server is temporarily down? What if Stripe processes a subscription renewal at 3am when no one’s watching?

Webhooks solve this. Stripe sends you an HTTP POST request every time something important happens: a payment succeeds, a subscription renews, a customer cancels. Your job is to listen for those events and update your database accordingly.

The Three Rules of Webhook Handling

1. Always verify the webhook signature

Stripe signs every webhook request with a secret key. Verify it before processing the event. If you skip this step, anyone can POST fake payment events to your endpoint.

const signature = request.headers.get('stripe-signature');
const event = stripe.webhooks.constructEvent(
  rawBody,
  signature,
  process.env.STRIPE_WEBHOOK_SECRET
);

2. Make every webhook handler idempotent

Stripe retries failed webhooks. If your server returns a 500 error, Stripe sends the same event again. And again. And again.

If your webhook handler isn’t idempotent, meaning it’s not safe to run multiple times with the same input, you’ll create duplicate records, charge customers twice, or corrupt your database.

The simplest pattern: check if you’ve already processed the event ID before doing anything else.

const existingEvent = await db.webhookEvents.findUnique({
  where: { stripeEventId: event.id }
});

if (existingEvent) {
  return { status: 200 }; // Already processed
}

// Process the event, then record it
await processPaymentSucceeded(event.data.object);
await db.webhookEvents.create({
  data: { stripeEventId: event.id, type: event.type }
});

3. Don’t trust event ordering

Stripe doesn’t guarantee webhooks arrive in chronological order. A customer.subscription.updated event might arrive before the customer.subscription.created event that preceded it.

If your logic depends on event order, you’ll hit race conditions. Instead, design your webhook handlers to reconcile state based on the most recent data from Stripe.

When CardDeckr processes a subscription update, it fetches the current subscription status directly from Stripe rather than assuming the webhook payload is the latest truth.

Common Webhook Events to Handle

For a subscription-based platform, these are the critical events:

  • checkout.session.completed means a checkout session succeeded. Create the subscription record.
  • customer.subscription.updated fires when a subscription changes (upgraded, downgraded, cancelled). Update your database.
  • customer.subscription.deleted means the subscription ended. Revoke access.
  • invoice.payment_succeeded confirms a recurring payment succeeded. Extend access period.
  • invoice.payment_failed signals a failed payment. Start dunning process or suspend access.

You don’t need to handle every Stripe event. There are over 100 of them. Focus on the ones that affect your application state.

Stripe Billing for Subscriptions: Plans, Pricing, Trials, Proration

Stripe Checkout handles one-off payments cleanly. But if you’re charging customers monthly, quarterly, or annually, you need Stripe Billing.

Billing is where Stripe gets powerful and complicated in equal measure.

Creating Products and Prices

In Stripe’s data model:

  • A Product is what you’re selling (“Pro Plan”, “Enterprise Access”).
  • A Price is how much you charge for it (“£29/month”, “£299/year”).

You can attach multiple prices to a single product, like monthly and annual billing for the same tier.

For CardDeckr, we defined subscription tiers as products and created both monthly and annual price options for each. Customers can switch between billing intervals without changing their access level.

Handling Trials Without Leaking Revenue

Stripe supports free trials at the subscription level. Set trial_period_days when creating the subscription, and Stripe won’t charge the customer until the trial ends.

But there’s a trap: if a customer upgrades from a free plan to a paid plan mid-trial, Stripe resets the trial unless you explicitly handle proration.

The safe pattern: When creating a subscription with a trial, record the trial end date in your database. When the customer upgrades, cancel the old subscription and create a new one with trial_end set to the original date.

This ensures they don’t get extra free days just because they upgraded.

Proration: Charging Fairly When Plans Change

If a customer upgrades mid-cycle, say switching from a £29/month plan to a £99/month plan on day 15, what should you charge?

Stripe’s proration logic handles this automatically. It calculates the unused value of the old subscription, credits it, and charges the prorated amount for the new plan.

You can customise this behaviour (immediate charge, invoice later, no proration), but the default is usually correct.

For CardDeckr, we let Stripe handle proration automatically. Customers who upgrade mid-cycle pay the difference immediately. Customers who downgrade receive credit applied to their next invoice.

Customer Portal: Self-Service Subscription Management

One of Stripe’s best-kept secrets is the Customer Portal, a hosted interface where customers can manage their own subscriptions without you building a UI.

Enable it in your Stripe dashboard, generate a portal session in your backend, and redirect the customer. They can:

  • Upgrade or downgrade their plan
  • Change payment methods
  • View invoice history
  • Cancel their subscription

This removes 90% of “how do I cancel?” support tickets.

Generating a Portal Session

const session = await stripe.billingPortal.sessions.create({
  customer: customerId,
  return_url: 'https://yourdomain.com/account'
});

// Redirect the customer to session.url

You control what actions are available through Stripe’s dashboard settings. If you don’t want customers to cancel immediately, you can require them to cancel at period end instead.

For platforms that need custom subscription management UI, you’ll build this yourself using Stripe’s API. But for most builds, the hosted portal is faster to ship and more reliable.

Stripe Connect Basics for Marketplace Platforms

If you’re building a marketplace where multiple sellers receive payments, you need Stripe Connect.

Connect lets you create Stripe accounts for your sellers and route payments to them automatically.

There are three integration types:

1. Standard accounts. Each seller creates their own Stripe account. You facilitate payments but don’t hold funds.

2. Express accounts. Stripe-hosted onboarding with your branding. Sellers get a simplified dashboard.

3. Custom accounts. You own the entire experience. Sellers never interact with Stripe directly.

For most marketplaces, Express is the right choice. It balances ease of onboarding with regulatory compliance.

Splitting Payments with Connect

When a customer buys through your marketplace, you can split the payment:

  • Application fee, which is the platform’s commission
  • Transfer, the amount sent to the seller’s connected account

Stripe handles tax calculations, refunds, and disputes across the split.

We haven’t built a Connect integration for CardDeckr since it’s a direct-to-consumer platform, but we’ve architected Connect flows for other clients. The key complexity is handling refunds, chargebacks, and account verification states.

If you’re evaluating Connect, factor in significant development time for edge cases. It’s not a one-afternoon integration.

Common Integration Mistakes and How to Avoid Them

1. Trusting the Checkout Success URL as Confirmation

Never grant access based solely on the success redirect. Always verify payment completion via webhook or API lookup.

Customers can manipulate URLs. Webhooks are cryptographically signed.

2. Storing Stripe Data That Changes

Don’t cache subscription status or payment methods in your database without a sync strategy. Stripe is the source of truth.

If a customer updates their card through the portal, your cached data becomes stale. Fetch fresh data from Stripe when you need it, or keep your local copy synchronised via webhooks.

3. Failing to Handle Webhook Retries

If your webhook handler crashes, Stripe retries for up to three days. If you don’t build idempotency, those retries cause duplicate actions.

Log every webhook event you process. Make handlers safe to run multiple times.

4. Ignoring Test Mode Webhooks in Development

Stripe has separate API keys for test and live modes. Your webhook endpoint receives events for both.

If you don’t filter by mode, a test payment in development might trigger a production action.

Always check event.livemode before processing.

5. Building Custom Billing Logic Too Early

Stripe’s built-in proration, dunning, and trial handling work well. Don’t replace them with custom logic until you’ve proven Stripe’s defaults don’t fit your model.

We’ve seen teams waste weeks building subscription state machines when Stripe would have handled it automatically.

How CardDeckr Uses These Patterns in Production

CardDeckr is a full-stack e-commerce and SaaS platform: a product shop with one-off purchases plus subscription tiers for premium features.

The Stripe integration handles:

  • Checkout for physical trading card products (single payments)
  • Subscription billing for tiered access plans (recurring payments)
  • Customer portal for self-service account management
  • Webhook processing for payment confirmations, subscription updates, and cancellations
  • Automated tests covering payment flows, subscription lifecycle, and edge cases

All of this runs through a single Stripe account with two product types: inventory items and subscription tiers.

The architecture prioritises reliability over speed. Every webhook event is logged. Every subscription state change is verified against Stripe’s API. Every test suite run includes payment flow regression checks.

This is what production-grade Stripe integration looks like. Not a tutorial example, but infrastructure built to handle real revenue.

When to Talk to Us

If you’re building a platform where payments are critical infrastructure and not an afterthought, we can scope and build it from scratch.

We handle:

  • Full Stripe integration for e-commerce, subscriptions, or marketplaces
  • Webhook architecture with idempotency, logging, and error handling
  • Database schema design for products, orders, customers, and billing
  • Admin dashboards for managing platform operations
  • Automated test coverage for payment flows and edge cases

Every build is custom. We don’t use templates or off-the-shelf commerce platforms. If Stripe is part of your product’s core logic, start a conversation and we’ll scope it properly.

Stripe integration isn’t hard. But doing it reliably, handling webhooks, edge cases, subscription lifecycle, and state synchronisation without leaking revenue or creating silent failures, requires discipline and production experience.

We’ve built it before. We’ll build it again.

Say hello

Quick intro