
September 07, 2026
14 min read
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.
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.
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.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.payment_succeededinvoice.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.
| Approach | Best for | Cashier method | Complexity |
|---|---|---|---|
| Stripe Checkout | SaaS MVP, client portals, quick launch | ->checkout([...]) | Low |
| Payment Element | Branded billing pages inside your layout | ->createSetupIntent() + Elements JS | Medium |
| Stripe Customer Portal | Self-serve invoice history and card updates | ->billingPortalUrl(route('billing')) | Low |
| Direct API (no Cashier) | Non-standard billing models | Manual SDK calls | High — 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.
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
- Swap test API keys for live keys in production
.envonly—never in the repo. - Register a live webhook endpoint and confirm
STRIPE_WEBHOOK_SECRETdiffers from test. - Enable Stripe Customer Portal with allowed products and cancellation policies.
- Add monitoring for webhook failures—Stripe Dashboard shows delivery errors, but alert your team too.
- Run
php artisan cashier:webhookif you need the signing URL scaffolded quickly. - Document tax behaviour—Stripe Tax or manual VAT/PAN handling for Nepal exports per your accountant’s advice.
- 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_failedexplicitly—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
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.

