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

In RoundlyConsulting\Webhooks\Events, all final readonly; none carries a secret, a signature or a raw body. Write your audit log from them:

EventWhenCarries
WebhookReceivedOn the worker, before the handler — on every attempt of a runwebhook (InboundWebhook: sanitized payload, delivery id, endpoint, isReplay, run, attempt; isFirstAttempt())
WebhookRejectedA request to a known endpoint was refused (steps 2–8); rate-limited rejections once per windowendpointId, reason (RejectionReason), status, ip, userAgent
WebhookProcessingFailedA handler failed on its last try, or the endpoint was deleted before the delivery randelivery, errorCode (handler_failed / endpoint_deleted)
DeliverySucceededA 2xx answerdelivery, attempt
DeliveryFailedEvery failed attemptdelivery, attempt, final, nextAttemptAt
EndpointDisabledAuto (consecutive failures, 410) or by handendpoint (outbound), reason (DisableReason)
EndpointSecretRotatedA rotationendpoint, direction, graceEndsAt

Unknown keys fire nothing, so a stranger cannot spam your listeners; a duplicate fires nothing.

Once per attempt — WebhookReceived

WebhookReceived fires on every attempt of a run — a handler that throws is retried, and the retry fires it again. A listener that must act once per delivery checks the attempt:

Event::listen(function (WebhookReceived $event): void {
    if (! $event->webhook->isFirstAttempt()) {
        return;   // a retry of a run whose handler threw
    }

    Cache::increment('webhooks.received.'.$event->webhook->type);
});

Queued listeners

WebhookProcessingFailed, DeliverySucceeded, DeliveryFailed, EndpointDisabled and EndpointSecretRotated use SerializesModels: a ShouldQueue listener receives the models as they are when it runs. WebhookReceived carries the sanitized payload, and a queued listener on it would write that payload into your queue store unencrypted, around payloads.encrypt. Keep WebhookReceived listeners synchronous, or queue only the delivery id and load the delivery in the job:

Event::listen(function (WebhookReceived $event): void {
    SyncInvoice::dispatch($event->webhook->deliveryId);   // the job loads it again
});

// in SyncInvoice::handle()
$delivery = Webhooks::inbound()->deliveries()->where('public_id', $this->deliveryId)->firstOrFail();

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.