Dwight Watson's blog

Using Laravel Cashier to handle Stripe webhooks

This blog post was originally published a little while ago. Please consider that it may no longer be relevant or even accurate.

Stripe webhooks can be a little bit annoying to verify. In Laravel I've generally done it using the authorize method of a FormRequest.

use Exception;
use Illuminate\Foundation\Http\FormRequest;
use Stripe\Webhook;

class StripeRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
try {
$signature = $this->header('Stripe-Signature');
$secret = config('services.stripe.webhook_secret');

Webhook::constructEvent($this->getContent(), $signature, $secret);
} catch (Exception $e) {
return false;
}

return true;
}

/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
//
];
}
}

The implementation is simple enough: use Stripe to construct a Webhook event from the request and if an Exception is thrown you know it's invalid. You need to provide the webhook secret in order to build this event.

However, I've found that it's super easy to use Laravel Cashier to do most of the legwork for you here, and you get a beautiful syntax for handing those webhooks too.

Install Cashier

Run composer require laravel/cashier to install the package.

Ignore Cashier's migrations

Call ignoreMigrations in a service provider to prevent the framework from automatically running Cashier's migrations next time you migrate.

use Illuminate\Support\ServiceProvider;
use Laravel\Cashier\Cashier;

class AppServiceProvider extends ServiceProvider
{
public function register()
{
Cashier::ignoreMigrations();
}
}

Add the API route

Register the route in routes/api.php - not your regular web routes file.

Route::post('/webhooks/stripe', [App\Http\Controllers\WebhooksController::class, 'handleWebhook'])->name('cashier.webhook');

Create the Webhook controller

Finally create a webhooks controller that extends Cashier's controller. You can then create methods that camelCase Stripe events and handle them - for example checkout.session.completed is mapped to handleCheckoutSessionCompleted and the payload is passed in as an argument.

<?php

namespace App\Http\Controllers;

use App\Models\Checkout;
use Illuminate\Support\Arr;
use Laravel\Cashier\Http\Controllers\WebhookController as CashierController;

class WebhooksController extends CashierController
{
/**
* Handle checkout session completed.
*
* @param array $payload
* @return \Symfony\Component\HttpFoundation\Response
*/
public function handleCheckoutSessionCompleted($payload)
{
$checkout = Checkout::where('stripe_checkout_session_id', Arr::get($payload, 'data.object.id'))
->firstOrFail();

// Handle checkout
}
}

Obviously there's a bit more code involved here than just using the custom authorize in a form request. However I like the simplification of being able to offload that (and the tests involved) to a well maintained first-party package, and get a fluent interface to handle events.

A blog about Laravel & Rails by Dwight Watson;

Picture of Dwight Watson

Follow me on Twitter, or GitHub.