Payment Links vs Checkout Sessions vs Invoices

Flint has three hosted ways to collect a payment. All three end in the same place, a paid order, so this is not a decision you can get irreversibly wrong. It is a decision about who starts the payment, how many buyers share the URL, and when the money is due.

  • A payment link is a reusable hosted checkout page with a stable URL. Create it once, put the URL anywhere, and every buyer who opens it gets their own checkout.
  • A checkout session is one hosted checkout for one buyer. Your app decides someone should pay right now and sends them to a single-use URL.
  • An invoice is a billed receivable. It records that a specific customer owes a specific amount, emails them, reminds them, accepts partial and offline payments, and renders a PDF.

Rule of thumb: payment links share, checkout sessions collect, invoices chase.

The Two-Question Decision#

1. Will more than one buyer use the same URL?

Yes: use a payment link. It is the only one of the three built to be shared. A checkout session URL belongs to one buyer, and an invoice belongs to one customer.

No: keep going.

2. Is the money due right now, or are you sending a bill?

Due now, and your app owns the moment: use a checkout session.

It is a bill, meaning the customer may pay in days or weeks, partially, or by check or wire: use an invoice.

If your situation pattern-matches one of these, you are done:

Share a URL

Payment link

  • A buy button on your marketing site
  • A QR code on packaging or a flyer
  • A pricing page selling plan signups
  • A donation page with suggested amounts
  • An event selling 500 tickets across tiers

Collect right now

Checkout session

  • The checkout button in your app's cart
  • A quote the buyer just accepted
  • An in-product subscription upgrade
  • A quick charge for a known amount
  • An agent or backend driving a purchase

Bill and track

Invoice

  • Consulting work billed net 15
  • B2B sales that route through accounts payable
  • A balance paid half by card, half by check
  • Anything finance wants a PDF and number for
  • Receivables that need reminder emails

Side by Side#

Payment linkCheckout sessionInvoice
What it isA reusable template that mints checkoutsOne payment attempt for one buyerA record of what a customer owes
The URLStable and shareable; each buyer gets their own checkoutSingle-use; the #checkout_token fragment authenticates the buyerA hosted public_url per invoice; revocable via regenerate-public-link
Who starts paymentThe buyer, whenever they find the linkYour app, at the moment of saleThe customer, on their schedule, against a balance you issued
Created fromline_items, donation amounts, event_config, or plan_idExactly one of order_id, quick_pay_item, or plan_idorder_id or quick_pay, plus recipient_email and due_at
Lifecycleactive and inactiveopen, then paid, expired, closed, or invalidateddraft, open, partially_paid, paid, or void
OrdersOne new order per completed checkout, source: "payment_link"Pays an existing order, or creates one for quick pay and plansOne backing order; the billing snapshot freezes at send
SubscriptionsYes, pass plan_idYes, pass plan_idNo
Due datesNoNo; sessions expire insteaddue_at, with a computed is_overdue
Email delivery and remindersNo, you distribute the URLNo, you redirect the buyerYes: Flint emails at send, send-reminder follows up, delivery attempts are tracked
Partial paymentsNoNoYes: partially_paid with a running amount_due_money
Offline paymentsNoNoYes: manual payments with external_reference_id
Document artifactNoNoPDF rendered from the frozen snapshot
ExpirationPer visit only; the link lives until deactivatedexpires_at, 24 hours by defaultNone; open until paid or voided
Turning it offPOST /deactivate, buyers see your inactive_messagePOST /closePOST /void, unpaid invoices only
Done signalorder.payment_succeeded per salecheckout_session.completedinvoice.paid
GuidePayment LinksCheckout SessionsInvoicing

One Engine Underneath#

The three surfaces are not three payment systems. They converge on the same two objects: a checkout session collects the money, and an order records what was sold.

text
payment link    buyer opens the shared URL      ─▶ checkout session ─▶ order
your backend    you create it for one buyer     ─▶ checkout session ─▶ order
invoice         customer opens the hosted page  ─▶ checkout session ─▶ order
                you record a check or wire      ─▶ manual payment   ─▶ invoice + order

Every session records where it came from (source, plus payment_link_id or invoice_id when applicable), and every order joins back to whatever collected it. That shared foundation is what makes the choice low-stakes:

  • The aftermath is identical. Refunds, receipts, disputes, payouts, and reporting all hang off the order, no matter which surface collected payment. GET /v1/orders is one reconciliation surface for all three.
  • Switching later is cheap. Nothing about starting with payment links locks you out of building cart checkout on sessions next quarter, or adding invoices when your sales cycle grows an approval step. The orders your first surface produced sit in the same ledger as the ones your next surface will.
  • Combining is normal. Plenty of merchants run all three at once: a payment link on the pricing page, checkout sessions inside the product, invoices for enterprise deals. Nothing needs to know which path a given order took except the code that started it.

The Same Sale, Three Ways#

One $120 workshop seat, sold through each surface. Amounts are integers in the currency's minor unit, so 12000 is $120.00.

One call, then the URL does the selling:

Bash
curl -X POST https://api.withflintpay.com/v1/payment-links \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: workshop-link-001" \
  -d '{
    "name": "Letterpress Workshop",
    "line_items": [{
      "name": "Workshop Seat",
      "quantity": 1,
      "unit_price_money": {"amount": 12000, "currency": "USD"}
    }],
    "customer_collection": {"require_email": true}
  }'

data.url is live the moment it returns, and the same URL works for every buyer: the website button, the email blast, the QR code on the flyer. Each completed checkout creates its own order carrying source: "payment_link". The seat keeps selling until you deactivate the link or cap it with max_completions.

Right when: seats are sold to the public and a shared URL is the storefront.

Boundaries Worth Knowing#

The two questions settle most cases. These are the borders where teams actually deliberate.

Payment links and checkout sessions both accept plan_id, and the split is the same as for one-time payments. A public pricing page wants a subscription signup link: one link per plan, shared everywhere that plan is sold. An in-product upgrade for a signed-in user wants a session, because sessions can carry a customer_id and prefilled_customer_info that a broadly shared link cannot.

Invoices do not do recurring billing. An invoice is one balance, billed once. Recurring charges belong to a subscription plan, which bills on schedule without new API calls from you.

"They'll pay later": session or invoice#

A checkout session's whole life is its expiration window, 24 hours by default. That is a collection window, not a receivable. If payment might land days later, arrive in pieces, or show up as a check, you want the object that can wait: the invoice, with due_at, reminders, and a running amount_due_money.

The litmus test: do you need to ask "how much is still owed?" Only invoices can answer that.

Quick pay exists on both sessions and invoices#

quick_pay_item on a session and quick_pay on an invoice both skip explicit order creation; Flint builds the backing order either way. So do not choose based on whether you have an order yet. Choose by collection semantics: pay-now is a session, bill-and-track is an invoice.

A deposit now, the balance later#

Mixed timing means mixed tools, and that is fine. Take the deposit through a payment link or checkout session at booking, then bill the remainder as an invoice when the work is done. Both payments land on orders in the same ledger, so reporting stays whole.

An invoiced order belongs to its invoice#

Once an order is invoiced, collect through the invoice: POST /v1/invoices/{invoice_id}/checkout-session returns an invoice-owned session that keeps the payment attached to the invoice lifecycle. Creating a generic checkout session against the backing order instead sidesteps the receivable: the invoice's balance, reminders, and events stop reflecting reality. Flint blocks the obvious collisions (send is rejected while a generic session is open on the order, and manual payments are rejected while online collection is active), but the correct move is simply to let the invoice own collection.

Fulfillment Is One Pattern#

Whatever mix of surfaces you run, one webhook endpoint covers it. order.payment_succeeded fires when an order is paid through hosted checkout on any of the three; invoices add their own events for the parts orders cannot see, like offline payments and email delivery.

Bash
curl -X POST https://api.withflintpay.com/v1/webhook-endpoints \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: webhook-fulfillment-001" \
  -d '{
    "url": "https://example.com/webhooks/flint",
    "enabled_events": [
      "order.payment_succeeded",
      "checkout_session.completed",
      "invoice.paid",
      "invoice.partially_paid"
    ],
    "description": "Fulfillment across all hosted surfaces"
  }'
SurfaceThe events that matterWhat the payload tells you
Payment linkorder.payment_succeeded, checkout_session.completedThe order carries source: "payment_link" and the link's metadata; the session carries payment_link_id, so you know which link converted
Checkout sessioncheckout_session.completed, order.payment_succeededThe session carries its order_id; fulfill from either object
Invoiceinvoice.paid, invoice.partially_paid, invoice.manual_payment_recorded, delivery eventsThe invoice carries order_id and the remaining amount_due_money
Subscription signup (link or session)subscription.created, subscription.activatedThe new subscription, ready to manage via the API

On every surface, the buyer's redirect is user experience, not proof of payment. Fulfill from the webhook or a backend fetch, never from the success URL alone. See Webhooks for signature verification and delivery semantics.

Common Mistakes#

  • Texting one checkout session URL to a hundred buyers. A session is one checkout for one buyer; the first payment puts it in a terminal state and everyone else finds a dead page. A shared URL is a payment link.
  • Minting a payment link per customer to collect what they owe. A link is a template with no concept of a balance: nothing tracks what is outstanding, reminds anyone, or records the check that arrives. Customer-owes-money is an invoice.
  • Using invoices for recurring billing. An invoice bills one balance once. Recurring revenue belongs on subscription plans collected through a link or session.
  • Collecting an invoiced order through a generic checkout session. Use POST /v1/invoices/{invoice_id}/checkout-session so the payment lands on the invoice, not just the order underneath it.
  • Fulfilling from the redirect. Buyers close tabs, and success URLs can be opened by anyone. The durable signals are order.payment_succeeded, checkout_session.completed, and the invoice.* events.
Rate this doc