Turn your Laravel Events into webhooks. Zero code changes.
Register your events in config and they're automatically webhook-enabled. Beautiful UI, secure signatures, queued delivery.
- Use your existing Laravel Events - Just like Broadcasting, register your current events in the config and they automatically become webhook-enabled. No need to modify or create new event classes
- Sync data to third-party services when orders are created, payments are processed, or users register
- Notify partners and integrations in real-time without polling
- Build event-driven architectures where your Laravel app triggers actions in other systems
- Manage webhooks visually through a built-in admin interface
composer require pylesoft/webhooks
php artisan vendor:publish --tag=webhooks.config
php artisan migrateEnable the UI in .env:
WEBHOOKS_UI_ENABLED=true1. Register your event in config/webhooks.php:
'events' => [
'orders.created' => [
'event' => \App\Events\OrderCreated::class,
'group' => 'Orders',
'label' => 'Order created',
],
],2. Create an endpoint at /webhooks in the UI.
3. Fire your event — webhooks dispatch automatically:
event(new OrderCreated($order));Done. The package listens for configured events, finds subscribed endpoints, and queues signed POST requests via Spatie's webhook server.
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"event_key": "orders.created",
"occurred_at": "2024-01-15T10:30:00+00:00",
"data": { "order_id": 123, "amount": 99.99 },
"meta": { "app_name": "My App", "environment": "production" }
}Payload data is extracted in this order:
webhookPayload()method on your event- Transformer class if configured
- Public properties of the event (auto-serialized)
Add a webhookPayload() method to your event:
class OrderCreated
{
public function __construct(public Order $order) {}
public function webhookPayload(): array
{
return [
'order_id' => $this->order->id,
'customer' => $this->order->customer->only(['id', 'email']),
'total' => $this->order->total,
];
}
}Or use a transformer for complex logic:
php artisan make:webhooks-transformer OrderTransformer --event=\App\Events\OrderCreated// config/webhooks.php
'orders.created' => [
'event' => \App\Events\OrderCreated::class,
'transformer' => \App\Webhooks\Transformers\OrderTransformer::class,
],This package listens for class-based events, not string-based Eloquent events like eloquent.created. Bridge them with $dispatchesEvents:
// app/Models/Order.php
protected $dispatchesEvents = [
'created' => \App\Events\OrderCreated::class,
];// app/Events/OrderCreated.php
class OrderCreated
{
public function __construct(public Order $order) {}
}Now Order::create() fires OrderCreated, which triggers webhooks.
For conditional logic, use an observer instead:
class OrderObserver
{
public function created(Order $order): void
{
if ($order->total >= 1000) {
event(new HighValueOrderCreated($order));
}
}
}'ui' => [
'enabled' => env('WEBHOOKS_UI_ENABLED', false),
'path' => env('WEBHOOKS_UI_PATH', '/webhooks'),
'middleware' => ['web', 'auth'],
],Override delivery settings:
'webhook_server' => [
'queue' => 'webhooks', // dedicated queue
'tries' => 5, // retry attempts
'timeout_in_seconds' => 10,
'verify_ssl' => true,
],See Spatie's docs for all options.
'events' => [
'orders.created' => [
'event' => \App\Events\OrderCreated::class, // required
'group' => 'Orders', // UI grouping
'label' => 'Order created', // display name
'description' => 'When an order is placed', // tooltip
'transformer' => OrderTransformer::class, // custom payload
],
],Dispatch without an event object:
use Pyle\Webhooks\Facades\Webhooks;
Webhooks::dispatch('orders.created', [
'order_id' => 123,
'amount' => 99.99,
]);Manage webhook endpoints programmatically using the WebhookEndpoints manager:
use Pyle\Webhooks\Facades\Webhooks;
$endpoint = Webhooks::endpoints()->create(
url: 'https://api.example.com/webhook',
events: ['orders.created', 'users.registered'],
description: 'Production sync endpoint',
enabled: true
);Update specific fields (partial updates supported):
// Update URL only
Webhooks::endpoints()->update($endpointId, url: 'https://new-url.com/webhook');
// Update multiple fields
Webhooks::endpoints()->update(
endpoint: $endpointId,
url: 'https://new-url.com/webhook',
events: ['orders.created', 'orders.updated'],
description: 'Updated description',
enabled: false
);Webhooks::endpoints()->delete($endpointId);The manager validates all input automatically:
- URLs must be valid and start with
https:// - Event keys must exist in your webhook catalog
- Descriptions are optional but limited to 255 characters
Webhooks are signed with HMAC SHA256. Each endpoint has its own secret (viewable in the UI).
Verify on the receiving end:
function verifySignature(Request $request, string $secret): bool
{
$expected = hash_hmac('sha256', $request->getContent(), $secret);
return hash_equals($expected, $request->header('Signature'));
}For self-documenting events, implement the contract:
use Pyle\Webhooks\Contracts\WebhookSubscribableEvent;
class OrderCreated implements WebhookSubscribableEvent
{
public static function webhookEventKey(): string { return 'orders.created'; }
public static function webhookEventGroup(): string { return 'Orders'; }
public static function webhookEventLabel(): string { return 'Order created'; }
public static function webhookEventDescription(): ?string { return null; }
public function webhookPayload(): array
{
return ['order_id' => $this->order->id];
}
}Config becomes minimal:
'orders.created' => ['event' => \App\Events\OrderCreated::class],None. Webhooks are queued — your request returns immediately.
Eager load before dispatch, or use webhookPayload():
public function webhookPayload(): array
{
return $this->order->load('customer', 'items')->toArray();
}3 attempts with exponential backoff (configurable via tries). Listen to WebhookCallFailedEvent for monitoring.
Use ngrok, webhook.site, or set verify_ssl to false in dev.
it('dispatches webhook', function () {
Queue::fake();
event(new OrderCreated($order));
Queue::assertPushed(\Spatie\WebhookServer\CallWebhookJob::class);
});Toggle "Enabled" in the UI, or: WebhookEndpoint::query()->update(['enabled' => false])
| Problem | Check |
|---|---|
| Webhooks not delivered | Queue worker running? Endpoint enabled? Event subscribed? |
| HTTPS errors locally | Use a tunnel or disable verify_ssl in dev |
| Signature mismatch | Using raw body? Correct secret? Header is Signature? |
| Config not updating | Run php artisan config:clear |
- PHP 8.3+, Laravel 12, Livewire 3.4+, Flux UI
- Queue worker for production
composer testMIT. See license.md.