Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Stripe Subscriptions for SaaS Complete Setup

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-php or 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.

SaaS Subscription ArchitectureLaravel AppAuth + gatesStripeProducts + subsLocal DBcustomer_idsubscription rowAPIsyncCheckoutHosted payment UIWebhooksAsync eventsRule: Stripe owns money; your DB owns feature access
Stripe Subscriptions for SaaS complete setup — app, Stripe, Checkout, webhooks, and local subscription mirror

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

  1. Open Stripe Dashboard → Product catalog → Add product.
  2. Set name, description, and optional tax code.
  3. Add a recurring Price: amount, currency, interval (month/year).
  4. Copy the Price ID (price_...) into your app config or database.
  5. 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:

ModelStripe configBest for
Flat monthlyOne Price, quantity 1Simple tools, solo founders
Per-seatOne Price, quantity = seat countTeam collaboration SaaS
Annual discountTwo Prices on same ProductLower churn, better cash flow
Free trialtrial_period_days on SubscriptionPLG products with upgrade path
Metered usageMetered Price + Usage RecordsAPI 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.

Checkout Subscription FlowPricingYour appCheckoutStripe subWebhookSuccess URL is UX onlyGrant access when webhook confirms payment
Hosted Checkout flow for Stripe Subscriptions for SaaS — never trust the success redirect alone

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.

Webhook Processing PipelineStripe POSTVerifysignatureRedis queueJob workeridempotentIdempotency tablestripe_event_id UNIQUE — skip duplicatesReturn 200 even on replay
Production webhook pipeline for Stripe Subscriptions for SaaS — verify, queue, idempotent writes

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.

Feature Access Decision TreeUser requestactive?Grant accesstrialing?Grant accesspast_due?Read-onlycanceled?Block accessAlways read local DB status synced from webhooks — not Stripe API per request
Gate SaaS features from webhook-synced subscription status — active, trialing, past_due, canceled

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:

  1. Create test Products and Prices in Stripe test mode.
  2. Complete Checkout with test card 4242 4242 4242 4242.
  3. Confirm webhook delivery with Stripe CLI: stripe listen --forward-to localhost:8000/stripe/webhook.
  4. Simulate invoice.payment_failed using Stripe test clocks.
  5. Verify cancel-at-period-end preserves access until expiry.
  6. Swap to live keys, create live Prices, update config, smoke-test one real charge.
  7. 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

It means creating Stripe Products and recurring Prices, sending users through Checkout or Customer Portal, storing stripe_customer_id locally, and processing invoice.paid, customer.subscription.updated, and customer.subscription.deleted webhooks so app access stays synced with billing.

Decide your billing model first: monthly versus annual, trial length, seat-based versus flat pricing, and whether mid-cycle upgrades prorate. Stripe maps every charge to a Product and recurring Prices. Your 2026 stack checklist includes a Stripe test account, Laravel 13.x on PHP 8.3 or higher, MySQL 9.7 or PostgreSQL 18, Redis 8.10 for webhook queues, HTTPS on your webhook URL, Composer 2.10, and stripe/stripe-php or Laravel Cashier. Register business details before live charges, and keep STRIPE_KEY, STRIPE_SECRET, and STRIPE_WEBHOOK_SECRET in .env only.

Every subscription needs a Product (what you sell) and a Price (amount and interval). Create them in the Stripe Dashboard under Product catalog, or via the Stripe PHP SDK with Product::create and Price::create. Name internal slugs like pro_monthly to match app config. Set unit_amount in cents, currency, and recurring interval. Copy Price IDs into config, or use lookup_key so staging and production reference the same logical plan without hard-coding IDs. Common patterns include flat monthly (quantity 1), per-seat (quantity equals seats), annual discount (two Prices on one Product), free trials via trial_period_days, and metered usage with Usage Records.

Install Laravel Cashier with composer require laravel/cashier, publish migrations, migrate, and add the Billable trait to your User model. Store plan Price IDs in config or a plans table. Create Checkout Sessions via newSubscription('default', $priceId) with optional trialDays() and allowPromotionCodes(), pointing success_url and cancel_url to your routes. Never expose secret keys to the browser. Gate premium routes with EnsureSubscribed middleware calling $user->subscribed('default'). Checkout keeps PCI scope smaller than a custom Payment Element UI, which suits most SaaS MVPs.

Webhooks are the source of truth; HTTP redirects and cron polling miss edge cases. Register a dedicated endpoint excluded from CSRF at stripe/webhook. Cashier registers this automatically, or verify signatures manually with Webhook::constructEvent and your whsec secret. Handle checkout.session.completed, customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, invoice.paid, and invoice.payment_failed. Queue every handler on Redis, return 200 quickly, and deduplicate with a stripe_event_id column. Stripe retries failed deliveries for up to three days.

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.

Stripe renamed and unified self-serve billing under Customer Portal. It lets subscribers update cards, cancel, and switch allowed plans.

Pass trial_period_days when creating the Subscription, or use Cashier's trialDays() on newSubscription(). Stripe creates a trialing subscription with no immediate charge. When the trial ends, Stripe invoices automatically. Mirror trialing status locally via customer.subscription.updated webhooks. Handle customer.subscription.trial_will_end to send reminder emails three days before conversion. Grant feature access during trialing the same as active, unless your terms restrict it. Never rely on the Checkout success page alone to confirm trial activation.

Users can reach the success URL before webhook delivery, creating a race condition where your app shows paid access before Stripe confirms the subscription. HTTP redirects lie; webhooks are authoritative. Show a processing state on the billing success page and grant premium features only after invoice.paid or customer.subscription.created updates your local subscriptions table. On a legal-tech portal I built, document upload unlocked only after invoice.paid fired, not when the user landed on /billing/success. Poll local subscription status or use Laravel broadcasting for real-time UI updates.

Laravel Cashier migrations cover most fields: users get stripe_id, subscriptions table stores stripe_id, stripe_status, stripe_price, quantity, trial_ends_at, and ends_at. Add indexes your reports need on user_id plus stripe_status and on stripe_id. Mirror Stripe status values locally: active, trialing, past_due, canceled, and unpaid. Store stripe_customer_id on the user record. Optionally track stripe_event_id on a processed_webhooks table for idempotent webhook handling. Cashier's subscription Eloquent model maps directly to these columns for $user->subscribed() checks.

Use middleware like EnsureSubscribed that calls $user->subscribed('default') and redirects unpaid users to pricing. Apply it to premium API endpoints, exports, and team seat routes. Map stripe_status to clear app rules: trialing and active get full access, past_due gets a grace-period read-only mode unless your terms require instant lockout, and canceled blocks access after the paid period ends. Pair subscription checks with two-factor authentication on account settings where billing changes happen. Customer Portal handles self-serve plan switches without custom UI.

Test mode passes; live mode exposes gaps I've seen repeatedly. Failed payments need Smart Retries plus invoice.payment_failed emails and a daily scheduled command flagging accounts past due after seven days. Upgrading mid-cycle creates proration line items; downgrades often apply at period end unless you set proration_behavior. Document both on your pricing page. cancel_at_period_end keeps access until the paid window closes; immediate cancel may require refunds under EU or Australian consumer rules. Webhook replay and success-URL race conditions cause users to see locked features right after paying.

Enable Stripe Smart Retries so cards re-attempt automatically. Configure Customer Portal so users update payment methods without a support ticket. Send email when invoice.payment_failed fires and show an in-app banner. Schedule a daily Laravel command to flag accounts still past_due after seven days. Treat past_due as grace-period read-only in your app, not instant lockout, unless your terms say otherwise. Test failure paths in Stripe test mode using test clocks before going live. Monitor webhook failure rates in the Stripe Dashboard after launch.

Store these in .env, never in git: STRIPE_KEY with your publishable key (pk_test or pk_live), STRIPE_SECRET with your secret key (sk_test or sk_live), STRIPE_WEBHOOK_SECRET with the whsec value from your webhook endpoint, CASHIER_CURRENCY such as usd, and CASHIER_CURRENCY_LOCALE such as en_US. Swap test keys for live keys only after every webhook path passes in test mode. Rotate webhook secrets after staff changes. Nepal-based SaaS founders often price in USD for global buyers; charge in one settlement currency per Price to avoid reconciliation pain.

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_failed using Stripe test clocks. Verify cancel-at-period-end preserves access until expiry. Enable Stripe billing emails, Customer Portal, and Smart Retries. Swap to live keys, create live Prices, update config, and smoke-test one real charge. Load-test webhook throughput before marketing pushes traffic spikes. Monitor queue backlog and webhook failure rates after launch.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: