CRITICAL Stripe Checkout · PayPal · Razorpay

Prices sent from the browser to the checkout

What you'd see: Your create-checkout endpoint accepts amount, price or total in the request body.

§ 01 — What's actually happening

Anything the browser sends is user input, including numbers the user never typed. Editing a fetch call in DevTools takes seconds. If your server bills whatever arrives, the price on your pricing page is a suggestion.

Why an AI tool writes it this way

The cart total already exists in frontend state, so passing it to the server feels like reuse rather than duplication. Recalculating server-side looks wasteful until you notice the first order for $0.50.

What it costs you

Full-price products bought for pennies, with a real Stripe receipt and a real fulfilment email. It usually surfaces in reconciliation weeks later, after the goods have shipped.

§ 02 — Before & after

The customer decides what to pay

Illustrative code, written for this page — never a client's project.

AI-generated

  • Amount taken from the request body
  • Quantity and discounts also trusted
  • A real receipt is issued, so it looks legitimate in your dashboard
// The browser says what it feels like paying const { amount, productId } = await req.json(); const session = await stripe.checkout.sessions.create({ line_items: [{ price_data: { currency: 'usd', unit_amount: amount, // 100000 or 1, entirely their choice product_data: { name: 'Pro plan' } }, quantity: 1 }], mode: 'payment' });

Human-reviewed

  • Server looks up the price from its own catalogue
  • Client sends only what to buy, never how much it costs
  • Coupons validated server-side against real, unexpired records
const { priceId, couponCode } = await req.json(); // Only prices you created, looked up by ID on the server const PRICES = { pro_monthly: 'price_1NabcXYZ', pro_yearly: 'price_1NdefXYZ' }; const stripePriceId = PRICES[priceId]; if (!stripePriceId) { return Response.json({ error: 'Unknown plan' }, { status: 400 }); } const coupon = couponCode ? await validateCoupon(couponCode) : null; // server-side const session = await stripe.checkout.sessions.create({ line_items: [{ price: stripePriceId, quantity: 1 }], discounts: coupon ? [{ coupon: coupon.id }] : [], mode: 'subscription', client_reference_id: user.id });

§ 03 — Check your own

How to tell in two minutes

You don't need us to run these. If any of them come back the wrong way, you have this problem.

  1. Open DevTools → Network, start a checkout, and look at the request payload. If you can see an amount, you have the bug.
  2. Confirm the server maps a plan identifier to a price it owns, rather than accepting price_data from the client.
  3. Check coupons and quantities too — a negative or huge quantity is the same class of bug.
  4. After payment, verify entitlement is granted by the webhook, not by the browser redirecting to /success. That URL can simply be visited.

Found it in your project? Fixing this one properly usually takes an engineer under an hour. Finding the other nine takes longer — which is what the free health check is for.

Get my free health check

Find out where you actually stand.

Free. No obligation. Reviewed by experienced engineers.

Get my free health check