Tutorials Tutorial

Laravel Cashier & Stripe: A Comprehensive Billing Guide for Subscriptions

Admin User
Admin User
Apr 29, 2026
4 min read

Key Takeaways

  • # Laravel Cashier & Stripe: A Comprehensive Billing Guide for Subscriptions
  • Laravel Cashier simplifies the complex world of subscription billing with Stripe, p...

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:

  1. A fresh or existing Laravel application (Laravel 10+ recommended).
  2. PHP 8.1+.
  3. Composer installed.
  4. 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.

  1. Go to your Stripe Dashboard.
  2. Navigate to Products > Product catalog.
  3. Click + Add product.
  4. Give your product a name (e.g., "Pro Plan").
  5. Under Pricing, click + Add another price.
  6. Configure the price (e.g., $10/month recurring).
  7. 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 #

  1. Go to your Stripe Dashboard > Developers > Webhooks.

  2. Click + Add an endpoint.

  3. For the Endpoint URL, use https://your-domain.com/stripe/webhook (replace your-domain.com with your actual domain).

  4. Select the events you want to listen to. For most Cashier features, customer.*, invoice.*, checkout.*, payment_intent.*, setup_intent.*, subscription.*, and webhook_endpoint.* are recommended.

  5. After creating the endpoint, click on it and reveal the Signing secret (starts with wh_).

  6. Add this secret to your .env file:

    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.


FAQs

What is the main benefit of using Laravel Cashier?
Laravel Cashier simplifies subscription billing with Stripe by providing an expressive API, abstracting away complex Stripe API interactions, webhook handling, and invoice management, allowing developers to focus on core application logic.
Can Laravel Cashier handle one-time payments or just subscriptions?
While primarily designed for subscriptions, Laravel Cashier can also handle one-off charges to a user's default payment method using the `charge()` method, making it versatile for various payment scenarios.

Want more content like this?

Explore more tutorials in the Tutorials section.

Explore Tutorials

You might also like