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#

text
create session -> redirect buyer to url -> buyer pays -> redirect back + webhook -> verify from your backend

A session moves through a small set of states:

StatusMeaning
openThe hosted page is live and the buyer can pay. Sessions are created in this state.
paidThe buyer completed payment (or subscription signup). Terminal.
expiredThe session passed expires_at without payment. The default lifetime is 24 hours. Terminal.
closedYou closed it with the close endpoint. Terminal.
invalidatedA 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:

FieldUse when
order_idYour app already created an order and owns cart state, pricing, or customer context. The default for most integrations.
quick_pay_itemYou want to charge a name and an amount without building an order first.
plan_idYou 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.

Bash
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"
    }
  }'
JSON
{
  "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.

Bash
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.

Bash
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 appends csId and orderId query parameters so your landing page knows which session and order completed.
  • cancel_redirect_url: where the buyer goes if they back out. The session stays open until 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.completed fires when a session becomes paid. The payload carries the session, including its order_id, so it's the natural trigger for fulfillment.
  • order.payment_succeeded fires on the underlying order, if you'd rather key fulfillment off orders regardless of which surface collected payment.
  • subscription.created and subscription.activated fire for plan sessions.

You can also read the state directly whenever you need it:

Bash
curl "https://api.withflintpay.com/v1/checkout-sessions/cs_1kmn0aExample?expand=order" \
  -H "Authorization: Bearer YOUR_API_KEY"
JSON
{
  "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.

FieldWhat it controls
customer_collectionWhich buyer details are collected, required, or prefilled.
redirectsSuccess, cancel, and post-expiration destinations.
expirationHow long the session stays payable.
tipWhether tipping is offered and which presets show.
couponWhether the page shows a coupon code field.
taxWhether tax is calculated and shown at checkout.
paymentsWhich payment methods and wallets are offered, plus your own payment reference.
legalTerms of service, refund and shipping policy links, and whether terms must be accepted.
custom_textMerchant copy on the page, like an order summary message.
themeBranding for the hosted page.
fulfillment_optionsPickup and shipping choices the buyer selects at checkout.
metadataUp 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:

JSON
{
  "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_id links the resulting order and payment to an existing customer record.
  • prefilled_customer_info fills the form ahead of time (email, phone, billing and shipping address); the buyer can still edit.
  • require_email, require_phone, and require_billing_address make fields mandatory before payment.
  • enable_address_autocomplete turns 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#

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

JSON
{
  "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.

Bash
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:

Bash
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.completed or a backend fetch.
  • Rebuilding the URL from the session ID. The #checkout_token fragment 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#

Rate this doc