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

# Сессии

**CheckoutSession** — это то, с чем взаимодействует покупатель:
одноразовый объект с ограниченным временем жизни, владеющий URL
размещённого checkout. Самая лёгкая из трёх ключевых сущностей. Большая
часть вашей логики работает с [Заказами](https://docs.infraio.xyz/ru/concepts/orders) и
**Payment Intent'ами** (см. ниже).

## Модель данных из трёх сущностей

```
CheckoutSession  ←  1:1  →  Order  ←  1:N  →  PaymentIntent
   (покупатель)             (каталог)         (каждая попытка оплаты)
```

| Сущность | Назначение | Время жизни |
| --- | --- | --- |
| **CheckoutSession** | Обращённая к покупателю — есть `session_key`, `checkout_url`, TTL | Минуты (по умолчанию 30) |
| **Order** | Состояние вашего каталога — позиции, суммы, возвраты | Постоянная запись |
| **PaymentIntent** | Одна попытка оплаты на одной сети/активе | Часы; рассчитывается или истекает |

Вы создаёте сессию и заказ вместе (через
`POST /b2b/v1/checkout-sessions/quick`). Каждый раз, когда покупатель
выбирает актив на странице checkout, открывается свежий PaymentIntent
для соответствующей сети. Если он переключает активы посреди checkout,
предыдущий intent переходит в `EXPIRED`, и стартует новый.

## Идентификатор

Сессия идентифицируется своим `session_key`:

```
cst_G-SO92J7HNWkwMHEHjD4oO1Z
```

URL-безопасный, около 24 символов после префикса. Размещённая страница
checkout — это `https://checkout.infraio.xyz/<session_key>` — рассматривайте
session_key как bearer-credential для именно этого checkout.

## Жизненный цикл — CheckoutSession

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

| Состояние | Значит |
| --- | --- |
| `ACTIVE` | Сессия создана и открыта. URL checkout работает. |
| `COMPLETED` | PaymentIntent на этой сессии рассчитан. Order теперь `PAID` (или `PARTIAL_PAID` при недоплате). |
| `EXPIRED` | `expires_at` прошло без расчёта. Все открытые Order'ы отменяются. |
| `CANCELED` | Явная отмена — либо покупатель нажал «cancel», либо вы вызвали endpoint отмены. |

Три терминальных состояния взаимоисключающие и финальные. Новая
CheckoutSession против того же Order может быть создана, если вы
хотите повторить (например, после недоплаты).

## TTL

- **По умолчанию:** 30 минут (настраивается через поле `expires_in` при
  создании, в секундах).
- **Границы:** Минимум и максимум не ограничены. Выбирайте значение,
  соответствующее ожидаемому окну принятия решения покупателем: менее 60
  секунд рискует выйти за тайминги для легитимных покупателей, а сессия,
  остающаяся открытой более 7 дней, почти наверняка заброшена.
- **Дефолт per-merchant:** Можно задать в панели, но соблюдает его только
  `POST /b2b/v1/checkout-sessions` (двухшаговый).
  `POST /b2b/v1/checkout-sessions/quick` использует **30 минут**, если
  `expires_in` опущен, независимо от настройки в панели. Чтобы на `/quick`
  использовать другой дефолт, отправляйте `expires_in` при каждом вызове.
- **Проверка:** Сессия, у которой `expires_at` прошло, трактуется как
  `EXPIRED`, даже если её состояние ещё не обновилось, поэтому не
  полагайтесь на значение состояния в точный момент истечения.

## Недоплата

Если покупатель отправляет меньше суммы сессии, PaymentIntent всё равно
рассчитывается на частичную сумму, а Order переходит в `PARTIAL_PAID`.
CheckoutSession переходит в `COMPLETED` (один PaymentIntent рассчитан),
поэтому она больше не пригодна к повторному использованию.

Чтобы принять недоплату как полную оплату, заказ можно разрешить в
`PAID` См. [Концепции → Заказы](https://docs.infraio.xyz/ru/concepts/orders) (разрешение недоступно
через API; чтобы остаться в self-serve, вместо этого собирайте остаток).
Чтобы собрать остаток, создайте **новую** CheckoutSession
против того же Order на остаточную сумму.

## Переплата

Если покупатель отправляет больше суммы сессии (редко, но случается
при ручных переводах), InfraIO Pay обнаруживает переплату в течение 24
часов и уведомляет вас. Автоматически она не возвращается. Выпустите
возврат через API возвратов или панель.

## Платежи неверным активом

Депозитный адрес генерируется для одной сессии, сети и актива. Если
покупатель отправит на него другой актив, платёж не будет сопоставлен, и
PaymentIntent останется открытым до истечения сессии. Поддержка может
помочь восстановить средства, но это не автоматика. Говорите покупателям
отправлять ровно тот актив, который показан на странице checkout.

В TRON, Solana и TON нет депозитного адреса, поэтому это относится только к EVM-сетям: покупатель платит напрямую на ваш кошелёк. См. [Сети с оплатой напрямую на кошелёк](https://docs.infraio.xyz/ru/concepts/chains#сети-с-оплатой-напрямую-на-кошелёк).

## Идемпотентность при создании

`POST /b2b/v1/checkout-sessions/quick` принимает поле
`idempotency_key` в теле запроса (заметьте: **поле тела, а не
HTTP-заголовок**). Сгенерируйте UUID, если у вас нет естественного ключа.

Ключ дедуплицирует **Order**, а не CheckoutSession. При повторе с тем
же ключом `/quick` возвращает *оригинальный заказ* (`order_id`
стабилен), но создаёт **свежую CheckoutSession** — новый `session_key`
и `checkout_url` каждый раз. Это намеренно: один Order может стоять за
несколькими попытками checkout (см. [Заказы](https://docs.infraio.xyz/ru/concepts/orders)),
поэтому повторный `/quick` отдаёт покупателю чистую сессию без
дублирования заказа.

Два поведения, о которых нужно знать:

- **Тело не хэшируется и не сравнивается.** Повторное использование
  ключа с *другим* телом **не** возвращает `409` — сервер молча
  возвращает заказ, уже сохранённый под этим ключом, и игнорирует
  новое тело. Поэтому рассматривайте `idempotency_key` как одноразовый
  токен для одного логического заказа; никогда не переиспользуйте его
  между разными корзинами.
- **Дедуплицируется только Order, не сессия.** Если вам нужен *тот же*
  URL checkout, сохраните `session_key` / `checkout_url` из первого
  ответа — повторный вызов `/quick` не вернёт старый. Чтобы перечислить
  каждую сессию, выпущенную против заказа, используйте
  `GET /b2b/v1/checkout-sessions/by-order/:order_id`.

## Endpoint'ы API

| Метод | Путь | Заметки |
| --- | --- | --- |
| `POST` | `/b2b/v1/checkout-sessions/quick` | Создание заказа + сессии одним вызовом |
| `POST` | `/b2b/v1/checkout-sessions` | Создание сессии против существующего заказа |
| `GET` | `/b2b/v1/checkout-sessions/by-order/:order_id` | Список всех сессий для заказа (история повторов) |

См. [Быстрый старт](https://docs.infraio.xyz/ru/get-started/quickstart) для полного тела
запроса создания и подписания.

## Что дальше

- [Концепции → Заказы](https://docs.infraio.xyz/ru/concepts/orders) — сущность Order (та,
  которую следует считать источником истины для обработки).
- [Концепции → Сети и активы](https://docs.infraio.xyz/ru/concepts/chains) — поддерживаемые
  сети и предположения окончательности per сеть.
- [Webhooks → Обзор](https://docs.infraio.xyz/ru/webhooks/overview) — какие события
  отправляются на каждом переходе состояния сессии.
