Why Flint is orders-first
A charge-first payment API asks you for an amount and a card. What was sold, which discount applied, and how much is still owed all live in your database, and the charge id is the only link back to them. Flint puts that record inside the API instead. You create an order, the order holds the line items and the running balance, and the money is collected against it.
Checkout sessions, payment intents, invoices, and payment links are the ways money reaches an order. They differ in who starts the payment and when it is due. The order does not change shape depending on which one you pick.
What each object records#
order ─────────────────────────────────────────────────────
line items, discounts, fees, tip, tax, metadata
pricing_amounts: what is owed
settlement_amounts: what has been collected and refunded
───────────────────────────────────────────────────────────
│ │ │
checkout session payment intent invoice
hosted page for one attempt in a bill with a due
one buyer your own UI date and reminders
│ │ │
└────────────────────┴─────────────────────┘
│
refunds, webhooks, reports, and support all key on order_id
A payment link is not in the diagram because it does not attach to one order. Each buyer who completes the link gets a new order of their own.
| Object | What it records | How long it lives | Reference |
|---|---|---|---|
| Order | What was sold, and how much has been paid or refunded | Permanent. open while a balance is due, closed after | Orders API |
| Checkout session | One buyer's hosted payment page | One attempt. Expires if unused | Checkout Sessions API |
| Payment intent | One attempt to move money, and whether it succeeded | One attempt | Payments API |
| Invoice | Who owes the balance, by when, and how much is still due | Until paid or voided | Invoices API |
| Payment link | A public URL that starts a new sale for each buyer | Until you deactivate it | Payment Links API |
One order from creation to paid#
Follow a single order through the model. Amounts are integers in the currency's minor unit, so 24000 is $240.00.
Create the order#
Send the line items, any discounts, and the metadata you want returned later. external_reference_id holds your own id for the sale and is a filter on the order list.
curl -X POST https://api.withflintpay.com/v1/orders \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: order-print-7781" \
-d '{
"external_reference_id": "studio-sale-7781",
"line_items": [{
"name": "Framed print, 24 x 36",
"quantity": 1,
"unit_price_money": {"amount": 24000, "currency": "USD"}
}],
"discounts": [{
"manual": {
"name": "Newsletter subscriber",
"amount_money": {"amount": 2000, "currency": "USD"}
}
}],
"metadata": {"channel": "studio-newsletter"}
}'
{
"data": {
"order_id": "ord_1kmn0aExample",
"status": "open",
"payment_status": "unpaid",
"external_reference_id": "studio-sale-7781",
"pricing_amounts": {
"subtotal_money": {"amount": 24000, "currency": "USD"},
"discount_money": {"amount": 2000, "currency": "USD"},
"total_money": {"amount": 22000, "currency": "USD"}
},
"settlement_amounts": {
"paid_money": {"amount": 0, "currency": "USD"},
"outstanding_money": {"amount": 22000, "currency": "USD"}
},
"line_items": [...],
"metadata": {"channel": "studio-newsletter"}
}
}
The response separates two questions. pricing_amounts is the price: what the line items add up to after discounts, fees, tip, and tax. settlement_amounts is the money: how much has arrived, how much went back out, and how much is still owed. Before any payment, outstanding_money equals total_money.
Change it while it is open#
An open order accepts changes, and Flint recomputes the totals on every one. Add line items, discounts, or fees through their own routes under the order. Set the requested tip, tax, metadata, customer, or delivery destination with PATCH /v1/orders/{order_id}. You never compute a total yourself.
curl -X POST https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/line-items \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: order-print-7781-hardware" \
-d '{
"line_items": [{
"name": "Wall mounting hardware",
"quantity": 1,
"unit_price_money": {"amount": 1500, "currency": "USD"}
}]
}'
{
"data": {
"order_id": "ord_1kmn0aExample",
"status": "open",
"pricing_amounts": {
"subtotal_money": {"amount": 25500, "currency": "USD"},
"discount_money": {"amount": 2000, "currency": "USD"},
"total_money": {"amount": 23500, "currency": "USD"}
},
"settlement_amounts": {
"outstanding_money": {"amount": 23500, "currency": "USD"}
}
}
}
The Orders API lists every mutation route. Tips & fees covers tips and fees, and Sales tax covers the tax request.
Collect the balance#
The same order can be paid through any of these. Pick the one that matches who is paying and when.
Point a checkout session at the order and redirect the buyer to its checkout_session.url.
curl -X POST https://api.withflintpay.com/v1/checkout-sessions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: checkout-print-7781" \
-d '{
"order_id": "ord_1kmn0aExample",
"redirects": {
"success_redirect_url": "https://example.com/orders/7781/thanks",
"cancel_redirect_url": "https://example.com/orders/7781"
}
}'
{
"data": {
"checkout_session": {
"checkout_session_id": "cs_1kmn0aExample",
"status": "open",
"order_id": "ord_1kmn0aExample",
"url": "https://checkout.withflintpay.com/checkout/cs_1kmn0aExample#checkout_token=..."
},
"checkout_access": {
"checkout_auth_token": "ckat_v1..."
}
}
}
In test mode, card 4242 4242 4242 4242 with any future expiry and any CVC pays the session. Testing runs this path end to end.
Create a payment intent under the order, then collect the card with Stripe Elements in your frontend. Omit amount_money and the intent takes the order's current balance.
curl -X POST https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/payment-intents \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: pi-print-7781" \
-d '{"payment_source_selection": {"card": {}}}'
{
"data": {
"payment_intent": {
"payment_intent_id": "pi_1kmn0aExample",
"status": "requires_payment_method",
"amount_money": {"amount": 23500, "currency": "USD"},
"order_id": "ord_1kmn0aExample"
},
"payment_collection": {
"stripe": {
"account_id": "acct_1a2b3c4d",
"publishable_key": "pk_test_...",
"elements": {
"next_step": "collect_payment_source",
"submit_to": "pay_order",
"mode": "payment",
"amount_money": {"amount": 23500, "currency": "USD"},
"payment_method_types": ["card"],
"payment_method_creation": "manual"
}
}
}
}
}
Pass payment_collection.stripe to the browser and mount Elements with it. The browser follows next_step to produce a credential, and your backend submits that credential for the selected payment intent with action: "confirm_payment_intents" to POST /v1/orders/{order_id}/pay. Flint confirms the payment. Embedded payments with Stripe Elements has the browser code and the pay request.
When the buyer pays later, draft an invoice against the order and issue it. Flint emails the customer a link to a hosted payment page with the PDF attached, sends reminders, and lets you record a check or wire against the balance.
curl -X POST https://api.withflintpay.com/v1/invoices \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: inv-print-7781" \
-d '{
"order_id": "ord_1kmn0aExample",
"collection": {"mode": "buyer_initiated"},
"payment_due": {"type": "absolute", "due_at": "2026-10-04T00:00:00Z"},
"recipient_email": "accounts@example.com",
"memo": "Due within 30 days of delivery."
}'
{
"data": {
"invoice_id": "inv_1kmn0aExample",
"status": "draft",
"order_id": "ord_1kmn0aExample",
"outstanding_money": {"amount": 23500, "currency": "USD"}
}
}
curl -X POST https://api.withflintpay.com/v1/invoices/inv_1kmn0aExample/issue \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: issue-inv-print-7781" \
-d '{"delivery_mode": "email"}'
Invoicing covers drafts, delivery, reminders, partial payments, and voiding.
One collection flow owns an order at a time. Creating a second checkout session while one is open returns CHECKOUT_SESSION_ALREADY_EXISTS, and starting a different flow, such as issuing an invoice, returns ORDER_COLLECTION_ALREADY_ACTIVE. To hand the buyer a new hosted URL, create the replacement with replace_checkout_session_id set to the current session. To move to an invoice, close the open session first. After an invoice is issued, it owns the balance: collect through POST /v1/invoices/{invoice_id}/checkout-session, not through a new order checkout session or payment intent.
Confirm the outcome on the order#
However the money arrived, the order is what you read afterward.
curl https://api.withflintpay.com/v1/orders/ord_1kmn0aExample \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"order_id": "ord_1kmn0aExample",
"status": "closed",
"payment_status": "paid",
"refund_status": "none",
"external_reference_id": "studio-sale-7781",
"pricing_amounts": {
"total_money": {"amount": 23500, "currency": "USD"}
},
"settlement_amounts": {
"paid_money": {"amount": 23500, "currency": "USD"},
"refunded_money": {"amount": 0, "currency": "USD"},
"outstanding_money": {"amount": 0, "currency": "USD"}
},
"checkout_session_ids": ["cs_1kmn0aExample"],
"payment_intent_ids": ["pi_1kmn0aExample"],
"refund_ids": [],
"metadata": {"channel": "studio-newsletter"}
}
}
statusis eitheropenorclosed.payment_statusmoves throughunpaid,partially_paid, andpaid.refund_statusis a separate axis, so a refunded order still reads as paid. Statuses & lifecycles has the full state tables.settlement_amountsholds the money facts:paid_moneyin,refunded_moneyout, andoutstanding_moneystill due.checkout_session_ids,payment_intent_ids, andrefund_idslist every attempt and every money movement that touched this order.metadataandexternal_reference_idcome back exactly as you set them at creation.
Webhooks follow the same rule. The order.paid event carries the order_id, so a fulfillment handler does the same fetch whether the buyer paid on a hosted page, in your form, or against an invoice. See Webhooks.
What the order gives you#
One total, computed once#
The price lives on the order and nowhere else. Every collection surface reads it from there, so a checkout page and an invoice for the same order cannot show different numbers. The only way to get a mismatch is to compute a total outside Flint and send that instead.
Existing orders survive failed and abandoned attempts#
A buyer leaves the hosted page, a card is declined, or a customer asks to be invoiced after the order was already built. With a charge-first API, each of these means rebuilding the purchase in a new object. For a merchant-created order, the order stays open after checkout expiry. Replace the session, create another payment intent, or issue an invoice, and the line items and balance carry over untouched.
A payment link creates an order for each resolved checkout. When that checkout expires unpaid, Flint closes the order if nothing was paid or authorized, no invoice or other active collection owns it, and it is not linked to a return. The order records closed_reason: "Checkout expired before payment" and emits order.closed. Resolve the link again to create a new checkout and order. Quick pay, subscription-plan, and invoice checkout expiry leave their orders open.
Refunds and payments point back to the order#
Which order did this refund come from? What did this payment intent pay for? Both are fields on the objects involved. Refunds target an order_id, orders list their payment intents and sessions, and GET /v1/orders?external_reference_id=... finds an order from your own id. Reconciliation works from the same fields.
Pay now can turn into pay later#
A sale that started as a checkout can become an invoice, and an invoice drafted against a partly paid order bills only what is still outstanding. Both surfaces attach to the same order, so the change is one create call rather than a copy of the sale into a new object.
Shortcuts that still create an order#
You do not have to call POST /v1/orders first. Three paths skip that step, and each creates the order for you.
Quick pay#
Send a name and an amount to a checkout session with quick_pay_item. Flint creates the order behind it and returns its order_id with the session.
curl -X POST https://api.withflintpay.com/v1/checkout-sessions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: checkout-sitting-fee-042" \
-d '{
"quick_pay_item": {
"name": "Portrait sitting fee",
"amount_money": {"amount": 12500, "currency": "USD"}
},
"redirects": {"success_redirect_url": "https://example.com/booked"}
}'
Store the returned order_id. Refunds and reports for this payment reference the order, not the session. Invoices have the same shortcut: pass quick_pay on invoice create instead of order_id.
Payment links#
A payment link is one URL that many buyers can open. Each completed checkout produces its own order with origin: "payment_link", and the link's metadata is copied onto every order it creates. Use a link for a public offer, and an order or an invoice when one named customer owes one amount.
Subscriptions#
A subscription starts from a subscription_plan_id and a saved payment method. Each billing cycle creates an order with origin: "subscription" and the subscription_id on it, so a renewal is refunded and reported the same way as a one-time sale.
When to skip the order#
If your own system already records what was sold and you only need Flint to move the money, create a payment intent with an amount and nothing else.
curl -X POST https://api.withflintpay.com/v1/payment-intents \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: pi-balance-topup-311" \
-d '{
"amount_money": {"amount": 5000, "currency": "USD"},
"payment_options": ["card"]
}'
An orderless intent has no line items in Flint and cannot be turned into an invoice. The purchase context stays in your system, with metadata and external_reference_id on the intent as the link back. Collect it on your own page with Stripe Elements, as Server-confirmed payments describes. A Flint-hosted checkout page always collects an order, so for a hosted page create the checkout session with quick_pay_item instead, and Flint creates a one-line order for the amount. See the Payments API.
Decide by ownership. If Flint is the record of the sale, create the order. If your application is, create the payment intent.
Common mistakes#
Reading the sale from the checkout session. The session is one attempt to collect. Line items, totals, and payment state belong to the order, and the redirect URL is a hint, not proof of payment. Fetch the order.
Using a payment link for one customer's balance. A link creates a new order per visit, so it cannot represent an amount that a specific customer already owes. Create an order or an invoice for that. Payment links vs checkout sessions vs invoices walks through the choice.
Computing totals in your own code. Sending an amount you calculated, when the order already has one, creates two prices for one sale. Change the order and let Flint recompute.
Treating orders as a hosted checkout feature. The order matters most after the payment: refunds, disputes, reports, and support all resolve against it, whichever surface collected the money.
Next steps#
- Testing: create an order, pay it on hosted checkout with a test card, and verify it.
- Embedded payments with Stripe Elements: order-backed payment intents in your own UI.
- Invoicing: billing a balance with due dates, reminders, and manual payments.
- Payment links vs checkout sessions vs invoices: which surface fits which sale.
- Statuses & lifecycles: every state an order, payment intent, and invoice can be in.
- Orders API: the full field list and every mutation route.
