4.4 KiB
Webhooks Reference
Use search-docs for authoritative documentation on webhooks.
Auto-Registered Routes
Cashier registers two routes automatically under the cashier.path prefix (config('cashier.path'), default stripe):
POST /{cashier.path}/webhooknamedcashier.webhookGET /{cashier.path}/payment/{id}namedcashier.payment
With the default config these are /stripe/webhook and /stripe/payment/{id}. If you set CASHIER_PATH=billing, they become /billing/webhook and /billing/payment/{id}.
CSRF Exclusion
Use the same path prefix you configured for Cashier here. If CASHIER_PATH=billing, exclude billing/* instead of stripe/*.
Laravel 11+ (bootstrap/app.php, default path example):
->withMiddleware(function (Middleware $middleware) {
$middleware->validateCsrfTokens(except: ['stripe/*']);
})
Laravel 10 (app/Http/Middleware/VerifyCsrfToken.php, default path example):
protected $except = [
'stripe/*',
];
Local Development with Stripe CLI
If you changed cashier.path, forward Stripe CLI events to that URL instead of /stripe/webhook.
stripe login
stripe listen --forward-to your-app.test/stripe/webhook
stripe trigger invoice.payment_succeeded
The CLI outputs a whsec_... signing secret specific to that session. Set it as STRIPE_WEBHOOK_SECRET locally. It is not the same as the Dashboard endpoint secret.
Registering Events in the Stripe Dashboard
Use the Artisan command to create the endpoint automatically with all required events:
php artisan cashier:webhook
Cashier's cashier:webhook command registers these events by default:
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedcustomer.updated/customer.deletedinvoice.payment_action_requiredinvoice.payment_succeededpayment_method.automatically_updated
Cashier's WebhookController has built-in handlers for all of the above except invoice.payment_succeeded. For renewal hooks, prefer WebhookReceived / WebhookHandled listeners unless you intentionally add your own controller method.
Custom Handlers: Extending WebhookController
Method name pattern: handle + StudlyCase of event type with dots replaced by underscores.
customer.subscription.created becomes handleCustomerSubscriptionCreated.
use Laravel\Cashier\Http\Controllers\WebhookController as CashierController;
class StripeWebhookController extends CashierController
{
public function handleCustomerSubscriptionCreated(array $payload)
{
$response = parent::handleCustomerSubscriptionCreated($payload);
// your logic after Cashier syncs the subscription
return $response;
}
}
If you add a method for an event Cashier does not handle internally, such as invoice.payment_succeeded, do not call parent::handle...() unless the base controller actually defines that method.
In a service provider, disable auto-registration and re-register both Cashier routes so the incomplete-payment flow and cashier:webhook command keep working:
Cashier::ignoreRoutes();
// routes/web.php
use App\Http\Controllers\StripeWebhookController;
use Illuminate\Support\Facades\Route;
use Laravel\Cashier\Http\Controllers\PaymentController;
Route::prefix(config('cashier.path'))
->name('cashier.')
->group(function () {
Route::get('payment/{id}', [PaymentController::class, 'show'])->name('payment');
Route::post('webhook', [StripeWebhookController::class, 'handleWebhook'])->name('webhook');
});
Keep the cashier.webhook route name unless you plan to pass --url explicitly to php artisan cashier:webhook.
Custom Handlers: Listening to Events
The simpler option when you do not need to replace Cashier's internal logic, or when you want to react to events such as invoice.payment_succeeded that Cashier does not process itself:
use Laravel\Cashier\Events\WebhookReceived;
use Laravel\Cashier\Events\WebhookHandled;
// WebhookReceived fires for every event before Cashier processes it
// WebhookHandled fires after Cashier processes it
Event::listen(WebhookReceived::class, function (WebhookReceived $event) {
if ($event->payload['type'] === 'invoice.payment_succeeded') {
// handle renewal
}
});
Signature Verification
VerifyWebhookSignature middleware is applied automatically when cashier.webhook.secret is set. No extra wiring is needed.