spatie/laravel-stripe-webhooks
Laravel package to handle Stripe webhooks: verifies Stripe signatures, stores valid webhook calls in the database, and dispatches configurable jobs or events per Stripe event type. You implement the business logic (payments, subscriptions, etc.).
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 Endpoint
Add to routes/web.php:
Route::stripeWebhooks('stripe/webhook');
Exclude this route from CSRF middleware in app/Http/Middleware/VerifyCsrfToken.php:
protected $except = [
'stripe/webhook',
];
Set Up Signing Secret
In .env:
STRIPE_WEBHOOK_SECRET=whsec_...
In config/stripe-webhooks.php:
'signing_secret' => env('STRIPE_WEBHOOK_SECRET'),
First Use Case: Handle a Payment Intent Succeeded Event Create a job:
php artisan make:job HandlePaymentIntentSucceeded
Update config/stripe-webhooks.php:
'jobs' => [
'payment_intent.succeeded' => \App\Jobs\StripeWebhooks\HandlePaymentIntentSucceeded::class,
],
Define Job
namespace App\Jobs\StripeWebhooks;
use Illuminate\Bus\Queueable;
use Spatie\WebhookClient\Models\WebhookCall;
class HandlePaymentIntentSucceeded
{
use Queueable;
public function __construct(public WebhookCall $webhookCall) {}
public function handle()
{
$payload = $this->webhookCall->payload;
$paymentIntent = \Stripe\PaymentIntent::retrieve($payload['data']['object']['id']);
// Process payment intent logic
}
}
Register Job in Config
'jobs' => [
'payment_intent.succeeded' => \App\Jobs\StripeWebhooks\HandlePaymentIntentSucceeded::class,
],
Queue Configuration
In .env:
STRIPE_WEBHOOK_CONNECTION=redis
STRIPE_WEBHOOK_QUEUE=stripe
Create Listener
php artisan make:listener HandleStripeWebhook
namespace App\Listeners;
use Spatie\WebhookClient\Models\WebhookCall;
class HandleStripeWebhook
{
public function handle(WebhookCall $webhookCall)
{
$event = \Stripe\Event::constructFrom(
json_decode($webhookCall->payload, true)
);
// Handle event logic
}
}
Register Listener in EventServiceProvider
protected $listen = [
'stripe-webhooks::payment_intent.succeeded' => [
\App\Listeners\HandleStripeWebhook::class,
],
];
\Log::info('PaymentIntent succeeded', ['payload' => $payload]);
Spatie\WebhookClient\WebhookCall factory:
$webhookCall = WebhookCall::factory()->create([
'payload' => json_encode(['data' => ['object' => ['id' => 'pi_123']]]),
]);
webhook_calls table:
if (!WebhookCall::where('payload->id', $payload['id'])->exists()) {
// Process
}
Signature Verification in Local Environments
Disable verification in .env for local testing:
STRIPE_SIGNATURE_VERIFY=false
Re-enable in production:
STRIPE_SIGNATURE_VERIFY=true
Queue Failures
Monitor failed jobs in Laravel Horizon or failed_jobs table. Retry manually:
dispatch(new \Spatie\StripeWebhooks\ProcessStripeWebhookJob(WebhookCall::find($id)));
Duplicate Events
Stripe may send duplicates. Use webhook_calls table to deduplicate:
if (WebhookCall::where('payload->id', $payload['id'])->exists()) {
return;
}
Payload Access Ensure payload is decoded correctly:
$payload = json_decode($webhookCall->payload, true);
$event = \Stripe\Event::constructFrom($payload);
webhook_calls table for failed events:
SELECT * FROM webhook_calls WHERE exception IS NOT NULL;
stripe listen --forward-to localhost/stripe/webhook
. with _ in config keys.Custom Job Handling
Extend ProcessStripeWebhookJob for pre/post-processing:
namespace App\Jobs;
use Spatie\StripeWebhooks\ProcessStripeWebhookJob;
class CustomStripeWebhookJob extends ProcessStripeWebhookJob
{
public function handle()
{
\Log::info('Custom logic before parent handle');
parent::handle();
\Log::info('Custom logic after parent handle');
}
}
Update config:
'model' => \App\Jobs\CustomStripeWebhookJob::class,
Dynamic Secret Handling For Stripe Connect, use route parameters:
Route::stripeWebhooks('stripe/webhook/{configKey}');
Configure secrets in stripe-webhooks.php:
'signing_secret_connect' => env('STRIPE_WEBHOOK_SECRET_CONNECT'),
Payload Transformation
Use Stripe\Event::constructFrom() for type-safe access:
$event = \Stripe\Event::constructFrom($payload);
$paymentIntent = $event->data->object;
.env and use env() in config:
'signing_secret' => env('STRIPE_WEBHOOK_SECRET'),
stripe/webhook-testing endpoint to simulate events:
curl -X POST https://api.stripe.com/v1/webhook_endpoints/test_constructed_webhook \
-u sk_test_...
How can I help you explore Laravel packages today?