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.

Laravel Stripe Integration for Subscriptions

By Kokil Thapa | Last reviewed: September 2026

Recurring revenue breaks fast when billing logic lives in controllers and webhook handlers are an afterthought. A proper Laravel Stripe integration for subscriptions keeps Stripe as the source of truth for plans, invoices, and payment state while your app owns access control. On production Laravel apps I maintain, I treat subscriptions like any other domain workflow: explicit states, idempotent webhooks, and server-side gates—not client-side flags. This guide walks through a complete Cashier-based setup on Laravel 12 or 13 with PHP 8.3+, from first checkout to failed-payment recovery. If you already handle one-time charges, see our Laravel payment integrations overview for how subscriptions fit beside Khalti, eSewa, and PayPal.

What do you need before starting Laravel Stripe Integration for Subscriptions?

Stripe subscriptions are not “add a form and charge monthly.” They are a state machine spanning your database, Stripe’s billing engine, and asynchronous webhook events. Cashier bridges Laravel and Stripe so you do not reimplement proration, trial periods, or invoice sync by hand.

Stack requirements

  • Laravel 12 or 13.x — Laravel 12 needs PHP 8.2+; Laravel 13 needs PHP 8.3+.
  • PHP 8.3 or 8.5 — match your production FPM version before you deploy.
  • Composer 2.10 — run composer require laravel/cashier.
  • A Stripe account with test keys — live keys only after webhook verification passes.
  • HTTPS on production — Stripe Checkout and webhooks require a valid TLS endpoint.

Install Cashier and publish assets

composer require laravel/cashier

php artisan vendor:publish --tag=cashier-migrations
php artisan migrate

php artisan vendor:publish --tag=cashier-config

Add Stripe keys to .env. Never commit live secrets to Git.

STRIPE_KEY=pk_test_...
STRIPE_SECRET=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
CASHIER_CURRENCY=usd
CASHIER_CURRENCY_LOCALE=en_US

For NPR billing, set CASHIER_CURRENCY=npr and create matching Prices in Stripe. Confirm supported presentment currencies in Stripe’s docs before you promise local pricing on a e-commerce Laravel build.

Subscription Billing ArchitectureLaravel AppUser + CashierStripe APIProducts + PricesWebhooksSigned eventsMySQL: users, subscriptions, subscription_itemsLocal mirror of Stripe subscription stateCheckout SessionBrowser redirectCustomer PortalSelf-serve billingMiddleware Gatesubscribed() check
Laravel Stripe integration for subscriptions: Cashier syncs Stripe state into your database while webhooks keep access flags accurate.

Make the User model billable

use Laravel\Cashier\Billable;

class User extends Authenticatable
{
    use Billable;
}

Cashier adds stripe_id, payment method columns, and relationship tables. Your authorization layer should read subscription status from these records, not from session flash data. Pair this with Laravel policies and gates so plan tiers map cleanly to permissions.

How do you create Stripe products and prices for Laravel subscriptions?

Define plans in the Stripe Dashboard or via API first. Each recurring plan needs a Product and a recurring Price ID like price_1ABC.... Store Price IDs in config or a plans database table—never hard-code amounts in Blade templates alone.

Example config-driven plans

// config/plans.php
return [
    'starter' => [
        'name' => 'Starter',
        'stripe_price_id' => env('STRIPE_PRICE_STARTER'),
        'features' => ['5 projects', 'Email support'],
    ],
    'pro' => [
        'name' => 'Pro',
        'stripe_price_id' => env('STRIPE_PRICE_PRO'),
        'features' => ['Unlimited projects', 'Priority support'],
    ],
];

On a client portal like Mijar Law Associates, subscription tiers often gate document storage or case-management modules. Keep feature flags in your app; let Stripe own the money.

Start a subscription via Checkout

Stripe Checkout is the fastest secure path. Cashier builds the session; you redirect the user.

// routes/web.php
Route::middleware('auth')->group(function () {
    Route::post('/subscribe/{plan}', [SubscriptionController::class, 'checkout'])
        ->name('subscribe.checkout');
    Route::get('/billing/success', [SubscriptionController::class, 'success'])
        ->name('billing.success');
    Route::get('/billing/cancel', [SubscriptionController::class, 'cancel'])
        ->name('billing.cancel');
});
// app/Http/Controllers/SubscriptionController.php
public function checkout(Request $request, string $plan)
{
    $priceId = config("plans.{$plan}.stripe_price_id");

    abort_unless($priceId, 404);

    return $request->user()
        ->newSubscription('default', $priceId)
        ->trialDays(14)
        ->allowPromotionCodes()
        ->checkout([
            'success_url' => route('billing.success') . '?session_id={CHECKOUT_SESSION_ID}',
            'cancel_url' => route('billing.cancel'),
        ]);
}

The default subscription name is Cashier’s primary subscription slot. Use named subscriptions only when you genuinely bill multiple independent recurring items per user.

Checkout Subscription Flow1. User clicks2. Cashier session3. Stripe Checkout4. Payment OK5. Webhook: customer.subscription.createdCashier writes subscriptions table row6. Middleware grants access via subscribed()Never trust success_url aloneGotcha: success redirect fires before webhook — gate features on DB state
Stripe Checkout completes in the browser, but Laravel should unlock subscription features only after Cashier processes the webhook.

Gate routes with subscribed middleware

// bootstrap/app.php or RouteServiceProvider
Route::middleware(['auth', 'subscribed'])->group(function () {
    Route::get('/dashboard/premium', [PremiumController::class, 'index']);
});

Cashier ships subscribed, subscribed:{name}, and not_subscribed middleware aliases. Register them if your Laravel version requires explicit middleware mapping.

How do you handle Stripe webhooks in a Laravel subscription app?

Webhooks are not optional for subscriptions. They drive renewals, failed payments, cancellations, and plan changes. Cashier includes a webhook controller; your job is to route Stripe events to it and extend handlers for business logic.

Register the webhook route

// routes/web.php — exclude CSRF for Stripe
Route::post('/stripe/webhook', [
    \Laravel\Cashier\Http\Controllers\WebhookController::class,
    'handleWebhook',
])->name('cashier.webhook');

In Stripe Dashboard → Developers → Webhooks, point to https://yourdomain.com/stripe/webhook. Subscribe at minimum to:

  • customer.subscription.created
  • customer.subscription.updated
  • customer.subscription.deleted
  • invoice.payment_succeeded
  • invoice.payment_failed

Copy the signing secret into STRIPE_WEBHOOK_SECRET. Cashier verifies signatures automatically—do not parse raw POST bodies yourself. See the official Stripe webhooks documentation for retry behaviour and idempotency expectations.

Extend webhook handling for app-specific work

// app/Listeners/StripeEventListener.php
use Laravel\Cashier\Events\WebhookReceived;

class StripeEventListener
{
    public function handle(WebhookReceived $event): void
    {
        if ($event->payload['type'] === 'invoice.payment_failed') {
            /* notify user, downgrade grace access */
        }
    }
}

Queue heavy side effects—emails, CRM updates, provisioning—so webhook responses stay under Stripe’s timeout. I have seen production outages when a webhook handler sent synchronous SMS inside the request thread.

Idempotency and ordering

Stripe may deliver the same event more than once. Stripe may also deliver events out of order during network blips. Store processed event IDs in a stripe_webhook_events table if you run custom logic beyond Cashier defaults. Always reconcile from Stripe API when in doubt:

$subscription = $user->subscription('default')->asStripeSubscription();

For API-first products, expose subscription status through a token-protected endpoint following Laravel API best practices rather than scraping Blade-only checks.

How do you test Laravel Stripe subscriptions before going live?

Stripe’s test mode mirrors live behaviour without moving real money. Use test card numbers from Stripe docs—4242 4242 4242 4242 succeeds; 4000 0000 0000 0341 fails on attach.

Local webhook forwarding

stripe login
stripe listen --forward-to http://127.0.0.1:8000/stripe/webhook

The CLI prints a temporary whsec_... secret. Put it in your local .env. Trigger test events:

stripe trigger customer.subscription.created
stripe trigger invoice.payment_failed

Validate JSON payloads during debugging with a JSON formatter—subscription objects are nested and easy to misread under pressure.

Feature tests with Cashier fakes

use Laravel\Cashier\Subscription;

public function test_premium_route_requires_subscription(): void
{
    $user = User::factory()->create();

    $this->actingAs($user)
        ->get('/dashboard/premium')
        ->assertRedirect('/billing');

    $user->subscriptions()->create([
        'type' => 'default',
        'stripe_id' => 'sub_test',
        'stripe_status' => 'active',
        'stripe_price' => config('plans.pro.stripe_price_id'),
    ]);

    $this->actingAs($user)
        ->get('/dashboard/premium')
        ->assertOk();
}

Test both happy paths and failure paths. A subscription site that only tests successful checkout will break silently on the first card expiry.

Checkout vs Payment ElementStripe Checkout+ Hosted PCI scope+ Fast to ship+ Built-in tax prompts− Less UI control− Redirect flowBest for MVPsPayment Element+ Embedded in Blade/Vue+ Full brand control+ More front-end work− More compliance care− SetupIntent wiringBest for custom UX
Choose Stripe Checkout for speed on early Laravel Stripe subscription projects; migrate to Payment Element when branding demands it.
ApproachBest forCashier methodComplexity
Stripe CheckoutSaaS MVP, client portals, quick launch->checkout([...])Low
Payment ElementBranded billing pages inside your layout->createSetupIntent() + Elements JSMedium
Stripe Customer PortalSelf-serve invoice history and card updates->billingPortalUrl(route('billing'))Low
Direct API (no Cashier)Non-standard billing modelsManual SDK callsHigh — avoid unless required

Most teams should start with Checkout plus Customer Portal. That combination covers subscribe, upgrade, cancel, and update card without custom PCI-sensitive forms. Deeper front-end integration belongs in a Vue with Laravel setup when you outgrow Blade redirects.

How do you manage upgrades, cancellations, and failed payments?

Subscription lifecycle code belongs in a dedicated service class—not scattered across controllers. Cashier exposes swap, cancel, resume, and quantity methods that map directly to Stripe API calls.

Plan swaps with proration

// Upgrade immediately with proration (default)
$user->subscription('default')->swap($newPriceId);

// Downgrade at period end — avoids mid-cycle refund confusion
$user->subscription('default')->swap($newPriceId, ['proration_behavior' => 'none']);

Document proration behaviour in your pricing FAQ. Users angry about partial charges rarely read Stripe receipts—they email you.

Cancellation patterns

// Cancel at period end — user keeps access until paid term finishes
$user->subscription('default')->cancel();

// Resume before term ends
$user->subscription('default')->resume();

// Immediate cancel — rare, use deliberately
$user->subscription('default')->cancelNow();

Mirror cancellation state in your UI. Show ends_at when a subscription is scheduled to lapse. Hide premium nav items only after Stripe status becomes canceled or the grace window you define expires.

Failed payment recovery

Stripe Smart Retries and dunning emails reduce involuntary churn. Configure them in Stripe Billing settings. In Laravel, listen for invoice.payment_failed and surface an in-app banner linking to the Customer Portal:

@if ($user->subscription('default')?->pastDue())
    <p class="alert alert-warning">
        Payment failed.
        <a href="{{ $user->billingPortalUrl(route('dashboard')) }}">
            Update your card
        </a>
    </p>
@endif

For trekking or booking platforms with mixed billing models—deposits plus recurring retainers—study how Adventure Third Pole Trek separates one-time bookings from ongoing packages before you overload one Stripe Price with unrelated logic.

Subscription State Machinetrialingactivepast_duecanceledrecoveredLaravel gates: subscribed() for active + trialingCustom grace policy for past_due before hard lockout
Map Stripe subscription statuses to Laravel middleware and UI—especially the past_due grace window before you revoke access.

Metered billing and multiple prices

If you bill on usage, attach metered Price IDs alongside a licensed base price. Report usage from queued jobs:

$user->subscription('default')->reportUsage($units);

Metered models need reconciliation jobs. Stripe totals can drift if your app crashes mid-report. A nightly artisan command comparing local usage logs to Stripe invoices catches gaps early.

Nepal and multi-gateway context

Stripe excels for international cards and USD/EUR billing. Nepali businesses often still need Khalti and eSewa alongside Stripe for local wallets. Treat them as separate payment rails—do not fake a Stripe subscription when the customer pays via bank transfer. Either record manual subscriptions in admin or use Stripe invoicing with offline payment methods.

On Nepal Gift Card, digital products used one-time Stripe charges; recurring retainers would have moved plan definitions into Cashier with the same webhook discipline described here. Compare recurring EMI math your customers expect using the Nepal EMI calculator when you display installment-style marketing copy next to true subscriptions.

Production checklist

  1. Swap test API keys for live keys in production .env only—never in the repo.
  2. Register a live webhook endpoint and confirm STRIPE_WEBHOOK_SECRET differs from test.
  3. Enable Stripe Customer Portal with allowed products and cancellation policies.
  4. Add monitoring for webhook failures—Stripe Dashboard shows delivery errors, but alert your team too.
  5. Run php artisan cashier:webhook if you need the signing URL scaffolded quickly.
  6. Document tax behaviour—Stripe Tax or manual VAT/PAN handling for Nepal exports per your accountant’s advice.
  7. Schedule ongoing maintenance to bump Cashier when Laravel minor releases ship security fixes.

Consult the official Laravel Cashier documentation when upgrading across Laravel major versions. Breaking changes land in upgrade guides—read them before running composer update on production.

Architect subscription billing inside modern Laravel architecture patterns: a SubscriptionService, form requests for plan changes, and policies for feature tiers. Keep Stripe IDs out of your Blade except for publishable keys. Log correlation IDs between checkout sessions and user accounts for support tickets.

If you expose billing to mobile or SPA clients, build thin endpoints documented via Scribe for Laravel rather than leaking Stripe secret keys to JavaScript. The publishable key is fine on the client; everything else stays server-side through API development endpoints you control.

SEO matters for marketing pages—not checkout. Still, keep subscription landing pages crawlable and out of authenticated middleware. Follow SEO setup for Laravel sites so pricing pages index while account billing routes stay blocked in robots.txt.

One-time payments differ materially—read Laravel PayPal integration for one-time payments and Khalti integration when your cart mixes single purchases with optional subscription upsells. ConnectIPS covers bank redirects another audience segment expects.

For enterprise contracts with custom net-30 terms, a pure Stripe subscription may be wrong. Hybrid models—Stripe for self-serve, manual invoicing for accounts—belong in enterprise application development scoping conversations early.

Key Takeaways

  • Install Laravel Cashier, run migrations, and use the Billable trait—do not build raw Stripe SDK subscription logic unless Cashier truly cannot model your case.
  • Create Products and Prices in Stripe first; reference Price IDs from config or database seeds, not hard-coded amounts in views.
  • Start with Stripe Checkout plus verified webhooks; never unlock premium features on success URL alone.
  • Handle swaps, cancellations, and invoice.payment_failed explicitly—past_due grace periods prevent angry support tickets.
  • Test with Stripe CLI triggers and PHPUnit subscription fixtures before switching live keys.
  • Pair international Stripe billing with local gateways when Nepali customers expect wallet or bank options.

People Also Ask

Does Laravel have official Stripe subscription support?

Yes. Laravel Cashier is the official first-party package for Stripe and Paddle billing. It ships migrations, webhook handling, subscription helpers, and middleware for common SaaS patterns. Install it via Composer and follow the Laravel billing documentation for your framework version.

What is the difference between Stripe Checkout and Laravel Cashier?

Cashier is the Laravel integration layer; Checkout is a Stripe-hosted payment UI. Cashier’s ->checkout() method creates a Checkout Session server-side and redirects the customer. Cashier then syncs resulting subscriptions into your database through webhooks.

How much does Stripe charge for subscriptions on a Laravel app?

Stripe pricing depends on country and card type—typically around 2.9% plus a fixed fee per successful charge for domestic cards. Subscriptions themselves have no separate platform fee beyond standard processing. Check Stripe’s pricing page for your settlement currency before you publish NPR or USD plan prices.

Can you use Stripe subscriptions with Laravel Sanctum APIs?

Yes. Authenticate API users with Sanctum, then expose endpoints that read $user->subscribed() or return Stripe Customer Portal URLs. Keep secret keys and webhook verification strictly on the server—mobile clients should never create subscriptions directly with the secret key.

Ship subscription billing you can maintain

A clean Laravel Stripe integration for subscriptions reduces billing bugs to webhook logs and database rows you can inspect. Start with Cashier, Checkout, and a verified webhook endpoint; add Payment Element or metered usage only when the product demands it. Need help wiring subscriptions into an existing Laravel product or a new SaaS build? Contact us or explore custom software development to scope billing alongside the rest of your application architecture.

Frequently Asked Questions

Laravel Cashier syncs Stripe subscription billing with your app database while webhooks keep access flags accurate and Stripe remains the source of truth for plans and invoices.

You need Laravel 12 or 13.x, PHP 8.2 or higher for Laravel 12 and PHP 8.3 or higher for Laravel 13, Composer 2.10, a Stripe test account, and HTTPS on production. Install laravel/cashier, publish migrations and config, add STRIPE_KEY, STRIPE_SECRET, and STRIPE_WEBHOOK_SECRET to .env without committing live secrets, create Stripe Products and recurring Prices first, attach the Billable trait to User, and plan webhook handling before exposing checkout.

Run composer require laravel/cashier, then php artisan vendor:publish --tag=cashier-migrations and migrate. Publish cashier-config and add Stripe keys plus CASHIER_CURRENCY to .env. Add use Billable to your User model. Cashier creates stripe_id, payment method columns, and subscription relationship tables. Your authorization layer should read subscription status from these records, not from session flash data or browser-side flags after checkout.

Define each recurring plan as a Stripe Product with a Price ID like price_1ABC in the Stripe Dashboard or API before writing checkout code. Store Price IDs in config/plans.php or a plans database table keyed by plan slug, never hard-code amounts only in Blade templates. Keep feature flags in your app while Stripe owns billing amounts, trials, and promotion codes. On client portals, subscription tiers often gate modules such as document storage while Stripe handles the money.

Add an authenticated POST route that resolves the plan stripe_price_id from config, then call newSubscription with the default subscription name, the Price ID, optional trialDays and allowPromotionCodes, and checkout with success_url and cancel_url routes. Cashier builds the Stripe Checkout session and redirects the user. Do not unlock premium features when the success page loads. Grant access only after Cashier processes the Stripe webhook and your database reflects an active subscription record.

No. Webhooks drive renewals, failed payments, cancellations, and plan changes. Without verified webhook delivery, subscription access flags in Laravel will drift from Stripe billing state.

Register POST /stripe/webhook to Cashier WebhookController with CSRF excluded, point Stripe Dashboard to your HTTPS endpoint, and subscribe to customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, invoice.payment_succeeded, and invoice.payment_failed. Copy the signing secret into STRIPE_WEBHOOK_SECRET so Cashier verifies signatures automatically. Extend handling via WebhookReceived listeners for notifications or grace access, queue heavy side effects, store processed event IDs for idempotency, and reconcile with asStripeSubscription when events arrive out of order or duplicate.

Use Stripe test mode with card 4242 4242 4242 4242 for success and 4000 0000 0000 0341 for attach failures. Forward webhooks locally using stripe listen --forward-to your /stripe/webhook URL and paste the temporary whsec secret into .env. Trigger events with stripe trigger customer.subscription.created and invoice.payment_failed. Write feature tests that create Subscription records with stripe_status active and assert premium routes redirect unsubscribed users. Test failure paths, not just successful checkout redirects.

Start with Stripe Checkout for SaaS MVPs, client portals, and quick launches using checkout with low complexity. Choose Payment Element when you need branded billing pages inside your layout via createSetupIntent plus Elements JS at medium complexity. Add Stripe Customer Portal through billingPortalUrl for self-serve invoice history, card updates, and cancellations. Most teams should use Checkout plus Customer Portal first. Avoid direct Stripe SDK billing unless Cashier cannot model your plan logic.

Use Cashier swap method on subscription default. Upgrades with default proration charge immediately for the plan difference. Downgrades at period end pass proration_behavior none to avoid mid-cycle refund confusion, and document that behaviour in your pricing FAQ because users email support rather than read Stripe receipts. Use cancel to end at period end so access continues until ends_at, resume before lapse, and cancelNow only when immediate termination is deliberate. Mirror cancellation state in your UI.

Listen for invoice.payment_failed via WebhookReceived to notify users and define grace access before downgrade. Enable Stripe Smart Retries and dunning emails in Stripe Billing settings. In your views, check subscription default pastDue and link users to billingPortalUrl to update their card. Hide premium navigation only after Stripe status becomes canceled or your defined grace window expires, not on the first failed charge if you allow recovery time. Queue notifications rather than sending synchronous SMS inside the webhook thread.

Wrap protected routes in auth and subscribed middleware, for example routes under /dashboard/premium. Cashier ships subscribed, subscribed with a name parameter, and not_subscribed middleware aliases. Register them explicitly if your Laravel version requires middleware mapping. Pair middleware with policies and gates so plan tiers map cleanly to permissions. Read subscription status from Cashier database records, never from checkout success alone or client-side JavaScript flags.

Yes. Set CASHIER_CURRENCY=npr in .env and create matching recurring Prices in Stripe. Confirm NPR is a supported presentment currency in Stripe documentation before advertising local pricing on your site.

Treat Stripe, Khalti, and eSewa as separate payment rails. Stripe excels for international cards and USD or EUR billing, while local wallets serve customers who do not use foreign cards. Do not fake a Stripe subscription when a customer pays via bank transfer. Either record manual subscriptions in admin or use Stripe invoicing with offline payment methods. Keep one-time local gateway charges separate from Cashier-managed recurring plans with the same webhook discipline on the Stripe side.

Swap test API keys for live keys only in production .env, never in the repository. Register a live webhook endpoint with a distinct STRIPE_WEBHOOK_SECRET, enable Stripe Customer Portal with allowed products and cancellation policies, and monitor webhook delivery failures with team alerts beyond the Stripe Dashboard alone. Confirm HTTPS, document tax behaviour with Stripe Tax or manual VAT and PAN handling per your accountant, schedule Cashier updates alongside Laravel security releases, and keep billing logic in a SubscriptionService with form requests and policies rather than scattered controllers.

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: