Verification-only webhook signature checking for Laravel — and outbound signing that uses
the same code, so the two cannot drift.
composer require cboxdk/laravel-webhook-signatureRoute::post('/webhooks/github', GithubController::class)
->middleware('webhook.signature:github');
Http::webhookSignature('outbound')->post($subscriber->url, $payload);No migrations, no models, no queued jobs, no webhook_calls table. Storing deliveries and
processing them are decisions your application has already made.
Nine schemes
github · stripe · slack · shopify · standard-webhooks · twilio · mailgun ·
postal · cbox, plus a generic HMAC driver you describe in configuration.
Postal is asymmetric — an RSA signature verified against a public key — which is why the
package is named for signatures rather than for HMAC.
Conformance
Every bundled scheme is tested against a signature produced by an implementation other than
this one. A round-trip test cannot catch a misread specification, because both halves are
wrong in the same way.
Four use the worked examples their providers publish (GitHub, Slack, Standard Webhooks,
Twilio), and the HMAC primitive is checked against RFC 4231 vectors. The rest are checked
against signatures generated by stripe/stripe-php, shopify/shopify-api,
mailgun/mailgun-php, the Standard Webhooks reference library, the openssl CLI, and an
independently written signer already running in production.
There has been no third-party security audit and none is claimed.
The conformance page
names the source for each scheme and states the limits.
What it handles that a hand-rolled verifier does not
Secret rotation. You cannot rotate a webhook secret atomically — sender and receiver
deploy separately — so an endpoint holds a set of live secrets and reports which one
verified, which is the signal that tells you the old one can safely go.
Replay defence. Timestamp binding where the provider supports it, plus optional
single-use enforcement. Off by default, because a guard over a per-node cache reports
success while enforcing nothing.
Typed failure reasons. "Our secret is missing" is distinguishable from "someone sent a
bad signature" — in your logs, in events, and in the status code: 401 for the caller's
mistake, 500 for ours, so a provider retries a misconfigured receiver instead of discarding
events it delivered correctly.
Provider details that break integrations. Stripe sends several valid signatures during
a rotation. The Standard Webhooks secret is base64 behind a whsec_ prefix. Shopify is
base64 where the others are hex. Twilio signs the URL, sorted byte-wise. GitHub's legacy
SHA-1 header is refused rather than accepted alongside SHA-256, and a failed Postal SHA-256
check never falls back to its SHA-1 header.
Debugging. php artisan webhook:verify takes a delivery you actually received and
tells you why it was refused — including whether a captured delivery failed only because it
aged out, or was never valid at all.
Quality
187 tests, 95.3% line coverage, PHPStan at level max with no baseline and no ignores, and
architecture tests that enforce the properties the security documentation claims —
cryptography confined to two files, no naive comparison reachable from a scheme.
Mutation score is 71.1% and is reported honestly rather than gated: the suite executes
almost everything and constrains rather less. The distribution and the plan are in
BUILD-STATUS.md.
A note on the version
This is 0.x while the package is proven against real traffic. Composer's caret excludes
0.2 from ^0.1, so a minor bump will require consumers to widen their constraint.
Requirements
PHP 8.4+, Laravel 12 or 13. CI covers PHP 8.4/8.5 against both majors.