Looking to hire Laravel developers? Try LaraJobs

webhooks-for-laravel maintained by pushery

Description
Customer-configurable outgoing webhooks for Laravel — endpoint subscriptions, signed delivery with retries, and a searchable delivery log.
Last update
2026/07/02 16:54 (dev-main)
License
Downloads
0
Tags

Comments
comments powered by Disqus

Webhooks for Laravel

Latest Version PHP Version PHPStan Code Style License

Customer-configurable outgoing webhooks for Laravel. Your customers register endpoints for the event types they care about; the package fans each event out to every matching endpoint, signs it, retries it with backoff, and keeps a searchable, partitioned delivery log with test-ping and one-click redelivery.

It builds on spatie/laravel-webhook-server (which sends a single signed HTTP call with retries) and adds the parts that make it a product: the subscription model, the delivery log, the event catalog, a versioned Stripe-style signature with secret rotation, an SSRF guard, a circuit breaker, and an optional management UI.

It is not an inbound webhook handler (for that, see spatie/laravel-webhook-client).

Requirements

  • PHP 8.4+ with ext-curl
  • Laravel 13
  • PostgreSQL 13+ (the delivery log uses jsonb, GIN indexes and declarative range partitioning)
  • A queue worker is required; Redis is recommended so retry backoff does not block other work

Installation

composer require pushery/webhooks-for-laravel

Publish the config and migrations, then migrate:

php artisan vendor:publish --tag=webhooks-config
php artisan vendor:publish --tag=webhooks-migrations
php artisan migrate

Quickstart

1. Describe the events your application can emit in config/webhooks.php:

'catalog' => [
    'invoice.paid' => [
        'description' => 'Fired when an invoice is paid in full.',
        'example' => ['invoice_id' => 'in_123', 'amount' => 4200],
    ],
],

2. Register an endpoint. The URL is SSRF-validated and a signing secret is generated:

use Webhooks\Facades\Webhooks;

$subscription = Webhooks::subscribe(
    owner: $team,                       // any Eloquent model, or null for a global endpoint
    url: 'https://example.com/webhooks',
    eventTypes: ['invoice.paid'],
);

$subscription->secret; // show this to the customer once — it signs their deliveries

3. Emit an event. It fans out to every active endpoint listening for the type:

use Webhooks\WebhookEvent;

WebhookEvent::dispatch('invoice.paid', ['invoice_id' => 'in_123', 'amount' => 4200], tenant: $team);

Each subscriber receives a signed POST with this JSON body:

{
  "id": "0192...-uuid",
  "type": "invoice.paid",
  "created_at": "2026-07-01T12:00:00+00:00",
  "data": { "invoice_id": "in_123", "amount": 4200 }
}

The id is stable across redeliveries, so consumers can deduplicate on it.

Verifying the signature

Every request carries an HMAC-SHA256 signature over "{timestamp}.{rawBody}" in the Webhook-Signature header, Stripe-style:

Webhook-Signature: t=1720000000,v1=5257a869e7...

A Laravel consumer can verify it with the shipped helper:

use Webhooks\Signing\SignatureVerifier;

$valid = SignatureVerifier::verify(
    header: $request->header('Webhook-Signature'),
    body: $request->getContent(),   // the RAW request body
    secret: $endpointSecret,
);

abort_unless($valid, 400);

In any language: split the header on ,, read t and each v1, reject if t is older than your tolerance (default 300s), then compare hmac_sha256("{t}.{body}", secret) against each v1 in constant time. During a secret rotation more than one v1 is present — accept the request if any one verifies. Deliveries are re-signed at send time, so queue latency never expires a legitimate signature.

Security (SSRF)

Webhook URLs are attacker-influenced, so every endpoint is validated when it is registered and again immediately before each delivery, with the connection pinned to the validated IP address so a rebinding DNS record cannot redirect it elsewhere. Private, loopback, link-local, unique-local, carrier-grade-NAT, multicast and cloud-metadata (169.254.169.254) addresses are refused, redirects are not followed, and TLS verification stays on. Configure endpoints.https_only, allowed_hosts and blocked_hosts in config/webhooks.php. IPv6-only endpoints are refused (fail-closed).

Reliability

  • Retries & backoff are handled by spatie/laravel-webhook-server (configure delivery.tries, delivery.timeout).
  • Circuit breaker: after circuit_breaker.threshold consecutive final failures an endpoint is auto-disabled and a Webhooks\Events\WebhookEndpointAutoDisabled event is fired; a single success resets the counter.
  • Events you can listen for (no dependency is added — broadcast them over Reverb for a live dashboard if you like): WebhookDeliverySucceeded, WebhookDeliveryFailed, WebhookEndpointAutoDisabled.
  • Per-endpoint rate limit (rate_limit.max_per_minute) stops one slow endpoint from starving the queue.
  • Horizon tags: each delivery job is tagged with its subscription and event type.

Payload validation (optional)

Give an event type a JSON Schema in the catalog and enable validate_payloads, and every dispatched payload is checked against it before any delivery is created — a malformed event never reaches a subscriber:

// config/webhooks.php
'catalog' => [
    'invoice.paid' => [
        'description' => 'Fired when an invoice is paid in full.',
        'schema' => [
            'type' => 'object',
            'required' => ['invoice_id', 'amount'],
            'properties' => [
                'invoice_id' => ['type' => 'string'],
                'amount' => ['type' => 'integer', 'minimum' => 1],
            ],
            'additionalProperties' => false,
        ],
    ],
],
'validate_payloads' => true,

A payload that does not satisfy the schema throws Webhooks\Exceptions\InvalidPayloadException, whose ->errors holds the formatted violations. Event types without a schema (and every event while validate_payloads is false) pass through unchecked, so the catalog stays a pure documentation aid until you opt a type in. Validation uses opis/json-schema.

Secret rotation

Set a subscription's previous_secret alongside a new secret; both are signed (two v1= values) so customers can update at their own pace, then clear previous_secret.

Delivery log & retention

Deliveries are stored in a monthly range-partitioned table. The scheduled command webhooks:partition-maintenance (registered to run daily) provisions upcoming partitions and drops those older than retention_months — a cheap metadata operation instead of a bulk DELETE. Ensure your scheduler is running.

Management UI (optional)

The package ships optional Livewire management screens as publishable stubs. Register the provider (it is not auto-registered, so the core stays headless):

// bootstrap/providers.php
Webhooks\WebhooksUiServiceProvider::class,

Then embed the components in your own authorized, branded pages:

<livewire:webhooks-subscriptions />
<livewire:webhooks-deliveries />

They require livewire/livewire. Publish the Blade stubs and restyle them to match your app — the stubs ship in two variants, publish exactly one:

composer require livewire/livewire

# Neutral Tailwind stubs — a blank canvas to restyle with any design system:
php artisan vendor:publish --tag=webhooks-ui

# …or stubs already built from Pushery's own design system, WireKit:
php artisan vendor:publish --tag=webhooks-ui-wirekit

Both variants render the same two components and publish to the same resources/views/vendor/webhooks/livewire path, so you own and restyle them from there. The WireKit variant needs pushery/wirekit installed and its @source included in your Tailwind build so the component utilities compile.

Configuration

Every option is documented inline in config/webhooks.php: the event catalog, delivery (tries/timeout/queue), signature header + tolerance, circuit breaker, rate limit, SSRF endpoint rules, retention, and Horizon tags.

Testing

composer test

Security

Please review the security policy and report vulnerabilities privately rather than opening a public issue.

Built by Pushery

This package is built and maintained by Pushery — a Berlin-based studio building Laravel applications, SaaS products, and open-source tools.

Building a Laravel UI? WireKit, Pushery's open-source Livewire component kit, gives you a polished component library out of the box. Browse the rest of our work at pushery.com.

Versioning

This package follows Semantic Versioning.

License

The MIT License (MIT). See LICENSE for details.