How to Connect Stripe with Laravel?
Laravel is one of the most popular PHP frameworks for building web applications, and Stripe is one of the most widely used payment platforms in the world. Connecting the two is a common requirement for SaaS products, online stores, and membership platforms. Laravel makes this easier than in most frameworks thanks to Laravel Cashier, an official package that wraps the Stripe API, manages customers and subscriptions, and processes webhooks out of the box. Getting the integration right means a smooth checkout, accurate billing, and subscription states that always match reality; getting it wrong can lead to trusted client-side prices, missed payments, or users keeping access after cancelling. In this guide, we'll connect Stripe with Laravel step by step, with code examples for every stage.
Before You Start
You'll need a Stripe account and a Laravel application; the examples in this guide use the structure of Laravel 11 and later. There are two main ways to integrate: using the official stripe/stripe-php library directly, or using Laravel Cashier on top of it. For most applications where payments belong to a user, Cashier is the better choice, because it handles customer records, subscriptions, invoices, and webhook signature verification for you. Install Cashier and run its migrations, which add the Stripe-related columns and subscription tables to your database.
composer require laravel/cashier
php artisan vendor:publish --tag="cashier-migrations"
php artisan migrateNext, add your Stripe keys to the .env file. Every Stripe account has a test mode with its own keys, so the entire integration can be built without moving real money. Never commit the .env file to version control. Finally, add the Billable trait to your user model, which gives every user methods for charging, subscribing, and managing billing.
# .env
STRIPE_KEY=pk_test_...
STRIPE_SECRET=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
CASHIER_CURRENCY=gbp// app/Models/User.php
use Laravel\Cashier\Billable;
class User extends Authenticatable
{
use Billable;
}
Creating a Checkout Session
Stripe Checkout is the fastest and most secure way to accept payments. Instead of building your own payment form, your application creates a Checkout Session and redirects the customer to a Stripe-hosted page that handles cards, digital wallets, and additional authentication such as 3D Secure. With Cashier, the checkout method creates the session and returns a response that redirects the user automatically.
The most important rule is never to trust prices coming from the request. The user should only choose which product or plan they want, and your application should decide the actual Stripe price ID on the server. Otherwise, anyone could modify the request and pay whatever amount they like.
// routes/web.php
use Illuminate\Http\Request;
Route::post('/checkout/{plan}', function (Request $request, string $plan) {
// Prices are resolved on the server, never taken from the request
$prices = [
'starter' => 'price_123',
'pro' => 'price_456',
];
abort_unless(isset($prices[$plan]), 404);
return $request->user()->checkout([$prices[$plan] => 1], [
'success_url' => route('checkout.success').'?session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => route('pricing'),
'metadata' => ['plan' => $plan],
]);
})->middleware('auth')->name('checkout');
Subscriptions and the Billing Portal
Recurring billing is where Cashier really shines. A single chain of methods starts a subscription through Stripe Checkout, optionally with a free trial. Once the subscription is active, Cashier stores its status in your database, so checking whether a user has access is a simple method call that doesn't need an API request.
Customers also need a way to update their card, download invoices, or cancel. Rather than building these screens yourself, you can redirect users to the Stripe Customer Portal, which handles all of it securely and in a way that stays consistent with your Stripe settings.
// routes/web.php
Route::post('/subscribe', function (Request $request) {
return $request->user()
->newSubscription('default', 'price_monthly')
->trialDays(14)
->checkout([
'success_url' => route('dashboard'),
'cancel_url' => route('pricing'),
]);
})->middleware('auth');
Route::get('/billing', function (Request $request) {
return $request->user()->redirectToBillingPortal(route('dashboard'));
})->middleware('auth');
// Anywhere in your application
if ($user->subscribed('default')) {
// grant access to premium features
}
Handling Webhooks
A redirect to your success page is not proof of payment. Customers can close the tab before being redirected, some payment methods confirm later, and subscriptions renew or fail months after checkout. Stripe reports all of these through webhooks. Cashier registers a webhook route at /stripe/webhook automatically, verifies each request's signature when STRIPE_WEBHOOK_SECRET is set, and keeps subscription records in your database up to date.
Because Stripe sends webhooks without a CSRF token, the route must be excluded from CSRF protection. For your own business logic, such as fulfilling orders, listen for Cashier's WebhookReceived event. Dispatch the actual work to a queued job, so the webhook responds quickly, and make that job idempotent, since Stripe may deliver the same event more than once.
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: [
'stripe/*',
]);
})// app/Listeners/StripeEventListener.php
namespace App\Listeners;
use App\Jobs\FulfillOrder;
use Laravel\Cashier\Events\WebhookReceived;
class StripeEventListener
{
public function handle(WebhookReceived $event): void
{
if ($event->payload['type'] === 'checkout.session.completed') {
$session = $event->payload['data']['object'];
// Queued job; must be idempotent because events can arrive twice
FulfillOrder::dispatch($session['id']);
}
}
}
Testing and Going Live
The Stripe CLI forwards webhook events to your local application and prints a temporary signing secret for your .env file. Use Stripe's test card numbers to simulate successful payments, declined cards, and cards that require authentication. For production, Cashier's Artisan command creates a webhook endpoint in your Stripe account with all the events Cashier needs.
stripe listen --forward-to localhost:8000/stripe/webhook
# In production, create the webhook endpoint with the events Cashier needs
php artisan cashier:webhookWhen everything works in test mode, activate your Stripe account, replace the test keys with live keys in your production environment, and make sure a queue worker is running so webhook jobs are processed. Running php artisan config:cache during deployment also ensures environment values are loaded efficiently.
Security and Best Practices
Keep your secret key and webhook secret only in environment variables, and never expose them in Blade templates or JavaScript. Because card details are entered on Stripe's hosted page, sensitive payment data never passes through your servers, which significantly reduces your PCI compliance scope. Always verify webhook signatures, which Cashier does automatically once the signing secret is configured.
For reliability, process webhooks in queued jobs, store the IDs of processed events to prevent double fulfillment, and log every payment event. When some payments require extra confirmation under Strong Customer Authentication rules, Cashier can direct customers to a payment confirmation page instead of failing silently. These practices turn a working checkout into a billing system you can trust.
Conclusion
Connecting Stripe with Laravel is straightforward with the right tools. Laravel Cashier handles customers, subscriptions, and webhook verification, while Stripe Checkout keeps sensitive payment data off your servers. By resolving prices on the server, fulfilling orders from webhooks through idempotent queued jobs, and testing every scenario before going live, you build a payment integration that stays reliable as your business grows. Whether you're selling a single product or running a subscription platform, getting the payment integration right is essential, and working with an experienced team that understands both Stripe and software architecture ensures your payments run on a foundation that lasts.