---
title: "The Webhooks facade — Webhooks for Laravel | Roundly"
description: "Tour the Webhooks facade — send, broadcast, for($owner), the inbound() and outbound() sub-accessors, the scheme and handler registries, prune and fake."
url: https://roundly-consulting.com/open-source/docs/webhooks-for-laravel/facade
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Webhooks for Laravel](https://roundly-consulting.com/open-source/docs/webhooks-for-laravel.md)

# The Webhooks facade

Everything goes through one facade, RoundlyConsulting\\Webhooks\\Facades\\Webhooks (global alias Webhooks), over the WebhooksManager. Flat verbs send; for($owner) scopes everything to one owner; inbound() and outbound() manage each direction:

```php
use RoundlyConsulting\Webhooks\DataTransferObjects\PruneOptions;
use RoundlyConsulting\Webhooks\Facades\Webhooks;

// Sending
Webhooks::send($endpoint, 'invoice.paid', ['invoice' => 42]);               // one endpoint
Webhooks::send($endpoint, 'invoice.paid', $payload, id: 'invoice-42-paid'); // idempotent
Webhooks::broadcast('system.maintenance', $payload);                        // owner-less endpoints only
Webhooks::for($team)->broadcast('invoice.paid', $payload);                  // every subscribed endpoint of $team

// The two directions — app-wide, or scoped to one owner
$created = Webhooks::inbound()->create(handler: 'billing');
$endpoints = Webhooks::for($team)->outbound()->endpoints();

// Registries, retention and testing
Webhooks::schemes()->extend('github', GitHubScheme::class);
Webhooks::handlers()->register('billing', BillingWebhookHandler::class);
$result = Webhooks::prune(new PruneOptions(dryRun: true));
$fake = Webhooks::fake();
```

## Manager methods

| Method | Returns | Does |
| --- | --- | --- |
| `send(OutboundEndpoint $endpoint, string $eventType, array\|Arrayable\|JsonSerializable $payload, ?string $id = null)` | `OutboundDelivery` | Queue one signed delivery; with $id a repeat returns the first delivery and queues nothing. |
| `broadcast(string $eventType, array\|Arrayable\|JsonSerializable $payload, ?Model $owner = null)` | `Collection<int, OutboundDelivery>` | Every enabled, subscribed endpoint of $owner; null means owner-less endpoints only. |
| `for(Model $owner)` | `Accessors\OwnerWebhooks` | A handle scoped to one owner: inbound(), outbound(), broadcast(), owner(). |
| `inbound()` | `Accessors\InboundWebhooks` | Receiving: endpoints, the pipeline, verification, replay, the log. |
| `outbound()` | `Accessors\OutboundWebhooks` | Sending: endpoints, send, ping, redelivery, the sweep, the log. |
| `schemes()` | `Accessors\SchemeRegistry` | The signature-scheme registry. |
| `handlers()` | `Accessors\HandlerRegistry` | The inbound handler registry. |
| `prune(?PruneOptions $options = null)` | `PruneResult` | Retention — what webhooks:prune runs. |
| `Webhooks::fake()` | `Testing\WebhooksFake` | Facade only — the recording fake, also bound in the container. |

Once Webhooks::fake() has run, the fake’s sent(), broadcasts(), redelivered(), replayed() and every assert…() are callable on the facade too — see Testing.

## inbound() — InboundWebhooks

| Method | Returns | Does |
| --- | --- | --- |
| `create(?string $handler = null, string $scheme = 'default', ?string $name = null, ?string $secret = null, ?InboundEndpointOptions $options = null, ?Model $owner = null)` | `CreatedInboundEndpoint` | A new endpoint: endpoint, url, secret (shown once). Unknown handler or scheme throws. |
| `update(InboundEndpoint $endpoint, InboundEndpointChanges $changes)` | `InboundEndpoint` | Name, handler, scheme, options; clear resets nullable fields. |
| `enable($endpoint) · disable($endpoint)` | `InboundEndpoint` | A disabled endpoint answers 404 like an unknown one. |
| `delete(InboundEndpoint $endpoint)` | `void` | Soft delete; the URL answers 404 and the secrets are wiped at once. |
| `rotateSecret(InboundEndpoint $endpoint, ?CarbonInterface $grace = null, ?string $secret = null)` | `RotatedSecret` | A new (or imported) secret; the old one verifies until the grace ends. |
| `find(int\|string $id) · endpoints()` | `InboundEndpoint · Collection` | Scoped handles see only their owner’s endpoints (EndpointNotFoundException). |
| `receive(string $key, Request $request)` | `Response` | The whole inbound pipeline — what the route runs. |
| `verify(InboundEndpoint $endpoint, Request $request)` | `VerifiedWebhook` | The scheme only: no size check, no dedupe, nothing stored (an RFC 9421 nonce is recorded at once). |
| `replay(InboundDelivery $delivery)` | `InboundDelivery` | Run the handler again on the stored payload, flagged as a replay. |
| `deliveries(?InboundEndpoint $endpoint = null)` | `Builder<InboundDelivery>` | The inbound log, as a query. |

## outbound() — OutboundWebhooks

| Method | Returns | Does |
| --- | --- | --- |
| `create(string $url, array $events = ['*'], ?string $name = null, array $headers = [], string $scheme = 'default', ?OutboundEndpointOptions $options = null, ?Model $owner = null)` | `CreatedOutboundEndpoint` | A new endpoint; the URL is judged by the SSRF guard. endpoint, secret (shown once). |
| `update(OutboundEndpoint $endpoint, OutboundEndpointChanges $changes)` | `OutboundEndpoint` | A new URL is judged again; new headers replace the old set. |
| `enable($endpoint) · disable($endpoint)` | `OutboundEndpoint` | Disable fires EndpointDisabled(manual) and cancels pending deliveries; enable resets the failure counter. |
| `delete(OutboundEndpoint $endpoint)` | `void` | Soft delete; pending deliveries cancelled; secrets, URL and custom headers wiped at once. |
| `rotateSecret(OutboundEndpoint $endpoint, ?CarbonInterface $grace = null)` | `RotatedSecret` | Both secrets sign during the grace. |
| `find(int\|string $id) · endpoints()` | `OutboundEndpoint · Collection` | The endpoints, owner eager-loaded. |
| `send($endpoint, string $eventType, $payload, ?string $id = null)` | `OutboundDelivery` | The same as Webhooks::send(). |
| `ping(OutboundEndpoint $endpoint)` | `OutboundDelivery` | A webhook.ping test event, whatever the endpoint subscribes to. |
| `redeliver(OutboundDelivery $delivery)` | `OutboundDelivery` | A new series under the same webhook-id. |
| `redeliverFailed(OutboundEndpoint $endpoint, ?CarbonInterface $since = null, bool $includeCancelled = false)` | `RedeliveryResult` | Every failed (optionally cancelled) delivery of the endpoint. |
| `dispatchDue()` | `int` | The sweep — what webhooks:retry runs; the number of jobs dispatched. |
| `deliveries(?OutboundEndpoint $endpoint = null)` | `Builder<OutboundDelivery>` | The outbound log, as a query. |
| `attempts(OutboundDelivery $delivery)` | `Collection<int, OutboundAttempt>` | Every attempt of every series, in order. |

## for($owner) — a boundary

Webhooks::for($owner) returns OwnerWebhooks with inbound(), outbound(), broadcast(string $eventType, $payload) and owner(). Its sub-accessors refuse another owner’s endpoint or delivery with EndpointNotFoundException, as if it did not exist; create() sets the owner, and find(), endpoints() and deliveries() see only that owner’s rows. The HasWebhooks trait’s webhooks() returns the same handle.

## schemes() and handlers() — the registries

| SchemeRegistry | HandlerRegistry |
| --- | --- |
| `extend(string $name, class-string\|Closure $scheme)` | `register(string $alias, class-string\|Closure $handler)` |
| `forget(string $name)` | `forget(string $alias)` |
| `names(): list<string>` | `aliases(): list<string>` |
| `has(string $name): bool` | `has(string $alias): bool` |
| `resolve(string $name): SignatureScheme` | `resolve(string $alias): InboundHandler` |
| `^[a-z0-9][a-z0-9_.-]{0,63}$` | `^[a-z0-9][a-z0-9_.-]{0,99}$` |
| `UnknownSchemeException` | `UnknownHandlerException` |

A scheme closure is a factory (it returns the scheme); a handler closure is the handler. Classes resolve through the container, so constructor injection works. Both registries are process-wide: register in a service provider’s boot() and they hold for every request, every job a queue worker runs and every Octane request. A registration made inside a request or job lasts for the rest of the process too, like Gate::define(). forget() removes a run-time registration; a configured one stays.

## The route macro

Route::webhooks(string $prefix = 'webhooks', array $middleware = \[\]): Route mounts the inbound pipeline on a route of your own — see Receiving webhooks.

## Data objects

In RoundlyConsulting\\Webhooks\\DataTransferObjects, all final readonly:

| Class | Fields and methods |
| --- | --- |
| `CreatedInboundEndpoint` | endpoint, url, secret — the secret is redacted in print\_r, var\_dump and json\_encode. |
| `CreatedOutboundEndpoint` | endpoint, secret (redacted). |
| `RotatedSecret` | secret (redacted), graceEndsAt (CarbonImmutable). |
| `InboundEndpointOptions` | ?rateLimitPerMinute, ?maxPayloadBytes, ?contentTypes, ?toleranceSeconds, ?schemeOptions |
| `InboundEndpointChanges` | ?name, ?handler, ?scheme, ?options, list<string> clear |
| `OutboundEndpointOptions` | ?rateLimitPerMinute, ?schemeOptions, ?description |
| `OutboundEndpointChanges` | ?url, ?events, ?name, ?headers, ?scheme, ?options, list<string> clear |
| `InboundWebhook` | deliveryId, endpoint, ?type, payload, headers, receivedAt, isReplay, run, attempt; get(), header(), owner(), isFirstAttempt() |
| `IncomingWebhook` | headers (lower-cased, first value), body, receivedAt, endpointKey, toleranceSeconds, schemeOptions, ?request; header() |
| `VerifiedWebhook` | ?deliveryId, ?timestamp, ?eventType, idIsSigned, slot, replayProtected = false, ?commit; committed() |
| `OutgoingWebhook` | id (msg\_…), timestamp, body, eventType |
| `SignatureHeaders` | headers (array<string, string>) |
| `SanitizeContext` | direction, endpoint, ?eventType |
| `PruneOptions · PruneResult` | dryRun = false, ?now · inboundPayloads, outboundBodies, attemptBodies, inboundDeliveries, outboundDeliveries, attempts, expiredSecrets, endpoints, dryRun; total() |
| `RedeliveryResult` | queued, skippedPruned |
| `DoctorFinding` | level, check, ?detail; message(), error(), warning() |
| `SignedRequestData` | uri, headers, body; server() |

## Enums

In RoundlyConsulting\\Webhooks\\Enums, string-backed, with translated labels (webhooks::enums.\*) and the enums-for-laravel helpers — label(), labels(), values(), options(), fromLabel(), is(). Values are the snake\_case of the case name:

| Enum | Values |
| --- | --- |
| `InboundStatus` | received, queued, processing, processed, failed |
| `OutboundStatus` | pending, sending, retrying, succeeded, failed, cancelled |
| `RejectionReason` | not\_found … sanitizer\_failed — status(): int, publicCode(): string (endpoint\_disabled reads not\_found) |
| `AttemptError` | http\_status, redirect\_not\_followed, timeout, connection, tls, unresolved, blocked, cannot\_pin, too\_many\_redirects, redirect\_downgrade, signing, worker\_lost, unexpected |
| `DisableReason` | consecutive\_failures, gone, manual |
| `DuplicateResponse` | accept, conflict |
| `SecretSlot` | current, previous |
| `SecretEncoding` | standard\_webhooks, raw |
| `Direction` | inbound, outbound |
| `FindingLevel` | error, warning |

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/webhooks-for-laravel.md)
