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

# Sesiones

Una **CheckoutSession** es con lo que interactúa el comprador: un
objeto con límite de tiempo, de un solo uso, que posee la URL del
checkout alojado. Es la más liviana de las tres entidades centrales.
La mayor parte de tu lógica trabaja con [Órdenes](https://docs.infraio.xyz/es/concepts/orders)
y los **Payment Intents** (ver abajo).

## El modelo de datos de tres entidades

```
CheckoutSession  ←  1:1  →  Order  ←  1:N  →  PaymentIntent
   (comprador)              (catálogo)        (cada intento de pago)
```

| Entidad | Propósito | Vida útil |
| --- | --- | --- |
| **CheckoutSession** | De cara al comprador: tiene `session_key`, `checkout_url`, TTL | Minutos (por defecto 30) |
| **Order** | Estado de tu catálogo: line items, totales, reembolsos | Registro permanente |
| **PaymentIntent** | Un intento de pago en una cadena/activo | Horas; liquida o expira |

Creas una sesión y una orden juntas (vía
`POST /b2b/v1/checkout-sessions/quick`). Cada vez que el comprador
elige un activo en la página de checkout, se abre un PaymentIntent
fresco contra la cadena correcta. Si cambian de activo a mitad de
checkout, el intent anterior va a `EXPIRED` y empieza uno nuevo.

## Identificador

Una sesión se identifica por su `session_key`:

```
cst_G-SO92J7HNWkwMHEHjD4oO1Z
```

URL-safe, unos 24 caracteres tras el prefijo. La página de checkout
alojada es `https://checkout.infraio.xyz/<session_key>`: trata la
session key como una credencial bearer para ese único checkout.

## Ciclo de vida — CheckoutSession

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

| Estado | Significa |
| --- | --- |
| `ACTIVE` | Sesión creada y abierta. La URL del checkout es usable. |
| `COMPLETED` | Un PaymentIntent de esta sesión liquidó. La Order ahora es `PAID` (o `PARTIAL_PAID` si pagaron de menos). |
| `EXPIRED` | `expires_at` pasó sin liquidación. Cualquier Order abierta se cancela. |
| `CANCELED` | Cancelación explícita: o el comprador pulsó "cancelar" o tú llamaste al endpoint de cancelación. |

Los tres estados terminales son mutuamente excluyentes y finales.
Se puede crear una nueva CheckoutSession contra la misma Order si
quieres reintentar (p. ej., tras un pago insuficiente).

## TTL

- **Por defecto:** 30 minutos (configurable vía el campo
  `expires_in` al crear, en segundos).
- **Límites:** No se aplica mínimo ni máximo. Elige un valor que
  coincida con la ventana de decisión esperada de tu comprador:
  menos de 60 segundos arriesga que compradores legítimos se queden
  sin tiempo, y una sesión que permanece abierta más de 7 días casi
  con certeza está abandonada.
- **Por defecto por comerciante:** Puedes fijar un valor por defecto
  en el dashboard, pero solo `POST /b2b/v1/checkout-sessions` (dos
  pasos) lo honra. `POST /b2b/v1/checkout-sessions/quick` usa
  **30 minutos** cuando se omite `expires_in`, independientemente del
  ajuste del dashboard. Para usar un valor por defecto distinto con
  `/quick`, envía `expires_in` en cada llamada.
- **Aplicación:** Una sesión cuyo `expires_at` ha pasado se trata como
  `EXPIRED` aunque su estado aún no se haya actualizado, así que no
  dependas del valor del estado en el momento exacto de la expiración.

## Pago insuficiente

Si un comprador envía menos que el importe de la sesión, el
PaymentIntent aún liquida por el importe parcial y la Order
transiciona a `PARTIAL_PAID`. La CheckoutSession se mueve a
`COMPLETED` (un PaymentIntent liquidado), así que ya no es
reutilizable.

Para aceptar el déficit como pago completo, la orden puede
resolverse a `PAID`. Ver [Conceptos → Órdenes](https://docs.infraio.xyz/es/concepts/orders)
(resolver no está disponible a través de la API; para seguir en
self-serve, cobra el remanente en su lugar). Para cobrar el remanente, crea una
**nueva** CheckoutSession contra la misma Order con el importe
residual.

## Sobrepago

Si el comprador envía más que el importe de la sesión (raro pero
ocurre con transferencias manuales), InfraIO Pay detecta el sobrepago en
24 horas y te notifica. No se reembolsa automáticamente. Emite el
reembolso vía la API de reembolso o el dashboard.

## Pagos con activo equivocado

La dirección de depósito por pedido (CREATE2) se genera para una sesión, una red y un
activo. Si un comprador envía un activo distinto a la dirección, el
pago no se reconoce y el PaymentIntent permanece abierto hasta que la
sesión expira. Soporte puede ayudar a recuperar los fondos, pero no es
automático. Indica a los compradores que envíen el activo exacto
mostrado en la página de checkout.

En TRON, Solana y TON no hay dirección de depósito, así que esto aplica solo a las redes EVM: el comprador paga directamente a tu wallet. Ver [Redes de pago directo a la wallet](https://docs.infraio.xyz/es/concepts/chains#redes-de-pago-directo-a-la-wallet).

## Idempotencia al crear

`POST /b2b/v1/checkout-sessions/quick` acepta un campo
`idempotency_key` en el body de la petición (nota: **campo del body,
no cabecera HTTP**). Genera un UUID si no tienes una clave natural.

La clave deduplica la **Order**, no la CheckoutSession. En un
reintento con la misma clave, `/quick` devuelve la *orden original*
(`order_id` es estable) pero emite una **CheckoutSession fresca**:
una nueva `session_key` y `checkout_url` cada vez. Eso es
intencional: una Order puede respaldar varios intentos de checkout
(ver [Órdenes](https://docs.infraio.xyz/es/concepts/orders)), así que un `/quick`
reintentado entrega al comprador una sesión limpia sin duplicar la
orden.

Dos comportamientos a tener en cuenta:

- **El body no se hashea ni compara.** Reutilizar una clave con un
  body *distinto* **no** devuelve `409`: el servidor devuelve
  silenciosamente la orden ya almacenada bajo esa clave e ignora
  el nuevo body. Así que trata un `idempotency_key` como un token
  de un solo uso para una sola orden lógica; nunca lo recicles
  entre distintos carritos.
- **Solo la Order se deduplica, no la sesión.** Si necesitas la
  *misma* URL de checkout de vuelta, persiste `session_key` /
  `checkout_url` de la primera respuesta: llamar a `/quick` de
  nuevo no devolverá la antigua. Para enumerar cada sesión emitida
  contra una orden, usa `GET /b2b/v1/checkout-sessions/by-order/:order_id`.

## Endpoints de la API

| Método | Ruta | Notas |
| --- | --- | --- |
| `POST` | `/b2b/v1/checkout-sessions/quick` | Crea order + session en una llamada |
| `POST` | `/b2b/v1/checkout-sessions` | Crea sesión contra una orden existente |
| `GET` | `/b2b/v1/checkout-sessions/by-order/:order_id` | Lista todas las sesiones de una orden (para historial de reintentos) |

Consulta el [Inicio rápido](https://docs.infraio.xyz/es/get-started/quickstart) para el body
completo de la petición de creación y la firma.

## Qué sigue

- [Conceptos → Órdenes](https://docs.infraio.xyz/es/concepts/orders): la entidad Order (la
  que tratas como fuente de verdad para fulfillment).
- [Conceptos → Cadenas y activos](https://docs.infraio.xyz/es/concepts/chains): redes
  soportadas y supuestos de finalidad por cadena.
- [Webhooks → Resumen](https://docs.infraio.xyz/es/webhooks/overview): qué eventos se
  disparan en cada transición de estado de sesión.
