symfony/webhook
Symfony Webhook component for sending and consuming webhooks. Helps build webhook endpoints, verify and parse incoming requests, and dispatch outgoing webhooks with consistent signatures and payload handling across integrations.
Install the package and Laravel adapter:
composer require symfony/webhook spatie/laravel-webhook-server
php artisan vendor:publish --provider="Spatie\WebhookServer\WebhookServerServiceProvider"
Configure the webhook server in config/webhook-server.php:
'webhooks' => [
'stripe' => [
'secret' => env('STRIPE_WEBHOOK_SECRET'),
'url' => '/stripe/webhook',
],
],
Define a webhook route in routes/web.php:
use Spatie\WebhookServer\WebhookHandler;
Route::post('/stripe/webhook', WebhookHandler::class);
Create a handler for Stripe events:
php artisan make:webhook-handler StripeWebhookHandler
// app/Handlers/Webhook/StripeWebhookHandler.php
namespace App\Handlers\Webhook;
use Spatie\WebhookServer\WebhookHandler as BaseHandler;
use Spatie\WebhookServer\WebhookModels\WebhookCall;
use Symfony\Component\HttpFoundation\Request;
class StripeWebhookHandler extends BaseHandler
{
public function handle(Request $request): string
{
$payload = $request->getContent();
$sigHeader = $request->headers->get('stripe-signature');
// Validate signature (Symfony's Webhook component handles this under the hood)
$this->validateWebhook($payload, $sigHeader);
// Process the event
$event = json_decode($payload, true);
// Your logic here...
return 'OK';
}
public function validateWebhook(string $payload, string $signature): void
{
// Use Symfony's Webhook component for validation
$remoteEvent = $this->parseRemoteEvent($payload, $signature);
}
protected function parseRemoteEvent(string $payload, string $signature): void
{
$parser = new \Symfony\Component\Webhook\Parser\JsonParser();
$remoteEvent = $parser->parse($payload, $signature, config('webhook-server.stripe.secret'));
}
}
Register the handler in config/webhook-server.php:
'webhooks' => [
'stripe' => [
'secret' => env('STRIPE_WEBHOOK_SECRET'),
'url' => '/stripe/webhook',
'handler' => \App\Handlers\Webhook\StripeWebhookHandler::class,
],
],
Test locally with a tool like ngrok to expose your local server to Stripe’s webhook tester.
stripe listen --forward-to localhost:8000/stripe/webhook
public function handle(Request $request): string
{
$event = json_decode($request->getContent(), true);
if ($event['type'] === 'payment_intent.succeeded') {
// Dispatch a Laravel event or queue a job
event(new PaymentSucceeded($event['data']['object']));
}
return 'OK';
}
JsonParser.Use Symfony’s WebhookSender to send webhooks with retries and signing:
Install the HTTP client:
composer require symfony/http-client
Create a sender service:
// app/Services/WebhookSenderService.php
namespace App\Services;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\Webhook\Sender\WebhookSender;
use Symfony\Component\Webhook\Sender\WebhookSenderInterface;
class WebhookSenderService
{
public function __construct(
private WebhookSenderInterface $sender
) {}
public function send(string $url, array $payload, string $secret): void
{
$this->sender->send($url, $payload, [
'headers' => [
'Content-Type' => 'application/json',
'X-Signature' => $this->generateSignature($payload, $secret),
],
]);
}
private function generateSignature(array $payload, string $secret): string
{
return hash_hmac('sha256', json_encode($payload), $secret);
}
}
Register the sender in a service provider:
// app/Providers/WebhookServiceProvider.php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\Webhook\Sender\WebhookSender;
class WebhookServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->singleton(WebhookSenderInterface::class, function () {
$client = HttpClient::create();
return new WebhookSender($client);
});
}
}
Use the sender in a job or controller:
use App\Services\WebhookSenderService;
class SendPaymentConfirmationJob implements ShouldQueue
{
public function handle(WebhookSenderService $sender): void
{
$sender->send(
'https://example.com/webhook',
['event' => 'payment.confirmed', 'data' => $this->paymentData],
config('services.stripe.webhook_secret')
);
}
}
Leverage spatie/laravel-webhook-server as a facade over Symfony’s WebhookReceiver:
Define a receiver:
// app/Handlers/Webhook/SlackWebhookHandler.php
namespace App\Handlers\Webhook;
use Spatie\WebhookServer\WebhookHandler;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Webhook\Parser\JsonParser;
use Symfony\Component\Webhook\RemoteEvent;
class SlackWebhookHandler extends WebhookHandler
{
public function handle(Request $request): string
{
$parser = new JsonParser();
$remoteEvent = $parser->parse(
$request->getContent(),
$request->headers->get('X-Slack-Signature'),
config('webhook-server.slack.secret')
);
// Process the event
$this->processSlackEvent($remoteEvent);
return 'OK';
}
private function processSlackEvent(RemoteEvent $event): void
{
$payload = json_decode($event->getPayload(), true);
if ($payload['type'] === 'event_callback') {
// Handle Slack event
}
}
}
Handle multiple events per payload (e.g., Stripe’s array of events):
use Symfony\Component\Webhook\Parser\RequestParserInterface;
class MultiEventParser implements RequestParserInterface
{
public function parse(Request $request): iterable
{
$payload = json_decode($request->getContent(), true);
foreach ($payload['data'] as $event) {
yield new RemoteEvent(
$event['id'],
$event['type'],
$event['object'],
$request->getContent()
);
}
}
}
Register the parser in your handler:
public function handle(Request $request): string
{
$parser = new MultiEventParser();
foreach ($parser->parse($request) as $event) {
$this->processEvent($event);
}
return 'OK';
}
Offload webhook processing to Laravel queues for scalability:
Dispatch a job in your handler:
public function handle(Request $request): string
{
$event = $this->parseEvent($request);
ProcessWebhookJob::dispatch($event);
return 'OK';
}
Create a job:
// app/Jobs/ProcessWebhookJob.php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Symfony\Component\Webhook\RemoteEvent;
class ProcessWebhookJob implements ShouldQueue
{
use Dispatchable, Queueable;
public function __construct(
private RemoteEvent $event
) {}
public function handle(): void
{
// Process the event (e.g., update DB, send notifications)
}
}
Configure queue workers:
How can I help you explore Laravel packages today?