whisper-money/.github/skills/cashier-stripe-development/references/webhooks.md

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.