Checkout sessions
A checkout session collects payment for one order from one buyer. Your backend creates it when the buyer is ready to pay. A hosted session gives you a URL on Flint's checkout page to send the buyer to. An embedded session gives your backend a scoped credential so your own storefront can run the checkout. Both kinds share the same order, expiration, replacement, and webhook behavior.
A session is single-use. For one URL that many buyers can open, such as a pricing page, a QR code, or an ad, use a payment link. For a bill with a due date, reminders, and a PDF, use an invoice. Payment links vs checkout sessions vs invoices compares the three.
How a hosted checkout works#
- Browser sends 1. the buyer clicks Pay to Your backend
- Your backend sends 2. create a checkout session to Flint
- Flint returns session and its url to Your backend
- Your backend returns 3. redirect to checkout_session.url to Browser
- Browser sends 4. the buyer pays on Flint's checkout page to Flint
- Flint returns 5. redirect to your success URL to Browser
- Flint sends 6. order.paid webhook to Your backend
The redirect in step 5 is for the buyer. Your backend learns about payment from the webhook in step 6, or by reading the session, as described in Confirm the payment.
A session's status records where it is:
status on checkout sessionOnly an open session takes payment. A buyer who opens the URL of a session that has ended sees a message that the checkout is no longer available, never a payment form, so an old link can't collect an old price.
Create a session#
Each create request names exactly one thing to sell:
| Field | Use it when |
|---|---|
order_id | Your backend already created an order with line items, pricing, and the customer. Most integrations use this. |
quick_pay_item | You want to charge a name and an amount without building an order first. |
subscription_plan_id | You want a hosted signup page for a subscription plan. |
Every session collects an order: with quick_pay_item or subscription_plan_id, Flint creates the order for you. A standalone PaymentIntent can't back a session, and sending payment_intent_id returns 400 UNKNOWN_FIELD. Collect a standalone PaymentIntent on your own page as Server-confirmed payments describes.
Payment links and invoices create their own sessions. A session made from a payment link has origin: "payment_link" and a payment_link_id, and an invoice's session carries invoice_id. Sessions you create through this endpoint have origin: "api".
Pay an existing order#
Create the order first, then create a session for it. Amounts 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"
}
}'
Flint responds with 201 Created:
{
"data": {
"checkout_session": {
"checkout_session_id": "cs_1kmn0aExample",
"status": "open",
"surface": "hosted",
"order_id": "ord_1kmn0aExample",
"origin": "api",
"expires_at": "2026-07-04T17:04:05Z",
"url": "https://checkout.withflintpay.com/checkout/cs_1kmn0aExample#checkout_token=cklt_v1..."
},
"checkout_access": {
"checkout_auth_token": "ckat_v1..."
}
}
}
Store checkout_session_id with your order. Send the buyer to checkout_session.url, as described in Send the buyer to checkout. checkout_auth_token is the credential for embedded checkout; a hosted integration doesn't need it.
Send an Idempotency-Key on every create. If the response is lost, retry with the same key and Flint returns the same session instead of creating a second one. Testing walks through this flow from creating the order to a test payment.
Hosted checkout pays an order with one payment method. If you staged payment legs with POST /v1/orders/{order_id}/payment-intents and two or more are unpaid, creating or replacing a hosted session returns 409 CHECKOUT_SPLIT_PAYMENT_UNSUPPORTED with the legs in payment_intent_ids. Cancel them so checkout collects the balance in one payment, or collect a split payment in your own UI as Embedded payments describes. Embedded sessions have no such limit.
Charge a quick amount#
When there is no cart to model, quick_pay_item charges a name and an amount in one call:
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"
}
}'
Flint creates an order with that one line item and returns its order_id on the session. Keep it: refunds, receipts, and reporting all work from the order. To tax the amount, add quick_pay_item.tax with taxable and line_item_tax_category.
Start a subscription signup#
subscription_plan_id creates a signup page for a plan. The buyer enters their email and pays with a card, Apple Pay, or Google Pay. When they finish, Flint creates the subscription and its first order, and saves the card with usage: "off_session" so the subscription can charge renewals.
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 '{
"subscription_plan_id": "plan_1kmn0aExample",
"redirects": {
"success_redirect_url": "https://example.com/welcome"
}
}'
Signup sends subscription.created. Manage the subscription afterward through the subscriptions API. For a public signup URL that every buyer can share, use a subscription signup link. To run signup in your own storefront, see Start a subscription.
Send the buyer to checkout#
Send the buyer to checkout_session.url exactly as returned:
- From a server-rendered page, respond with an HTTP redirect to the URL.
- From a single-page app, navigate the browser with
window.location.assign(url).
The #checkout_token fragment is what lets the buyer's browser open the session. Don't strip it, and don't rebuild the URL from the session ID. Treat the URL as belonging to that buyer: deliver it to them, and keep it out of logs, analytics, and shared links.
Handle the return#
redirects sets where the buyer goes when they leave checkout:
success_redirect_url: where the buyer lands after paying. Flint addscsIdandorderIdquery parameters with the session and order IDs.cancel_redirect_url: where the buyer goes if they leave without paying. The session staysopen, so they can come back to the same URL and pay until it expires.
To send buyers who open an expired session to your own page, set expiration.expiration_url. Without it, they see Flint's expired-checkout page.
A buyer can close the tab before the success redirect, or open the success URL directly, so the redirect proves nothing about payment. Confirm payment from your backend.
Confirm the payment#
Subscribe to webhooks for the result:
The webhook events catalog lists each payload. You can also read the session at any time:
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",
"terminal_reason": "payment_succeeded",
"order_id": "ord_1kmn0aExample",
"payment_intent_ids": ["pi_1kmn0aExample"],
"order": {
"order_id": "ord_1kmn0aExample",
"status": "closed",
"payment_status": "paid"
}
}
}
Embed checkout in your storefront#
Set surface: "embedded" when your storefront renders the checkout. The response has the same checkout_session and checkout_access.checkout_auth_token, but the session has no url, because there is no Flint page to send the buyer to.
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-cart-123" \
-d '{
"order_id": "ord_1kmn0aExample",
"surface": "embedded"
}'
Keep the checkout credential on your backend, tied to your own signed browser session. When your backend calls Flint on the buyer's behalf, it sends the session ID and the credential in place of your API key:
GET /v1/checkout-sessions/cs_1kmn0aExample
X-Checkout-Session-ID: cs_1kmn0aExample
X-Checkout-Session-Secret: ckat_v1...
Each request carries one kind of credential. Sending X-Checkout-Session-ID or X-Checkout-Session-Secret together with Authorization or X-API-Key returns 400 AMBIGUOUS_AUTH. In a backend that holds both, choose the credential in one place rather than in each handler.
Flint doesn't accept these calls from a browser on your domain. Relay them through your backend, with CSRF protection, origin checks, and rate limits on your own routes. Build your own checkout covers the full integration, and Securing a headless checkout covers the security boundary.
Send page_origin with the origin of your checkout page if buyers can enter gift card codes there. It names the one origin that can show the gift card challenge Flint requires after repeated failed codes. Build your own checkout covers the challenge.
An embedded success_redirect_url is where Flint returns the buyer after a redirect-based payment step. It must be an absolute HTTPS URL with no query string or fragment, and it's required when the session offers Affirm.
What the checkout credential can do#
The credential works only for its own session and that session's order. It acts for the customer the session was created with, from the order's customer_id or customer_collection.customer_id, and that doesn't change if the order gets a customer later. A session created without a customer acts for one only after the buyer confirms a verification code, and then only with the credential that confirmation issues. See Confirm the buyer's email.
The checkout credential can
- Read the session, its order, and the order's PaymentIntents
- Create and cancel payment legs (PaymentIntents) on the order
- Start, resume, read, list, and cancel the order's payment attempts
- Quote, select, read, and clear delivery, and check pickup availability
- Change line-item modifiers and gift card recipients
- Apply and remove gift cards
- Preview and apply promotion codes, and reprice and remove discounts
- Set the requested tip, or turn on automatic tax with a tax location
- Save
buyer_contact - List the checkout customer's active saved payment methods
- Request and confirm a verification code for the buyer
- Save the card the buyer typed, when they ask and
save_payment_method_offeredistrue - Send the order's receipt, once the session is
paidorpartially_paid
Needs your API key
- Read or change any other order or session
- Make any other change to line items or order fields
- Apply a discount by ID, or create a manual discount
- Change the session's
metadataorexternal_reference_id - Add, edit, or remove payment methods
- Manage customers, subscriptions, or merchant settings
- Refund, or handle disputes
A session read with the checkout credential leaves out metadata and external_reference_id, and adds what the buyer's page needs, such as payment_collection for mounting the payment form.
Configure the checkout#
Each of these fields is optional, and most default to the merchant's checkout settings from the dashboard. Within an object, each field you leave out falls back to the merchant setting on its own: sending "tip": {"enabled": true} turns tipping on and keeps the merchant's tip percentages.
| Field | What it sets |
|---|---|
surface | hosted (default) or embedded. |
page_origin | Embedded only. The origin of the page that renders your checkout, such as https://shop.example.com. Only that origin can show the session's gift card challenge. |
customer_collection | The customer, and which buyer details are required or prefilled. |
redirects | Where the buyer goes after paying or canceling. |
expiration | How long the session stays open, and where an expired link sends the buyer. |
payments | Which payment methods appear, plus a payment note and reference. |
tip | Whether tips are offered, and the presets. |
promotion_config | Automatic promotions and promotion-code entry. |
tax | Automatic tax for the order Flint creates from quick_pay_item or subscription_plan_id. |
delivery_method_ids | The delivery methods checkout offers for shipping, pickup, and local delivery. |
legal | Links to terms of service, refund and shipping policies, and a contract, and whether the buyer must accept the terms. |
custom_text | An order summary message and the shipping address label. |
theme | The page title and brand colors. |
external_reference_id | Your own ID for the session, which you can filter the list by. |
metadata | String key-value pairs for your own use, returned on every read. |
surface, page_origin, redirects, custom_text, external_reference_id, and metadata have no merchant default.
Customer details#
customer_collection sets which customer the checkout is for and what it asks the buyer:
{
"order_id": "ord_1kmn0aExample",
"customer_collection": {
"prefilled_customer_info": {
"email": "ada@example.com",
"phone": "+15555550100"
},
"require_email": true,
"require_phone": false,
"require_billing_address": false,
"enable_address_autocomplete": true
}
}
- With
order_id, the customer comes from the order. Set the order'scustomer_idbefore you create the session. If you sendcustomer_collection.customer_id, it must match: a different customer returns400 CHECKOUT_CUSTOMER_CONFLICT, and an order with no customer returns400 CHECKOUT_CUSTOMER_NOT_SET_ON_ORDER. While a session is open, setting the order's customer returns409 ORDER_CUSTOMER_CHECKOUT_ACTIVE; close the session first. - With
quick_pay_itemorsubscription_plan_id,customer_collection.customer_idlinks the new order to an existing customer. prefilled_customer_infofills in email, phone, and billing and shipping addresses, and the buyer can edit them. For a customer with saved addresses, Flint fills in any address you leave out.require_email,require_phone, andrequire_billing_addressmake a field mandatory before payment. Subscription signups always require email.enable_address_autocompletesuggests addresses as the buyer types.
Tax is calculated from the order's tax location. With order_id, prefilled addresses don't change it; set tax.location on the order before you create the session. With quick_pay_item or subscription_plan_id and automatic tax on, an address you send in prefilled_customer_info sets the tax location: the shipping address when the order ships, otherwise the billing address, or the shipping address when you sent no billing address. Addresses Flint fills in from the customer don't set it, and a delivery destination on the order takes precedence.
Require only what fulfillment needs; each extra field is one more thing the buyer has to fill in before paying.
Buyer contact#
A checkout can save the email and phone the buyer types before they pay, so a reload can prefill them. Send them as buyer_contact with the session's checkout credential, for example when a field loses focus:
PATCH /v1/checkout-sessions/cs_1kmn0aExample
X-Checkout-Session-ID: cs_1kmn0aExample
X-Checkout-Session-Secret: ckat_v1...
Content-Type: application/json
{
"buyer_contact": {
"email": "ada@example.com",
"phone": "+15555550100"
}
}
- Only the session's own checkout credential can set
buyer_contact, and only while the session isopen. With the checkout credential, this route also acceptstimezoneand, on a subscription plan session,subscription_terms. Any other field returns403 CHECKOUT_SESSION_UPDATE_FIELD_NOT_ALLOWED. Your API key can readbuyer_contactbut not set it: sending it returns the same error. - An omitted field keeps its saved value, and
nullclears it.emailmust be a valid address andphonemust be in E.164 format. Flint stores both exactly as sent, so a value with surrounding spaces is rejected. - Saving the values the session already holds changes nothing.
- Reads return
buyer_contactwithemail,phone,is_email_cleared,is_phone_cleared, andupdated_at. A value the buyer hasn't entered, or cleared, isnull. The correspondingis_*_clearedflag istrueonly after an explicit clear, including when the buyer clears a merchant prefill before saving any contact. Saving a value resets its flag tofalse. The contact is omitted until the buyer saves or clears a field. - When a payment made with the checkout credential omits
buyer_contact.emailorbuyer_contact.phone, Flint uses the saved value. The order records the contact the payment used asbuyer_contact.emailandbuyer_contact.phone. - If the merchant turned on checkout reminder emails, saving an email on a hosted session schedules at most one reminder, sent if the buyer leaves without paying. Invoice, subscription signup, and return checkouts don't send reminders.
- Flint clears the saved contact 30 days after the session ends. Deleting the customer the session was created for, or the customer its order belongs to, clears it too.
buyer_contact never changes customer_collection.prefilled_customer_info.
When a clear flag is true, keep that field empty instead of restoring a prefill. Omit the corresponding buyer_contact.email or buyer_contact.phone when paying; Flint sends no saved value for a cleared field. If a delivery selection already carries that phone, create a new selection through POST /v1/checkout-sessions/{checkout_session_id}/delivery-selections with the current expected_delivery_selection_id, the same choices, and a recipient containing the values to keep without phone. The new selection replaces the recipient. A delivery option that requires a phone reports it in input_requirements until the buyer supplies one.
Saved payment details#
Hosted checkout offers buyers an unchecked "Save my details for faster checkout at {business name}" option under the card fields. When the buyer checks it and the payment succeeds, Flint saves the card for the customer the checkout acts for, with usage: "on_session". That card pays only in later checkouts the buyer completes, never a subscription or an automatic invoice. A guest proves their email with a six-digit code before a card can be saved, or gives a US or Canadian mobile phone number to confirm by text after paying. A returning buyer gets a code when they type their email, and the code opens the cards they saved.
The option is on by default; turn it off with checkout.saved_payment_details. It never appears for Apple Pay, Google Pay, ACH debit, or Affirm, for partial payments, on invoice, subscription, and return checkouts, or when customer accounts are merchant hosted. When Flint wallet support is on, checkout instead offers to save the card with Flint for faster checkout at other stores that use Flint, and the option to save it with your business doesn't appear.
Cards buyers save in checkout covers the codes, phone numbers, and what each card can pay. To offer the option in an embedded checkout, see Offer to save the card.
Tips#
{
"tip": {
"enabled": true,
"tip_percent_options": [10, 15, 20],
"default_tip_percent": 15,
"is_custom_tip_enabled": true
}
}
tip_percent_options takes exactly three percentages, each from 1 to 100 with up to four decimal places: 15 means 15%. Offer tips at hosted checkout covers fixed-amount presets for small orders and how the tip lands on the order.
Promotions and tax#
promotion_config.automatic_enabled turns automatic promotions on or off for the session. promotion_config.codes_enabled shows or hides the promotion-code field; without it, the session follows the merchant's checkout.promotion_code_entry_enabled setting. If the merchant turned promotion codes off with promotions.codes_enabled: false, creating a session with codes_enabled: true returns 400 PROMOTION_CODES_DISABLED. Codes apply to the order before payment.
tax.enabled turns on automatic tax for the order Flint creates from quick_pay_item or subscription_plan_id. With order_id, the order's own tax settings apply. When the order uses automatic tax and the merchant has no active tax connection, creating the session returns 400 AUTOMATIC_TAX_CONNECTION_REQUIRED. Sales tax covers how tax is calculated.
Session rules#
One open session per order#
An order has at most one open session. Creating another for the same order returns 409 CHECKOUT_SESSION_ALREADY_EXISTS, with the open session's ID in existing_checkout_session_id. The existing session and its URL keep working. If two requests create a session for the same order at the same moment, one can return 409 ORDER_CHECKOUT_SESSION_CHANGED. Retry the request.
To replace the session on purpose, for example to send the buyer a fresh URL, pass its ID as replace_checkout_session_id:
{
"order_id": "ord_1kmn0aExample",
"replace_checkout_session_id": "cs_1kmn0aCurrent"
}
The replacement happens in one step, and only if that ID is still the order's open session:
- If another session replaced it first, Flint returns
409 CHECKOUT_SESSION_CURRENT_CHANGEDwith the newer session incurrent_checkout_session_id. - If the buyer is in the middle of paying, Flint returns
409 CHECKOUT_PAYMENT_RESOLVING. Retry once the payment finishes. - If the response is lost, retry with the same
Idempotency-Keyto get the same new session.
The old session becomes invalidated with terminal_reason: "superseded", and its superseding_checkout_session_id points to the new one, on reads and on the checkout_session.invalidated event. Replacement works only with order_id. An invoice's session is managed through POST /v1/invoices/{invoice_id}/checkout-session.
Changing the order during checkout#
A change you make to the order with your API key that affects what the buyer pays ends its open session. This covers line items, discounts, charges, tax, and tips, as well as closing the order. The session becomes invalidated with terminal_reason: "order_mutated" in the same request, so the old URL can't collect the old total. Create a new session and send the buyer its URL.
Changes made through the session's own checkout credential, such as the buyer adding a tip or applying a code, stay part of that checkout.
While the buyer is paying, Flint rejects these changes and leaves both the order and the session as they were. You get 409 CHECKOUT_PAYMENT_RESOLVING, or 409 PAYMENT_ATTEMPT_IN_PROGRESS while a payment attempt is in progress. Retry after the payment finishes.
While a hosted session is open, the order keeps at most one unpaid payment leg (PaymentIntent). Creating another with POST /v1/orders/{order_id}/payment-intents returns 409 ORDER_PAYMENT_LEG_CHECKOUT_ACTIVE, with the unpaid leg in payment_intent_ids and the session in existing_checkout_session_id. Cancel that leg first, or close the session and collect a split payment in your own UI.
Expiration#
A session expires 24 hours after creation unless the merchant set a different default. Shorten it when the price or stock won't hold that long:
{
"order_id": "ord_1kmn0aExample",
"expiration": {
"expires_in_seconds": 1800,
"expiration_url": "https://example.com/quote-expired"
}
}
Every read returns the deadline as expires_at. An invoice's session takes its deadline from the invoice's public link instead, so the buyer's link keeps working for the invoice's whole collection window.
When an unpaid payment-link session expires, Flint also closes its order with closed_reason: "Checkout expired before payment" and emits order.closed, provided nothing was paid or authorized, no invoice or other active collection owns the order, and the order is not linked to a return. Pending discounts and inventory reservations are released, and the unpaid delivery destination is cleared. Orders behind existing-order, quick pay, subscription-plan, and invoice sessions stay open after session expiry. Resolve the payment link again to start a new checkout and order.
Close a session#
Close a session when it should no longer take payment: the quote was withdrawn, the buyer abandoned it, or you need to set the order's customer.
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_message": "Quote withdrawn"}'
The session becomes closed, and reason_message is stored as closed_reason for your records. The buyer never sees it. Closing while the buyer is paying returns 409 CHECKOUT_PAYMENT_RESOLVING, and closing a session that has already ended returns 409 INVALID_STATUS_TRANSITION with the session's status in current_status.
After a session ends#
The checkout credential keeps limited access after the session leaves open, so the buyer's page can show the result:
| Session status | What the checkout credential can do |
|---|---|
paid, partially_paid, invalidated | Read the session, its order and PaymentIntents, a payment attempt, and the delivery selection. A paid or partially_paid session can also send the receipt. After a paid session, the buyer can also confirm a card they saved with a mobile phone number. Anything else returns 409 CHECKOUT_SESSION_NOT_OPEN. |
closed, expired | Nothing. Requests return 401 INVALID_CHECKOUT_SESSION. |
Support and back-office tools should use your API key, which works for every session regardless of status.
Finish a payment after expiry#
A session can reach expires_at while the buyer is in the middle of a payment, for example on a 3D Secure screen. Rather than strand that payment, the session stays usable for it alone. The session read shows recovery_mode: true, the attempt in recovery_payment_attempt_id, and the deadline in recovery_expires_at, which is at most an hour after the attempt itself expires.
In recovery, the checkout credential can call only:
Any other call returns 403 CHECKOUT_RECOVERY_RESTRICTED, and reading a different attempt returns 403 CHECKOUT_RECOVERY_ATTEMPT_MISMATCH. If the attempt succeeds, the session becomes paid, or partially_paid if it collected only part of the balance. If it fails, the session expires. For an existing-order or quick pay checkout, create a new session to collect any balance left. For a payment-link checkout, resolve the link again; its unpaid order closes when the conditions above are met.
Retrieve and list sessions#
Read one session with GET /v1/checkout-sessions/{checkout_session_id}. Add expand to include related records in the same response: customer, invoice, order, payment_intents, or payment_link, comma-separated. Each expansion needs your key to have read access to that resource; otherwise the request returns 403 INSUFFICIENT_SCOPE.
List sessions for reconciliation and support tools:
curl "https://api.withflintpay.com/v1/checkout-sessions?status=open&order_id=ord_1kmn0aExample" \
-H "Authorization: Bearer YOUR_API_KEY"
The list filters by status, order_id, customer_id (the order's customer), payment_link_id, origin, and external_reference_id, and by time with created_after, created_before, updated_after, updated_before, expires_after, and expires_before. query searches session IDs, external_reference_id, metadata, and payment notes. Results use cursor pagination and can be ordered with sort_by and sort_direction.
With your API key, PATCH /v1/checkout-sessions/{checkout_session_id} updates metadata and external_reference_id in any status. New metadata keys merge into the existing ones, a key set to null is removed, and "metadata": null clears them all.
Errors#
Errors lists every code with its remediation.
Next steps#
- Testing: create an order and a session, and pay with a test card.
- Build your own checkout: run an embedded session from your own storefront.
- Webhooks: receive and verify the events that drive fulfillment.
- Checkout sessions API reference: every field on every endpoint.
