Payments
Decide prices and access on the server
Create Stripe Checkout Sessions with server-side Price IDs and grant access from verified webhooks, not the success redirect.
Why it matters
Anything the browser sends can be edited in DevTools, including the amount. The success URL can also be opened without paying.
How to fix it
- Create the Checkout Session on the server using a Price ID you choose, not an amount from the request.
- Grant access when a verified webhook arrives (for example
checkout.session.completed), not when the user lands on the success page. - Store the Stripe customer and subscription IDs against your user.
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: process.env.STRIPE_PRICE_PRO!, quantity: 1 }],
client_reference_id: user.id,
success_url: `${origin}/thanks`,
cancel_url: `${origin}/pricing`,
});How to verify
- Open the success URL directly without paying; nothing should unlock.
- Change a price in the request body; the checkout amount must not change.
Related checks
- Verify the Stripe webhook signature
- Keep Stripe test and live keys apart
- Treat user and AI input as untrusted
Tick it off in the full checklist