Events
Events
Both outcomes are announced, so a host can react to verification without decorating the middleware or duplicating the check.
| Event | Dispatched when |
|---|---|
Cbox\WebhookSignature\Events\WebhookVerified |
A delivery was proven authentic |
Cbox\WebhookSignature\Events\WebhookRefused |
A delivery was refused, with the typed reason |
They fire from the manager, so they cover Webhooks::verify() as well as the route
middleware.
The refusal event is the one to listen to
A refusal is one of three quite different things, and once the middleware has returned its 401 that distinction is gone unless something recorded it:
Event::listen(function (WebhookRefused $event) {
if ($event->isConfigurationFault()) {
// Our secret is missing. Every genuine delivery is being dropped right now.
// This is an outage, not an attack — page someone.
Alert::page('Webhook endpoint misconfigured', $event->endpoint);
return;
}
Metrics::increment('webhooks.refused', [
'endpoint' => $event->endpoint,
'reason' => $event->reason->value,
]);
});
What each reason usually means in production:
no_secret_configured— an unset environment variable. Yours, and urgent.stale_timestampin a burst — clock drift, or a provider replaying a backlog. Check NTP before widening the window.signature_mismatchat a low rate — the control working as intended.signature_mismatchfor every delivery from one sender — a wrong secret, or something upstream rewriting the request body.replayed— either a provider retry the guard correctly absorbed, or someone resending captured traffic.
Nothing on the event is derived from a secret, so it is safe to forward to any log, metrics backend or alerting system.
Watching a rotation finish
WebhookVerified carries the label of the secret that verified, which is the signal that
tells you a rotation is complete:
Event::listen(fn (WebhookVerified $e) => Metrics::increment('webhooks.verified', [
'endpoint' => $e->endpoint,
'secret' => $e->webhook->secretId ?? 'unlabelled',
]));
While anything still reports the old label, removing the old secret would start refusing genuine deliveries. See secret rotation.
Testing against them
Event::fake();
$this->postSignedWebhook('/webhooks/github', 'github', ['action' => 'opened']);
Event::assertDispatched(WebhookVerified::class);
What they are not
Not a processing hook. The events fire during verification, which happens inside the
request; doing work in a listener puts that work on the provider's clock, and providers
time out. Dispatch a queued job from your controller instead, keyed on
VerifiedWebhook::idempotencyKey().