Platform integration
API strategy
Webhook signing key rotation without downtime: overlap windows, JWKS distribution, ephemeral keys, and an emergency runbook in seven practical steps.

Webhook signing keys are the shared secret between you and every consumer verifying your events. Rotate them badly and you break every integration at once. Rotate them well and nobody notices — including the attacker whose stolen key just stopped working.
By the end of this guide you will have a rotation scheme that supports overlapping keys, a JWKS-style endpoint for consumers that want to fetch keys automatically, and a runbook for emergency rotation when a secret leaks. Prerequisites: a working webhook signing setup (HMAC-SHA256 or asymmetric), a way to store secrets (a KMS, a secrets manager, or at minimum an encrypted database column), and a webhook consumer you control for testing. Time required: about half a day for the first implementation, plus follow-up work to migrate existing consumers.
If you don't have signature verification working yet, start with our HMAC-SHA256 verification guide first — rotation only matters once verification is in place.
Rotation policy follows rotation reason. The two reasons look identical from the outside but need different mechanics.
Scheduled rotation is preventative. You rotate every 90 days (or 30, or 180 — pick a number, write it down) because keys that live forever accumulate exposure. The window between old key and new key can be long — days or weeks — because there's no active threat.
Emergency rotation is reactive. A secret leaked, an employee left, a consumer's database got dumped. The window has to close fast, ideally within minutes, and you accept that some in-flight webhooks will fail verification and need to retry.
Decide which case you're building for first. The mechanics below cover both, but the timers and alerting differ.
If your current signature header contains only the signature (X-Signature: sha256=abc123...), you can't rotate without a flag day. Fix that first.
Add a key identifier to the signature header so the receiver knows which key to verify against. Two common shapes:
X-Signature: t=1699999999,v1=abc123...,kid=key_2026_01
or split across headers:
X-Signature-Key-Id: key_2026_01
X-Signature: sha256=abc123...
Stripe uses a comma-separated inline form (t=, v1=) but does not include a kid. During rotation, Stripe emits multiple v1= signatures — one per active secret — and the verifier tries each configured secret until one matches. That works, but it doesn't scale past a handful of active secrets and it forces every verifier to do the extra work. Adding an explicit kid, as shown above, lets the verifier go straight to the right key. GitHub uses a separate X-Hub-Signature-256 header without a kid; rotation there is a manual operation on the webhook config. For production rotation across many consumers, the kid approach is the one that scales.
Update your consumers to read the kid, look up the corresponding key, and verify. Ship that change before you touch anything else — this is the foundation.
Store keys in a table keyed by kid, with lifecycle state:
At any moment, one key is active (used to sign outgoing webhooks) and zero or more are retiring (still valid for verification of in-flight deliveries but not used for new signatures). Consumers accept a signature if it matches any non-revoked key.
This overlap window is the entire point. It's what lets you rotate without a flag day.
Rotation is a state transition, not a swap. Run it as a script or scheduled job with these steps:
next.active and the old key to retiring.revoked and stop accepting signatures made with it.The retirement window has to be at least as long as your maximum webhook retry duration. If your webhook retry policy retries for up to 72 hours, a 24-hour retirement window will drop deliveries that were signed with the old key and are still being retried. Match them.
Manual key distribution scales to about three consumers. Beyond that, publish a JWKS (JSON Web Key Set) endpoint so consumers fetch keys automatically.
Expose a public endpoint like https://api.yourdomain.com/.well-known/webhook-jwks.json:
{
"keys": [
{
"kid": "key_2026_01",
"kty": "oct",
"alg": "HS256",
"k": "base64url-encoded-secret",
"status": "active"
},
{
"kid": "key_2025_10",
"kty": "oct",
"alg": "HS256",
"k": "base64url-encoded-secret",
"status": "retiring"
}
]
}
One warning: JWKS for symmetric HMAC keys means you're distributing the raw secret over the wire. That's fine over TLS to authenticated consumers (put the endpoint behind the same auth as your API), but it's not how JWKS was designed. If you can, use asymmetric signing (Ed25519 or RSA) and publish only the public key. Consumers verify with the public key; you sign with the private key that never leaves your infrastructure.
Set Cache-Control: max-age=300 on the response so consumers refresh every five minutes. Shorter caches mean faster propagation for emergency rotation; longer caches reduce load. Five minutes is the common trade-off.
For events that carry particularly sensitive payloads — financial transactions, PII exports, admin actions — consider ephemeral webhook keys: keys that live for hours instead of months, rotated automatically without human involvement.
The pattern:
This works well for asymmetric signing where consumers only need the public key. It works badly for HMAC because the frequent secret distribution multiplies exposure. Reserve it for the events that actually justify the operational cost.
Scheduled rotation is a script. Emergency rotation is a runbook, because the trade-offs shift.
Write it down before you need it. It should cover:
revoked, skipping retiring. Accept that in-flight deliveries signed with it will fail — they'll retry and pick up the new key.next and active.Cache-Control: max-age=0 for the duration of the incident.Test the runbook on a non-production system quarterly. The first time you run it in anger should not be the first time you run it.
Rotating without an overlap window. The most common mistake. You flip the key, consumers using the old one start rejecting signatures, integrations go dark. Always have both keys valid for verification during the transition.
Retirement window shorter than your retry window. Webhooks signed with the old key are still being retried when the old key gets revoked. Deliveries fail permanently. Match the retirement window to your maximum retry duration — see the dead letter queue guide for how those two systems interact.
Distributing symmetric secrets via public JWKS. JWKS was designed for public keys. If you must use it for HMAC secrets, put the endpoint behind authentication. Better: switch to asymmetric signing.
No kid in the signature header. You cannot rotate cleanly without one. Retrofitting a kid onto existing consumers is annoying but not optional. Do it before your first rotation, not during.
Treating consumers as homogeneous. Some pull JWKS every minute. Some cache for 24 hours. Some hard-code the key. Your rotation window has to accommodate the slowest consumer you support, or you have to publish the deprecation policy that says otherwise.
Rotating everything at once across products. If you're a multi-product SaaS, don't align rotation timestamps across products — you'll create a coordinated failure surface. Stagger them.
Once rotation is boring, it's working. That's the goal.
Stay up to date on the ever changing agentic landscape.