bambamboole / laravel-webhooks
Attribute-driven outgoing webhooks for Laravel: discover webhook events, manage subscriptions, deliver signed payloads.
Requires
- php: ^8.4
- illuminate/database: ^13.0
- illuminate/support: ^13.0
- spatie/laravel-package-tools: ^1.16 || ^2.0
- spatie/laravel-webhook-server: ^3.10
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/boost: ^2.4
- laravel/pint: ^1.16
- orchestra/testbench: ^11.0
- pestphp/pest: ^5.0
- pestphp/pest-plugin-laravel: ^5.0
- pestphp/pest-plugin-phpstan: ^5.0
- rector/rector: ^2.4
This package is auto-updated.
Last update: 2026-08-01 23:12:37 UTC
README
Attribute-driven outgoing webhooks for Laravel: annotate event classes, manage subscriptions in the database, and deliver signed payloads through spatie/laravel-webhook-server.
Installation
composer require bambamboole/laravel-webhooks
Delivery goes through spatie/laravel-webhook-server, which is
installed as a dependency. The webhook_subscriptions and webhook_deliveries migrations run automatically. Publish
the config if you need to change defaults:
php artisan vendor:publish --tag=laravel-webhooks-config
Defining webhook events
Annotate any class with #[WebhookEvent] and give it a payload method. By default the package scans app/Events;
adjust webhooks.scan_paths in the config to change that.
use Bambamboole\LaravelWebhooks\Attributes\WebhookEvent; #[WebhookEvent( name: 'invoice.paid', title: 'Invoice paid', summary: 'Sent when an invoice is paid.', tags: ['billing'], )] final class InvoicePaid { public function __construct(public int $invoiceId, public int $amount) {} /** * @return array{invoiceId:int, amount:int} */ public function webhookPayload(): array { return [ 'invoiceId' => $this->invoiceId, 'amount' => $this->amount, ]; } }
Every delivery wraps the payload in a stable envelope:
{
"id": "9d3cā¦",
"event": "invoice.paid",
"createdAt": "2026-07-03T12:34:56.000000Z",
"data": {
"invoiceId": 987,
"amount": 6500
}
}
WebhookEventRegistry::all() returns the discovered definitions, which an app can use to power its own webhook
setup UI:
use Bambamboole\LaravelWebhooks\WebhookEventRegistry; $events = collect(app(WebhookEventRegistry::class)->all()) ->map(fn ($event) => [ 'name' => $event->name, 'title' => $event->title, 'summary' => $event->summary, ]);
Managing subscriptions
Subscriptions live in the webhook_subscriptions table via the shipped Eloquent model. events holds the subscribed
event names; '*' subscribes to everything. Secrets are encrypted at rest and used to sign deliveries.
use Bambamboole\LaravelWebhooks\Models\WebhookSubscription; WebhookSubscription::create([ 'name' => 'Billing system', 'url' => 'https://example.com/webhooks', 'secret' => 'signing-secret', 'headers' => ['X-Tenant' => 'acme'], 'events' => ['invoice.paid', 'invoice.refunded'], ]);
To source subscriptions from somewhere else, bind your own repository ā the database repository is only a default
(bindIf):
use Bambamboole\LaravelWebhooks\WebhookSubscription; use Bambamboole\LaravelWebhooks\WebhookSubscriptionRepository; final class CustomWebhookSubscriptionRepository implements WebhookSubscriptionRepository { public function forEvent(string $eventName, object $event): iterable { yield new WebhookSubscription( url: 'https://example.com/webhooks', secret: 'signing-secret', headers: ['X-Webhook-Source' => 'app'], id: 'subscription-123', ); } }
Dispatching
Webhook event classes are regular Laravel events ā fire them and the package delivers them. Listeners for every
discovered #[WebhookEvent] class are registered automatically:
event(new InvoicePaid(invoiceId: 987, amount: 6500));
Discovery scans the configured paths at every boot. If that cost matters to you, set webhooks.auto_listen to
false and wire the dispatcher yourself:
use Bambamboole\LaravelWebhooks\DispatchWebhookEvent; use Illuminate\Support\Facades\Event; Event::listen(InvoicePaid::class, DispatchWebhookEvent::class);
Each active subscription for the event receives one signed call through spatie's queue-backed webhook server. Calls
are signed with the subscription secret (unsigned when no secret is set) and include a timestamp against replay
attacks (webhooks.dispatcher.use_timestamp).
Delivery log
Every call attempt is recorded in the webhook_deliveries table (UUIDv7 keys): event name, url, payload, attempt,
status, HTTP response status, and error details. Retries of the same call share a call_uuid. The status is
succeeded, failed (a retry follows), or final_failed (retries exhausted).
use Bambamboole\LaravelWebhooks\Models\WebhookDelivery; WebhookDelivery::where('status', WebhookDelivery::STATUS_FAILED)->latest()->get(); $delivery->subscription; // the WebhookSubscription it was delivered to, if any
Calls dispatched through spatie's webhook server directly (without this package's dispatcher) are not recorded.
The log is prunable: rows older than webhooks.deliveries.prune_after_days (default 30, null keeps them forever)
are removed when your app schedules pruning:
Schedule::command('model:prune', ['--model' => [WebhookDelivery::class]])->daily();
Development
composer test # pest composer check # pint + phpstan + rector + pest composer serve # workbench app