← All articles

How to Get Your Stripe Webhook Secret (whsec_)

Webhooker Team Updated 11 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.

To get your Stripe webhook secret, the whsec_ value most codebases load as STRIPE_WEBHOOK_SECRET:

  1. Open the Webhooks tab in Workbench.
  2. Select the endpoint you want the secret for.
  3. Click Click to reveal next to the signing secret and copy the whsec_... value.

Every endpoint has its own secret, and the same URL registered in test mode and in live mode has two different ones. For local development, stripe listen prints a separate secret, and stripe listen --print-secret prints only that value.

The rest of this page covers the cases where those steps do not give you a working secret: the endpoint does not exist yet, you are looking at the old dashboard, you copied an API key by mistake, or the secret works locally and fails in production.

Where do you find the Stripe webhook secret in the Dashboard?

Stripe moved webhook management into Workbench, which replaced the old Developers Dashboard. Most videos and older guides still say “Developers, then Webhooks”, which is why the click path people remember no longer matches what they see.

In Workbench, open the Webhooks tab and select the endpoint. Stripe now calls these event destinations, and a webhook endpoint is one type of destination. The signing secret sits on the endpoint’s page behind Click to reveal. The revealed value is whsec_ followed by a long random string. Copy it into your environment as STRIPE_WEBHOOK_SECRET or similar, never into source control.

If your account still shows the older Developers Dashboard, the same endpoint list is at dashboard.stripe.com/webhooks. It is the same secret in an older interface.

If the Webhooks tab is empty, there is no secret to find yet. Stripe only creates one when you register an endpoint, which the next section covers.

How do you create a Stripe webhook secret?

You do not generate the secret yourself. Stripe creates it when you register a webhook endpoint, so creating a secret means creating an endpoint. According to Stripe’s webhook docs, the Workbench flow is:

  1. In the Webhooks tab, click Create an event destination.
  2. Choose Your account, the API version, and the event types you want.
  3. Pick Webhook endpoint as the destination type and enter your HTTPS endpoint URL.
  4. On the settings page that follows, click Reveal secret and copy the whsec_ value.

If you register endpoints through the API instead, the /v2/core/event_destinations endpoint returns the secret in webhook_endpoint.signing_secret when you ask for it with include. The older Webhook Endpoint object only returns secret at creation, so store it then or fetch it from Workbench later.

To replace a secret on an endpoint that already exists, roll it, as described further down. WooCommerce and other Stripe plugins that ask for a “webhook secret” want this same value, taken from the endpoint you registered for the plugin’s URL.

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.

Is the webhook secret the same as your Stripe secret key?

Mixing these two up happens a lot, 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.

How do you get the webhook secret from the Stripe CLI?

For local development you do not use the dashboard secret at all. stripe listen prints its own whsec_ secret when it starts, and that secret only verifies events the CLI forwards to your machine. Start forwarding like this:

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, not to any endpoint you registered in the dashboard, and it is what signs the events the CLI pushes to your machine. Stripe’s CLI reference says it does not change between restarts of listen, so you can keep it in a local, uncommitted .env. To script it, --print-secret prints the secret and exits:

export STRIPE_WEBHOOK_SECRET="$(stripe listen --print-secret)"

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

stripe trigger payment_intent.succeeded

Two CLI details matter here. In test mode Stripe adds an extra signature with a fake v0 scheme alongside the real v1, meant to exercise your header parsing. Ignore every scheme other than v1, because accepting another one opens the door to a downgrade attack. And 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. The rest of the local loop, including what the CLI does not reproduce, is in the Stripe webhooks guide.

Not sure which secret belongs to which endpoint? Send one event to our online webhook tester and paste a candidate secret: it tells you whether that secret signed the request, and says so if you pasted an sk_ API key by mistake.

Why do test mode and live mode have different 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: 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.

How do you roll 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 should you store STRIPE_WEBHOOK_SECRET?

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.

Can you verify Stripe webhooks without 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 a single edge service keeps the secret in one place, so a rotation touches one configuration instead of every service. Webhooker can do that with a Stripe source that holds the whsec_ value, rejects requests failing the signature or the 300-second timestamp window, and delivers the rest out of band under its own signature, checked with the same code for every provider.

If the check stays in your own code, every receiving service needs the unparsed body, a parser that treats v1 as a list, and a tolerance on t. 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 --print-secret, or use the secret printed in the first line of stripe listen --forward-to localhost:PORT/webhook.

Why can’t I find a webhook secret in my Stripe account?

Because no endpoint exists yet. Stripe creates a signing secret only when you register a webhook endpoint, so an empty Webhooks tab means there is nothing to reveal. Click Create an event destination, choose Webhook endpoint, enter your URL, and the secret appears on the endpoint’s settings page. If you only need to test locally, stripe listen gives you a secret without registering anything.

What value goes in STRIPE_WEBHOOK_SECRET?

The endpoint’s signing secret, the string starting with whsec_ that Workbench shows behind Click to reveal. Not sk_..., not pk_..., not rk_.... Locally, set it to the whsec_ value stripe listen prints on startup instead, and keep separate variables for test and live so a deploy cannot swap them.

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.