The moment your app takes money, a new class of bug starts to matter — the kind where the cost of getting it wrong isn’t a crash, it’s revenue walking out the door. AI builders wire up Stripe quickly and convincingly, and the demo checkout always works. Here are the payment bugs that work in the demo and leak money in production.
The browser decides the price
The most common one: the amount or the product is chosen on the client and trusted on the server. A checkout that sends { amount: 4900 } or a price_id from the frontend is a checkout where anyone can open DevTools and send { amount: 1 }. They pay a cent; your app charges a cent.
The fix is to never trust a price from the client. Use fixed Stripe Price IDs that live server-side, look up the amount on the server from the thing being bought, and create the PaymentIntent or Checkout Session there. The client says what it wants, never how much it costs.
The webhook nobody verifies
The second one is subtler and worse. Stripe tells your app a payment succeeded in two ways: the browser gets redirected to a success URL, and Stripe sends a webhook to your server. The redirect is just a URL — anyone can visit /success without paying. If you grant access, credits or a subscription based on the user landing on the success page, you’re giving the product away.
Access must be granted from the verified webhook, server-to-server — and the webhook signature must be checked (stripe.webhooks.constructEvent with your signing secret). An unverified webhook endpoint is one anyone can POST a fake “payment succeeded” event to.
Test keys, live keys, and secret keys
- Test keys in production means real customers hit a checkout that never charges. Live keys in a test build means you charge real cards while debugging.
- The Stripe secret key (
sk_...) must be server-side only. If it’s bundled into the frontend, anyone can issue refunds, read customers, and create charges as you. (More on how secrets leak into the client.)
Double-charges and double-grants
Without idempotency, a double-clicked button or a retried webhook can charge twice or grant the product twice. Use idempotency keys on charge creation, and make webhook handlers safe to run more than once for the same event.
How to test it
- In Stripe test mode, replay the success URL without completing a payment — do you get the product?
- Tamper the amount or price in the checkout request — does the server accept it?
- POST a made-up event to your webhook endpoint — does it grant anything without a valid signature?
- Search your bundle for
sk_— is the secret key where the browser can read it?
Payment logic is exactly the kind of thing that looks done because the happy-path demo charges your own test card correctly — while the unhappy paths quietly leak money. It’s a core part of a seniorgrade audit: a senior engineer traces where the price is decided, where access is granted, and whether the webhook is real. The free self-check is a quick first pass before you flip on live payments.