# How to Get Your Stripe Webhook Secret (whsec_)

> What goes in STRIPE_WEBHOOK_SECRET and where to find it: Workbench, Webhooks, your endpoint, Click to reveal. Plus the CLI secret, test vs live.

Source: https://webhooker.eu/blog/stripe-webhook-signing-secret
Last updated: 2026-09-14

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

1. Open the [Webhooks tab in Workbench](https://dashboard.stripe.com/workbench/webhooks).
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](https://docs.stripe.com/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](https://docs.stripe.com/webhooks#create-webhook-endpoint), 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](https://docs.stripe.com/api/webhook_endpoints/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”:

- `pk_test_...` / `pk_live_...` is the publishable key. It goes in your frontend.
- `sk_test_...` / `sk_live_...` is the secret API key. It authenticates your calls to Stripe.
- `rk_...` is a restricted API key, a scoped version of the above. If your account was created after May 2026 you likely have these by default, and Stripe now recommends them over plain secret keys.
- `whsec_...` is the webhook signing secret. It verifies that requests hitting your server actually came from Stripe.

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](https://webhooker.eu/blog/webhook-signature-verification-failed) 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:

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

the first line of output hands you a secret:

```text
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](https://docs.stripe.com/cli/listen) 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:

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

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

```bash
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](https://webhooker.eu/blog/stripe-webhooks-guide#testing-stripe-webhooks-locally).

Not sure which secret belongs to which endpoint? Send one event to our [online webhook tester](https://webhooker.eu/webhook-tester?provider=stripe) 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:

```text
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](https://webhooker.eu/blog/verify-stripe-webhook-signatures#verifying-the-signature-correctly) 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](https://docs.stripe.com/ips) 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](https://webhooker.eu/#features) can do that with a [Stripe source](https://docs.webhooker.eu/receive/verification/#supported-schemes) that holds the `whsec_` value, rejects requests failing the signature or the 300-second timestamp window, and [delivers the rest out of band](https://docs.webhooker.eu/deliver/retries-and-replay/) under its own signature, checked with the [same code for every provider](https://docs.webhooker.eu/deliver/verify-signatures/).

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](https://webhooker.eu/blog/verify-stripe-webhook-signatures), and read [why a valid signature alone does not stop replays](https://webhooker.eu/blog/webhook-replay-attacks-timestamp-tolerance) 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.
