spatie/laravel-stripe-webhooks
Laravel package to handle Stripe webhooks: verifies Stripe signatures, logs valid calls to the database, and dispatches configurable jobs or events per webhook type. Provides the plumbing for receiving and validating webhooks; you implement the business logic.
Installation
composer require spatie/laravel-stripe-webhooks
php artisan vendor:publish --provider="Spatie\StripeWebhooks\StripeWebhooksServiceProvider"
php artisan vendor:publish --provider="Spatie\WebhookClient\WebhookClientServiceProvider" --tag="webhook-client-migrations"
php artisan migrate
Configure Stripe Webhook Secret
Add your Stripe webhook secret to .env:
STRIPE_WEBHOOK_SECRET=whsec_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Define Route
In routes/web.php:
Route::stripeWebhooks('stripe/webhook');
Add the route to $except in app/Http/Middleware/VerifyCsrfToken.php:
protected $except = [
'stripe/webhook',
];
First Use Case
Create a job for handling a specific Stripe event (e.g., charge.succeeded):
php artisan make:job HandleStripeChargeSucceeded
Update config/stripe-webhooks.php:
'jobs' => [
'charge_succeeded' => \App\Jobs\HandleStripeChargeSucceeded::class,
],
Job-Based Handling
config/stripe-webhooks.php with Stripe event names (replace . with _).public function handle(WebhookCall $webhookCall) {
$payload = json_decode($webhookCall->payload, true);
// Process payload (e.g., update user subscription)
}
Event-Based Handling
EventServiceProvider:
protected $listen = [
'stripe-webhooks::charge.succeeded' => [
\App\Listeners\HandleChargeSucceeded::class,
],
];
WebhookCall payload in listeners:
public function handle(WebhookCall $webhookCall) {
$event = Event::constructFrom($webhookCall->payload);
// Access Stripe objects (e.g., $event->data->object)
}
Queue Integration
ShouldQueue to avoid timeouts:
use Illuminate\Contracts\Queue\ShouldQueue;
class HandleChargeSucceeded implements ShouldQueue { ... }
config/stripe-webhooks.php:
'queue' => 'stripe-webhooks',
Payload Transformation
$event = Event::constructFrom($webhookCall->payload);
$charge = $event->data->object; // Stripe\Charge
Testing Locally
.env:
STRIPE_SIGNATURE_VERIFY=false
stripe listen --forward-to localhost/stripe/webhook
Handling Duplicates
webhook_calls table to deduplicate:
if (!WebhookCall::where('payload->id', $payload['id'])->exists()) {
// Process unique event
}
Retrying Failed Webhooks
dispatch(new ProcessStripeWebhookJob(WebhookCall::find($id)));
Signature Verification
VerifyCsrfToken::$except causes 419 errors.Queue Timeouts
ShouldQueue and monitor queue workers.Duplicate Events
webhook_calls table for existing payload IDs before processing.Environment Config
.env.env() in config/stripe-webhooks.php:
'signing_secret' => env('STRIPE_WEBHOOK_SECRET'),
Log Webhook Payloads
StripeWebhookProfile:
public function shouldProcess(Request $request): bool {
\Log::info('Webhook payload:', $request->getContent());
return true;
}
Inspect Database
webhook_calls for failed events:
SELECT * FROM webhook_calls WHERE exception IS NOT NULL;
Stripe CLI Testing
stripe listen --forward-to to test locally without deploying.Custom Models
ProcessStripeWebhookJob for pre/post-processing:
class CustomWebhookJob extends ProcessStripeWebhookJob {
public function handle() {
\Log::info('Custom logic before parent');
parent::handle();
\Log::info('Custom logic after parent');
}
}
'model' => \App\Jobs\CustomWebhookJob::class,
Dynamic Secret Handling
Route::stripeWebhooks('stripe/webhook/{configKey}');
config/stripe-webhooks.php:
'signing_secret_connect' => env('STRIPE_CONNECT_SECRET'),
Webhook Profiling
WebhookProfile to filter requests:
class CustomProfile implements WebhookProfile {
public function shouldProcess(Request $request): bool {
return $request->ip() === 'trusted-ip';
}
}
'profile' => \App\Profiles\CustomProfile::class,
idempotency keys for critical operations.if (!isset($payload['data']['object'])) {
throw new \Exception('Invalid payload structure');
}
How can I help you explore Laravel packages today?