Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Webhooks Laravel Package

liberusoftware/webhooks

A Laravel package for handling webhooks, offering tools to receive, verify, and dispatch webhook requests between your application and external services with a simple, framework-friendly API.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a pre-1.0-community package with no dependents — treat the README as the contract you verify against the code before relying on it. Budget an hour to read src/, config/, and the migrations before writing a single line of application code.

Install and publish:

composer require liberusoftware/webhooks
php artisan vendor:publish --provider="Liberu\Webhooks\WebhooksServiceProvider"
php artisan migrate

Expect the publish step to drop a config file (config/webhooks.php) and a set of migrations covering, at minimum: registered endpoints, per-endpoint subscriptions/filters, delivery attempts (the "evidence" store), and logs. Check the migration filenames first — they tell you the real domain model, which is usually more accurate than the docs.

Where to look first, in order:

  1. config/webhooks.php — signing algorithm, secret length, retry/backoff schedule, timeout, queue connection, retention.
  2. The migrations — names of the endpoint and delivery tables, and whether deliveries are keyed by a UUID (they almost always are, and that's your replay handle).
  3. The service provider bindings — what's bound as a singleton vs. resolved per call; this determines whether config changes take effect at runtime.
  4. The signing class — confirm the exact string that gets hashed (body only? timestamp + body? method + URL + body?).

First use case: register an endpoint for an internal event, dispatch it, and read the delivery evidence row.

$endpoint = WebhookEndpoint::create([
    'url' => 'https://consumer.test/hooks/orders',
    'secret' => config('webhooks.secret') ?? Str::random(48),
    'events' => ['order.created', 'order.refunded'],
    'active' => true,
]);

// then, wherever the domain event fires
Webhook::dispatch('order.created', $order->toWebhookPayload(), $endpoint);

The payoff of this package over a hand-rolled Http::post is rows in the delivery table: status code, response body, attempt number, next retry time, and signature used. Get one of those rows visible (via Tinker or a quick Blade page) before going further. Everything else — replay, rotation, backoff — is an operation on that row.

Implementation Patterns

Dispatch from listeners, not models. Keep webhook dispatch out of booted() model hooks and out of controllers. A queued listener gives you retries, ordering, and a place to shape the payload independently of your schema:

class SendOrderCreatedWebhook implements ShouldQueue
{
    public $afterCommit = true;

    public function handle(OrderCreated $event): void
    {
        Webhook::event('order.created')
            ->payload(OrderPayload::from($event->order)->toArray())
            ->toSubscribers();   // fan-out to every active endpoint filtering on this event
    }
}

afterCommit matters: dispatch before the transaction commits and a rolled-back order still produces a webhook.

Fan-out vs. targeted sends. Build two call sites — ->toSubscribers('order.created') for broadcast semantics, and ->to($endpoint) for one-off/manual sends (support tooling, backfills). The second is what you'll use constantly during integration with a customer.

Filtering belongs at registration, not at dispatch. Register narrow event subscriptions per endpoint rather than sending everything and letting the consumer ignore it. If the package supports payload-level filters (field matches), push them into the endpoint record — it keeps the delivery log meaningful and makes "why didn't I get this?" answerable by looking at one row.

Signing and verification. The receiver-side check is the part you actually have to get right:

$raw = $request->getContent();
$expected = hash_hmac('sha256', $request->header('X-Webhook-Timestamp').'.'.$raw, $secret);

abort_unless(hash_equals($expected, $request->header('X-Webhook-Signature')), 401);
abort_if(abs(time() - (int) $request->header('X-Webhook-Timestamp')) > 300, 401);

Always sign and verify the raw body (getContent()), never $request->all() re-encoded — JSON key ordering and float formatting will differ and every signature will fail. Verify against the timestamp too, or a captured request is replayable forever.

Consume the evidence store as an operational queue. The delivery table is the thing you'll lean on daily:

  • Failed + next_attempt_at in the past → the retry worker isn't running.
  • Repeated 4xx → the customer's endpoint is misconfigured; stop retrying sooner than you would on 5xx.
  • Repeated 5xx / timeouts → their problem, keep backing off, and surface it in a dashboard.

If the package doesn't ship a pruning command, write one; delivery logs grow at the rate of events × endpoints and are the single biggest source of table bloat in webhook systems.

Replay deliberately. Replay from the evidence row (by delivery UUID), not by re-firing the domain event — re-firing re-runs your payload shaping and can produce a different body than the customer originally saw. Two distinct tools:

Webhook::replay($delivery->id);                   // resend exact stored payload
Webhook::replayRange($endpoint, $from, $to);      // backfill after an outage

Rate-limit bulk replays. A thousand-row replay against a struggling consumer is indistinguishable from an attack.

Rotation with an overlap window. Rotate by writing the new secret, keeping the old one valid for verification/accepted-signature purposes for a bounded window (24–72h), notifying the consumer, then dropping the old. If the package models secrets as a list per endpoint, this is free — check for that. Otherwise, add a previous_secret and rotated_at column yourself rather than doing a hard swap.

Backoff should be tuned per endpoint class, not globally. The default schedule (something like 1m, 5m, 30m, 2h, 6h, 24h over ~48h) is a reasonable baseline for external customers. Internal services consuming your webhooks should be on a much tighter, shorter schedule. If backoff lives only in config, that's a reason to run more than one queue/connection rather than fighting the config.

Testing. Http::fake() covers the sending side, but the interesting assertions are on persisted state:

Http::fake(['consumer.test/*' => Http::response('ok', 200)]);

Webhook::dispatch('order.created', $payload, $endpoint);

$this->assertDatabaseHas('webhook_deliveries', [
    'endpoint_id' => $endpoint->id,
    'status' => 'delivered',
]);

Also add a signature-verification contract test against a fixed payload + secret, so a package upgrade that changes the signing string fails loudly in CI rather than silently in production.

Gotchas and Tips

  • Verify the package before you build on it. Zero stars, zero dependents, a release date in the future relative to typical index snapshots. Pin the version exactly ("liberusoftware/webhooks": "0.x.y"), commit the lockfile, and read src/ for anything you depend on. If the migrations get rewritten between patch releases, you'll find out the hard way.
  • The signing string is the #1 integration failure. Document it explicitly for consumers, and don't change it once anyone has integrated without a versioned signature header.
  • Queue timeouts vs. HTTP timeouts. Set the package's HTTP timeout well below your queue worker's --timeout, or slow consumers will get the worker killed mid-delivery and produce duplicate sends. Same for retry_after — it must exceed the HTTP timeout.
  • Delivery is at-least-once, never exactly-once. Tell your consumers to dedupe on the delivery UUID or an idempotency key. Put that in the header and in the docs; it's not a bug you can fix.
  • Ordering isn't guaranteed once retries kick in. A failed order.updated that retries can land after order.deleted. Include a sequence number or timestamp in the payload and let consumers reconcile.
  • Serialization bites. $model->toArray() leaks internal columns (and sometimes relations) to third parties. Always pass through an explicit payload class or resource. Watch Carbon serialization format changes across Laravel versions if you sign the body — the signed bytes must match what's sent.
  • SSRF surface. Endpoint URLs are user-supplied. Block private ranges, localhost, and cloud metadata IPs (169.254.169.254) at registration and at send time, and disable redirects.
  • Secret storage. Secrets should be encrypted casts, not plaintext, and never rendered back into an admin UI after creation. Check whether the package's model does this; if not, wrap it.
  • Config caching. php artisan config:cache in production means anything you mutate at runtime (per-tenant signing config, for example) won't take effect. Keep such settings in the database, resolved per call.
  • Extension points to look for: a contract for the HTTP client, an interface for payload shaping, and an event fired on delivery success/failure. The failure event is the one worth wiring first — it's how you get alerting without polling the delivery table.
  • Retention. Add a scheduled command to prune delivered rows older than 30–90 days, but retain failures longer (they're your audit trail when a customer disputes a webhook). Replay requires the payload, so if you prune, prune by age and success status.
  • Observability. Tag delivery metrics by event name and status class, not by endpoint URL — URL cardinality will explode your metrics bill.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib