Stripe
Stripe webhook not firing
The payment can succeed in Stripe while your handler never runs, or runs and rejects the signature. The homepage will still return 200.
Symptoms
- A customer paid, and the order, license, or welcome email never appeared.
- The Stripe event exists, and the deliveries are missing, failing, or pointed at an old URL.
- Test mode updates the app. Live mode does not. Or the reverse.
- The handler logs “signature verification failed” on events you can see in the Dashboard.
Common causes
The endpoint URL is the preview host
The endpoint was created while the app lived on a Lovable, Bolt, Replit, or Vercel preview URL. Production is a different host. Stripe is still posting to the one you registered first.
The signing secret is from a different endpoint
Each endpoint has its own secret, and test mode and live mode do not share them. The env var on the server is often the secret from the endpoint you created locally.
The events you handle are not selected
Checkout completion is checkout.session.completed. payment_intent.succeeded is a different event. A handler that only reads one of them looks identical to a webhook that never arrives.
The raw body was parsed before the check
Stripe signs the exact bytes. request.json(), a framework parser, or a proxy that re-encodes the body makes the signature fail. The event was delivered. Your code threw it away.
The handler returns an error or times out
A 500, a 404, or a slow edge function counts as a failed delivery. Stripe retries, then backs off. An edge function that verifies the event and then does too much extra work is a common timeout.
A bank debit is not paid on the first event
Some payment methods complete later. The first event is not the paid event. Code that only listens for the immediate card event will miss them and look like a silent webhook.
Fix it
Open the event and read the delivery
In the Stripe Dashboard, open the payment and then the event. Read the endpoint URL, the response code, and the error. A missing delivery means Stripe is not calling this endpoint. A 400 usually means the signature. A 500 or a timeout means your handler ran and failed.
Match the URL to production
The endpoint should be the https URL customers use, including the path. Update it when you leave the preview host. Send a test delivery to that exact URL and confirm your logs show it.
Compare the secret to the deployed env
Reveal the signing secret on that endpoint, in the same mode as the payment. Compare it to the variable the deployed server actually has, not the one in a local file. Restart or redeploy after you change it.
Verify against the raw body
Read the raw request bytes, check the Stripe-Signature header, and only then parse JSON. If a framework parses the body first, turn that off for this route. The free signature checker on this site can confirm a secret against a pasted raw body in your browser.
Return 2xx only after the write is durable
Record the event id before you do slow work, and ignore a duplicate id. Return a non-2xx if you could not record it, so Stripe retries. Returning 200 and then crashing loses the payment with no retry.
Subscribe to the events your code reads
Select checkout.session.completed for Checkout, and any later events you depend on for delayed payment methods. If you use both modes, create the endpoint in both.
Catch it automatically
A production FunnelWatch check stops on the Checkout page and confirms the price. It does not need a webhook to do that. On Scale, a restricted read key can list recent payment events whose webhooks are still pending. FunnelWatch refuses a live secret key. Pair that with the browser signature checker if you want to test a secret without sending it to us.
Watch the next break before a customer does.
Map the path once. The next publish does not get to fail in private.
Questions
Does a 200 on the homepage mean the webhook works?
No. The homepage and the webhook route are different requests. Stripe can be failing the webhook while the marketing page looks fine.
Should I paste a live secret key into FunnelWatch?
No. Scale accepts a restricted key that can read events. The signature checker runs in your browser and does not send the signing secret to FunnelWatch.
Why did retries stop?
Stripe retries a failed delivery for a limited time and then marks the endpoint as failing. Fix the handler and resend the event from the Dashboard rather than waiting for another customer.