Rotating secrets
Pass an array instead of a single secret. A request is valid if any secret matches, tried in order, so deliveries signed with the old secret keep working while the provider switches over.
import { verifyWebhook } from 'verihook';
const result = await verifyWebhook('stripe', request, [ process.env.STRIPE_WEBHOOK_SECRET!, // new process.env.STRIPE_WEBHOOK_SECRET_OLD!, // old, remove once the rotation ends]);Arrays work everywhere a secret does: verifyWebhook, verifyWebhookOrThrow, the provider shortcuts (verifyStripe, verihook/stripe, …) and every adapter. A secret resolver may return one too:
import { verihookHono } from 'verihook/hono';
app.post('/webhooks/github', verihookHono('github', (c) => [c.env.GITHUB_SECRET, c.env.GITHUB_SECRET_OLD]));- Empty entries are skipped, so an unset
*_OLDvariable is harmless. If every entry is empty, verification fails withINVALID_SECRET. - If no secret matches, the result is the first secret’s failure (
code,reason,hint). Put the current secret first. - Each extra secret costs one more HMAC, and only when the earlier ones fail.
A rotation, step by step
Section titled “A rotation, step by step”- Add the new secret first in the list and deploy:
[NEW, OLD]. - Rotate the secret in the provider’s dashboard. Many providers switch to the new secret at once, so both must already be accepted when you rotate.
- Once no delivery is signed with the old secret any more (wait out the provider’s retry window), remove it:
[NEW]or justNEW.
Stripe and Mollie send one signature per active secret while they roll a secret, and verihook checks all of them, so a single secret already works there. The array is for the providers that don’t, and for your side of the switch.