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)| Entity | Purpose | Lifetime |
|---|---|---|
| CheckoutSession | Buyer-facing — has session_key, checkout_url, TTL | Minutes (default 30) |
| Order | Your catalog state — line items, totals, refunds | Permanent record |
| PaymentIntent | One pay attempt on one chain/asset | Hours; 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-SO92J7HNWkwMHEHjD4oO1ZURL-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
| State | Means |
|---|---|
ACTIVE | Session created and open. The checkout URL is usable. |
COMPLETED | A PaymentIntent on this session settled. The Order is now PAID (or PARTIAL_PAID if underpaid). |
EXPIRED | expires_at passed without settlement. The cleanup worker flipped the state and cancelled any open Orders. |
CANCELED | Explicit 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_infield 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. ThePOST /b2b/v1/checkout-sessions/quickpath always falls back to 30 minutes ifexpires_inis omitted, regardless of the per-merchant setting. If you need a different default on the quick path, sendexpires_inexplicitly on every call. - Enforcement: Lazy on read + a periodic cleanup worker. A
session whose
expires_athas passed is treated asEXPIREDeven 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 anidempotency_keyas 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_urlfrom the first response — calling/quickagain won’t return the old one. To enumerate every session minted against an order, useGET /b2b/v1/checkout-sessions/by-order/:order_id.
API endpoints
| Method | Path | Notes |
|---|---|---|
POST | /b2b/v1/checkout-sessions/quick | Create order + session in one call |
POST | /b2b/v1/checkout-sessions | Create session against an existing order |
GET | /b2b/v1/checkout-sessions/by-order/:order_id | List all sessions for an order (for retry history) |
GET | /checkout/:session_key | Public — what the buyer’s browser hits |
See the Quickstart for the full create request body and signing.
What’s next
- Concepts → Orders — the Order entity (the one you treat as ground truth for fulfillment).
- Concepts → Chains & assets — supported networks and finality assumptions per chain.
- Webhooks → Overview — which events fire at each session state transition.