All posts

Field guide

AI wrote my payment flow. Is it safe? Stripe security for vibe-coded apps

Payments are where a security bug costs you actual money, and AI tools wire up Stripe quickly by skipping the parts that keep it safe. Here are the four things to check before you take a real payment, starting with the one that lets people pay nothing.

Last updated

A payment flow is the one place where a security gap turns directly into lost money, either yours or a fraudster's. AI tools are good at getting Stripe to work and bad at getting it safe, because the safe version has steps the demo does not need. Before you flip on real payments, check these four things, in order of how much they matter.

1. Verify your webhook signatures

This is the big one. Stripe tells your app about completed payments through a webhook, a public URL anyone can POST to. If your handler grants access on a "payment succeeded" event without verifying it came from Stripe, anyone can forge that event and get access for free. Verify the signature against the raw body with your webhook secret, and reject anything that fails. The full how-to is in webhook signature verification.

Want to see exactly what Prbl flags? Watch it scan a demo app, no repo or account needed.

See a live scan →

2. Keep the secret key server-side

Stripe gives you two keys. The publishable key (pk_) belongs in the browser. The secret key (sk_) can create charges and read your account, and must never leave your server. AI tools sometimes inline the secret key to make a call work, which ships it to every visitor. If a sk_live_ key is in your bundle, rotate it now and move the call server-side; see why a key in an env var still leaks.

3. Decide the amount on the server

Never charge an amount the client sent you. A user can change it and pay a penny. Have the client tell you what they are buying, and let your server look up the price from your own data:

// the risky pattern: trust the client's amount
const session = await stripe.checkout.sessions.create({
  line_items: [{ price_data: { unit_amount: req.body.amount }, quantity: 1 }],
});

// safe: server decides the price from a product id
const product = PRODUCTS[req.body.productId];      // your own data
const session = await stripe.checkout.sessions.create({
  line_items: [{ price: product.stripePriceId, quantity: 1 }],
});

4. Let Stripe handle the card data

Do not store card numbers. Stripe's elements and hosted checkout handle the card through its own systems, so the raw data never touches your server, which keeps you out of most compliance scope. If your app is holding card details, switch to Stripe's flows; the safest card data is the data you never store.

Frequently asked questions

What is the most common payment mistake in AI-built apps?

Trusting a webhook without verifying its signature. The generated handler receives a 'payment succeeded' event and grants access, but never checks the event actually came from Stripe. Since the webhook URL is public, anyone can POST a fake event and get access without paying. Verifying the signature against the raw body with your webhook secret is the fix, and it is the single highest-value check.

Is it safe that my Stripe key is in the frontend?

It depends which key. The publishable key (pk_) is meant to be in the browser and is fine there. The secret key (sk_) must never reach the client; it can create charges and refunds and read your account. If you find an sk_ key in your bundle, rotate it immediately and move every call that uses it to your server. AI tools sometimes inline the secret key to make a call work.

Why does verifying the amount on the server matter?

Because anything the client sends can be tampered with. If your server creates a charge using a price the browser passed in, a user can change it and pay one cent. The server should decide the amount from your own product data, keyed by a product id, never trust a price submitted by the client. Let the client say what they want to buy, not what it costs.

Do I need to store card details securely?

You should not store card details at all. Stripe handles the card data through its own elements and tokens, so the raw card number never touches your server, which keeps you out of most compliance scope. If you find yourself storing card numbers, stop and use Stripe's hosted or embedded flows. The safest card data is the card data you never hold.

Check your payment flow

Two of these, a leaked secret key and an unverified webhook, a scan can flag directly. Before you take real money, it is worth confirming neither is in your app. Run a free scan and check the parts of your payment flow that ship to the browser.

Prbl scans your live app or your codebase for exactly the kinds of issues above.

AI Wrote My Payment Flow. Is It Safe? Stripe Security for Vibe-Coded Apps: Prbl