Platform integration

API strategy

Webhook signing key rotation: a practical guide to rotating without downtime

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

7 minute read
Decorative imagery showcasing Pontil's brand

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.

Step 1 — Decide why you're rotating

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.

Step 2 — Move to key IDs, not raw secrets

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.

Step 3 — Support multiple active keys at once

Store keys in a table keyed by kid, with lifecycle state:

Field
Purpose

kid

Stable identifier, e.g. `key_2026_01`

secret

The signing material (encrypted at rest)

status

`active`, `next`, `retiring`, `revoked`

created_at

Issue time

activated_at

When it started signing

retires_at

When it stops verifying


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.

Step 4 — Build the rotation flow

Rotation is a state transition, not a swap. Run it as a script or scheduled job with these steps:

  1. Generate a new key with a fresh kid. Mark it next.
  2. Publish the new key to consumers (see step 5).
  3. Wait for consumers to pick it up. For pull-based distribution via JWKS, this is roughly your cache TTL plus a safety margin. For push-based, wait for delivery confirmation.
  4. Flip the new key to active and the old key to retiring.
  5. Continue signing with the new key. Consumers verify against either.
  6. After the retirement window (24 hours is a reasonable default for scheduled rotation), mark the old key 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.

Step 5 — Publish keys via JWKS

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.

Step 6 — Publish ephemeral keys for high-sensitivity events

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:

  • Generate a fresh key every N hours (1 to 6 is typical).
  • Publish it via JWKS with a short cache TTL (30–60 seconds).
  • Sign outgoing webhooks with the current key.
  • Retire keys after 2N hours to cover retry windows.

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.

Step 7 — Build the emergency rotation runbook

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:

  1. Detect and confirm. Who declares the incident. What evidence counts.
  2. Revoke immediately. Move the compromised key straight to revoked, skipping retiring. Accept that in-flight deliveries signed with it will fail — they'll retry and pick up the new key.
  3. Issue a new active key. Same mechanics as scheduled rotation, but without the wait between next and active.
  4. Force JWKS refresh. Set the response Cache-Control: max-age=0 for the duration of the incident.
  5. Notify consumers. Even with JWKS, tell them out-of-band. Some consumers cache aggressively; some don't use JWKS at all.
  6. Audit. Log every webhook signed with the revoked key in the exposure window. That's your blast radius.

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.

Common pitfalls

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.

Join our weekly newsletter

Stay up to date on the ever changing agentic landscape.

POSTS

Related content

Platform integration

API strategy

Webhook signature verification: a practical HMAC-SHA256 guide

7 minute read

Platform integration

Agent infrastructure

Webhook retry with exponential backoff: a practical implementation guide

7 minute read

Agent infrastructure

Platform integration

Webhook reliability patterns: how to deliver events agents can actually trust

9 minute read