A payment-first API answers one question: did this amount move? Stripe's core payment object, the PaymentIntent, requires two parameters, amount and currency. Your app answers everything else. What did the customer buy? What do they get? What does an upgrade cost halfway through the month? What are they owed back, and how much of that was tax? Each answer is money logic, and when it's wrong, your business pays the difference.
A commerce API keeps those answers in the API. Flint is an order-first commerce API: the order exists before the payment and holds the line items, discounts, tax, and balance, and every payment, renewal, and refund settles against it. Annual plans, seats, coupons, credit packs, invoices, and merch become API calls and settings. Your app keeps the product and the feature gates, and the money math, the record of the sale, and the subscription state come from Flint.
If Claude Code, Codex, or Cursor built your app and Stripe already charges for one plan, the question is what happens to your codebase as the app sells more.
What a payment-first API leaves in your app#
Stripe Billing adds subscriptions, proration, a customer portal, and Smart Retries on top of PaymentIntents, and Stripe Tax calculates tax. Each of them bills, retries, or taxes a charge, and you wire them together. The record of the sale and the state your product reads still live in your database, joined to Stripe by IDs. Four kinds of work land there.
The record of what was sold#
What it is. A row for each purchase: which plan, how many seats, which coupon, how much tax, which add-ons, linked to the payment by an ID. Support reads it, refunds start from it, and a dispute response is built from it.
How AI-written code gets it wrong. The agent writes the row from the success page, which Stripe says a buyer isn't guaranteed to reach, or from a webhook handler that inserts twice when two deliveries race. A plan changed in the Stripe Dashboard reaches your table only if a handler listens for that event. Sooner or later, a string in metadata becomes the only link between the two records.
What it costs. Your support team answers from a copy that has drifted, and every later answer inherits the drift. Stripe as your source of truth lists the five ways the two records come apart.
The money math#
What it is. Proration credits, discounts, tax, and refunds. Stripe computes a proration, but your integration decides what happens to it on every plan change. Stripe's proration docs say "Negative prorations aren't automatically refunded and positive prorations aren't immediately billed," and the default, create_prorations, adds the difference to a later invoice. They also note that a customer who changes plans while the current period's invoice is unpaid can get credit for time they never paid for, and that the two workarounds can lead to double payment if the old invoice gets paid later.
Refunds are an amount. A Stripe refund names a Charge or PaymentIntent and, optionally, how much. Refunding one discounted month, or half of a credit pack bought with a coupon, means your code works out the number, including its share of the tax.
How AI-written code gets it wrong. "Let users upgrade" becomes one update call with the default proration behavior. The user gets the higher tier today and pays the difference at the next invoice, weeks later. A refund uses the list price, not what the buyer paid after the discount. Floating-point math drops or adds a cent, and nobody notices until month end.
What it costs. Every error is your money: revenue you never collected, refunds larger than the sale, or an overcharge that turns into a dispute. Stripe partial refunds works through the discount and tax arithmetic.
The state you sync#
What it is. Whether this user has access right now. Stripe's guide to tracking active subscriptions describes the typical integration: store an access expiration timestamp on your site, extend it after invoice.paid once you've retrieved the subscription and confirmed it's active, and revoke access when it moves to canceled or unpaid. The status follows invoices, and the same guide says active "doesn't necessarily indicate that all outstanding invoices associated with the subscription have been paid."
How AI-written code gets it wrong. An is_pro boolean flipped by one event. Stripe doesn't guarantee event order, so an older event processed late flips it back. No branch for unpaid or incomplete_expired, because the prompt never mentioned them.
What it costs. Access left on after a cancellation is service you deliver for free. Access cut off for a customer who paid is a support ticket, and sometimes a lost customer.
The evidence and the reconciliation#
What it is. When a customer disputes a charge, Stripe usually gives you 7 to 21 days to respond, depending on the card network, and one submission. The evidence is your record: what they bought, the terms they accepted, and what they used. Payouts arrive as one deposit made of many balance transactions, and matching them to customers means walking each one back to your table through the IDs you stored.
How AI-written code gets it wrong. It stores a session ID and a status, which is nothing a bank will accept as evidence. It never builds the payout match, because no prompt asked for it.
What it costs. A lost dispute takes the disputed amount and the dispute fee. An unexplained deposit costs your accountant's hours, and a revenue number you can't defend.
How it compounds as the app grows#
Each feature a SaaS app adds after its first plan touches all four categories at once. With Stripe PaymentIntents, every entry in the middle column is code you write. Stripe Billing handles the recurring charge for several rows, and the mapping to your own records stays with you.
| Feature added | What a payment-first integration adds to your code | On Flint |
|---|---|---|
| Annual plan | A second price, a rule for switching mid-period, and both mapped to one entitlement | A second plan with billing_interval: "yearly" at its own price; change-plan moves a subscriber over and credits the unused monthly time |
| A coupon | Code rules, how many periods each lasts, and what a refund returns on a discounted period | A promotion with recurrence of once, repeating, or forever; it stays attached through plan changes |
| Seats | A stored quantity, the charge or credit when it changes mid-period, and a record of who paid for how many | quantity_options on the plan, up to 10 choices from 1 to 100; change-plan with quantity and billing_starts: "now" charges today minus the unused-time credit |
| Upgrades mid-month | Proration settings on every request, and the unpaid-invoice and refund cases around them | POST /v1/subscriptions/{id}/change-plan computes the credit; POST /v1/subscription-previews shows the charge first |
| Credit pack or one-time add-on | A charge outside the subscription, a row saying what it bought, and the refund math | An order with the pack as a line item, paid with the saved card through POST /v1/orders/{order_id}/pay |
| Refund one month | Find that period's charge, work out the amount and its tax, and record the refund | Each renewal is its own order: POST /v1/refunds with refund_type: "full" and its order_id |
| Sales tax | Rates by address, tax per line after discounts, and its reversal on refunds | Automatic tax with the saas tax category on each renewal order; refunds reverse it |
| Invoice for a business customer | An invoice rebuilt from your own record, kept in step with it, and its follow-ups | POST /v1/invoices with a hosted page, card or ACH debit, a PDF, a PO reference, and reminder emails |
| Physical merch | A product catalog, stock counts, shipping addresses, and a fulfillment trigger | Catalog products with inventory; hosted checkout collects the address; fulfill on order.paid |
| Gift cards | Codes, a balance ledger, redemption at checkout, and refunds back to a card | Products with product_type: "gift_card"; Flint issues the cards, keeps the ledger, and applies them at checkout. Gift card value pays one-time orders, not subscription renewals |
Every Flint entry is an API call or a setting you can try in a free sandbox. Create a test account and have your agent add a yearly plan first, then move a test subscriber onto it with change-plan.
The rows interact, which is where generated code breaks. A coupon changes what the refund for that month returns. A seat change mid-period changes the basis for the next upgrade's credit. An agent working one prompt at a time satisfies each request and misses the rule that connects them. Vibe coding a store follows the same pattern through ten prompts for a store that ships goods.
Who pays when the money logic is wrong#
The seller does, on every platform. What changes is how many of these failures your code can cause.
| Failure | Who pays | On Flint |
|---|---|---|
| A retried request charges twice | You: the refund, and a dispute if the buyer goes to their bank first | Duplicate protection on hosted checkout and renewals is Flint's job. Your own POSTs send an Idempotency-Key, and a second payment while one is in flight returns 409 PAYMENT_IN_PROGRESS |
| An upgrade credit is miscalculated | You: revenue lost, or an overcharge you refund | Flint computes the credit, and refuses one larger than the new charge before charging |
| A refund pays back more than the item cost | You: the difference | Line-item refunds reverse that line's discount and tax, and a refund over what's left on the order fails with AMOUNT_EXCEEDS_REFUNDABLE |
| Tax is under-collected | You: a seller generally owes the tax it should have collected | Calculated per line, after discounts, from the buyer's address. Registering and filing stay with you |
| Access stays on after a cancel | You: service delivered for free | Your app still gates access, from subscription.canceled and the subscription's current status |
| A dispute has no evidence | You: the disputed amount and the dispute fee | The dispute is linked to its payment and order, with its deadline. Responding stays with you |
| A deposit doesn't match orders | You: reconciliation hours, and revenue you can't explain | Each payout lists its balance transactions, each with its order_id |
| Card numbers reach your server | You: PCI DSS obligations for every system that touches card data | On hosted checkout, card numbers go into Stripe's frames and never reach your servers or Flint's |
Three rows stay with the seller on any platform that isn't a merchant of record: registering and filing sales tax, responding to disputes, and deciding who gets access. The others come from money math, a second record, or card data on your servers, and Flint keeps all three out of your code. Flint doesn't take on your legal obligations as a seller. What you're responsible for when you sell online covers those.
When a payment-first API is enough#
One product, one price, digital delivery, and no partial refunds: a hosted checkout and one access flag cover it, and there's little money logic to get wrong. Flint covers that case too, with a payment link per plan and no backend code.
That changes with the second thing you sell. An annual option, a coupon, or a credit pack each adds a price your code has to record, a rule your refunds have to respect, and a state your access check has to read, and each one touches the ones before it.
What the same features look like on Flint#
A Flint subscription plan carries its interval options, quantity options, trial, and line items. Subscribers sign up through a payment link or a checkout session your app opens for a signed-in user, and every renewal is an ordinary order with line items, tax, and a receipt. When a renewal fails, Flint emails the customer a link to update their card and retries on days 1, 4, 9, and 16 after the decline. Subscribers update their card, pause, and cancel in a hosted buyer account that can run on your own domain.
An upgrade is one call. Preview it first with POST /v1/subscription-previews and mode: "plan_change" to show the customer what they'll pay, then send it:
curl -X POST https://api.withflintpay.com/v1/subscriptions/sub_1kmn0aExample/change-plan \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: change-plan-sub-ada-001" \
-d '{
"subscription_plan_id": "plan_1kmn0bExample",
"billing_starts": "now"
}'
A subscriber 12 days into a $29 monthly period, moving to a $49 plan, has 19 of 31 days unused. Flint credits 2900 * 19 / 31, rounded half up to 1777 cents, and charges the new period at once:
{
"data": {
"order": {
"order_id": "ord_1kmn0aExample",
"discounts": [
{
"customer_facing_name": "Unused time on Starter",
"source": "plan_change_credit",
"applied_money": {"amount": 1777, "currency": "USD"}
}
],
"pricing_amounts": {
"subtotal_money": {"amount": 4900, "currency": "USD"},
"discount_money": {"amount": 1777, "currency": "USD"},
"total_money": {"amount": 3123, "currency": "USD"}
}
},
"order_payment_attempt": {"status": "succeeded"}
}
}
If the charge is declined, the subscription stays exactly as it was. For a downgrade, send "billing_starts": "next_renewal" and nothing is charged until the next period. Flint doesn't refund unused time on its own. When a customer asks for one month back, refund that renewal's order:
curl -X POST https://api.withflintpay.com/v1/refunds \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: refund-ord-1kmn0a-full" \
-d '{
"refund_type": "full",
"order_id": "ord_1kmn0aExample",
"reason": "requested_by_customer"
}'
Your app keeps one piece of state: who has access. Provision on subscription.activated, or on subscription.created while the status is trialing. Ask for a new card on subscription.past_due, and revoke on subscription.canceled. Deliveries can repeat and arrive out of order, so fetch GET /v1/subscriptions/{subscription_id} and act on its current status. Subscription billing covers trials, pauses, dunning settings, and testing renewals on a daily plan.
Flint charges 3.79% + 35¢ per card payment, 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. Sandbox keys are free.
To have your agent build it, add the docs MCP server first:
claude mcp add --transport http flint-docs https://developers.withflintpay.com/mcp
Then paste this into Claude Code or Codex:
Use the flint-docs MCP server. With the sandbox key in
FLINT_API_KEY, create Pro Monthly and Pro Yearly plans,
each at its own price, and a Team plan with seat
quantities from 1 to 10. Open a Flint checkout session
for the signed-in user to subscribe. Gate features from
subscription webhooks, fetching the subscription and
acting on its current status before granting or revoking.
Add an upgrade button that previews the charge with
POST /v1/subscription-previews, shows it, and then calls
change-plan with billing_starts "now". Test with card
4242 4242 4242 4242 on a daily plan and show me the
renewal orders at the end.
Build it#
Subscription billingWhy Flint is orders-firstRefundsBuild with AI agentsFlint's sandbox is free and needs no credit card. Sign up, create a monthly and a yearly plan, subscribe with test card 4242 4242 4242 4242, and move the subscription to the yearly plan to see the credit for unused time on the order.
FAQ#
Is Stripe a payment-first API?#
Yes. Stripe's core payment object, the PaymentIntent, requires an amount and a currency, and a refund names a charge or PaymentIntent and an amount. Stripe Billing and Stripe Invoicing add subscriptions and invoices on top, each built around a bill or a charge. The record of what was sold lives in your database; Does Stripe have an Orders API? covers the history.
What's the difference between a payments API and a commerce API?#
A payments API moves an amount and tells you whether it succeeded. A commerce API takes the sale itself: line items, discounts, tax, and the customer. It computes the total, keeps the order, and settles payments, renewals, and refunds against it. On Flint, a refund can name a line item, a webhook carries the order, and a payout lists the order behind each transaction. Stripe vs order-first payment APIs compares the two models on one order, start to finish.
Is Stripe enough for my SaaS app?#
For one plan at one price with digital delivery and no partial refunds, yes. Once the app adds an annual option, seats, coupons, add-ons, refunds of single periods, or invoices for business customers, each one adds money logic and a second record to your code. A commerce API like Flint handles those as API calls, so the app can grow without that code.
Do I need Stripe Billing for subscriptions?#
On Stripe, Billing is the subscription product, priced at 0.7% of Billing volume on pay as you go, separately from card processing. On Flint, subscriptions are part of the same API as orders and refunds, and payments that start or renew one cost 0.40% on top of the 3.79% + 35¢ card rate. Migrating from Stripe covers moving subscribers without charging them twice for the period Stripe already billed.
Is Flint a merchant of record?#
No. Flint's automatic tax calculates and collects US sales tax at checkout, and you register in the states where you owe it and file the returns. A merchant of record such as Paddle or Lemon Squeezy acts as the seller for tax and files it, charges 5% + 50¢ per transaction, and doesn't allow physical goods. Flint sells physical goods, digital products, and subscriptions on one account, in USD.
Does Flint run on Stripe?#
Yes. Stripe processes every card payment on Flint, and you don't need a Stripe account of your own. On Flint's hosted checkout, card numbers go from the buyer's browser into Stripe's frames and never reach your servers or Flint's.
Sources#
Everything attributed to Stripe, Paddle, and Lemon Squeezy above comes from their own documentation and pricing pages, checked on 2026-10-26.
- Create a PaymentIntent:
amountandcurrencyare the required parameters. - Create a refund: a refund specifies a Charge or PaymentIntent, with an optional
amount. - Prorations: negative prorations aren't refunded automatically and positive ones aren't billed immediately;
create_prorationsis the default; credit for unpaid time when an invoice is unpaid, and the double-payment risk of the workarounds. - Using webhooks with subscriptions: the access expiration timestamp on your site, confirming
activeafterinvoice.paid, revoking oncanceledorunpaid,activenot meaning every invoice is paid, Smart Retries, and the Customer Portal. - Fulfill orders: customers aren't guaranteed to visit the landing page after paying.
- Receive Stripe events in your webhook endpoint: no guaranteed event order, and occasional duplicate deliveries.
- Respond to disputes: usually 7 to 21 days to respond depending on the card network, and one submission.
- Stripe Billing pricing: 0.7% of Billing volume on pay as you go.
- Paddle pricing and what you can't sell on Paddle: 5% + 50¢ per checkout transaction, Paddle as merchant of record, physical products prohibited.
- Lemon Squeezy pricing and prohibited products: 5% + 50¢ per transaction, merchant of record that files and pays taxes, "Physical goods of any kind" prohibited.
