Stripe Webhook Secret: Find It and Verify Every Event

Find your Stripe webhook secret (whsec_), verify every event on the raw body, store it safely in Supabase or your hosting, and roll it fast if it ever leaks.

Illustration of a webhook envelope with an orange key wax seal being checked at a gate, representing Stripe webhook secret verification
On this page · 8 sections
  1. What is the Stripe webhook secret (signing secret)?
  2. Where do I find the webhook secret in the Stripe dashboard?
  3. Why are test-mode and live-mode secrets different?
  4. How does signature verification work?
  5. Why should the secret never be in frontend code?
  6. Where should I store the secret on Supabase or my own hosting?
  7. What do I do if the secret leaked?
  8. Want a second pair of eyes?

Key takeaways

  • The webhook secret (whsec_…) is per endpoint and per mode. It is not your API key.
  • Verify the signature on the raw request body before trusting any event.
  • Test and live mode have different secrets, so your hosting needs both set correctly.
  • Never put the secret in frontend code or in a variable the browser can read.
  • If it leaks, roll the secret in the dashboard and redeploy.
  • The app owner is responsible for keys and payment confirmation; Stripe secures its own infrastructure.

The Stripe webhook secret is a signing secret, a string starting with whsec_, that Stripe uses to sign every event it sends to your endpoint. Your server uses the same string to check that an event really came from Stripe and was not altered on the way.

If your app was built with an AI tool, this is one of the places I check first. Payment confirmation depends on it, and the tools tend to get keys and payment paths wrong. Below: where to find the secret, how verification works, where to keep it, and what to do if it leaks.

What is the Stripe webhook secret (signing secret)?

When you create a webhook endpoint in Stripe, Stripe generates a signing secret for it. Every event sent to that endpoint carries a Stripe-Signature header, computed with that secret.

Anyone can send a POST request to your public URL. Without verification, someone could post a fake "payment succeeded" event and your app might mark an order as paid. The secret is what separates a real event from a forged one.

How is it different from the API key?

  • Secret API key (sk_…): lets your server call Stripe to create charges, refunds, and so on.
  • Publishable key (pk_…): safe in the browser, used for checkout forms.
  • Webhook signing secret (whsec_…): lets your server verify incoming events from Stripe.

A common AI-generated mistake is mixing these up, for example using the API key where the signing secret belongs.

Where do I find the webhook secret in the Stripe dashboard?

The exact menu names can change, so treat this as the general path:

  1. Open the Stripe dashboard and go to Developers, then Webhooks.
  2. Click the endpoint that points to your app.
  3. Find the "Signing secret" section and choose to reveal it.
  4. Copy the value that starts with whsec_.

If you have no endpoint yet, create one with your public URL and the events you want. The secret appears once it exists.

What about the Stripe CLI secret?

When you forward events to localhost with the Stripe CLI, the CLI prints its own whsec_ value. That one is only for local testing and is different from your dashboard endpoint secret. Do not copy it into production.

Illustration of a payment dashboard with a masked signing secret field next to a padlock
The signing secret lives in the endpoint settings, revealed only when you ask for it.

Why are test-mode and live-mode secrets different?

Stripe keeps test and live data completely separate. Each mode has its own endpoints, so each has its own signing secret. An event from live mode signed with a live secret will fail verification if your server holds the test secret, and the reverse.

Typical symptoms of a mismatch:

  • Everything works in test, but live payments never confirm.
  • Verification errors appear right after launch.
  • Orders stay "pending" even though Stripe shows them paid.

Before going live, check that production hosting holds the live secret, the live API key, and that the live endpoint URL is registered in live mode. This is easy to miss when an AI builder wired everything up in test mode.

How does signature verification work?

The Stripe-Signature header contains a timestamp and one or more signatures. Stripe computes an HMAC of the timestamp plus the exact raw body using your secret. Your server repeats the computation and compares the results.

You should not write this yourself. Stripe's official libraries have a function (in Node, stripe.webhooks.constructEvent) that takes the raw body, the header, and the secret, and throws an error if anything is wrong. It also rejects timestamps that are too old, which helps against replayed events.

The raw body trap

A bug I run into often: a framework parses the JSON first, then the code re-serializes it for verification. Even a whitespace change breaks the signature. You must pass the untouched raw bytes. In Next.js route handlers, read the text of the request; in Express, use a raw body parser on that route only.

What to do when verification fails

  • Return a 400 response and do nothing else with the payload.
  • Do not disable verification to "make it work". That turns your endpoint into an open door.
  • Check the secret, the mode, and the raw body, in that order.

A generic security scanner will not catch a missing verification step, because it is business logic. When I review an AI-built app, I read the webhook handler by hand for exactly this reason.

Envelope with two matching seals compared under a magnifying glass, representing signature verification
Verification is a comparison: your server recomputes the signature and checks it matches.

Why should the secret never be in frontend code?

Anything shipped to the browser is public. If the secret sits in client code, a public environment variable, or a committed file in a public repository, anyone can read it and sign fake events that pass your verification.

In AI-built apps, watch for:

  • Variables with prefixes like NEXT_PUBLIC_ or VITE_, which are bundled into the browser code.
  • Secrets pasted into a prompt, which then land in source files.
  • A .env file committed to GitHub.

The webhook handler must run on a server: an API route, a serverless function, or a backend service. Keys in the browser are the app owner's responsibility, not the platform's.

Where should I store the secret on Supabase or my own hosting?

On Supabase

If the handler is an Edge Function, store the secret with Supabase's secrets feature (the CLI command supabase secrets set or the dashboard equivalent) and read it from the function environment. Never write it into a table the client can query. Also confirm the webhook endpoint does not require a user login token, since Stripe will not send one; the signature is its authentication.

On your own hosting

Use the host's environment variable or secrets manager (Vercel, Fly.io, AWS, and similar all have one). Set separate values for preview, test, and production. Keep a .env file out of version control with .gitignore.

A simple checklist

  • One secret per environment and mode.
  • Stored server-side only.
  • Not in logs, error messages, or screenshots.
  • Known to as few people as needed.

What do I do if the secret leaked?

Act quickly, but calmly:

  1. In the dashboard, open the endpoint and roll the signing secret. Stripe lets you keep the old one valid for a limited time so you can switch without dropping events.
  2. Update the environment variable in your hosting and redeploy.
  3. Remove the secret from the code, and if it was in a Git repository, treat it as exposed forever, because history keeps it.
  4. Review recent events and orders for anything suspicious, such as paid orders with no matching payment in Stripe.
  5. If your API key was exposed too, roll that as well.

Make order fulfillment check the payment state with Stripe where it matters, and make handlers idempotent so a duplicate event does not create a duplicate order. This is not a guarantee against abuse, but it limits the damage.

Want a second pair of eyes?

If you want someone to look at how your app handles Stripe events, email me@filipeeduardo.dev with the link or the handler code and I will tell you what I would check first.

Frequently asked questions

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

No. The API key lets you call Stripe. The webhook secret only verifies events Stripe sends to you.

Can I have more than one webhook secret?

Yes. Each endpoint has its own, and test and live modes are separate, so a project often has several.

Can I skip verification while testing?

It is better not to. Use the Stripe CLI to forward real signed test events, so the code path you test is the one you ship.

Does the platform protect my secret for me?

The platform secures its infrastructure. Where you put the secret and who can read it is up to you as the app owner.

How often should I rotate it?

There is no fixed rule. Rotate when someone leaves the team, when you suspect exposure, or as part of your own security routine.

stripe webhook secret

Filipe Eduardo

Senior Software Engineer. Seven years building web and mobile products end to end and leading the teams that ship them.

Follow along

Want to talk
about this?

I work on problems like this every day. Tell me about yours.

Get in touch