Your stripe webhook secret is wrong, and Stripe has already stopped retrying while your app still thinks nothing is broken.
The endpoint exists. Events are selected. Customers pay. In the Stripe Dashboard under Developers → Webhooks, delivery rows turn red. Response code 400. Or your handler returns 200 while skipping verification entirely, which is worse.
What you see when the secret does not match
Stripe signs every webhook POST with a value derived from the signing secret you copied when you created the endpoint. Your server must recompute that signature and compare it. When the secret in your environment does not match the endpoint Stripe is calling, verification fails.
Typical symptoms:
- Dashboard shows 400 Bad Request or a body containing
Webhook signature verification failed - Logs mention
No signatures found matching the expected signature - Events flip to Failed after several retries, then stop
- Test events from the Dashboard fail while the rest of your app looks healthy
Stripe does not keep knocking forever. It retries with backoff for up to three days on live endpoints, then marks the event failed. Your customer already paid. Your database never updated. There is no email from Stripe saying your secret was wrong. You only see it if you open the delivery log.
If you have not registered an endpoint at all, that is a different problem. Follow stripe webhook to wire the live URL and events first. This article assumes Stripe can reach your server but rejects the payload or your code never checks it.
Signing secret vs endpoint secret
Stripe uses one name in the Dashboard: Signing secret. Developers also call it the webhook secret, endpoint secret, or whsec_ value. They mean the same thing for a given endpoint.
Do not confuse it with your API secret key (sk_live_... or sk_test_...). The API key proves you to Stripe when your server creates Checkout sessions or reads customers. The signing secret proves Stripe to you when it POSTs events to your URL. Putting the API key in your webhook handler does not verify anything.
- Test vs live. Each mode has its own
whsec_. A secret copied in Test mode fails against live payments. - Stripe CLI vs Dashboard.
stripe listenprints a temporary secret for your laptop, not the production endpoint secret. - Old endpoint, new deploy. Recreating an endpoint generates a new signing secret. Update env vars before the next deploy.
One endpoint, one secret, one mode. Keep them aligned across Dashboard, hosting env vars, and constructEvent.
Your stripe webhook secret must match production
The signing secret is not a suggestion. Without the correct stripe webhook secret in production, every verified handler returns an error and Stripe eventually gives up.
Check your host’s environment panel. Vercel, Railway, Render, and Lovable publish flows all have a place for secrets. The variable name must match what your code reads — often STRIPE_WEBHOOK_SECRET or STRIPE_SIGNING_SECRET. A typo in the name means the handler reads undefined and verification always fails.
Also confirm you deployed after setting the variable. Some platforms inject env at build time only. A secret added yesterday does not help if production still runs last week’s build.
If deliveries show 200 but access never grants, verification may be bypassed. Some generated handlers comment out signature checks and ship that way.
No signature check is a security hole
Webhook URLs are not secret. If your route only parses JSON and trusts the body, anyone can POST a fake checkout.session.completed event. Your handler grants access. No money moved. Stripe never sent that event.
Signature verification closes that gap. Stripe includes a Stripe-Signature header. Your code uses the signing secret to confirm the POST came from Stripe. Skip that step and your payment gate is a public API.
AI coding tools often generate a webhook route that parses event.type and updates the database. Verification is a few extra lines the model drops when rushing to “make payments work.”
The lovable deploy checklist includes env vars and webhooks among the settings publish screens rarely surface.
Fix the stripe webhook secret in Stripe and on your host
Work in Live mode if real customers are paying. Repeat in Test mode for staging.
- Open Stripe Dashboard and switch to Live (or Test for staging).
- Go to Developers → Webhooks and click the endpoint that matches your production URL. If none exists, create one first — see the stripe webhook article.
- Under Signing secret, click Reveal and copy the value starting with
whsec_. - Open your hosting provider’s environment settings for the production deployment.
- Set
STRIPE_WEBHOOK_SECRET(or whatever name your code expects) to that exact value. No quotes in the UI field. No trailing newline. - Confirm your handler passes the raw request body to Stripe’s verification helper. Frameworks that parse JSON first break signature checks. Use the raw string Stripe signed.
- Redeploy or restart the service so the new env var loads.
- In the Dashboard, open the endpoint and click Send test webhook. Confirm the delivery log shows 200 OK and your server logs show a verified event.
If verification still fails, compare character by character. Rotate the secret in the Dashboard with Roll secret, update the host, redeploy, and test again. Old secrets stop working immediately after a roll.
Pre-launch checklist
- Live endpoint exists in Stripe Dashboard (not only CLI tunnel)
- Signing secret copied from the same endpoint URL production uses
whsec_stored in production env vars, not in client-side code or git- Test and live secrets are not swapped
- Handler verifies
Stripe-Signatureon every POST - Raw request body used for verification, then parsed
- Test webhook from Dashboard returns 200
- Failed deliveries in the log are cleared after the fix
Stripe will retry a bad secret for days, then stop. A missing check never retries because there is nothing to fix until someone forges a payment.
FAQ
Is the stripe webhook secret the same as my API secret key?
No. Your API secret key (sk_live_...) authenticates outbound calls from your server to Stripe. The webhook signing secret (whsec_...) lets your server verify inbound POSTs from Stripe. They are different values for different directions.
Where do I find the stripe webhook secret in the Dashboard?
Open Developers, then Webhooks, click your endpoint, and reveal Signing secret. Each endpoint in Test and Live mode has its own whsec_ value. Copy the one that matches the mode your production site uses.
What happens if I skip webhook signature verification?
Anyone who discovers your webhook URL can send fake payment events. Your app may grant access, ship digital goods, or write orders for charges that never happened. Stripe will not block those forged POSTs for you.