In RoundlyConsulting\Webhooks\Events, all final readonly; none carries a secret, a signature or a raw body. Write your audit log from them:
| Event | When | Carries |
|---|---|---|
WebhookReceived | On the worker, before the handler — on every attempt of a run | webhook (InboundWebhook: sanitized payload, delivery id, endpoint, isReplay, run, attempt; isFirstAttempt()) |
WebhookRejected | A request to a known endpoint was refused (steps 2–8); rate-limited rejections once per window | endpointId, reason (RejectionReason), status, ip, userAgent |
WebhookProcessingFailed | A handler failed on its last try, or the endpoint was deleted before the delivery ran | delivery, errorCode (handler_failed / endpoint_deleted) |
DeliverySucceeded | A 2xx answer | delivery, attempt |
DeliveryFailed | Every failed attempt | delivery, attempt, final, nextAttemptAt |
EndpointDisabled | Auto (consecutive failures, 410) or by hand | endpoint (outbound), reason (DisableReason) |
EndpointSecretRotated | A rotation | endpoint, 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 cryptoBy 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.