
September 09, 2026
12 min read
By Kokil Thapa | Last reviewed: September 2026
Recurring revenue dies quietly when billing is bolted on after launch. A proper Stripe Subscriptions for SaaS Complete Setup wires products, Checkout, webhooks, and your app database into one reliable loop before the first paying customer arrives. I've shipped subscription flows on Laravel applications for client portals and eCommerce systems, and the failures are almost never Stripe API syntax. They are missing webhook handlers, stale local subscription state, and no plan for failed payments. This guide walks through a production-grade path using Laravel 13, PHP 8.3+, and patterns that also apply if you run Symfony or plain PHP. For a Laravel-specific deep dive, see the companion post on Laravel Stripe integration for subscriptions.
What do you need before setting up Stripe subscriptions for SaaS?
Start with billing model clarity. Monthly vs annual, trial length, seat-based vs flat pricing, and whether upgrades prorate matter before you touch the Stripe Dashboard. Read SaaS pricing models explained if those choices are still open. Stripe maps every charge to a Product and one or more recurring Prices.
Your stack checklist for 2026:
- Stripe account in live mode only after test mode passes every webhook path.
- Laravel 13.x on PHP 8.3 or higher (PHP 8.5 is the current anchor; Laravel 12 still works on PHP 8.2 until you upgrade).
- MySQL 9.7 or PostgreSQL 18 for local subscription mirrors.
- Redis 8.10 for queues that process webhooks without blocking HTTP responses.
- HTTPS on your webhook URL — see HTTPS setup with Let's Encrypt and Certbot.
- Composer 2.10 and
stripe/stripe-phpor Laravel Cashier.
Environment variables belong in .env, never in git:
STRIPE_KEY=pk_test_...
STRIPE_SECRET=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
CASHIER_CURRENCY=usd
CASHIER_CURRENCY_LOCALE=en_US Enable Stripe Tax or handle VAT manually if you sell across borders. Nepal-based SaaS founders often price in USD for global buyers and NPR for local ones. Use a Nepal forex rates tool for internal reporting, but let Stripe charge in one settlement currency per Price to avoid reconciliation pain.
Register your business details in Stripe before live charges. For a greenfield SaaS, pick a stack deliberately — choosing a tech stack for a new SaaS helps you avoid rewriting billing in six months.
How do you create Stripe products and prices for a SaaS billing model?
Every subscription line item needs a Product (what you sell) and a Price (how much and how often). Create them in the Dashboard or via API. Naming matters: use internal slugs like pro_monthly that match your application config.
Dashboard setup
- Open Stripe Dashboard → Product catalog → Add product.
- Set name, description, and optional tax code.
- Add a recurring Price: amount, currency, interval (month/year).
- Copy the Price ID (
price_...) into your app config or database. - Repeat for each tier and billing interval you offer.
API setup (repeatable across environments)
// Stripe PHP SDK via composer require stripe/stripe-php
\Stripe\Stripe::setApiKey(config('services.stripe.secret'));
$product = \Stripe\Product::create([
'name' => 'Pro Plan',
'metadata' => ['plan_slug' => 'pro'],
]);
$price = \Stripe\Price::create([
'product' => $product->id,
'unit_amount' => 2900,
'currency' => 'usd',
'recurring' => ['interval' => 'month'],
'lookup_key' => 'pro_monthly',
]); Use lookup_key so staging and production can reference the same logical plan without hard-coding Price IDs. Stripe documents this pattern in their Billing subscriptions overview.
Common SaaS Price patterns:
| Model | Stripe config | Best for |
|---|---|---|
| Flat monthly | One Price, quantity 1 | Simple tools, solo founders |
| Per-seat | One Price, quantity = seat count | Team collaboration SaaS |
| Annual discount | Two Prices on same Product | Lower churn, better cash flow |
| Free trial | trial_period_days on Subscription | PLG products with upgrade path |
| Metered usage | Metered Price + Usage Records | API platforms, overage billing |
Annual plans at roughly ten months' price often convert well. Model the break-even with a Nepal SIP calculator mindset: small monthly savings compound for the buyer and improve your LTV.
How do you integrate Stripe Checkout with Laravel for SaaS subscriptions?
Laravel Cashier wraps the Stripe PHP SDK and handles Customer creation, Checkout Sessions, and subscription Eloquent models. Install it on Laravel 13:
composer require laravel/cashier
php artisan vendor:publish --tag=cashier-migrations
php artisan migrate Add the Billable trait to your User model. Store plans in config or a plans table. Never expose secret keys to the browser.
Create a Checkout Session
// routes/web.php
Route::middleware('auth')->post('/subscribe/{plan}', function (string $plan) {
$priceId = config("plans.{$plan}.stripe_price_id");
return auth()->user()
->newSubscription('default', $priceId)
->trialDays(14)
->allowPromotionCodes()
->checkout([
'success_url' => route('billing.success') . '?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('pricing'),
]);
}); The success page should show a "processing" state until webhooks confirm the subscription. On a legal-tech portal I built, we showed document upload only after invoice.paid fired — not when the user landed on /billing/success. That pattern matches how Mijar Law Associates client portal gates paid features.
Gate features in middleware
// app/Http/Middleware/EnsureSubscribed.php
public function handle(Request $request, Closure $next, string $plan = 'default')
{
$user = $request->user();
if (! $user || ! $user->subscribed($plan)) {
return redirect()->route('pricing')
->with('error', 'An active subscription is required.');
}
return $next($request);
} Apply the middleware to routes that serve premium API endpoints, exports, or team seats. Pair subscription checks with Laravel two-factor authentication on account settings where billing changes happen.
For custom billing UI without Checkout, use the Payment Element and confirm a Subscription server-side. Checkout is faster to ship and PCI scope stays smaller. Most SaaS MVPs should start there. Need help wiring the API layer? See API development services.
How should you handle Stripe webhooks for subscription lifecycle events?
Webhooks are the source of truth for subscription state. HTTP redirects lie. Cron polling misses edge cases. Stripe retries failed webhook deliveries for up to three days.
Minimum event list
checkout.session.completed— link Customer to User if not done yet.customer.subscription.created— initial row in local subscriptions table.customer.subscription.updated— plan changes, cancel_at_period_end, status shifts.customer.subscription.deleted— revoke access after period ends.invoice.paid— extend service period, send receipt email.invoice.payment_failed— trigger dunning email, show in-app banner.
Register the endpoint in Stripe Dashboard → Developers → Webhooks. Use a dedicated route excluded from CSRF:
// bootstrap/app.php (Laravel 13)
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: [
'stripe/webhook',
]);
}) // Cashier registers Route::post('/stripe/webhook', ...) automatically.
// Verify signature manually if you roll your own handler:
$payload = @file_get_contents('php://input');
$sig = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';
$event = \Stripe\Webhook::constructEvent(
$payload,
$sig,
config('services.stripe.webhook_secret')
); Queue every webhook handler. Return 200 quickly. Process business logic in a job on Redis — same pattern as Laravel queues with Redis in production. Official reference: Stripe webhooks documentation.
Local database schema
Cashier migrations cover most fields. Add indexes your reports need:
Schema::table('subscriptions', function (Blueprint $table) {
$table->index(['user_id', 'stripe_status']);
$table->index('stripe_id');
}); Mirror stripe_status values: active, trialing, past_due, canceled, unpaid. Your app should treat past_due as grace-period read-only, not instant lockout, unless your terms say otherwise.
What billing edge cases break Stripe subscriptions in production?
Test mode passes. Live mode exposes gaps. These failures show up repeatedly on production Laravel applications.
Failed payments and dunning
Stripe Smart Retries re-attempts cards automatically. Configure Customer Portal so users update payment methods without a support ticket. Send email when invoice.payment_failed fires. Schedule a daily command via Laravel scheduled tasks to flag accounts still past due after seven days.
Proration on upgrade and downgrade
Upgrading mid-cycle creates proration line items. Downgrades often apply at period end unless you pass proration_behavior. Document this on your pricing page to cut chargeback disputes.
Cancel at period end vs immediate
cancel_at_period_end = true keeps access until the paid window closes. Immediate cancel refunds may be required by local consumer rules — verify before selling in the EU or Australia.
Webhook replay and race conditions
Users can hit success URL before webhook delivery. Show a spinner and poll local subscription status, or use Echo broadcasting — see Laravel broadcasting with Reverb for real-time UI updates.
Security and compliance
Rotate webhook secrets after staff changes. Run dependency vulnerability scanning on stripe/stripe-php. Log billing events without storing full card numbers — Stripe handles PAN data.
Enable Stripe Customer Portal for self-serve plan changes:
// BillingController.php
public function portal(Request $request)
{
return $request->user()->redirectToBillingPortal(
route('dashboard')
);
} Track conversion events in your analytics stack — analytics setup for a new SaaS product pairs well with Stripe metadata on Checkout Sessions (client_reference_id, UTM fields).
After launch, treat billing like any production subsystem. Monitor webhook failure rates in Stripe Dashboard. Alert on queue backlog. Include subscription renewal dates in your runbook alongside backups from automated server backups complete setup.
If you also run a WooCommerce or Shopify storefront, subscription logic differs — compare WooCommerce subscription setup and Shopify subscription app development. Custom Laravel SaaS billing sits closer to custom software development than off-the-shelf plugins.
For eCommerce with one-time and recurring mixes, study how Quick And Easy Nepalese Grocery handles zone-based logic — subscription SaaS adds billing state on top of similar entitlement patterns. Ongoing fixes belong in support and maintenance scope once paying users depend on the flow.
Before going live, run through this checklist:
- Create test Products and Prices in Stripe test mode.
- Complete Checkout with test card
4242 4242 4242 4242. - Confirm webhook delivery with Stripe CLI:
stripe listen --forward-to localhost:8000/stripe/webhook. - Simulate
invoice.payment_failedusing Stripe test clocks. - Verify cancel-at-period-end preserves access until expiry.
- Swap to live keys, create live Prices, update config, smoke-test one real charge.
- Enable Stripe billing emails and your app notification copies.
Read how to start a subscription-based online business for positioning and ops context beyond code. Load-test webhook throughput under testing and optimization before a marketing push sends traffic spikes.
Stripe's Subscriptions API reference is the final authority on parameter names. Cashier abstracts much of it, but knowing the raw objects helps when you debug odd invoice line items at 11 PM.
Key Takeaways
- Create Stripe Products and recurring Prices with lookup keys before writing application code.
- Use Checkout for faster PCI-safe launch; grant access only after webhook confirmation, not on success URL alone.
- Queue webhook handlers on Redis and deduplicate with a stripe_event_id column.
- Map stripe_status to clear app rules: trialing and active get full access, past_due gets grace, canceled gets blocked.
- Enable Customer Portal, Smart Retries, and billing emails before your first live subscriber.
- Test failed payments, proration, and cancel-at-period-end in Stripe test mode with test clocks.
People Also Ask
Is Stripe Checkout enough for SaaS subscriptions?
Yes, for most MVPs. Checkout hosts payment UI, handles SCA, and creates Subscriptions in one session. You add webhooks and middleware gates in your app. Custom Payment Elements make sense when you need embedded pricing tables inside a complex onboarding wizard.
What is the difference between Stripe Customer Portal and Billing Portal?
Stripe renamed and unified self-serve billing under Customer Portal. It lets subscribers update cards, cancel, and switch plans you allow. Configure allowed products and proration behavior in Dashboard → Settings → Customer portal.
How do free trials work with Stripe subscriptions?
Pass trial_period_days when creating the Subscription or use Cashier's trialDays(). Stripe creates a trialing subscription with no immediate charge. When the trial ends, Stripe invoices automatically. Handle customer.subscription.trial_will_end to send reminder emails three days before conversion.
Can Nepal-based SaaS companies use Stripe for subscriptions?
Stripe availability depends on your business registration country and Stripe's supported regions. Many Nepal-founded SaaS products serve global customers through a US or Singapore entity while pricing displays USD. Confirm current country support in Stripe's official docs before building checkout flows around it.
Ship billing before marketing scales
A complete Stripe Subscriptions for SaaS Complete Setup is Products, Checkout, webhook-synced local state, and explicit rules for trials, failures, and cancellations. Stripe moves money reliably once you respect its event-driven model. Your app stays trustworthy when access always follows webhook-confirmed status, not optimistic redirects. If you want this wired into a Laravel SaaS or client portal without billing surprises in production, contact us or explore e-commerce and subscription development services to scope the build.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

