Skip to Content
ConceptsSessions

Sessions

A CheckoutSession is what the buyer interacts with — a TTL’d, single-use object that owns the hosted checkout URL. It’s the thinnest of the three core entities; the heavier lifting happens in Orders and Payment Intents (see below).

The three-entity data model

CheckoutSession ← 1:1 → Order ← 1:N → PaymentIntent (buyer) (catalog) (each pay attempt)
EntityPurposeLifetime
CheckoutSessionBuyer-facing — has session_key, checkout_url, TTLMinutes (default 30)
OrderYour catalog state — line items, totals, refundsPermanent record
PaymentIntentOne pay attempt on one chain/assetHours; settles or expires

You create a session and an order together (via POST /b2b/v1/checkout-sessions/quick). Each time the buyer picks an asset on the checkout page, a fresh PaymentIntent is opened against the right chain. If they switch assets mid-checkout, the previous intent goes to EXPIRED and a new one starts.

Identifier

A session is identified by its session_key:

cst_G-SO92J7HNWkwMHEHjD4oO1Z

URL-safe, ~24 chars after the prefix (18 random bytes base64url-encoded, no padding). The hosted checkout page is https://checkout.infraio.xyz/<session_key> — treat the session key as a bearer credential for that one checkout.

Lifecycle — CheckoutSession

StateMeans
ACTIVESession created and open. The checkout URL is usable.
COMPLETEDA PaymentIntent on this session settled. The Order is now PAID (or PARTIAL_PAID if underpaid).
EXPIREDexpires_at passed without settlement. The cleanup worker flipped the state and cancelled any open Orders.
CANCELEDExplicit cancel — either buyer hit “cancel” or you called the cancel endpoint.

The three terminal states are mutually exclusive and final. A new CheckoutSession against the same Order can be created if you want to retry (e.g., after underpayment).

TTL

  • Default: 30 minutes (configurable via the expires_in field on create, in seconds).
  • Bounds: There’s no hard min/max enforced server-side. Use sensible values — under 60 seconds risks legitimate buyers timing out; over 7 days holds capacity on a token that’s almost certainly abandoned. Pick a number that matches your buyer’s expected decision window.
  • Per-merchant default: Configurable via dashboard, but only the legacy POST /b2b/v1/checkout-sessions (two-step) path honors it. The POST /b2b/v1/checkout-sessions/quick path always falls back to 30 minutes if expires_in is omitted, regardless of the per-merchant setting. If you need a different default on the quick path, send expires_in explicitly on every call.
  • Enforcement: Lazy on read + a periodic cleanup worker. A session whose expires_at has passed is treated as EXPIRED even if the state field hasn’t been written yet, so don’t rely on reading the state via the API at the exact moment of expiry.

Underpayment

If a buyer sends less than the session amount, the PaymentIntent still settles for the partial amount and the Order transitions to PARTIAL_PAID. The CheckoutSession moves to COMPLETED (one PaymentIntent settled), so it’s no longer reusable.

To accept the shortfall as full payment, the order can be resolved to PAID — see Concepts → Orders (no public resolve API today; for a self-serve flow, collect the remainder instead). To collect the remainder, create a new CheckoutSession against the same Order with the residual amount.

Overpayment

If the buyer sends more than the session amount (rare, but happens with manual transfers), the on-chain scanner captures the overpayment within 24 hours and emits a merchant alert. There’s no automatic refund — issue one manually via the refund API or the dashboard.

Wrong-asset payments

The deposit address is generated per (session, chain, asset) tuple. If a buyer sends the wrong asset to the address, the on-chain matcher doesn’t recognise it and the PaymentIntent stays open until TTL expiry. We can recover the funds but it’s a support flow, not automatic — instruct buyers to send the exact asset shown on the checkout page.

Idempotency on create

POST /b2b/v1/checkout-sessions/quick accepts an idempotency_key field in the request body (note: body field, not HTTP header). Auto-generate a UUID for it if your client doesn’t have a natural key — the SDK does this by default.

The key deduplicates the Order, not the CheckoutSession. On a retry with the same key, /quick returns the original order (order_id is stable) but mints a fresh CheckoutSession — a new session_key and checkout_url each time. That’s intentional: one Order can back several checkout attempts (see Orders), so a retried /quick hands the buyer a clean session without duplicating the order.

Two behaviours differ from a Stripe-style idempotency layer — don’t get caught out:

  • The body is not hashed or compared. Reusing a key with a different body does not return 409 — the server silently returns the order already stored under that key and ignores the new body. So treat an idempotency_key as a one-shot token for a single logical order; never recycle one across different carts.
  • Only the Order is deduplicated, not the session. If you need the same checkout URL back, persist session_key / checkout_url from the first response — calling /quick again won’t return the old one. To enumerate every session minted against an order, use GET /b2b/v1/checkout-sessions/by-order/:order_id.

API endpoints

MethodPathNotes
POST/b2b/v1/checkout-sessions/quickCreate order + session in one call
POST/b2b/v1/checkout-sessionsCreate session against an existing order
GET/b2b/v1/checkout-sessions/by-order/:order_idList all sessions for an order (for retry history)
GET/checkout/:session_keyPublic — what the buyer’s browser hits

See the Quickstart for the full create request body and signing.

What’s next