Checkout Sessions
A checkout session is one hosted checkout for one buyer. When your application knows what someone should pay right now, create a session, send the buyer to its URL, and Flint handles the rest: payment methods and wallets, tips, coupons, tax, authentication challenges like 3D Secure, and the receipt. Your backend gets a durable record of the outcome and a webhook when the buyer completes payment.
Sessions are single-use by design. If you want one URL that many buyers can open (a pricing page, a QR code, an ad), use Payment Links. If you're collecting a receivable that needs delivery, reminders, and PDFs, use Invoices. Not sure which fits? See Payment Links vs Checkout Sessions vs Invoices.
If the question is "a buyer clicked Pay in my app, where do I send them?" the answer is a checkout session.
How Sessions Work#
create session -> redirect buyer to url -> buyer pays -> redirect back + webhook -> verify from your backend
A session moves through a small set of states:
| Status | Meaning |
|---|---|
open | The hosted page is live and the buyer can pay. Sessions are created in this state. |
paid | The buyer completed payment (or subscription signup). Terminal. |
expired | The session passed expires_at without payment. The default lifetime is 24 hours. Terminal. |
closed | You closed it with the close endpoint. Terminal. |
invalidated | A newer session was created for the same order and replaced it. Terminal. |
Only open sessions can be paid. A buyer who opens the URL of a session in a terminal state sees a clear "this checkout is no longer active" page instead of a payment form, so a stale link can never collect a stale price.
Create a Session#
Every create call includes exactly one of three fields, which decides what the session sells:
| Field | Use when |
|---|---|
order_id | Your app already created an order and owns cart state, pricing, or customer context. The default for most integrations. |
quick_pay_item | You want to charge a name and an amount without building an order first. |
plan_id | You want hosted signup for a subscription plan. |
Payment links and invoices create checkout sessions for you behind the scenes; the session's source field tells you where it came from. Create sessions directly (source api) when your application drives the checkout moment.
Pay an Existing Order#
The order-first flow: your backend creates the order, then asks for a hosted checkout page that collects its balance. Amounts on the order are integers in the currency's minor unit.
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-order-tote-001" \
-d '{
"order_id": "ord_1kmn0aExample",
"redirects": {
"success_redirect_url": "https://example.com/thanks",
"cancel_redirect_url": "https://example.com/cart"
}
}'
{
"data": {
"checkout_session_id": "cs_1kmn0aExample",
"status": "open",
"order_id": "ord_1kmn0aExample",
"source": "api",
"url": "https://checkout.withflintpay.com/checkout/cs_1kmn0aExample#checkout_token=...",
"expires_at": "2026-07-03T17:04:05Z"
}
}
Save data.checkout_session_id for your records and data.url for the redirect. If you haven't created an order yet, Accept Your First Payment walks this same flow end to end, starting from the order.
Charge a Quick Amount#
When there's no cart to model, quick_pay_item charges a name and an amount in one call. Flint creates the backing order for you, so you still end up with the same durable commerce record as the order-first flow.
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-consult-001" \
-d '{
"quick_pay_item": {
"name": "Design consultation",
"amount_money": {"amount": 9900, "currency": "USD"}
},
"redirects": {
"success_redirect_url": "https://example.com/thanks"
}
}'
The response includes the generated order_id alongside the session. Keep both: refunds, receipts, and reporting all hang off the order. quick_pay_item also accepts an optional tax object (taxable, tax_category) if the amount should be taxed.
Start a Subscription Signup#
Pass a plan_id to get a hosted signup page for a subscription plan. The buyer enters their details and payment method; when they complete signup, Flint creates the subscription and its first order and payment.
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-pro-signup-001" \
-d '{
"plan_id": "plan_1kmn0aExample",
"redirects": {
"success_redirect_url": "https://example.com/welcome"
}
}'
Confirm signup through the subscription.created and subscription.activated webhook events, then manage the subscription through the subscriptions API. For a reusable public signup URL instead of one session per buyer, see subscription signup links.
Send the Buyer to Checkout#
Send the buyer to data.url exactly as returned:
- Server-rendered app: respond with an HTTP redirect to the URL.
- Single-page app: navigate the browser (
window.location.assign(url)).
The #checkout_token fragment on the URL is what authenticates the buyer on the hosted page. Don't strip it, rebuild the URL from the session ID, or store the URL in logs. It's fine to deliver the URL to the buyer (that's its job); treat it like something only that buyer should hold.
Handle the Return#
The redirects object controls where the buyer lands afterward:
success_redirect_url: where the buyer goes after paying. Flint appendscsIdandorderIdquery parameters so your landing page knows which session and order completed.cancel_redirect_url: where the buyer goes if they back out. The session staysopenuntil it expires or you close it, so the buyer can return and finish later from the same URL.expiration.expiration_url: where buyers are sent if they open the session after it expires. Without it they see Flint's expired-checkout page.
The success redirect is user experience, not proof of payment. A buyer can close the tab before the redirect fires, or visit the URL directly. Verification belongs to your backend, below.
Verify the Outcome#
Subscribe to webhooks for the durable signal:
checkout_session.completedfires when a session becomespaid. The payload carries the session, including itsorder_id, so it's the natural trigger for fulfillment.order.payment_succeededfires on the underlying order, if you'd rather key fulfillment off orders regardless of which surface collected payment.subscription.createdandsubscription.activatedfire for plan sessions.
You can also read the state directly whenever you need it:
curl "https://api.withflintpay.com/v1/checkout-sessions/cs_1kmn0aExample?expand=order" \
-H "Authorization: Bearer YOUR_API_KEY"
{
"data": {
"checkout_session_id": "cs_1kmn0aExample",
"status": "paid",
"order_id": "ord_1kmn0aExample",
"payment_intent_ids": ["pi_1kmn0aExample"],
"order": {
"order_id": "ord_1kmn0aExample",
"status": "paid"
}
}
}
Never fulfill based on the browser redirect alone. Treat the webhook, or a backend fetch like the one above, as the signal that money actually moved.
Configure the Hosted Page#
Everything below is optional. Sections you omit inherit the merchant's checkout settings from the dashboard, so a bare create already renders a complete, working page. Pass a section to override it for this session only.
| Field | What it controls |
|---|---|
customer_collection | Which buyer details are collected, required, or prefilled. |
redirects | Success, cancel, and post-expiration destinations. |
expiration | How long the session stays payable. |
tip | Whether tipping is offered and which presets show. |
coupon | Whether the page shows a coupon code field. |
tax | Whether tax is calculated and shown at checkout. |
payments | Which payment methods and wallets are offered, plus your own payment reference. |
legal | Terms of service, refund and shipping policy links, and whether terms must be accepted. |
custom_text | Merchant copy on the page, like an order summary message. |
theme | Branding for the hosted page. |
fulfillment_options | Pickup and shipping choices the buyer selects at checkout. |
metadata | Up to your own string key-value pairs for correlation, returned on every read. |
Customer Details#
customer_collection decides what you ask the buyer for and what you already know:
{
"order_id": "ord_1kmn0aExample",
"customer_collection": {
"customer_id": "cus_1kmn0aExample",
"prefilled_customer_info": {
"email": "ada@example.com",
"phone": "+15555550100"
},
"require_email": true,
"require_phone": false,
"require_billing_address": false,
"enable_address_autocomplete": true
}
}
customer_idlinks the resulting order and payment to an existing customer record.prefilled_customer_infofills the form ahead of time (email, phone, billing and shipping address); the buyer can still edit.require_email,require_phone, andrequire_billing_addressmake fields mandatory before payment.enable_address_autocompleteturns on address suggestions as the buyer types.
The fewer fields you require, the faster buyers get through checkout. Require only what fulfillment actually needs.
Tips#
{
"tip": {
"enabled": true,
"tip_percentages": [10, 15, 20],
"default_tip_percentage": 15,
"is_custom_tip_enabled": true
}
}
Percentages are whole numbers: 15 means 15%. See Tips & Fees for how tips land on the order.
Coupons and Tax#
coupon.enabled shows a code entry field; codes are validated against your coupons and applied to the order before payment. tax.enabled calculates and displays tax at checkout based on your tax settings; address-dependent tax appears once the buyer provides an address.
Lifecycle Rules#
One Open Session per Order#
An order has at most one open session at a time. Creating a new session for an order automatically invalidates any session that's still open for it, atomically, so there's no window where two live checkout pages could both collect payment. The old URL immediately stops accepting payment.
This makes "regenerate the checkout link" safe: just create a new session. If instead you're retrying the same create call after a timeout, reuse the original Idempotency-Key; that replays the original session rather than minting (and invalidating) anything. See Idempotency.
The Order Locks While Checkout Is Open#
While a session is open, merchant-side changes to the order's financial fields (line items, charges, amounts) are rejected with 409 CHECKOUT_SESSION_ACTIVE. The buyer might be looking at the total right now, so the total can't change underneath them. Buyer-driven changes on the hosted page, like tips and coupons, still work.
To change the price mid-flight: close the session, edit the order, create a new session, and send the buyer the new URL.
Expiration#
Sessions expire 24 hours after creation by default. Tighten that when the price or cart context goes stale sooner:
{
"order_id": "ord_1kmn0aExample",
"expiration": {
"expires_in_seconds": 1800,
"expiration_url": "https://example.com/quote-expired"
}
}
The computed deadline comes back as expires_at on every read. Expiration also releases the order lock, so an abandoned checkout doesn't hold the order hostage.
Closing a Session#
Close a session yourself when it's no longer valid: the buyer abandoned it, the quote was withdrawn, or you need to edit the order.
curl -X POST https://api.withflintpay.com/v1/checkout-sessions/cs_1kmn0aExample/close \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{"reason": "cart updated"}'
The session becomes closed and the reason is stored on closed_reason for your audit trail.
Retrieve and List Sessions#
Fetch one session by ID, optionally expanding related records in the same call (expand=customer,invoice,order,payment_intents,payment_link). List sessions with filters for reconciliation and support tooling:
curl "https://api.withflintpay.com/v1/checkout-sessions?status=open&order_id=ord_1kmn0aExample" \
-H "Authorization: Bearer YOUR_API_KEY"
The list endpoint supports status, order_id, customer_id, payment_link_id, source, created/updated/expires time bounds, and a free-text query over metadata and payment notes, with standard cursor pagination. PATCH /v1/checkout-sessions/{id} updates metadata on a live session without disturbing it.
Common Mistakes#
- Reusing one session URL for many buyers. A session is one checkout for one buyer. For a shareable URL, use Payment Links.
- Treating the success redirect as payment confirmation. Redirects get lost. Fulfill from
checkout_session.completedor a backend fetch. - Rebuilding the URL from the session ID. The
#checkout_tokenfragment authenticates the buyer; a reconstructed URL drops it and the page can't load the session. - Editing the order while its session is open. You'll get
409 CHECKOUT_SESSION_ACTIVE. Close the session first, then edit, then create a fresh session.
Next Steps#
- Accept Your First Payment: the full order-to-paid walkthrough with test cards.
- Checkout Sessions API Reference: every field on every endpoint.
- Webhooks: durable payment notifications for fulfillment.
- Embedded Payments with Stripe Elements: collect payment inside your own UI instead of redirecting.
- Payment Links vs Checkout Sessions vs Invoices: choosing the right hosted surface.
