<!-- Source: https://docs.infraio.xyz/en/concepts/sessions -->
<!-- Last updated: 2026-10-04 -->

# Sessions

A **CheckoutSession** is what the buyer interacts with: a time-limited,
single-use object that owns the hosted checkout URL. It's the lightest
of the three core entities. Most of your logic works with [Orders](https://docs.infraio.xyz/en/concepts/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-SO92J7HNWkwMHEHjD4oO1Z
```

URL-safe, about 24 characters after the prefix. 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

```mermaid
stateDiagram-v2
    [*] --> ACTIVE
    ACTIVE --> COMPLETED: a PaymentIntent settles
    ACTIVE --> EXPIRED:   expires_at elapsed
    ACTIVE --> CANCELED:  buyer or merchant cancels
    COMPLETED --> [*]
    EXPIRED --> [*]
    CANCELED --> [*]
```

| 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. Any open Orders are canceled. |
| `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_in` field
  on create, in seconds).
- **Bounds:** There is no enforced minimum or maximum. Choose a value
  that matches your buyer's expected decision window: under 60 seconds
  risks timing out legitimate buyers, and a session that stays open for
  more than 7 days is almost certainly abandoned.
- **Per-merchant default:** You can set a default in the dashboard, but
  only `POST /b2b/v1/checkout-sessions` (two-step) honors it.
  `POST /b2b/v1/checkout-sessions/quick` uses **30 minutes** when
  `expires_in` is omitted, regardless of the dashboard setting. To use a
  different default with `/quick`, send `expires_in` on every call.
- **Enforcement:** A session whose `expires_at` has passed is treated
  as `EXPIRED` even if its state hasn't updated yet, so don't rely on
  the state value 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](https://docs.infraio.xyz/en/concepts/orders) (resolving isn't
available through the API; to stay self-serve, 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), InfraIO Pay detects the
overpayment within 24 hours and notifies you. It isn't refunded
automatically. Issue the refund through the refund API or the
dashboard.

## Wrong-asset payments

The deposit address is generated for one session, network, and asset.
If a buyer sends a different asset to it, the payment isn't matched and the
PaymentIntent stays open until the session expires. Support can help
recover the funds, but it isn't automatic. Tell buyers to send the exact
asset shown on the checkout page.

On TRON, Solana and TON there is no deposit address, so this applies to EVM networks only: the buyer pays your wallet directly. See [Direct-to-wallet networks](https://docs.infraio.xyz/en/concepts/chains#direct-to-wallet-networks).

## Idempotency on create

`POST /b2b/v1/checkout-sessions/quick` accepts an `idempotency_key`
field in the request body (note: **body field, not HTTP header**).
Generate a UUID if you don't have a natural key.

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](https://docs.infraio.xyz/en/concepts/orders)), so a retried `/quick` hands the buyer
a clean session without duplicating the order.

Two behaviors to be aware of:

- **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

| 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) |

See the [Quickstart](https://docs.infraio.xyz/en/get-started/quickstart) for the full create
request body and signing.

## What's next

- [Concepts → Orders](https://docs.infraio.xyz/en/concepts/orders) — the Order entity (the
  one you treat as ground truth for fulfillment).
- [Concepts → Chains & assets](https://docs.infraio.xyz/en/concepts/chains) — supported
  networks and finality assumptions per chain.
- [Webhooks → Overview](https://docs.infraio.xyz/en/webhooks/overview) — which events fire
  at each session state transition.
