Guides · Updated October 23, 2026

Rotated your Stripe keys? Check your webhook secret

You rotated a Stripe key, or rolled a webhook secret, and everything looked fine. But if your server is still using an old or wrong whsec_… value, signature verification fails on every event. Stripe shows the deliveries as failed; your customers pay and nothing changes in your app. This guide shows how to tell which secret is which, fix it, redeploy, and recover the events you missed.

Two different secrets

Stripe keeps these separate (Stripe: API keys):

So rotating or expiring an API key doesn't change any signing secret, and rolling an endpoint's signing secret doesn't touch your API keys. The trouble comes from the other direction: people rotate everything during a clean-up, paste values into the wrong variables, or roll the signing secret and forget that the server still has the old one.

How the signing secret ends up wrong

The failure is quiet

Verification failure normally makes your handler return a 4xx, and nothing in your app crashes or alerts. From the founder's side, new subscriptions don't activate, cancellations and failed payments don't revoke access, and no one gets an error message. The only places it shows are the endpoint's Event deliveries tab in Stripe and your server logs.

In live mode, Stripe retries an undelivered event for up to three days with exponential backoff (Stripe: Automatic retries). Sandbox deliveries are retried three times over a few hours. So a short outage can heal by itself once the right secret is in place, and a longer one leaves events that need a manual resend.

Checklist

1. Find which secret your server is using

Look where the value is set: your host's environment variable settings for the production environment, and any .env file or secrets manager your deploy reads. Check the variable name your code actually reads (STRIPE_WEBHOOK_SECRET is common, but it's whatever you passed to the signature check) and which environment it's set for. On Vercel, for example, Production and Preview have separate values (Vercel: Environment variables).

2. Compare it with the right endpoint in Stripe

  1. In the Dashboard, switch to the mode you're debugging (live or test/sandbox).
  2. Open Workbench, then Webhooks, and select the endpoint whose URL is your production webhook URL. Check the URL carefully if you have more than one.
  3. Click Reveal secret and compare it with your server's value.

Stripe's own advice is to print the secret your code uses and compare it. We'd rather you not write a full secret into logs. This temporary snippet shows enough to compare without exposing the value; delete it after use:

// Temporary, in your webhook route. Remove it after you've compared. Never log the full secret.
const s = process.env.STRIPE_WEBHOOK_SECRET ?? "";
console.log({
  set: s.length > 0,
  startsWithWhsec: s.startsWith("whsec_"),
  hasStrayWhitespaceOrQuotes: s !== s.trim() || /^["']|["']$/.test(s),
  length: s.length,
  last4: s.slice(-4),
});

Compare length and last4 against the revealed secret, and look for stray whitespace or quotes copied in with the value. If they differ, you've found it. If you can't tell, set the variable again from the Reveal secret value rather than trusting what's there.

3. Change the variable, then redeploy

Set the new value in your host's environment settings. Most hosts only apply environment variable changes to new deployments or restarts. Vercel's docs say changes aren't applied to previous deployments, only to new ones (Vercel: Environment variables). So trigger a redeploy (or restart) and confirm the new deployment is the one serving your webhook URL. If you have a Preview environment pointing a test endpoint at it, set that variable separately.

4. Send a test event

In a sandbox, trigger an event your endpoint subscribes to, for example stripe trigger payment_intent.succeeded (Stripe: Test your handler), and check that the endpoint's Event deliveries tab shows Delivered with a 200. For the sandbox endpoint, use the sandbox endpoint's secret. For live mode, rather than creating real charges, use the resend step below on a real failed event as your test.

5. Resend the events you missed

  1. Find them. Open the endpoint's Event deliveries tab. It lists events as Delivered, Pending or Failed and shows the HTTP status of each attempt and when the next retry is due (Stripe: View event deliveries). Events marked Pending may still be retried automatically for up to three days in live mode.
  2. Resend in the Dashboard. Open an event and click Resend. Stripe's docs say this works for up to 15 days after the event was created.
  3. Or use the Stripe CLI. stripe events resend <event_id> --webhook-endpoint=<endpoint_id> works for up to 30 days after creation (Stripe CLI: events resend). The CLI makes sandbox requests by default; for live mode add --live.
  4. List a whole backlog. The List Events API accepts delivery_success=false to return events that failed delivery to at least one endpoint, and only returns events from the last 30 days (Stripe: Process undelivered events, API: List events).

Two cautions from Stripe's docs. A manual resend doesn't cancel the automatic retries, so the same event can reach you twice; record processed event IDs and skip repeats (returning 200 to stop further retries). And Stripe doesn't guarantee event order, so don't assume a resent customer.subscription.updated is newer than your current data. If you can, set access from the subscription's current status fetched from the API, rather than from the event payload alone (Stripe: Event ordering).

6. Check your own data for the gap

Resending covers events Stripe still holds. Events older than the resend windows, or events your handler dropped for other reasons, can't be replayed. Find the time the secret went wrong (the first Failed delivery on the endpoint is a good marker) and look at everything that changed after it:

For anything the resend didn't fix, compare the Stripe subscription or payment itself with your app's record and correct your data directly. The step-by-step method is in How to find Stripe customers who paid but don't have access.

Make it hard to miss next time

PaidCheck can flag failed webhook events from the last 30 days and disabled endpoints with a Stripe-only check, and compare Stripe with who has access in your app. Paid plans run checks daily and send an alert email when a check finds a new issue. It can't see which secret your server uses; it shows you the effect.

FAQ

Does rotating my Stripe API key change my webhook signing secret?

No. Stripe's docs say webhook signing secrets aren't API keys: they're per-endpoint secrets you find in the Webhooks section of the Dashboard. Rotating or expiring an API key leaves them alone. Rolling an endpoint's signing secret in the Dashboard does change that endpoint's secret.

What happens to events while signature verification is failing?

Your handler rejects them, usually with a 4xx status, so Stripe records the delivery as failed. In live mode, Stripe's docs say it keeps retrying an undelivered event for up to three days with exponential backoff. Once your server has the right secret, a later retry can succeed on its own. Events that exhaust their retries need to be resent manually.

Can I resend events after the three days are up?

Yes, within limits. Stripe's docs say you can click Resend on an event in the Dashboard for up to 15 days after it was created, or run `stripe events resend <event_id> --webhook-endpoint=<endpoint_id>` with the Stripe CLI for up to 30 days. The List Events API only returns events from the last 30 days.

Why is my webhook secret different in test mode, live mode and the Stripe CLI?

Stripe generates a unique secret for each endpoint, and if you use the same endpoint URL for test and live mode the secret is different for each one. The secret that `stripe listen` prints for local forwarding is different again from the secret of a Dashboard-managed endpoint. All of them start with whsec_, which makes them easy to mix up.

Will resending an event stop Stripe's automatic retries?

No. Stripe's docs say a manual resend doesn't cancel the automatic retries, even if it returns a 2xx. Make your handler idempotent by recording processed event IDs, and return 200 for events you've already handled.

Can PaidCheck tell me which webhook secret my server is using?

No. PaidCheck doesn't see your server's environment variables and can't read your webhook secret. It reads Stripe through a read-only restricted key and compares what Stripe says with who has access in your app, so it can show you the effect of a broken endpoint (failed events, wrong access), not the cause.