Why Payments Deserve Extra Scrutiny
Everything else in your app fails softly. A broken button is annoying. A payments bug can charge someone twice, give away a paid feature for free, or leave you holding a customer's money with no record of what they paid for. AI is very good at producing checkout code that looks complete — a form, a button, a "Payment successful" message — while missing the parts that only matter once real money and real attackers are involved.
The good news: you don't need to understand payment processing to do this safely. You need to avoid one common mistake — building your own card form — and add one thing AI tools often skip by default: a webhook that confirms payment happened, instead of trusting the browser.
If an AI tool ever generates a plain <input> for a card number, expiry, or CVC that your own server code reads or stores, stop. That puts you in scope for PCI compliance rules most solo builders can't meet, and a leaked or logged card number is a serious incident. The fix below avoids this entirely by never letting card details reach your code.
The Safe Default: Hosted Checkout
Payment providers such as Stripe offer a hosted checkout page: your app sends the customer to a page Stripe controls, the customer enters their card there, and Stripe redirects back to your app afterward. Your code never sees a card number — it only ever sees a "this session paid" confirmation.
- Stripe Checkout — a hosted page you redirect to, good for one-time payments and subscriptions, works well with Bolt, Lovable, Replit, and v0.
- Stripe Payment Links — an even simpler option: a shareable checkout URL you can create without writing any code at all, useful if you just need to sell one thing.
Both keep card handling entirely on Stripe's side. Building a custom card form with Stripe's lower-level Elements API is possible and sometimes necessary for a fully embedded checkout experience, but it's more surface area to get wrong — start with hosted Checkout unless you have a specific reason not to.
The Prompt to Ask For
Be specific that you want the hosted flow, not a custom form, and ask for the webhook in the same prompt so it isn't skipped:
Add Stripe Checkout to my app for a one-time payment of $[amount] for [product/feature]. Use Stripe's hosted Checkout page — do not build a custom card input form. Include: (1) a backend endpoint that creates a Checkout Session, (2) a webhook endpoint that listens for the payment-succeeded event and unlocks the feature only when that event arrives, and (3) test mode using Stripe's test API keys. Explain where my secret key goes and confirm it is never sent to the browser.
When the AI returns code, check for these three things before you consider it done:
- The secret key lives only in backend/server code (an environment variable), never in any file that ships to the browser.
- The feature unlocks from the webhook handler, not from the redirect page the customer lands on after paying — the next section explains why this matters.
- Prices are set in your backend code or in Stripe itself, not read from a value the browser sends — otherwise anyone could edit the page and "buy" your product for $0.01.
Why the Success Page Isn't Proof of Payment
After Stripe Checkout completes, Stripe redirects the browser back to a URL you chose — often something like /success. It's tempting to unlock the paid feature the moment that page loads. Don't: that redirect happens in the customer's browser, which means anyone can type yoursite.com/success directly, with no payment at all, and your app would unlock the feature for free.
A webhook is a message Stripe's servers send directly to your server, not through the customer's browser, the moment a payment actually succeeds. Because it comes server-to-server, the customer can't fake, skip, or intercept it. It's slower to set up than reading the success page, but it's the only signal you can actually trust.
In practice: your webhook endpoint receives an event, verifies it really came from Stripe (using a signing secret Stripe gives you), and only then marks the order as paid or unlocks the feature in your database. The success page the customer sees can show a friendly "thank you" message, but it should not be what grants access.
Show me the webhook handler you just created. Confirm it verifies the Stripe signature on every incoming request before trusting the event, and confirm the feature unlock happens inside the webhook handler, not on the /success page.
Test Mode Before Live Mode
Stripe (and most providers) give you two parallel sets of API keys: test mode and live mode. Test mode behaves identically but no real money moves, and Stripe publishes test card numbers that simulate success, decline, and other outcomes.
- Build and test your entire checkout and webhook flow in test mode first, using Stripe's published test cards.
- Only switch to live keys once a full test-mode purchase — checkout, webhook, unlock — works end to end.
- Keep both keys out of your repository. If you're using an AI coding tool connected to your codebase, confirm it's reading the key from an environment variable or secrets manager, not a hardcoded string that could end up committed or pasted into a chat.
If you're not sure whether a key or credential is safe to share with an AI tool at all, the developer-focused Sanitizing Code and Data Before Sending to AI guide covers what to scrub before pasting anything into a conversation.
If You're Adding a Subscription
Recurring billing (monthly or annual plans) adds a few more events worth handling beyond the initial payment: renewal succeeded, renewal failed (an expired card, for example), and cancellation. Each of these should also be driven by a webhook, not by checking Stripe every time a user opens your app.
Extend the Stripe integration to a monthly subscription. Add webhook handling for subscription renewal succeeded, renewal failed, and subscription canceled, and show me how each one should update the user's access in my database.
Pre-Launch Payments Checklist
- Hosted checkout, not a custom card form — your app should never receive a real card number.
- Secret key server-side only — never in browser-shipped code, never committed to the repo.
- Prices set on your backend or in Stripe — never trusted from a value the browser sends.
- Feature unlock happens in the webhook handler — the success page is just a thank-you message.
- Webhook signature verified — so a fake request can't fake a payment.
- Full flow tested in test mode — checkout, webhook, and unlock all confirmed before switching to live keys.
Related Guides
Growing Your Vibe Coded App
Adding features, handling real users, and keeping an eye on hosting and storage costs as your app grows.
Sanitizing Code and Data Before Sending to AI
What to scrub before pasting code or credentials into an AI conversation — including API keys.