Skip to content

How to Take Payments in a Vibe-Coded App (Claude, Codex, Cursor)

Claude Code or Codex built your app. Charge for it with hosted checkout and subscriptions from an API, so no card data or money math lives in generated code.
Getting Paid11 min readBy Flint Pay, Product & API Team

Claude Code, Codex, or Cursor got your app working in a weekend. Charging for it is the next prompt, and it's the one where generated code costs the most when it's wrong. A layout bug shows up on screen. A billing bug charges someone twice, keeps a canceled user on the paid plan, or refunds the wrong amount, and the data looks normal until a customer or your accountant finds it.

So the decision that matters isn't which button to add. It's where the money logic lives: in the code your agent writes, or behind an API that already handles it.

The short answer#

Put the money behind a commerce API and keep your app's part small. On Flint, charging for a SaaS app takes three pieces:

  1. A plan in Flint. A $29 monthly plan with a 14-day trial is one API call.
  2. Flint's hosted checkout. Your server creates a checkout session and sends the user to it. Card details go into Flint's payment page and never pass through your app.
  3. One webhook handler. Grant access while the subscription is trialing or active, ask for a new card when it goes past_due, and turn access off when it's canceled.

Everything else runs in Flint: renewals, retries on declined cards, receipts and trial reminders, a hosted account where users update their card or cancel, refunds, US sales tax calculation, and dispute deadlines. Your app reads one status. Create a sandbox to start. Test keys are free, so your agent can build and test the whole flow before you verify a business.

Three ways to take payments, and what each leaves in your app#

Commerce API (Flint)Payments API (Stripe Checkout and Billing)Merchant of record (Paddle, Lemon Squeezy)
What it keeps for youThe order, the subscription, payments, refunds, and buyer emails, as one recordPayments, plus Billing's subscriptions and invoicesThe sale itself: it resells your product and is the seller
What your app keepsWhich features each plan unlocksThe record of what was sold, subscription state synced from webhooks, refund amounts, and the code that joins themWhich features each plan unlocks
Sales taxCalculated and collected for US sales; you register and fileStripe Tax, priced per transaction; you register and fileCollected and filed by the merchant of record
What you can sellSoftware, services, and physical goodsSoftware, services, and physical goodsDigital products; physical goods are prohibited

A payments API is built around one question: did this amount move? Every question after that, such as what the user bought, what they're owed after an upgrade, or which period a refund covers, gets answered in your code. Why payment-first APIs fall short goes through those answers one at a time.

Why money code is the wrong thing to vibe code#

An agent checks its work by running the app: sign up, pay with a test card, watch the dashboard unlock. Billing bugs live where that loop never goes:

  • Events that arrive twice, or out of order. Payment providers retry webhooks and don't promise an order. A handler that checks "is this user upgraded yet?" and then writes will upgrade twice when two deliveries race.
  • Things that happen weeks later. A renewal fails on day 31. A trial converts overnight. A customer disputes a charge two months after paying.
  • Cases nobody prompted for. Moving a user from $29 to $49 on day 12 of a 31-day month means crediting 19 unused days of the old plan. Nobody asks the agent for that until a customer writes in about paying twice.
  • Wrong numbers that look right. A refund 34 cents too high, or tax charged on the price before a discount, is an ordinary row in a table.

When money code is wrong, your business pays the difference. A customer who disputes a double charge before you refund it costs you a dispute fee, and Stripe doesn't return the fee for receiving a dispute even when you win. Sales tax you should have collected is still owed. Access left on after a cancellation is revenue you gave away. What you're responsible for when you sell online lists the obligations, and Vibe coding a store? Don't vibe code the commerce engine shows how they pile up one prompt at a time.

Step 1: give your agent the Flint docs#

Connect the Flint docs MCP server so the agent looks up each endpoint's schema instead of guessing from memory. In Claude Code:

Shell
claude mcp add --transport http flint-docs https://developers.withflintpay.com/mcp

In Codex, Cursor, or any other MCP-capable agent, add https://developers.withflintpay.com/mcp as an HTTP MCP server. It's read-only and needs no API key. AI agents covers the other options, including the whole API reference as one file.

Then create a sandbox key on the dashboard's API keys page and put it in your server environment as FLINT_API_KEY. Test keys start with flint_test_. The key belongs on the server only: never in client code, and never in a variable your framework exposes to the browser.

To let the agent create plans and checkouts in your sandbox itself, sign in with the Flint CLI (flint login) and add flint mcp serve as a local MCP server. It exposes CLI commands as tools with your credential, and the key never passes through the agent's arguments.

Step 2: create the plan#

A plan is what goes on your pricing page. Amounts are integers in cents, so 2900 is $29.00:

cURL
curl -X POST https://api.withflintpay.com/v1/subscription-plans \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $FLINT_API_KEY" \
  -H "Idempotency-Key: plan-pro-monthly-v1" \
  -d '{
    "name": "Pro Monthly",
    "billing_interval": "monthly",
    "billing_interval_count": 1,
    "currency": "USD",
    "trial_period_days": 14,
    "line_items": [
      {
        "name": "Pro Plan",
        "unit_price_money": {"amount": 2900, "currency": "USD"},
        "quantity": 1
      }
    ]
  }'

Save data.subscription_plan_id. With a trial, Flint collects and validates the card at signup without charging it, then charges the first month when the trial ends. A user who cancels during the trial is never charged.

Selling something once instead, like a lifetime license or a pack of credits? Skip the plan and use a checkout session with type: "quick_pay", or a payment link. Each sale is still an order you can refund and report on.

Step 3: send users to checkout#

When a user signs up in your app, create a Flint customer with their email and store its customer_id on your user record. When they click Upgrade, your server creates a checkout session for the plan:

cURL
curl -X POST https://api.withflintpay.com/v1/checkout-sessions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $FLINT_API_KEY" \
  -H "Idempotency-Key: upgrade-usr-1042-pro-monthly" \
  -d '{
    "type": "subscription_plan",
    "subscription_plan_id": "plan_1kmn0aExample",
    "customer_id": "cus_1kmn0aExample",
    "redirects": {
      "success_url": "https://yourapp.com/welcome"
    }
  }'

Redirect the user to the url in the response, exactly as returned. The user pays on Flint's page with a card, Apple Pay, or Google Pay. Flint saves the card for renewals, creates the subscription, and tells your server what happened over a webhook.

The success page is for the user, not for your access logic. A user can pay and close the tab before it loads, so access comes from the webhook in the next step.

If your app was built in Lovable, Bolt, v0, or Replit and has no server, a payment link for the plan does the same job with zero API calls from the app. Flint matches the subscription to a customer by the email the buyer enters, so match users by email too.

Step 4: grant access from the webhook#

Register a webhook endpoint for the subscription events, store the whsec_... secret it returns as FLINT_WEBHOOK_SECRET, and add one route. In a Next.js app:

TypeScript
// app/api/webhooks/flint/route.ts
import { Webhook } from "standard-webhooks";

type FlintEvent = {
  webhook_event_id: string;
  event_type: string;
  data: { object: { subscription_id?: string } };
};

const webhook = new Webhook(process.env.FLINT_WEBHOOK_SECRET!);

export async function POST(request: Request) {
  // Verify against the exact bytes Flint sent, before parsing anything.
  const body = await request.text();
  let event: FlintEvent;
  try {
    event = webhook.verify(body, Object.fromEntries(request.headers)) as FlintEvent;
  } catch {
    return new Response("invalid signature", { status: 400 });
  }

  const subscriptionId = event.data.object.subscription_id;
  if (event.event_type.startsWith("subscription.") && subscriptionId) {
    // Deliveries can repeat or arrive out of order, so read the current state.
    const response = await fetch(
      `https://api.withflintpay.com/v1/subscriptions/${subscriptionId}`,
      { headers: { Authorization: `Bearer ${process.env.FLINT_API_KEY}` } },
    );
    const { data: subscription } = await response.json();
    await setPlanAccess(subscription.customer_id, subscription.status);
  }

  return new Response(null, { status: 200 });
}

setPlanAccess is your code, and it should set a value rather than toggle one: trialing and active mean on, canceled and paused mean off, and past_due means the card failed and Flint is retrying, so keep access and show a banner asking for a new card, or restrict it, whichever fits your product. Because the handler writes the current status every time, a duplicate delivery writes the same value again and changes nothing.

Flint follows the Standard Webhooks spec, so standard-webhooks verifies the signature without any crypto code of your own. Webhooks covers registering the endpoint and the 10-second response budget.

To receive sandbox events on your laptop without a public tunnel, run the Flint CLI:

Shell
flint listen --forward-to http://localhost:3000/api/webhooks/flint

Step 5: test what a demo doesn't#

Clicking through checkout once proves the happy path. Ask your agent to run these too, in the sandbox:

TestHowWhat should happen
A paid signupPay with 4242 4242 4242 4242, any future expiry, any CVCsubscription.created, then access on
A failed renewalUse a daily plan, save the 4000 0000 0000 0341 card, and point the subscription at it with POST /v1/subscriptions/{id}/payment-methodThe next daily renewal fails, subscription.past_due arrives, and Flint emails the user a link to update their card
A cancellationCancel from Flint's buyer account, with cancellations set to end at the period's closesubscription.cancellation_scheduled now, subscription.canceled when the period ends, then access off
A mid-month upgradeCall POST /v1/subscriptions/{id}/change-plan with billing_starts: "now"The new period's order carries a credit for the unused days
A duplicate deliveryResend an event that already succeededNothing changes

The daily plan matters: renewals arrive within a day, so the agent can watch a whole billing cycle, including the retry path, without waiting a month. Testing lists every test card.

Step 6: go live#

Your code doesn't change for live mode. Same base URL, same requests, same webhook signatures. The steps are:

  1. Verify your business on the dashboard: business details, the owner's identity, and a bank account for payouts.
  2. Create a live key (it starts with flint_live_) and put it in your production environment.
  3. Re-create the plan and the webhook endpoint with the live key. IDs created in the sandbox don't exist in live mode, and a leftover plan_... ID from testing is the most common launch-day bug.
  4. Pay once with a real card, check that access turns on, and refund it.

Going live has the full checklist.

The prompt#

Paste this into Claude Code or Codex once the docs server is connected:

Text
Add paid plans to this app with Flint. Use the Flint docs MCP
server for every endpoint, and the sandbox key in FLINT_API_KEY
on the server only.

1. Create a "Pro Monthly" plan at $29 with a 14-day trial.
2. Create a Flint customer when a user signs up and store its ID.
3. Add an Upgrade button that creates a checkout session for the
   plan on the server and redirects to the URL it returns.
4. Add a webhook route that verifies the signature on the raw
   body, fetches the subscription, and sets the user's access
   from its status.
5. Run the tests: a paid signup, a failed renewal on a daily
   plan with the 0341 card, a cancellation, and a duplicate
   delivery. Show me the results.

What stays yours#

Flint takes the money, the records, and the billing lifecycle out of your app. A few things stay with you on any platform:

  • What each plan unlocks. Check it on your server for every request that needs it, not only by hiding buttons in the UI.
  • Sales tax registration and filing. Flint's automatic tax calculates and collects US sales tax at checkout. Registering in the states where you owe it and filing returns are yours.
  • Disputes. Flint records each dispute with its response deadline, and you answer it in the dashboard or your own interface.
  • Your policies. Refunds, cancellation terms, and whether users get a pause before they cancel are your settings; Flint enforces them.

When your app sells more#

Most apps don't stay at one plan. Here's what the next few features are on Flint:

You want to addOn Flint
An annual planAnother plan with billing_interval: "yearly"
Upgrades and downgradesPOST /v1/subscriptions/{id}/change-plan, with a credit for unused time on immediate changes
Discount codesPromotions, applied by Flint to orders and renewals
Refunding one monthA refund on that renewal's order
An invoice for a business customerInvoices against an order, with a due date and reminders
Merch with shipping and stockCatalog products with inventory, delivery options, and tax per address

Each one is a call on the same orders and customers, so your app gains a feature without gaining a second record to keep in sync. On a payments API, each of those rows is new code in your app. The Stripe go-live checklist for vibe-coded apps goes through 20 such items one by one.

Sign up for Flint, connect the docs server, and paste the prompt.

FAQ#

Do I need a Stripe account to take payments with Flint?#

No. Flint processes cards on Stripe, so you don't need a Stripe account of your own. You verify your business once on the Flint dashboard with the details needed to turn on payments: your business details, the owner's identity, and a bank account for payouts.

Can I add payments to a Lovable, Bolt, v0, or Replit app?#

Yes. Apps without a server can sell through a Flint payment link: create the link in the dashboard, paste its URL into your builder's chat, and ask for a button that opens it. No API key or card form goes into the generated code. Add payments to Lovable, Bolt, v0, and Replit apps has the prompts.

Is it safe to let Claude Code or Codex write payment code?#

It's safer when the code it writes handles IDs and statuses instead of card numbers and totals. With hosted checkout, card details never reach your servers. Review three things in what the agent wrote: no flint_test_ or flint_live_ key in client code, signature verification on the raw request body, and access set from the subscription's fetched status rather than from the success page.

How much does Flint cost?#

Card payments cost 3.79% + 35¢ with Stripe's processing included and no monthly fee. Payments that start or renew a subscription add 0.40%, and automatic tax adds 0.50% on payments that carry tax. Card disputes are passed through at cost. The sandbox is free. Pricing has the full rate card.

Is Flint a merchant of record?#

No. Your business is the seller, and Flint's automatic tax calculates and collects US sales tax so you have the records to file from. A merchant of record such as Paddle or Lemon Squeezy files sales tax as the seller, charges 5% + 50¢ per transaction, and doesn't allow physical goods. Flint vs Paddle compares the two in detail.

Can I charge users outside the US?#

Yes. Flint charges in US dollars and accepts cards issued in other countries, with a 1.50% international card fee.

Sources#

Third-party facts above were checked on 2026-10-26.