Laravel Cashier & Stripe: A Comprehensive Billing Guide for Subscriptions
Laravel Cashier simplifies the complex world of subscription billing with Stripe, providing an expressive and fluent API for handling recurring payments, invoices, and more. This tutorial will walk you through setting up Laravel Cashier (Stripe version) in your application, from installation to managing subscriptions and one-off charges.
Why Laravel Cashier? #
Directly integrating with Stripe can be daunting, requiring extensive knowledge of their API, webhooks, and complex billing logic. Cashier abstracts away much of this complexity, allowing you to focus on your application's core features while delegating payment processing to a robust, pre-built solution.
Key Features: #
- Subscription Management: Easily create, update, and cancel subscriptions.
- Invoice Handling: Generate and retrieve PDF invoices.
- Payment Methods: Store and manage customer payment methods securely.
- Webhooks: Automatically synchronize subscription statuses with Stripe.
- One-Off Charges: Process single payments alongside subscriptions.
Prerequisites #
Before you begin, ensure you have:
- A fresh or existing Laravel application (Laravel 10+ recommended).
- PHP 8.1+.
- Composer installed.
- A Stripe account (free to sign up).
1. Installation #
First, install Cashier via Composer:
composer require laravel/cashier
Next, run the database migrations. Cashier will add several columns to your users table and create a new subscriptions table.
php artisan migrate
Finally, publish the Cashier configuration file. This allows you to customize various settings.
php artisan vendor:publish --tag="cashier-config"
2. Configure Stripe API Keys #
You'll need your Stripe API keys (publishable and secret) from your Stripe Dashboard. Add them to your .env file:
STRIPE_KEY="pk_test_YOUR_STRIPE_PUBLISHABLE_KEY"
STRIPE_SECRET="sk_test_YOUR_STRIPE_SECRET_KEY"
STRIPE_WEBHOOK_SECRET=null # We'll set this later
3. Prepare Your User Model #
To enable your User model to interact with Cashier, you need to use the Billable trait. This trait provides methods for handling subscriptions, payments, and invoices.
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Cashier\Billable; // Import the Billable trait
class User extends Authenticatable
{
use HasFactory, Notifiable, Billable; // Use the Billable trait
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
'stripe_id', // Add stripe_id to fillable
'pm_type', // Add pm_type to fillable
'pm_last_four', // Add pm_last_four to fillable
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
4. Create Products and Prices in Stripe #
Before you can create subscriptions, you need to define your products and their associated prices (plans) in the Stripe Dashboard.
- Go to your Stripe Dashboard.
- Navigate to Products > Product catalog.
- Click + Add product.
- Give your product a name (e.g., "Pro Plan").
- Under Pricing, click + Add another price.
- Configure the price (e.g., $10/month recurring).
- Take note of the Price ID (e.g.,
price_12345ABCDEF). You'll use this in your Laravel application.
Repeat this for any other plans you offer (e.g., "Premium Plan").
5. Handling Subscriptions #
A. Collecting Payment Method Information #
To subscribe a user, you first need to collect their payment method. Stripe recommends using Stripe Elements for a secure and PCI-compliant way to do this. Cashier provides a helper to create a "setup intent" for this purpose.
Route (web.php):
use App\Http\Controllers\SubscriptionController;
Route::get('/subscribe', [SubscriptionController::class, 'showSubscriptionForm'])->name('subscription.form');
Route::post('/subscribe', [SubscriptionController::class, 'storeSubscription'])->name('subscription.store');
Controller (SubscriptionController.php):
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Laravel\Cashier\Cashier;
class SubscriptionController extends Controller
{
public function showSubscriptionForm(Request $request)
{
return view('subscribe', [
'intent' => $request->user()->createSetupIntent()
]);
}
public function storeSubscription(Request $request)
{
$request->user()->newSubscription(
'default', 'price_1P6yQ2Rv0P7c38jUuO0M2zT4' // Replace with your Price ID
)->create($request->paymentMethodId);
return redirect('/dashboard')->with('success', 'Subscription successful!');
}
}
Blade View (subscribe.blade.php):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Subscribe</title>
<script src="https://js.stripe.com/v3/"></script>
</head>
<body>
<h1>Subscribe to Pro Plan</h1>
<form id="payment-form" action="{{ route('subscription.store') }}" method="POST">
@csrf
<div id="card-element">
<!-- A Stripe Element will be inserted here. -->
</div>
<!-- Used to display form errors. -->
<div id="card-errors" role="alert"></div>
<button id="card-button" data-secret="{{ $intent->client_secret }}">
Subscribe
</button>
</form>
<script>
const stripe = Stripe('{{ config('cashier.key') }}'); // Your publishable key
const elements = stripe.elements();
const cardElement = elements.create('card');
cardElement.mount('#card-element');
const form = document.getElementById('payment-form');
const cardButton = document.getElementById('card-button');
const clientSecret = cardButton.dataset.secret;
form.addEventListener('submit', async (e) => {
e.preventDefault();
cardButton.disabled = true;
const { setupIntent, error } = await stripe.confirmCardSetup(
clientSecret, {
payment_method: {
card: cardElement,
billing_details: { name: '{{ Auth::user()->name }}' }
}
}
);
if (error) {
const errorDisplay = document.getElementById('card-errors');
errorDisplay.textContent = error.message;
cardButton.disabled = false;
} else {
let hiddenInput = document.createElement('input');
hiddenInput.setAttribute('type', 'hidden');
hiddenInput.setAttribute('name', 'paymentMethodId');
hiddenInput.setAttribute('value', setupIntent.payment_method);
form.appendChild(hiddenInput);
form.submit();
}
});
</script>
</body>
</html>
B. Creating a Subscription #
Once you have the paymentMethodId, you can create a subscription:
$user = Auth::user();
// Create a new subscription for the 'default' plan using a specific price ID
$user->newSubscription('default', 'price_1P6yQ2Rv0P7c38jUuO0M2zT4')
->create($paymentMethodId);
// With a trial period (e.g., 7 days)
$user->newSubscription('default', 'price_1P6yQ2Rv0P7c38jUuO0M2zT4')
->trialDays(7)
->create($paymentMethodId);
C. Swapping Subscriptions #
Users can change their plans easily:
$user = Auth::user();
$user->subscription('default')->swap('price_ANOTHER_PRICE_ID');
By default, Cashier will prorate the charges. To prevent proration:
$user->subscription('default')->swap('price_ANOTHER_PRICE_ID')->noProrate();
D. Cancelling Subscriptions #
$user = Auth::user();
$user->subscription('default')->cancel();
This will keep the user subscribed until their current billing period ends. To cancel immediately:
$user->subscription('default')->cancelNow();
E. Resuming Subscriptions #
If a user cancelled their subscription but wants to reactivate it before the billing period ends:
$user = Auth::user();
$user->subscription('default')->resume();
F. Checking Subscription Status #
Cashier provides convenient methods to check a user's subscription status:
$user = Auth::user();
if ($user->subscribed('default')) {
// User is subscribed
}
if ($user->subscribedToPrice('price_1P6yQ2Rv0P7c38jUuO0M2zT4')) {
// User is subscribed to a specific price
}
if ($user->onTrial('default')) {
// User is on a trial period
}
if ($user->cancelled('default')) {
// User has cancelled, but still within billing period
}
if ($user->ended('default')) {
// User's subscription has fully ended
}
6. Handling Webhooks #
Webhooks are crucial for keeping your application's subscription data synchronized with Stripe. For instance, if a user's card expires, Stripe will notify your app via a webhook.
A. Define Webhook Route #
Cashier automatically registers a route for handling webhooks. To secure it, you need to configure a webhook secret.
B. Configure Webhook Secret #
-
Go to your Stripe Dashboard > Developers > Webhooks.
-
Click + Add an endpoint.
-
For the Endpoint URL, use
https://your-domain.com/stripe/webhook(replaceyour-domain.comwith your actual domain). -
Select the events you want to listen to. For most Cashier features,
customer.*,invoice.*,checkout.*,payment_intent.*,setup_intent.*,subscription.*, andwebhook_endpoint.*are recommended. -
After creating the endpoint, click on it and reveal the Signing secret (starts with
wh_). -
Add this secret to your
.envfile:STRIPE_WEBHOOK_SECRET="wh_YOUR_WEBHOOK_SECRET"
C. Handling Specific Webhook Events #
If you need to handle specific webhook events beyond Cashier's default behavior, you can extend Cashier's WebhookController:
// app/Http/Controllers/StripeWebhookController.php
namespace App\Http\Controllers;
use Laravel\Cashier\Http\Controllers\WebhookController as CashierWebhookController;
class StripeWebhookController extends CashierWebhookController
{
/**
* Handle a cancelled customer subscription.
*
* @param array $payload
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function handleCustomerSubscriptionDeleted(array $payload)
{
// Perform custom actions when a subscription is deleted
// e.g., send a custom email, update user roles, etc.
return parent::handleCustomerSubscriptionDeleted($payload);
}
}
Then, update your cashier.php config file to point to your custom controller:
// config/cashier.php
'webhook' => [
'controller' => \App\Http\Controllers\StripeWebhookController::class,
// ...
],
7. One-Off Charges #
Cashier also allows you to make one-time charges to a user's default payment method.
use Illuminate\Http\Request;
use Auth;
// ...
public function processOneTimePayment(Request $request)
{
$user = Auth::user();
try {
$user->charge(
1000, // Amount in cents (e.g., $10.00)
$request->paymentMethodId // Or null to use default payment method
);
return back()->with('success', 'Payment successful!');
} catch (\Exception $e) {
return back()->with('error', $e->getMessage());
}
}
For charge() to work without explicitly providing paymentMethodId, the user must have a default payment method on file (e.g., from a previous subscription setup intent).
8. Invoices and Receipts #
Cashier makes it easy to retrieve and display invoices for your users.
$user = Auth::user();
// Get all invoices
$invoices = $user->invoices();
// Get upcoming invoice (if any)
$upcomingInvoice = $user->upcomingInvoice();
To download an invoice PDF:
Route::get('/user/invoice/{invoiceId}', function (Request $request, $invoiceId) {
return $request->user()->downloadInvoice($invoiceId, [
'vendor' => 'Laravel World',
'product' => 'Pro Subscription',
]);
})->middleware(['auth']);
9. Testing #
When testing, always use Stripe's test API keys and test cards provided in their documentation. You can also simulate webhook events directly from the Stripe Dashboard for thorough testing.
Conclusion #
Laravel Cashier dramatically simplifies the integration of Stripe for subscription billing, allowing developers to implement powerful payment features with minimal code. By following this guide, you should now have a solid foundation for building a robust billing system in your Laravel application. Remember to consult the official Cashier documentation for more advanced features and edge cases.