To add Stripe payments to an app built with AI, pick the Stripe product that matches what you sell (a Payment Link, Checkout, or Checkout with a subscription), keep the Stripe secret key on the server in an edge function, and let a webhook, not the success page, write the paid status to your database. Then test everything in Stripe's test mode before going live. An AI builder can write all of the code, but it needs these decisions spelled out, one step at a time.
This guide covers the three Stripe options, the server-side setup, the webhook, the customer portal, testing, and the exact prompts to send. For the wider context of building the app itself, see our guide to creating an app with AI.
Payment Links, Checkout or subscriptions: which one fits
Use a Payment Link when you sell something simple and do not need your app to react in real time, use Checkout when your app decides what to charge and who is paying, and use a Checkout session in subscription mode when access depends on a recurring plan. According to Stripe's Payment Links documentation, a Payment Link is a page hosted by Stripe that you create without code and share by link, QR code or an embeddable buy button. It can sell a product, a subscription or accept donations, with limited visual customisation.
Checkout is the same kind of payment page, but your code creates it through the Checkout Sessions API, as described in Stripe's Checkout documentation. That is what lets your app attach the payment to a logged-in user, choose the price from your database and send the customer back to the right screen. Checkout can be a Stripe-hosted page or a form embedded in your site.
| Option | Code needed | Knows which user paid | Best for | Main limit |
|---|---|---|---|---|
| Payment Link | None to create it | Only by matching the email or a URL parameter afterwards | A first sale, a one-off product, a donation, a pre-sale | Limited customisation; your app is not in the loop unless you add a webhook |
| Checkout (one-time payment) | A server function | Yes, you pass your user id when creating the session | Buying credits, a paid report, a booking deposit | You maintain the function and the webhook |
| Checkout (subscription mode) | A server function plus webhook handling | Yes | SaaS plans, memberships, anything billed every month or year | Renewals, failed payments and cancellations all arrive later and must be handled |
The secret key never goes in the browser
The Stripe secret key must live only on a server, because anyone who holds it can create charges, issue refunds and read customer data. Everything the browser needs is a short-lived Checkout URL that your server returns after calling Stripe. An AI builder that puts the secret key in a React file has made the app unsafe, and it is the first thing to check in any generated payment code.
The standard pattern is an edge function, a small piece of server code that runs next to your database. The browser calls it with the user's login, the function checks who the user is, creates the Checkout session with the secret key, and returns the URL. The key itself is stored as an environment variable, never in the code. Our guide to vibe coding security lists the other checks worth running on generated code.
Why the database is updated from the webhook, not the success page
Mark an order as paid only when Stripe tells your server so through a webhook, never when the customer lands on your success page. The success page is just a browser tab: it can be closed before it loads, opened by someone who typed the address, or never reached because the customer's connection dropped after paying. A webhook is Stripe calling your server directly, and Stripe's webhook documentation describes it as the way to react to asynchronous events such as a bank confirming a payment, a dispute, or a recurring payment succeeding.
A webhook handler needs four properties:
- It verifies the signature. Each request carries a
Stripe-Signatureheader. The handler checks it with your endpoint secret, so a stranger posting fake "paid" events is rejected. The timestamp inside the header also limits replays. - It answers quickly with a success code. Stripe expects a 2xx response. In live mode it keeps retrying a failed delivery for up to three days with growing delays; in test mode it makes three attempts over a few hours.
- It is safe to run twice. Retries mean the same event can arrive more than once. Store the event id, or write the status in a way that gives the same result on a second run.
- It is reachable without a user login. Stripe is not a logged-in user of your app, so this one function must accept requests without your normal login check, and rely on the signature instead.
What to store in the database
Store the Stripe customer id, the subscription id, the plan and the subscription status next to each user, and update them only from webhook events. Your app then reads one local row to decide what a user can do, without calling Stripe on every page. For subscriptions, Stripe's subscription webhook guide lists the events and statuses to handle:
| Stripe signal | What it means | What your app does |
|---|---|---|
invoice.paid | An invoice, including a renewal, was paid | Grant or extend access (Stripe advises confirming the subscription status is active first) |
invoice.payment_failed | A payment attempt failed | Warn the customer and ask them to update their card |
customer.subscription.updated | Plan, renewal or status changed | Copy the new plan and status into your table |
customer.subscription.deleted | The subscription ended | Remove paid access |
Status past_due | The last payment failed, Stripe may retry | Notify the customer; decide how long to keep access |
Status canceled or unpaid | No longer paying | Revoke access, per Stripe's guidance |
Because users read their own plan from this table, it needs row level security: a user may read their own row and nobody may write to it from the browser. Only the webhook function, running with a server-side key, writes. Our guide to Supabase row level security explains the policy.
Let customers cancel through the customer portal
Use Stripe's customer portal for cancellations, card updates and invoices instead of building those screens. According to Stripe's customer portal documentation, customers can update billing details and payment methods, change or pause a subscription, cancel immediately or at the end of the billing period, and view or pay invoices. You configure it in the Stripe Dashboard.
Your app needs one button, "Manage billing", that calls a second edge function. The function creates a portal session for the logged-in user's Stripe customer id and returns a URL. Two limits from the same page matter: the portal cannot be shown inside an iframe, and a new session link expires after 5 minutes, so create it when the user clicks, not in advance. Cancellations done there reach your database through the same webhook events, so no extra code is needed.
Test mode and test cards
Test everything with Stripe's test keys before touching live mode, because test environments are isolated and move no real money. Per Stripe's testing guide, you must never use real card details in testing. These are the cards worth trying:
| Scenario | Card number |
|---|---|
| Successful payment | 4242 4242 4242 4242 |
| Generic decline | 4000 0000 0000 0002 |
| Insufficient funds | 4000 0000 0000 9995 |
| 3D Secure, authenticate unless already set up | 4000 0025 0000 3155 |
For the expiry date use any future date, for the CVC any three digits, and any value for the other fields. Run at least four scenarios: a successful payment, a declined card, a cancellation from the portal, and a webhook delivered twice. Check the database row after each one.
Fees, tax and invoices
Stripe's standard card fee is a percentage of each payment plus a fixed amount, and both vary by country, so read the figure for your own country on stripe.com/pricing before setting your prices. We did not find a US rate on the pages we could read in November 2026, so we print no card fee here. The same page shows Billing, the subscription product, as a separate charge on recurring volume (0.7% on pay-as-you-go, checked in November 2026 on the French pricing page), and Stripe Tax as another optional line.
At a high level: Checkout and Payment Links both support Stripe Tax, which calculates sales tax or VAT for you, and Stripe can send receipts and invoices automatically. Whether you must register for and collect tax depends on where you and your customers are, so ask an accountant before launch rather than asking an AI builder.
The prompts to send, one step at a time
Send these prompts in order and check each result before the next, because one prompt that asks for payments, webhooks and a portal at once produces code that is hard to debug. Create your Stripe account, a product and a price in test mode first, and have the price id ready. Replace the parts in [brackets].
- Checkout. "Add a Pricing page with a [Pro] plan at [$19 a month]. The Subscribe button calls a server function that checks the user is logged in, creates a Stripe Checkout session in subscription mode for price id [price_xxx] with the user's id attached, and returns the URL. Redirect the browser to it. The Stripe secret key must only be used inside that function."
- Database. "Create a subscriptions table with user id, Stripe customer id, subscription id, plan, status and current period end. Add row level security so a user can read only their own row and nothing can be written from the browser."
- Webhook. "Create a Stripe webhook function that verifies the Stripe-Signature header with the webhook secret, then handles checkout.session.completed, invoice.paid, invoice.payment_failed, customer.subscription.updated and customer.subscription.deleted by updating the subscriptions table. It must be safe to receive the same event twice, and it must not require a user login."
- Access. "Read the subscriptions table to decide what a user can use. Show a locked state with an upgrade button on [feature] for users whose status is not active or trialing. Never trust the success page to unlock anything."
- Portal. "Add a Manage billing button in settings that calls a server function creating a Stripe customer portal session for the current user and returns its URL."
- Failures. "When a payment fails, show a banner asking the user to update their card, with a link to the portal."
The first message matters most; our guide on how to write prompts to build an app explains why.
How Cadrant does it
Cadrant, our builder, generates React web apps with a Supabase backend, and it handles Stripe through Supabase edge functions in your own Supabase project. You ask for the integration in chat, for example "Add Stripe payment for subscriptions". Cadrant writes the edge function code and deploys it to your Supabase project. Per our edge functions documentation, the browser calls the function through the Supabase API and the keys never reach it.
You add the key yourself in the project's Environment panel, under the Database panel, as a variable such as STRIPE_SECRET_KEY, and the webhook secret as a second variable. Per the environment variables page, variables are available only to server code and can be changed without redeploying the app. Never paste a secret key into the chat. The AI also writes the database migrations with row level security, so the subscriptions table in step 2 above follows the same rules as the rest of your data.
Cadrant is not the only way to do this. Any builder that generates server functions can follow the same steps, and if you want full control of billing logic, a developer reviewing the generated code before launch is still a good idea. Stripe handles the money and the card data in every case; your code only decides who gets access.
Once payments work, publish on your own domain; our guide to deploying a web app covers that step.