spatie/laravel-webhook-client
Receive and process incoming webhooks in Laravel. Verify signatures, store webhook payloads, and handle them in queued jobs. Flexible configuration for multiple webhook endpoints and secure validation.
Installation:
composer require spatie/laravel-webhook-client
php artisan vendor:publish --provider="Spatie\WebhookClient\WebhookClientServiceProvider" --tag="webhook-client-config"
php artisan vendor:publish --provider="Spatie\WebhookClient\WebhookClientServiceProvider" --tag="webhook-client-migrations"
php artisan migrate
Configure .env:
WEBHOOK_CLIENT_SECRET=your_webhook_secret_here
Set Up Routing:
// routes/web.php
Route::webhooks('webhook-receiving-url');
Create a Job:
php artisan make:job ProcessWebhook
Extend Spatie\WebhookClient\Jobs\ProcessWebhookJob and implement handle():
// app/Jobs/ProcessWebhook.php
public function handle()
{
$payload = $this->webhookCall->payload;
// Process payload (e.g., update a model, trigger an event)
}
Update Config:
// config/webhook-client.php
'process_webhook_job' => App\Jobs\ProcessWebhook::class,
Test with a Webhook Sender: Use tools like webhook.site or Postman to send a signed request to your endpoint.
Receiving and Processing a Stripe Webhook:
ProcessWebhook job to handle events like payment_intent.succeeded:
public function handle()
{
$payload = json_decode($this->webhookCall->payload, true);
if ($payload['type'] === 'payment_intent.succeeded') {
// Update your order model or trigger a notification
}
}
Incoming Request:
WebhookController.SignatureValidator.Filtering:
WebhookProfile to filter requests (e.g., only process POST requests or specific payloads):
// app/WebhookProfiles/CustomProfile.php
public function shouldProcess(Request $request): bool
{
return $request->isMethod('post') && $request->has('event_type');
}
Update config:
'webhook_profile' => App\WebhookProfiles\CustomProfile::class,
Storage:
webhook_calls table (customizable via webhook_model).// config/webhook-client.php
'store_headers' => ['Authorization', 'X-Custom-Header'],
Processing:
ProcessWebhookJob) to handle the payload asynchronously.public function handle()
{
$payload = json_decode($this->webhookCall->payload, true);
if ($payload['action'] === 'opened') {
// Create a GitHub issue in your app
}
}
Response:
WebhookResponse:
// app/WebhookResponses/CustomResponse.php
public function respondToValidWebhook(Request $request, WebhookConfig $config)
{
return response()->json(['status' => 'success', 'message' => 'Webhook processed'], 200);
}
Update config:
'webhook_response' => App\WebhookResponses\CustomResponse::class,
Multiple Webhook Endpoints:
Configure multiple entries in webhook-client.php under configs:
'configs' => [
[
'name' => 'stripe',
'signing_secret' => env('STRIPE_WEBHOOK_SECRET'),
'process_webhook_job' => App\Jobs\ProcessStripeWebhook::class,
],
[
'name' => 'github',
'signing_secret' => env('GITHUB_WEBHOOK_SECRET'),
'process_webhook_job' => App\Jobs\ProcessGitHubWebhook::class,
],
],
Route them separately:
Route::webhooks('stripe-webhook', 'stripe');
Route::webhooks('github-webhook', 'github');
Testing: Use Laravel’s HTTP tests to simulate webhooks:
public function test_webhook_received()
{
$response = $this->postJson('/webhook-receiving-url', ['key' => 'value'], [
'Signature' => hash_hmac('sha256', '{"key":"value"}', env('WEBHOOK_CLIENT_SECRET')),
]);
$response->assertStatus(200);
}
Retry Logic:
Implement retries for failed jobs using Laravel’s retryAfter:
public function handle()
{
try {
// Process logic
} catch (\Exception $e) {
throw $e->retryAfter(5); // Retry after 5 seconds
}
}
Logging: Log webhook payloads and errors for debugging:
public function handle()
{
\Log::info('Webhook payload', ['payload' => $this->webhookCall->payload]);
// Processing logic
}
Signature Mismatches:
signing_secret in .env matches the secret used by the webhook sender.// In a custom SignatureValidator
\Log::debug('Computed signature:', [$computedSignature, $request->header('Signature')]);
Queue Failures:
php artisan queue:failed
CSRF Exceptions:
419 errors.app/Http/Middleware/VerifyCsrfToken.php:
protected $except = ['webhook-receiving-url'];
Payload Size Limits:
input size limit (e.g., 1MB).bootstrap/app.php:
$app->useInputBinding(function ($request) {
$request->enableHttpMethodParameterOverride();
$request->merge([
'payload' => $request->getContent(),
]);
});
Time Synchronization:
Inspect Webhook Calls:
Query the webhook_calls table to debug:
php artisan tinker
>>> \Spatie\WebhookClient\Models\WebhookCall::latest()->first();
Event Listeners:
Listen to InvalidWebhookSignatureEvent for debugging:
// app/Providers/EventServiceProvider.php
protected $listen = [
\Spatie\WebhookClient\Events\InvalidWebhookSignatureEvent::class => [
\App\Listeners\LogInvalidWebhook::class,
],
];
Custom Middleware: Add middleware to log all incoming webhooks:
// app/Http/Middleware/LogWebhooks.php
public function handle($request, Closure $next)
{
\Log::info('Incoming webhook', [
'url' => $request->url(),
'headers' => $request->header(),
'payload' => $request->getContent(),
]);
return $next($request);
}
Register in app/Http/Kernel.php:
protected $middleware = [
\App\Http\Middleware\LogWebhooks::class,
];
Custom Models:
Extend WebhookCall to add custom fields:
// app/Models/CustomWebhookCall.php
class CustomWebhookCall extends \Spatie\WebhookClient\Models\WebhookCall
{
protected $casts = [
'metadata' => 'array',
];
}
Update config:
'webhook_model' => App\Models\CustomWebhookCall::class,
Dynamic Signing Secrets:
Use a SignatureValidator to fetch secrets dynamically (e.g., from a database):
How can I help you explore Laravel packages today?