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

Webhook Laravel Package

laravel-notification-channels/webhook

The Webhook notification channel lets you deliver Laravel notifications as HTTP POST requests to any URL. Define payloads with a simple toWebhook() method and send JSON to external services or custom endpoints using Laravel's standard notification API.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up

composer require laravel-notification-channels/webhook

The package is auto-discovered; there is nothing to publish. As of 2.7.0 it supports Laravel 12.x (2.6.0 covered earlier versions), so check the constraint if you are on an older LTS — Composer will resolve the right tag for you, but pin deliberately (see Gotchas). The whole surface area is two classes — WebhookChannel and WebhookMessage — so the fastest way to get oriented is to open vendor/laravel-notification-channels/webhook/src/ and read both files. They are short, and doing this once will answer most API questions faster than the README.

The first use case: notify an external system that something happened

Add the channel to an existing (or new) notification:

use NotificationChannels\Webhook\WebhookChannel;
use NotificationChannels\Webhook\WebhookMessage;

class InvoicePaid extends Notification implements ShouldQueue
{
    public function __construct(public Invoice $invoice) {}

    public function via($notifiable): array
    {
        return [WebhookChannel::class];
    }

    public function toWebhook($notifiable): WebhookMessage
    {
        return WebhookMessage::create()
            ->url($notifiable->webhook_url)
            ->headers(['X-Signature' => $this->signature()])
            ->data([
                'event'      => 'invoice.paid',
                'invoice_id' => $this->invoice->id,
                'amount'     => $this->invoice->amount,
            ]);
    }
}

Then $user->notify(new InvoicePaid($invoice));. That is the entire day-to-day loop: pick an event, build a payload, POST it.

Where to look first when something is off

  1. Does toWebhook() return a message with a URL — either set via ->url() or resolved from the notifiable?
  2. Is the URL correct for the notifiable instance (on-demand notifications and collections will not have a route method)?
  3. Is the payload JSON-serializable (no Eloquent models, no Carbon without toIso8601String())?
  4. Is the notification queued? If not, you are blocking the request on a third-party HTTP call.

Implementation Patterns

Two ways to route, and you should pick one per project

  • Message-level URL (->url(...)) — good for a fixed endpoint: your own internal event bus, a Zapier catch hook, a Discord/Slack webhook.
  • Notifiable-level route — implement routeNotificationForWebhook() on the model (e.g. return $this->webhook_url;) and let the channel resolve it. This is the pattern for multi-tenant SaaS where each customer registers their own endpoint.

Mixing both is where confusion starts. Adopt a convention: message-level URL for internal, notifiable route for customer endpoints.

Model the endpoint, don't scatter URLs

class WebhookEndpoint extends Model
{
    protected $casts = ['is_active' => 'boolean', 'secret' => 'encrypted'];

    public function tenant() { return $this->belongsTo(Tenant::class); }
}

Store URL, secret, and an is_active flag. Rotating a secret or disabling a broken endpoint becomes an update, not a deploy. When an endpoint is inactive, short-circuit via() to return [].

Always queue, and configure retries explicitly

class InvoicePaid extends Notification implements ShouldQueue
{
    public $tries = 5;
    public $backoff = [10, 60, 300];

    public function failed($e) { /* log, mark endpoint degraded */ }
}

The channel itself does not retry — retries are a property of the queued notification. Without ShouldQueue, a slow receiver blocks your web request and a 500 silently drops the event.

Sign your payloads yourself

The package posts JSON; it does not sign anything. If receivers verify authenticity (Stripe-style), build an HMAC in the notification and send it as a header:

$body = json_encode($this->payload());
$sig  = hash_hmac('sha256', $body, $endpoint->secret);

Add an X-Idempotency-Key (event id + timestamp) so receivers can dedupe retries.

Testing — two complementary layers

// 1. Assert the notification carried the right message
Notification::fake();
$user->notify(new InvoicePaid($invoice));
Notification::assertSentTo($user, InvoicePaid::class, function ($n) use ($user, $invoice) {
    $msg = $n->toWebhook($user);
    return $msg->payload['invoice_id'] === $invoice->id;
});
// 2. Assert the outbound HTTP call (recent versions use Laravel's HTTP client)
Http::fake();
$user->notify(new InvoicePaid($invoice));
Http::assertSent(fn ($req) => $req->url() === $user->webhook_url
    && $req['event'] === 'invoice.paid');

The first layer is fast and stable; the second catches header/URL regressions. Use Http::fake() in feature tests so CI never touches a real endpoint.

Local development

Point webhook_url at a request bin (webhook.site, or ngrok http 8000 to a local route that dumps $request->all()). You will rebuild the payload shape several times before it is right — do not do that against a real consumer.

Gotchas and Tips

Silent failures are the default. Laravel's HTTP client does not throw on 4xx/5xx unless ->throw() is called. Unless you have verified the channel throws, treat a 200-less response as an event you lost. Wrap sends in a delivery log table (notification, endpoint, status, response_body, attempt) so you can replay. This is the single most valuable thing you can add on top of the package.

Timeouts. The default HTTP timeout is generous. A hung receiver will occupy a queue worker for a long time. If the message class exposes a timeout, set it (5–10s); if not, set a global Http::globalOptions(['timeout' => 10]) in a service provider.

SSL verification. There is usually a ->verify(false) escape hatch. It exists for local self-signed receivers. Never ship it to production; if you find it in a diff, that is a bug.

Do not send models. ->data($invoice) will attempt to serialize the model and either drop attributes or blow up. Always pass a flat, explicit array. Cast dates with ->toIso8601String() and money as cents/int, not floats.

Payload shape is your contract. Once a customer integrates, the field names and types are frozen. Version your events ("event": "invoice.paid", "v": 1) from day one; it costs nothing now and saves a migration later.

Slack/Discord/Teams need their own payload shapes. This channel is generic JSON — it will not format a Slack attachment for you. You either hand-build {"text": ...} / {"embeds": [...]} in toWebhook(), or use the provider-specific channel (laravel-notification-channels/slack, etc.) for those targets and keep this one for custom endpoints.

Version constraints and maturity. 2.7.0 is the current release and its only change is Laravel 12.x compatibility — a good signal the package is still maintained, but it also tells you the cadence is maintenance-only, not feature-driven. Pin to ^2.7 (not ^1.x, which is now two majors behind and will not resolve on Laravel 12). The API is still thin — two classes, modest community, no dependents to lean on — so budget an afternoon to read the source before making it load-bearing. The upside: it is small enough to fork or replace with a ~40-line custom channel if the package ever stalls again.

Extension point. Because the channel resolves the URL and message from your notification, all your logic belongs in toWebhook() and a small builder if you have many event types:

abstract class BaseWebhookNotification extends Notification implements ShouldQueue
{
    abstract protected function eventName(): string;
    abstract protected function payload(): array;

    public function toWebhook($notifiable): WebhookMessage
    {
        return WebhookMessage::create()
            ->url($notifiable->routeNotificationForWebhook())
            ->headers(['X-Event' => $this->eventName()])
            ->data(['event' => $this->eventName()] + $this->payload());
    }
}

That keeps every concrete notification to two small methods and puts signing, headers, and retry config in one place.

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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo