← All articles

Where to Find Your Stripe Webhook Signing Secret

Webhooker Team 8 min read
Flat illustration of the Stripe webhook signing secret: three panels with only the middle one open, revealing a single blue key wired to a pulse node.

Your Stripe webhook signing secret is in the Webhooks tab in Workbench. Open the endpoint you want the secret for, then click Click to reveal. The value starts with whsec_. Every endpoint gets its own secret, and the same URL registered in test mode and live mode has two different ones.

That is the short answer. The rest of this page is for the cases where the short answer does not help: you are looking at the wrong dashboard, you copied an API key by mistake, or the secret you have works locally and fails in production.

Where the secret lives in the Stripe Dashboard

Stripe moved webhook management into Workbench, which replaced the old Developers Dashboard. Most guides you will find still describe the old screens, which is why the click path people remember no longer matches what they see.

The current path:

  1. Open the Webhooks tab in Workbench.
  2. Select the endpoint you want the secret for. Stripe calls these event destinations now; a webhook endpoint is one type of destination.
  3. Find the signing secret field and click Click to reveal.

The revealed value looks like whsec_ followed by a long random string. Copy it into your environment as something like STRIPE_WEBHOOK_SECRET and never into source control.

If you are still on the older Developers Dashboard, the equivalent page is at dashboard.stripe.com/webhooks. Same secret, older interface.

One thing that surprises people: the secret is per endpoint, not per account. You can register up to 16 endpoints, and each one signs with its own key. Point three services at three URLs and you are managing three secrets. Point two URLs at one handler that only knows one secret and roughly half your events will fail verification, which is a confusing bug because the other half keeps working fine.

Your signing secret is not your API key

This is the single most common mixup, and Stripe’s naming does not help. Four different values in the dashboard are all called some flavor of “key” or “secret”:

Only the last one belongs in signature verification. The API key proves who you are when you call Stripe. The signing secret proves it was Stripe when Stripe calls you. Traffic goes in opposite directions, and the values are not interchangeable.

If you hand sk_live_... to constructEvent, every single event fails and the error message says nothing about which key you used. It just says the signature did not match, which sends people hunting through their body parsing for an hour. We wrote up the six causes behind a failed signature check because the wrong secret is only one of them, and they all produce the same unhelpful error.

Getting a signing secret from the Stripe CLI

For local development you do not use the dashboard secret at all. When you forward events with the CLI:

stripe listen --forward-to localhost:4242/webhook

the first line of output hands you a secret:

Ready! Your webhook signing secret is 'whsec_...' (^C to quit)

Use that one while testing locally. It belongs to the CLI’s forwarding session, not to any endpoint you registered in the dashboard, and it is what signs the events the CLI pushes to your machine. Grab it from the output each time rather than pinning it in a committed .env.

You can also trigger a specific event to test the handler:

stripe trigger payment_intent.succeeded

Two details worth knowing here. First, in test mode Stripe adds an extra signature with a fake v0 scheme alongside the real v1. It exists to help you test parsing. Ignore every scheme that is not v1; accepting anything else is a downgrade attack waiting to happen. Second, if you want the CLI to mirror an endpoint you already registered, --load-from-webhooks-api pulls that endpoint’s path and event list instead of forwarding everything.

Test mode and live mode have separate secrets

Registering https://api.example.com/stripe in test mode and again in live mode gives you two endpoints and two different whsec_ values. Nothing warns you when you mix them up.

The symptom is unmistakable once you know it: verification passes in staging and fails on every event in production. Or the reverse, if the deploy pipeline copied the live secret into a test environment. When the signature check works in one environment and fails completely in the other, check the mode before you check anything else. A live event checked against a test secret will never match, no matter how correct the rest of your code is.

Rolling the secret without dropping events

Secrets should be rotated periodically, and immediately if one has leaked into a log, a screenshot, or a repository.

In Workbench, open the endpoint, click the overflow menu (⋯), and choose Roll secret. Stripe lets you expire the current secret right away or keep it alive for up to 24 hours. Take the 24 hours unless you are responding to an actual compromise.

Here is what makes the overlap safe. While both secrets are valid, Stripe signs each request twice, and the Stripe-Signature header carries one v1= value per active secret:

Stripe-Signature: t=1740000000,v1=5257a869e7...,v1=8fd3b0c412...

So the sequence is: roll with a delay, deploy code that accepts both the old and new secret, wait out the window, then remove the old one. Your verifier has to treat v1 as a list rather than a single string for this to work. Code that grabs the first v1 and stops will drop half the deliveries during the overlap, and the failures will look random. Stripe’s own libraries handle the list correctly; if you rolled your own check, this is the part to look at.

Where to keep the secret once you have it

The signing secret is the only thing standing between your endpoint and anyone who has guessed your URL. Webhook URLs are not secret in any meaningful sense — they end up in browser history, in proxy logs, in screenshots, and in the dashboard of anyone with account access. Assume the URL is public and let the signature do the work.

Practical version: keep it in your secret manager or environment, never in the repository. Give test and live separate variables so a deploy cannot cross them. Do not log the value while debugging, and if you already did, roll it. Stripe also publishes the IP ranges its webhooks come from, which is a reasonable second layer, though allowlisting alone proves nothing about the payload.

Verifying without holding the secret in your app

Once you have the secret, something has to do the actual HMAC check on every request, on every service that receives Stripe events. Each copy of that code is another place the raw body can get parsed too early, and another environment where the secret has to be rotated in sync.

Moving the check to the edge keeps it in one place. That is how Webhooker works: you register a Stripe source, set the scheme to HMAC-SHA256, paste the whsec_ value once, and verification runs at the gateway before a payload is accepted. Requests that fail never reach your application. Requests that pass arrive as events that already cleared the signature and timestamp checks, and the ingest URL answers in a few milliseconds because delivery happens out of band. The docs walk through setting up a Stripe source, and pricing is per event with a DPA on paid plans.

If you would rather keep the crypto in your own code, that is fine too. Start with verifying Stripe webhook signatures step by step, and read why a valid signature alone does not stop replays before you ship it.

Frequently asked questions

How do I find my Stripe webhook signing secret?

Open the Webhooks tab in Workbench, select the endpoint, and click Click to reveal. The value begins with whsec_. If you are testing locally instead, run stripe listen --forward-to localhost:PORT/webhook and use the secret printed in the first line of output.

Is the webhook signing secret the same as my Stripe secret key?

No. The secret key (sk_... or rk_...) authenticates requests your server sends to Stripe. The signing secret (whsec_...) verifies requests Stripe sends to your server. Using the API key for verification fails every event with a generic signature mismatch error.

Why does my signing secret work in test mode but not live?

Because they are different secrets. The same URL registered in both modes produces two endpoints with two independent whsec_ values. Confirm the secret in your live environment belongs to the live endpoint.

Can I use one signing secret for several endpoints?

No. Stripe generates a unique secret per endpoint. If several endpoints route to one handler, that handler needs to know which secret applies, usually by keying on the path it was called at.

Is the webhook URL itself a secret?

Treat it as public. URLs leak through logs, proxies, and browser history, and anyone who learns yours can POST forged payloads at it. Signature verification, not URL obscurity, is what makes the endpoint safe.

What happens to in-flight events when I roll the secret?

Nothing, as long as you use the delayed expiry. During the overlap Stripe signs each request with every active secret and sends one v1= value per secret in the header. Your verifier needs to accept any matching v1, not just the first one it finds.