Launch in Days, Not Weeks
Professional one-page website — only a few slots left this month
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.
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.
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.
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.
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.
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 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.
In Stripe’s data model:
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.
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.
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.
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:
This removes 90% of “how do I cancel?” support tickets.
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.
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.
When a customer buys through your marketplace, you can split the payment:
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.
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.
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.
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.
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.
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.
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:
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.
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:
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