Skip to content

Payment API vs Commerce API: Why Payment-First Falls Short

A payment-first API tells you whether money moved. Plans, upgrades, refunds, and tax become money logic in your app. A commerce API like Flint keeps them out.
Comparison14 min readBy Flint Pay, Product & API Team

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 addedWhat a payment-first integration adds to your codeOn Flint
Annual planA second price, a rule for switching mid-period, and both mapped to one entitlementA second plan with billing_interval: "yearly" at its own price; change-plan moves a subscriber over and credits the unused monthly time
A couponCode rules, how many periods each lasts, and what a refund returns on a discounted periodA promotion with recurrence of once, repeating, or forever; it stays attached through plan changes
SeatsA stored quantity, the charge or credit when it changes mid-period, and a record of who paid for how manyquantity_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-monthProration settings on every request, and the unpaid-invoice and refund cases around themPOST /v1/subscriptions/{id}/change-plan computes the credit; POST /v1/subscription-previews shows the charge first
Credit pack or one-time add-onA charge outside the subscription, a row saying what it bought, and the refund mathAn order with the pack as a line item, paid with the saved card through POST /v1/orders/{order_id}/pay
Refund one monthFind that period's charge, work out the amount and its tax, and record the refundEach renewal is its own order: POST /v1/refunds with refund_type: "full" and its order_id
Sales taxRates by address, tax per line after discounts, and its reversal on refundsAutomatic tax with the saas tax category on each renewal order; refunds reverse it
Invoice for a business customerAn invoice rebuilt from your own record, kept in step with it, and its follow-upsPOST /v1/invoices with a hosted page, card or ACH debit, a PDF, a PO reference, and reminder emails
Physical merchA product catalog, stock counts, shipping addresses, and a fulfillment triggerCatalog products with inventory; hosted checkout collects the address; fulfill on order.paid
Gift cardsCodes, a balance ledger, redemption at checkout, and refunds back to a cardProducts 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.

FailureWho paysOn Flint
A retried request charges twiceYou: the refund, and a dispute if the buyer goes to their bank firstDuplicate 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 miscalculatedYou: revenue lost, or an overcharge you refundFlint computes the credit, and refuses one larger than the new charge before charging
A refund pays back more than the item costYou: the differenceLine-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-collectedYou: a seller generally owes the tax it should have collectedCalculated per line, after discounts, from the buyer's address. Registering and filing stay with you
Access stays on after a cancelYou: service delivered for freeYour app still gates access, from subscription.canceled and the subscription's current status
A dispute has no evidenceYou: the disputed amount and the dispute feeThe dispute is linked to its payment and order, with its deadline. Responding stays with you
A deposit doesn't match ordersYou: reconciliation hours, and revenue you can't explainEach payout lists its balance transactions, each with its order_id
Card numbers reach your serverYou: PCI DSS obligations for every system that touches card dataOn 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
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:

Response
{
  "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
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:

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

Then paste this into Claude Code or Codex:

Text
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 billingPlans, trials, plan changes with the unused-time credit, dunning, and the entitlement handler.Why Flint is orders-firstOne order from creation to paid, and how every payment surface attaches to it.RefundsRefund an order in full, by amount, by line item, or to store credit.Build with AI agentsThe docs MCP server and the one-file API reference for Claude Code, Codex, and Cursor.

Flint'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.