Stripe webhook not working on Replit? How to fix it
Short answer
- Check the webhook endpoint's delivery log in the Stripe Dashboard first: it shows the HTTP status your app returned.
- Signature errors almost always mean the wrong signing secret or a parsed (not raw) request body.
- The endpoint URL must be your published domain, and test and live mode each have their own keys, endpoints and secrets.
The customer pays, Stripe shows the payment, and your app still says Free plan. Webhooks are how Stripe tells your app a payment happened, and they fail quietly. The good news: Stripe records every delivery attempt with the response your app returned, so you can see exactly what broke.
Symptoms
- Payments succeed in Stripe, but orders or subscriptions are not updated in your app.
- Stripe shows webhook deliveries failing with 400, 401, 403, 404 or 500.
- Logs show
No signatures found matching the expected signature for payload. - It worked with the Stripe CLI or in the preview, and fails live.
Likely causes
- The endpoint URL points to the
replit.devpreview URL, an old domain, or a wrong path. STRIPE_WEBHOOK_SECRETis from another endpoint, from the Stripe CLI, or from the other mode (test vs live).express.json()parsed the body before signature verification, which needs the raw bytes.- Auth, CSRF or rate-limit middleware rejects Stripe's request (a 401 or 403).
- The handler throws or takes too long, so Stripe sees a 500 or a timeout.
- Replit's Stripe connector and your own keys are mixed in the same environment.
Step-by-step fix
1. Read Stripe's delivery log
In the Stripe Dashboard, open your webhook endpoint and look at recent events. The status code and response body tell you which step below applies. 404 is the URL, 400 the signature, 401/403 middleware, 500 your handler.
2. Point the endpoint at production
Set the endpoint to https://yourdomain.com/api/stripe/webhook, your custom domain or .replit.app URL, never the preview. Make sure the path matches your route exactly.
3. Use the matching signing secret
Each endpoint has its own signing secret (whsec_…), and the Stripe CLI uses a different one. Copy the secret of this endpoint, in this mode, into the deployment secrets as STRIPE_WEBHOOK_SECRET, then publish again.
4. Verify the raw body
Register the webhook route with a raw body parser, before the global JSON parser.
app.post("/api/stripe/webhook", express.raw({ type: "application/json" }), async (req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(req.body, req.get("stripe-signature"), process.env.STRIPE_WEBHOOK_SECRET);
} catch (err) {
return res.status(400).send(`Webhook error: ${err.message}`);
}
res.sendStatus(200); // answer fast
await handleEvent(event); // then process (idempotently)
});
app.use(express.json()); // global JSON parser AFTER the webhook route
5. Exempt the route from auth and CSRF
Stripe is not a logged-in user. The signature check is the authentication. Make sure session, CSRF and rate-limit middleware skip this one path.
6. Make the handler idempotent
Stripe retries failed deliveries, so the same event can arrive more than once. Store processed event IDs and skip repeats. Use one Stripe method per environment: Replit's docs warn that mixing the Replit connector and your own keys in the same environment causes conflicts.
Copy-paste prompt for Replit Agent
Open a new Agent chat (a fresh chat keeps old attempts out of its context) and paste this:
Stripe webhooks are failing in my published Replit app. Stripe's delivery log shows: [status code + response body].
Please:
1. Make POST /api/stripe/webhook use express.raw({ type: "application/json" }) and register it before express.json().
2. Verify the signature with stripe.webhooks.constructEvent and STRIPE_WEBHOOK_SECRET; return 400 on failure.
3. Exclude this route from auth, CSRF and rate-limit middleware.
4. Return 200 quickly, then process the event; store processed event IDs so duplicates are ignored.
5. Handle checkout.session.completed, customer.subscription.updated and customer.subscription.deleted.
Do not put any key in the code. Tell me which secrets must exist in the deployment.How to verify the fix
- Use Send test event on the endpoint in the Stripe Dashboard (test mode) and get a 200.
- Make a real test-mode checkout on the live URL and see the order or plan update in the app.
- Resend the same event from Stripe and confirm nothing is duplicated.
- After switching to live mode, check the live endpoint's first deliveries.
Payment flows deserve a full check before launch. They are part of the Replit Agent security checklist.
FAQ
Why does Stripe say no signatures found matching the expected signature?
Either the signing secret does not belong to this endpoint and mode, or the request body was parsed as JSON before verification. Use the endpoint's own whsec_ secret and a raw body parser on the webhook route.
Can I use my replit.dev preview URL for Stripe webhooks?
Use it only for quick tests. Live webhooks should point to your published domain, which is stable and runs the deployed code.
Does Stripe retry failed webhooks?
Yes, Stripe retries failed deliveries automatically over a period of time. That is why your handler must ignore events it has already processed.