Skip to content

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_timestamp in a burst — clock drift, or a provider replaying a backlog. Check NTP before widening the window.
  • signature_mismatch at a low rate — the control working as intended.
  • signature_mismatch for 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().