NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Webhooks for Laravel

DI and actions

The facade is the recommended default, not the only way in. RoundlyConsulting\Webhooks\WebhooksManager is the facade’s root — a scoped binding you can inject — and every facade call ends in one action. All three styles run the same code:

// Send an event
Webhooks::send($endpoint, 'invoice.paid', ['invoice' => 42]);
$webhooks->send($endpoint, 'invoice.paid', ['invoice' => 42]);                       // WebhooksManager $webhooks, injected
app(SendWebhookAction::class)->execute($endpoint, 'invoice.paid', ['invoice' => 42]);

// Create an inbound endpoint
$created = Webhooks::inbound()->create(handler: 'billing');
$created = $webhooks->inbound()->create(handler: 'billing');
$created = app(CreateInboundEndpointAction::class)->execute(null, 'billing');

// Run the inbound pipeline (what the route does)
$response = Webhooks::inbound()->receive($key, $request);
$response = $webhooks->inbound()->receive($key, $request);
$response = app(ReceiveWebhookAction::class)->execute($key, $request);

// Redeliver
Webhooks::outbound()->redeliver($delivery);
$webhooks->outbound()->redeliver($delivery);
app(RedeliverWebhookAction::class)->execute($delivery);

Inject the manager

use RoundlyConsulting\Webhooks\Models\OutboundDelivery;
use RoundlyConsulting\Webhooks\Models\OutboundEndpoint;
use RoundlyConsulting\Webhooks\WebhooksManager;

final readonly class InvoiceWebhooks
{
    public function __construct(private WebhooksManager $webhooks) {}

    public function paid(OutboundEndpoint $endpoint, int $invoice): OutboundDelivery
    {
        return $this->webhooks->send($endpoint, 'invoice.paid', ['invoice' => $invoice], id: "invoice-{$invoice}-paid");
    }
}

The fake extends the manager, so an injected WebhooksManager is the fake too under Webhooks::fake(). The manager’s other public methods (createInboundEndpoint(), rotateOutboundSecret(), redeliverFailed() and the rest) are internal — the sub-accessors call them so the fake sees every call. Use the sub-accessors.

Call an action

use RoundlyConsulting\Webhooks\Actions\Inbound\CreateInboundEndpointAction;
use RoundlyConsulting\Webhooks\Actions\Outbound\SendWebhookAction;

app(SendWebhookAction::class)->execute($endpoint, 'invoice.paid', ['invoice' => 42]);
app(CreateInboundEndpointAction::class)->execute($team, 'billing');   // the owner comes first

Every host-facing action is final readonly, resolved from the container, with one execute(). The actions skip the fake and the owner scope — they take what you hand them — so use them to compose your own use cases, and the facade or the manager everywhere else.

Facade method → action

Namespaces are relative to RoundlyConsulting\Webhooks\Actions:

Facade methodActionexecute()
inbound()->create()Inbound\CreateInboundEndpointAction?Model $owner = null, ?string $handler = null, string $scheme = 'default', ?string $name = null, ?string $secret = null, ?InboundEndpointOptions $options = null
inbound()->update()Inbound\UpdateInboundEndpointActionInboundEndpoint $endpoint, InboundEndpointChanges $changes
inbound()->enable() · disable()Inbound\SetInboundEndpointStateActionInboundEndpoint $endpoint, bool $enabled
inbound()->delete()Inbound\DeleteInboundEndpointActionInboundEndpoint $endpoint
inbound()->rotateSecret()Inbound\RotateInboundSecretActionInboundEndpoint $endpoint, ?CarbonInterface $grace = null, ?string $secret = null
inbound()->receive()Inbound\ReceiveWebhookActionstring $key, Request $request
inbound()->verify()Inbound\VerifyInboundRequestActionInboundEndpoint $endpoint, Request $request, ?string $body = null, bool $deferCommit = false
inbound()->replay()Inbound\ReplayInboundDeliveryActionInboundDelivery $delivery
outbound()->create()Outbound\CreateOutboundEndpointActionstring $url, array $events = ['*'], ?string $name = null, array $headers = [], string $scheme = 'default', ?OutboundEndpointOptions $options = null, ?Model $owner = null
outbound()->update()Outbound\UpdateOutboundEndpointActionOutboundEndpoint $endpoint, OutboundEndpointChanges $changes
outbound()->enable() · disable()Outbound\SetOutboundEndpointStateActionOutboundEndpoint $endpoint, bool $enabled
outbound()->delete()Outbound\DeleteOutboundEndpointActionOutboundEndpoint $endpoint
outbound()->rotateSecret()Outbound\RotateOutboundSecretActionOutboundEndpoint $endpoint, ?CarbonInterface $grace = null
send()Outbound\SendWebhookActionOutboundEndpoint $endpoint, string $eventType, array|Arrayable|JsonSerializable $payload, ?string $id = null
broadcast() · for($owner)->broadcast()Outbound\BroadcastWebhookActionstring $eventType, array|Arrayable|JsonSerializable $payload, ?Model $owner = null
outbound()->ping()Outbound\PingEndpointActionOutboundEndpoint $endpoint
outbound()->redeliver()Outbound\RedeliverWebhookActionOutboundDelivery $delivery
outbound()->redeliverFailed()Outbound\RedeliverFailedDeliveriesActionOutboundEndpoint $endpoint, ?CarbonInterface $since = null, bool $includeCancelled = false
outbound()->dispatchDue()Outbound\DispatchDueDeliveriesAction—
prune()PruneAction?PruneOptions $options = null

Two notes: CreateInboundEndpointAction takes the owner first, and VerifyInboundRequestAction also accepts the raw body when it was already read and $deferCommit — true hands you the verdict’s commit closure to run before you act. AcceptInboundDeliveryAction, ProcessInboundDeliveryAction, AttemptDeliveryAction, ScheduleRetryAction, RecordEndpointFailureAction, DisableOutboundEndpointAction and CancelPendingDeliveriesAction are internal building blocks.

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

By donating, you agree to our donation terms.

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.