Everything else tells you when it breaks. Payments don't.
That's the part that makes this different from every other problem in your app.
A broken page throws an error. A failing API returns a status code. Someone notices, and you fix it.
A webhook that never arrives produces nothing at all. The payment succeeded. The money moved. Your customer has a receipt from Stripe. Your application simply never found out, so your database still thinks they're on the free plan — and nothing anywhere is flagged as wrong.
The same goes in the other direction. A failed renewal that doesn't reach your app leaves someone using a product they stopped paying for. A duplicate webhook processed twice grants access twice, or charges twice. A refund issued at the provider and never reflected in your database leaves the record permanently wrong.
None of that appears in your logs as a problem. It appears weeks later as a support ticket, a chargeback, or a number that doesn't reconcile — and by then you're reconstructing history rather than fixing code.
There's a reason AI builders miss this specifically.
Checkout is visible. Correctness isn't.
AI tools generate the checkout flow well, and they should — it's the part you can see. A button, a hosted payment page, a redirect back. You test it with a card number from the docs, it works, you move on.
What they don't generate is everything that makes the money correct: verifying that a webhook actually came from Stripe, handling the same event arriving twice, reconciling what the provider believes against what your database believes, and deciding what happens when a charge fails three weeks after signup.
Those aren't features. They're guarantees, and they only exist if someone deliberately wrote them. Nobody prompts for "make sure a retried webhook doesn't grant access twice," because nobody knows retries happen until they've been bitten by one.
So what's usually wrong?
What you see, and what's usually behind it
What you see | What's usually wrong |
Test mode works, live doesn't | Live keys not set, or the webhook endpoint registered in test mode only |
Payment succeeds, nothing happens in the app | Webhook never delivered, or the endpoint errored and the retries gave up |
Some users charged twice | Webhook not idempotent — a retry processed as a new event |
Someone cancelled and still has access | No handling for customer.subscription.deleted |
Card expired, user kept access | Failed renewals never reach the application |
Refunded and still active | Refund processed at the provider, ignored by the app |
Free users have paid features | Entitlement checked once at signup and never again |
Anyone can get a subscription | Webhook signature unverified — the endpoint trusts any request |
Amount charged doesn't match the plan | Price or quantity sent from the browser rather than derived server-side |
Numbers don't reconcile at month end | Nothing compares provider state against database state |
Webhooks stopped arriving entirely | Endpoint returned errors, provider disabled it, nobody was told |
Marketplace payouts wrong | Split logic implemented in the app rather than by the provider |
The eight causes underneath
Webhook signature never verified. The endpoint accepts any request that reaches the URL. Anyone who finds it — and URLs get found — can grant themselves a lifetime subscription with a single POST. This is the most serious finding on this page and it's present more often than you'd expect.
Webhooks not idempotent. Stripe, Paddle and every other provider retry delivery. A retry processed as a fresh event charges twice, grants twice, or sends two confirmation emails. The fix is a record of processed event IDs and nothing more, which is why its absence is so frustrating.
Test mode working, live mode not. Different API keys, a different webhook endpoint, and a different dashboard. The gap only becomes visible once real money is involved, which is the worst possible time to find it.
Subscription state drifting. A renewal fails, a card expires, someone cancels — all of it happens at the provider. If your app isn't listening for those events, your database is a snapshot from signup day that slowly diverges from reality.
Refunds unhandled. The provider processes it and your application never knows. Access stays live, the record stays wrong, and the customer has been refunded and retained simultaneously.
Amounts calculated client-side. Price, quantity or discount sent from the browser to your server and used as given. Anyone can change what they're charged by editing a request. Generated checkout code does this routinely because it's the simplest thing that works.
No reconciliation. Nothing compares what the provider says happened against what your database believes happened. Divergence is invisible by design, which is why it's usually discovered by an accountant rather than an engineer.
Failed webhooks disappearing. The endpoint errors, the provider retries for a while, then stops delivering entirely. There's a delivery log showing all of it, and nothing that tells you to go and look.
Three things you can check right now — all diagnostic, none of them changes anything.
Three checks, and nothing more
Unlike the rest of this cluster, we're not going to suggest you fix anything yourself here.
Changing live payment code without knowing what you're doing can cost real money. These three tell you where you stand and stop there.
1. Open your webhook delivery log In the Stripe dashboard: Developers → Webhooks → your endpoint. Look at recent attempts. Anything showing failures or retries is an event your application never processed. If deliveries stopped entirely at some date, that's when your data started diverging.
2. Search your webhook handler for a signature check Look for constructEvent, a webhook secret, or any signature verification at all. If there isn't one, your endpoint trusts anything that reaches it — and that's a route to free access, usually discovered by someone who wasn't looking for it.
3. Cancel a test subscription and check your database Cancel at the provider, then look at your own record. If it still says active, state is drifting and every cancellation since launch is likely wrong too.
If any of those come back badly, stop and get help rather than experimenting. This is the one area where a wrong fix is worse than no fix.
When to stop and get help
Money is moving incorrectly right now. Charged twice, refunded and retained, or paying customers without access. Every hour has a direct cost and a customer attached to it.
The signature check is missing. That's not a bug to schedule. It's an open door.
You can't tell how far back it goes. If webhooks stopped arriving in March, the question is no longer what's broken — it's how many records are wrong and which ones.
You're about to launch. The cheapest moment to get this right is before real cards are involved.
Here's how we approach it.
We follow the money, not the code path
An engineer traces three things and compares them: what the provider recorded, what your application recorded, and whether those two can ever disagree without anyone noticing.
That last question is the one that matters. A payment system that's correct today but has no way of detecting divergence will be wrong eventually and won't tell you.
You get verified webhooks, retries handled safely, subscription and access state that follows what actually happened at the provider, refunds that revoke access, and a way to see divergence when it occurs rather than finding it in a spreadsheet.
What we won't do
Quote a flat price before reading the payment flow. Nobody can size this responsibly without looking.
Fix checkout and leave reconciliation. Checkout is the part that already works.
Move you to a different provider without a real reason. Stripe, Paddle, Razorpay and Lemon Squeezy are all fine; the integration usually isn't.
And about the records that are already wrong —
Fixing the past is a separate job
Once the code is correct, there's usually a second question: how many existing records are wrong, and what do we do about them.
Sometimes it's a handful and a script fixes it in an afternoon. Sometimes webhooks have been failing for four months and you have hundreds of accounts in a state nobody can reconstruct without going back through the provider's own event history.
We scope that separately and only after the audit shows how far the divergence goes, because guessing at it is how a two-week job becomes a two-month one. If some of it genuinely can't be reconstructed, you'll hear that too.
Why we won't quote up front
You'll have noticed there's no price on this page, and that's deliberate rather than evasive.
Payments have to be correct, not approximately correct. Two apps with identical-looking checkout flows can be a day apart or a month apart depending on how long webhooks have been failing, whether anyone touched the subscription state manually, and how many providers are involved.
Anyone who quotes you a fixed number before reading the flow is either guessing or scoping something smaller than what you need. You'll get a fixed scope and a fixed price after the audit, which costs you nothing and takes 48 hours.
How it runs
01 — Audit. You send the repository and read access to your provider dashboard if you can. An engineer traces checkout, webhooks, state and refunds, and reports back within 48 hours. Free.
02 — Triage. What's costing money now, separated from what's untidy.
03 — Secure. Signature verification and idempotency first, because those are the exposed ones.
04 — Reconcile. Subscription and access state made to follow the provider, including the events nobody was listening for.
05 — Verify. Tested against real provider events — declined cards, failed renewals, cancellations, refunds, duplicate deliveries.
06 — Repair. Historical records corrected, if that's in scope. Quoted separately.
07 — Handover. What was wrong, what changed, and a way to see divergence before a customer does.
Questions
Stripe, Paddle, Lemon Squeezy, Razorpay, PayPal? All of them. The failure classes are identical; only the API differs.
Can you fix the records that are already wrong? Usually. It's scoped separately once the audit shows how far the divergence goes, and we'll tell you honestly if some of it can't be reconstructed.
We think we've been charging people incorrectly. Tell us immediately and say so on the form. That's a different and faster process.
Can you add marketplace or split payments? Yes. It's also the point where payment logic normally has to move out of a no-code layer entirely, and we'll explain why before starting.
How long does it take? Signature verification and idempotency are usually days. Full reconciliation depends on how far state has drifted, which is exactly what the audit establishes.
Do you need access to our Stripe account? Read access to the dashboard makes the audit considerably more useful, because the delivery log tells us things the code can't. Restricted keys are fine.
Will you sign an NDA? Yes, mutual, before you send anything. One senior engineer reads your code, never used for training, access removed after delivery.
Money is moving wrongly right now. Tick the box on the form. Live financial incidents go first.
Send us the repository
You'll get what's failing in the payment flow, what's exposed, how far the state has drifted — and a fixed scope to correct it.
Read access is enough. Mutual NDA first if you'd prefer.
Also broken?
Authentication · Supabase · Security audit · Stuck at 80% · Database and multi-tenancy · Lovable