A customer paid. Your stripe webhook never fired, and now you are reading support emails instead of shipping product.
Stripe shows a green Paid badge. Your customer has a receipt. Your app still treats them like a stranger. No account upgrade, no license key, no confirmation email from your backend. The checkout page worked. The part that tells your server money arrived did not.
A customer paid but your app stays silent
This is the most common failure after launch. Checkout redirects back to your site with a success URL. The customer sees a thank-you page. That page is cosmetic. It does not prove your backend processed the payment.
Stripe does not call your database directly. It sends an HTTP POST to a URL you register. If that URL is wrong, offline, or returns an error, Stripe retries for a while and then stops. No alert pops up in your editor. No banner appears in the Stripe Dashboard unless you look at webhook delivery logs.
AI coding tools are good at rendering a pay button and a success screen. They often skip the server route that must run when Stripe confirms payment. You end up with a pretty funnel and a silent backend.
Stripe says paid. Your database was not updated
Open your app database after a real payment. The user row still shows free. The order table is empty. The subscription column is null. Stripe has the money. Your product logic never ran.
That logic almost always lives in a webhook handler: verify the event, read the customer ID, write a row, grant access, enqueue an email. If the handler file exists only on your laptop or only runs on localhost, production data never changes.
Some teams add a client-side call on the success page: fetch('/api/confirm-payment'). That is fragile. The user can close the tab before the request finishes. A webhook is Stripe calling you, independent of the browser.
- Payment record exists in Stripe Dashboard
- No matching row in your orders or subscriptions table
- User still lacks the feature they paid for
- Webhook delivery log shows failures or no attempts at all
Your stripe webhook endpoint is missing in production
Local development often uses the Stripe CLI: stripe listen --forward-to localhost:3000/api/webhooks/stripe. That tunnel dies when you close the terminal. It is not your production endpoint.
After you deploy to Vercel, Railway, or a VPS, you need a public HTTPS URL on the live host, for example https://yourdomain.com/api/webhooks/stripe. If you only registered a localhost URL in the Stripe Dashboard, live payments have nowhere to go.
Common production gaps:
- Webhook route not deployed (missing from build output or wrong path)
- Endpoint registered under Test mode but site uses Live keys
- Firewall or auth middleware blocking Stripe’s POST
- Server returns 404 because the framework route name differs from what you registered
Deployments from Lovable, Bolt, or similar editors often ship the checkout UI without a durable API route on the same domain. See lovable deploy for the other production settings editors rarely ask about.
Webhook events your app must handle
Registering an endpoint is not enough. You must subscribe to the right webhook events. Subscribe to too few and payments complete without triggering your code. Subscribe to everything and you add noise without fixing the bug.
Start with these:
checkout.session.completed— one-time payments through Stripe Checkoutinvoice.paid— subscriptions and recurring billing
If your integration uses Payment Intents without Checkout, add payment_intent.succeeded. For a typical AI-built SaaS with Checkout, the first two events cover most launches.
In the Stripe Dashboard, open each failed delivery and read the response body. A 500 error with a stack trace means Stripe reached your server but your handler crashed. A timeout means the URL is wrong or the server is down. No delivery row at all usually means the endpoint was never added in Live mode.
Fix it in the Stripe Dashboard
Walk through this list on the live site, with live keys, before the next customer pays.
- Log in to Stripe Dashboard and switch to Live mode (toggle in the top right). Test mode webhooks do not fire for real charges.
- Go to Developers → Webhooks.
- Click Add endpoint (or edit the existing one if the URL is wrong).
- Set Endpoint URL to your production HTTPS path on the live host, for example
https://yourdomain.com/api/webhooks/stripe. Not localhost. Not a preview deployment URL unless that preview is what customers use. - Under Events to send, select
checkout.session.completedandinvoice.paid(add others only if your stack requires them). - Save the endpoint. Stripe shows a signing secret for this endpoint. Your server must verify signatures with that secret — covered in stripe webhook secret, not here.
- Send a test event from the Dashboard or complete a small live payment. Confirm the delivery log shows 200 OK and your database updates.
Repeat the same steps in Test mode with your staging URL and test keys. Test and live are separate worlds. Fixing one does not fix the other.
Why the AI built checkout without a webhook
Generators optimize for visible progress: a pricing page, a Stripe Checkout redirect, a success message. Webhooks are invisible until they fail. The model may paste example keys, skip environment variables on the host, or leave the handler as a comment. You only discover the gap when a real card is charged.
Treat the webhook route as part of the payment feature, not an optional extra. If checkout ships, the handler and Dashboard endpoint ship in the same pull request.
Pre-launch checklist
- Live endpoint URL exists in Stripe Dashboard under Live mode
- Production server exposes the route and returns 200 for valid events
checkout.session.completedand/orinvoice.paidare selected- Signing secret from the Dashboard is stored in production env vars (see sibling article)
- Test payment in Test mode updates the database on staging
- Small live payment updates the database on production
- Webhook delivery log has no unresolved failures
Stripe will keep your money safe. It will not guess how your app should react. You have to tell it where to knock.
FAQ
Why does Stripe show paid but my app does nothing?
The payment succeeded in Stripe, but your server never received or handled the webhook event. Without a live endpoint listening for checkout.session.completed or invoice.paid, your database and app stay unchanged.
Do I need separate stripe webhook endpoints for test and live?
Yes. Test mode and live mode use different API keys and different webhook endpoints. A URL that works with test keys does not automatically exist in live mode.
Which stripe webhook events should I subscribe to first?
Start with checkout.session.completed for one-time Checkout payments and invoice.paid for subscriptions. Add payment_intent.succeeded only if your integration uses Payment Intents directly instead of Checkout.