# 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.

- Author: Filipe Eduardo, Senior Software Engineer (https://filipeeduardo.dev/)
- Published: October 8, 2026
- Topic: AI-built apps · Tags: stripe webhook secret
- Canonical URL: https://filipeeduardo.dev/blog/stripe-webhook-secret

![Illustration of a webhook envelope with an orange key wax seal being checked at a gate, representing Stripe webhook secret verification](https://cms.filipeeduardo.dev/wp-content/uploads/2026/10/stripe-webhook-secret.webp)

## 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](https://filipeeduardo.dev/blog/ai-app-builder-production), 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](https://filipeeduardo.dev/blog/stripe-webhook), 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](https://filipeeduardo.dev/blog/stripe-webhook-events). 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](https://cms.filipeeduardo.dev/wp-content/uploads/2026/10/stripe-webhook-secret-1.webp)

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](https://filipeeduardo.dev/blog/no-code-ai-app-builder).

## 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](https://filipeeduardo.dev/blog/web-app-security-audit), 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](https://cms.filipeeduardo.dev/wp-content/uploads/2026/10/stripe-webhook-secret-2.webp)

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](https://filipeeduardo.dev/blog/leaked-api-keys-ai-built-app), 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.
