All packages
Webhooks for Laravel
Secrets and rotation
- Generated — whsec_ + standard base64 of secrets.bytes (32) random bytes. The HMAC key is the decoded bytes, and the prefix makes them recognisable to secret scanners.
- Imported (inbound, create(secret: '…')) — stored verbatim, 1–1 024 bytes, no control characters. How the string becomes key bytes is the scheme’s choice. On the default scheme a whsec_… secret must be standard base64 that decodes to at least one byte, else InvalidEndpointException; custom schemes (a Stripe-style whsec_ used raw) are not held to that.
- At rest — Laravel’s encrypted cast on secret and previous_secret, both hidden. Never in resources, about, events, logs or exception messages (#[SensitiveParameter] everywhere).
- Shown once — in CreatedInboundEndpoint, CreatedOutboundEndpoint and RotatedSecret, each of which redacts itself in print_r, var_dump and json_encode.
Rotation
$rotated = Webhooks::inbound()->rotateSecret($inboundEndpoint); // grace: secrets.grace_seconds (1 day)
$rotated = Webhooks::outbound()->rotateSecret($outboundEndpoint, grace: now()->addHours(6));
$rotated = Webhooks::inbound()->rotateSecret($inboundEndpoint, grace: now()); // no overlap
$rotated = Webhooks::inbound()->rotateSecret($inboundEndpoint, secret: $sendersNewSecret); // import (inbound)
$rotated->secret; // the new secret, shown once
$rotated->graceEndsAt; // when the old one stops working- The current secret becomes the previous one, the new one becomes current, and previous_secret_expires_at is the grace end. A grace longer than secrets.max_grace_seconds (7 days) or in the past throws SecretRotationException.
- Rotating again during a grace drops the oldest secret at once. The row is locked, so two concurrent rotations keep a consistent pair. EndpointSecretRotated fires, with no secret in it.
- Inbound, both secrets verify during the grace and the delivery records which one matched (signature_slot current or previous). Outbound, both sign — two v1 entries, the new one first — so the receiver can switch whenever it likes.
- Grace is enforced at use time; webhooks:prune clears expired previous secrets.
php artisan webhooks:rotate-secret outbound 12 --grace=21600 # prints the new secret onceDeleting an endpoint
delete() wipes the secret and the previous secret at once — outbound, the URL and custom headers too. A deleted endpoint is dead; undeleting it by hand would need a new secret through rotateSecret(). This is deliberate: a deleted endpoint’s secret is not kept around for years. The row stays for the log until webhooks:prune has removed every delivery that points at it.
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.