Payments Preflight · Check 6 of 15

Never trust prices or amounts from the client

Create Checkout Sessions and PaymentIntents on the server using Price IDs from your own config. Never accept an amount, price or plan from the browser.

What it is

The browser should send what the customer wants (a product or plan key). Your server decides the price and creates the Checkout Session or PaymentIntent.

Why it matters

Anything the client sends can be edited. If your API accepts amount or a Price ID straight from the request, a user can pay one cent for your top plan.

How to check

How to fix it

  1. Map an allowed plan key to a Price ID on the server and reject anything else.
  2. Grant access in fulfillment, after retrieving the Checkout Session and checking payment_status.
  3. Treat the success URL as a page to show, not proof of payment.
const PRICES = { pro: process.env.PRICE_PRO!, team: process.env.PRICE_TEAM! } as const;
const plan = (await req.json()).plan as keyof typeof PRICES;
if (!PRICES[plan]) return new Response("Unknown plan", { status: 400 });
const session = await stripe.checkout.sessions.create({
  mode: "subscription",
  line_items: [{ price: PRICES[plan], quantity: 1 }],
  success_url: `${origin}/billing?session_id={CHECKOUT_SESSION_ID}`,
});

Prompt for your AI coding tool

Paste into Cursor, Claude Code, Lovable or similar. Review the diff before you accept it.

Check my Stripe checkout code. The client must only send a plan or product key. The server must map it to a Price ID from an allowlist, reject anything else, and never read amount, unit_amount or price from the request. Grant access only after a verified webhook or after retrieving the Checkout Session server-side and checking payment_status. Show the diff.

Sources

Related checks

All payments checks

Sponsor this page: get in touch.

Get new checks by email

New checks and real leak post-mortems, about once a month. No spam.