Payments

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

  1. The endpoint URL points to the replit.dev preview URL, an old domain, or a wrong path.
  2. STRIPE_WEBHOOK_SECRET is from another endpoint, from the Stripe CLI, or from the other mode (test vs live).
  3. express.json() parsed the body before signature verification, which needs the raw bytes.
  4. Auth, CSRF or rate-limit middleware rejects Stripe's request (a 401 or 403).
  5. The handler throws or takes too long, so Stripe sees a 500 or a timeout.
  6. 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.

Get a free audit