Skip to main content
20% off 3 months · code LAUNCH20 · until 31 October

Chapter 5 of 9 · For everyone

How do you add payments to an app built with AI?

23 September 2026 · by Nordva, who make Nordva Launch

On this pageThe simplest start: a Payment Link (no code)
  1. The simplest start: a Payment Link (no code)
  2. AI builder path: use the builder’s own Stripe connector
  3. Coding agent path: Checkout and a webhook on Cloudflare Workers
  4. Test mode, then live mode
  5. Taxes and who the seller is
  6. What Stripe charges
  7. After the first payment
  8. Back to the top

The short answer

Start with a Stripe Payment Link: no code, one link, and you can take money today. If you use Lovable, Bolt or Replit, use the Stripe connector each one documents. With a coding agent, have it create a Checkout Session on your server, verify the webhook's signature, and unlock the paid features only when that webhook arrives. Test with test keys first.

This chapter is for both paths. Pick your price before you start: how to price your first SaaS and free plan or free trial cover that. Everything below uses Stripe, because all three AI builders covered here connect to it and it has a no-code option.

A Payment Link is a Stripe-hosted payment page you share as a link. Stripe describes it as a way to “accept payments online without writing code” (Stripe: Payment Links). It handles one-off payments and subscriptions, sends receipts, and lets you refund from the Dashboard.

  1. Sign up at Stripe and open Payment Links in the Dashboard, then click +New (Stripe: create a payment link).
  2. Pick an existing product or add a new one with its price (monthly, yearly or one-off).
  3. Under After the payment, choose to redirect to a page on your site, such as /welcome (Stripe: after the payment).
  4. Click Create link and put it behind a “Buy” or “Upgrade” button.

The limit: Stripe takes the money, but your app does not know who paid. For a handful of early customers you can check the Stripe Dashboard and switch people to paid by hand. Adding ?client_reference_id=<your user id> to the link attaches your own id to the payment (letters, digits, dashes and underscores, up to 200 characters), and Stripe includes it in the checkout.session.completed webhook, which is how you later match a payment to a user (Stripe: track a payment link); our page on UTM tags on Payment Links shows how. Once you have more than a few customers, move to one of the paths below.

AI builder path: use the builder’s own Stripe connector

Each builder does this differently, and the details change often. What each one’s documentation says on 23 September 2026:

Lovable. Lovable offers two routes: built-in payments, where Lovable sets up the payment account for you and which needs a paid Lovable plan, and connecting your own Stripe account, which also works on the Free plan (Lovable: Stripe). For your own account, you ask for payments in the project chat, say you want your own Stripe account, and paste a key into the Connect Stripe form Lovable shows (never into the chat itself). Lovable suggests a restricted key (rk_); a secret key (sk_) also works. It checks the key with Stripe and stores it as a backend secret called STRIPE_SECRET_KEY, never in your code. It needs Lovable Cloud (the default) or your own Supabase project, and subscriptions need sign-in in your app, because Lovable matches a subscription to a user by email. A test key (sk_test_ or rk_test_) keeps everything in test mode; to go live you ask Lovable to swap in a live key and check that your products exist in live mode. By default it does not use webhooks: the app asks Stripe for payment status directly. Webhooks are optional and set up by you in the Stripe Dashboard.

Bolt. Open the gear icon in your project, choose Stripe, paste a test secret key, pick the Stripe products to use and apply the changes (Bolt: Stripe for payments). It needs a Bolt Database or Supabase project and working user login first, and it does not work with Firebase. Bolt writes the edge functions for creating checkout sessions, handling webhooks (it registers and verifies them for you) and managing subscriptions.

Replit. Ask Agent to add Stripe; it starts you in a Stripe sandbox so you can test with fake money (Replit: Stripe payments). The Stripe integration is only on Replit’s paid plans (Core and Pro), not the free Starter plan, and it needs an App with a backend. To go live you open Publish and install the Replit Integrated Payments app on your live Stripe account, with no keys to copy.

Other builders: we only list the ones whose Stripe setup we could read in their own documentation. If yours has no documented Stripe integration, the Payment Link above works with any site, because it is just a link.

Whichever builder you use, test with card 4242 4242 4242 4242, any future date and any three-digit code (Stripe: testing), then pay once for real with your own card after switching to live.

Coding agent path: Checkout and a webhook on Cloudflare Workers

This continues the app from chapter 4: a Worker with a D1 database and signed-in users. You need three pieces.

1. A route that starts checkout. Your server creates a Checkout Session with the price, a success_url and your user’s id in client_reference_id, then sends the browser to the Stripe-hosted page (Stripe API: create a Checkout Session). Use mode: "subscription" for a monthly or yearly plan and mode: "payment" for a one-off.

2. A webhook that unlocks access. Stripe says you cannot rely on the success page alone, because a customer may pay and never reach it, for example if their connection drops before the page loads (Stripe: fulfill orders). So mark the user as paid only when the checkout.session.completed event arrives, after checking that its payment_status is not unpaid (the same guide explains why), and mark them as not paid when customer.subscription.deleted arrives, which Stripe sends when a subscription ends (Stripe: subscription events).

Before trusting an event, check its signature using the raw request body, the Stripe-Signature header and your whsec_ signing secret (Stripe: webhooks). In a Worker, use the asynchronous constructEventAsync, which accepts a crypto provider (stripe-node: Webhooks.ts); Stripe.createSubtleCryptoProvider() gives it the Web Crypto one that Workers have, and stripe-node’s own Deno example also reads the body with request.text() and calls constructEventAsync (stripe-node example). Read the body with request.text(), never request.json(): any change to the body makes the check fail.

3. The Customer Portal for cancellations. Rather than building “cancel”, “change card” and “download invoice” screens, turn on Stripe’s customer portal in the Dashboard. Your server creates a portal session with the customer’s id and a return_url, and sends the user there (Stripe: integrate the customer portal). Customers can update payment methods, cancel now or at the end of the period, and see past invoices (Stripe: customer portal).

Here is the shape of it. It was checked against the documentation linked above, not run, so treat it as the target your agent builds and tests, not a finished file:

import Stripe from "stripe";

export default {
  async fetch(request, env) {
    const stripe = new Stripe(env.STRIPE_SECRET_KEY);
    const url = new URL(request.url);

    if (url.pathname === "/api/checkout" && request.method === "POST") {
      const user = await getSignedInUser(request, env); // from chapter 4
      if (!user) return new Response("Sign in first", { status: 401 });
      const session = await stripe.checkout.sessions.create({
        mode: "subscription",
        line_items: [{ price: env.STRIPE_PRICE_ID, quantity: 1 }],
        client_reference_id: user.id,
        customer_email: user.email,
        success_url: `${url.origin}/welcome`,
        cancel_url: `${url.origin}/pricing`,
      });
      return Response.redirect(session.url!, 303);
    }

    if (url.pathname === "/api/stripe-webhook" && request.method === "POST") {
      const body = await request.text(); // raw body, unparsed
      const signature = request.headers.get("Stripe-Signature");
      if (!signature) return new Response("No signature", { status: 400 });
      let event;
      try {
        event = await stripe.webhooks.constructEventAsync(
          body,
          signature,
          env.STRIPE_WEBHOOK_SECRET,
          undefined,
          Stripe.createSubtleCryptoProvider(),
        );
      } catch {
        return new Response("Bad signature", { status: 400 });
      }
      if (event.type === "checkout.session.completed") {
        const s = event.data.object;
        if (s.payment_status === "unpaid") return new Response("ok"); // not paid yet
        await env.DB.prepare("UPDATE users SET plan = 'paid', stripe_customer_id = ? WHERE id = ?")
          .bind(s.customer, s.client_reference_id).run();
      }
      if (event.type === "customer.subscription.deleted") {
        await env.DB.prepare("UPDATE users SET plan = 'free' WHERE stripe_customer_id = ?")
          .bind(event.data.object.customer).run();
      }
      return new Response("ok");
    }

    return new Response("Not found", { status: 404 });
  },
};

Keep keys out of your code. Store them as Worker secrets with npx wrangler secret put STRIPE_SECRET_KEY and npx wrangler secret put STRIPE_WEBHOOK_SECRET (Cloudflare: secrets). Then, in the Stripe Dashboard, add a webhook endpoint pointing at https://your-domain/api/stripe-webhook and subscribe it to the two events above. Stripe only accepts webhook endpoints at a public HTTPS address (Stripe: webhooks), which a deployed Worker already has.

A prompt to paste to your agent

Add paid plans to this app with Stripe. Rules:
1. Create a Stripe Checkout Session on the server (never in the browser), mode
   "subscription", with the signed-in user's id in client_reference_id.
   Redirect the browser to session.url.
2. Add POST /api/stripe-webhook. Read the raw body with request.text(), verify it
   with stripe.webhooks.constructEventAsync and Stripe.createSubtleCryptoProvider(),
   and return 400 if verification fails.
3. Grant paid access only in the webhook, on checkout.session.completed, and
   only if the session's payment_status is not "unpaid". Remove it on customer.subscription.deleted. The success page only says "thanks".
4. Add a "Manage billing" button that creates a Stripe customer portal session
   for the user's stripe_customer_id and redirects there.
5. Read STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET and STRIPE_PRICE_ID from env.
   Never write a key into code, and tell me which ones to set with
   `npx wrangler secret put`.
6. Use test keys (sk_test_) only. Give me a test checklist: successful payment
   with 4242 4242 4242 4242, a declined card, a cancellation in the portal.
Plan first and show me the plan before writing code.

Test mode, then live mode

Stripe has separate keys for testing and for real money: sandbox keys start with sk_test_ and pk_test_, live keys with sk_live_ and pk_live_, and “objects in one mode aren’t accessible to the other” (Stripe: API keys). That last part catches most people: your test product, price and webhook do not exist in live mode. Before going live, recreate the product and price in live mode, create a live webhook endpoint (it has its own whsec_ secret), swap all three secrets, and pay once with your own card. If it worked in test and fails live, this checklist goes through the usual causes.

Taxes and who the seller is

When you sell, you are normally the seller, and depending on where you and your customers are, you may have to register for, charge and file sales tax, VAT or GST. Stripe offers two kinds of help, described here only as Stripe’s documentation describes them:

  • Stripe Tax calculates and collects tax on your payments and tracks where you may need to register. You still register in each place, and you file and pay the tax yourself or through Stripe and its partners (Stripe: how Stripe Tax works).
  • Managed Payments makes Stripe the merchant of record for digital products such as SaaS: Stripe handles sales tax, VAT and GST in more than 80 countries, plus fraud, disputes and transaction-level customer support (Stripe: Managed Payments). It works with Checkout and Payment Links, not with Stripe Connect, your business must be in a supported country and pass Stripe’s eligibility review, and each product needs one of the eligible tax codes for digital products (Stripe: eligibility).

This is not legal or tax advice. Rules differ by country, and some also depend on how much you sell. Check your own country’s tax authority, or ask an accountant, before you take your first real payment.

What Stripe charges

Rates vary by country, and Stripe’s pricing page shows the ones for the country you view it from, so open it yourself. On 23 September 2026 we were served the Swiss version, which listed a card fee of 2.9% plus a fixed amount per domestic card payment, with international cards and currency conversion costing more (Stripe pricing). The same page listed subscriptions (Billing, pay as you go) at 0.7% of billing volume (Stripe pricing), Stripe Tax Basic with the no-code setup at 0.5% per transaction (Stripe pricing), and Managed Payments at 3.5% per successful transaction on top of the payment fees (Stripe pricing). There was no monthly fee on the standard plan.

After the first payment

Once people pay, the next question is which channel sent them. Nordva Launch can connect your Stripe account read-only, on every plan including Free, and show which referring site brought each paying customer; chapter 7 sets up the measuring side. If you only sell through one link you share in one place, you already know the answer and do not need it.

Sources

The whole guide as one PDF to keep, with every prompt in one place, worksheets, fixes for when something breaks and the launch checklist.

Get the PDF
Read next