132 lines
4.4 KiB
Markdown
132 lines
4.4 KiB
Markdown
# 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}/webhook` named `cashier.webhook`
|
|
- `GET /{cashier.path}/payment/{id}` named `cashier.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):**
|
|
|
|
```php
|
|
->withMiddleware(function (Middleware $middleware) {
|
|
$middleware->validateCsrfTokens(except: ['stripe/*']);
|
|
})
|
|
```
|
|
|
|
**Laravel 10 (`app/Http/Middleware/VerifyCsrfToken.php`, default path example):**
|
|
|
|
```php
|
|
protected $except = [
|
|
'stripe/*',
|
|
];
|
|
```
|
|
|
|
## Local Development with Stripe CLI
|
|
|
|
If you changed `cashier.path`, forward Stripe CLI events to that URL instead of `/stripe/webhook`.
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
php artisan cashier:webhook
|
|
```
|
|
|
|
Cashier's `cashier:webhook` command registers these events by default:
|
|
|
|
- `customer.subscription.created`
|
|
- `customer.subscription.updated`
|
|
- `customer.subscription.deleted`
|
|
- `customer.updated` / `customer.deleted`
|
|
- `invoice.payment_action_required`
|
|
- `invoice.payment_succeeded`
|
|
- `payment_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`.
|
|
|
|
```php
|
|
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:
|
|
|
|
```php
|
|
Cashier::ignoreRoutes();
|
|
```
|
|
|
|
```php
|
|
// 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:
|
|
|
|
```php
|
|
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. |