Guía: Reserva de stock para e-commerce (ATP)¶
Cómo funciona el sistema de reservas de OpenTPV y cómo usar sus endpoints desde una tienda web o integración. Audiencia: integradores y equipo OpenTPV.
La idea en 30 segundos¶
Cuando un cliente web agrega productos al carro o entra a pagar, la tienda reserva esas unidades en OpenTPV. La reserva es una promesa con vencimiento (TTL): descuenta disponibilidad, no stock físico. El stock físico recién baja cuando se emite el documento tributario (boleta/factura), que además consume la reserva. Si el cliente abandona, la reserva vence sola y las unidades vuelven a estar disponibles.
Dos números que no hay que confundir:
| Concepto | Fórmula | Quién lo usa |
|---|---|---|
| ATP físico | stock bodega − reservas activas | ERP / TPV mostrador |
| ATP publicable | ATP físico − colchón | lo que la tienda debe publicar |
El colchón (buffer) es un margen configurable que no se publica, para absorber el
desfase con las ventas de mostrador. Solo aplica cuando la tienda comparte bodega con
el mostrador (bodegaEcommerce = 0); con bodega dedicada no hay desfase que absorber.
Flujo completo¶
1. Tienda arranca → GET /reservas/configuracion (¿cuándo reservar? ¿TTLs?)
2. Catálogo / carro → GET /reservas/disponibilidad (publicar atpPublicable)
3. Carro o checkout → POST /reservas (token = id del carro)
… cliente navega → PATCH /reservas/{token}/renovar (heartbeat)
… cliente abandona → DELETE /reservas/{token} (o vence sola por TTL)
4. Pedido confirmado → POST /documento/notaventa (con tokenReserva)
la reserva pasa a colgar del pedido, con TTL largo
5. Emisión del DTE → POST /documento/notaventa/{folio}/documentar
AQUÍ baja el stock físico y se consume la reserva
… o cancelación → POST /documento/notaventa/{folio}/anular (libera la reserva)
Reglas de oro:
- Una reserva nunca mueve stock físico. Solo la emisión del documento lo hace.
- El token es el identificador del carro (máx. 64 caracteres, lo genera la tienda).
Reenviar
POST /reservascon el mismo token reemplaza las cantidades: es la forma correcta de sincronizar el carro (idempotente). - La reserva es todo-o-nada: si falta stock de una línea, no se reserva ninguna y la respuesta 409 detalla los faltantes.
- Tras confirmar el pedido (paso 4), dejar de renovar el token: la renovación aplica el TTL corto de carro y acortaría la protección del pedido confirmado.
Autenticación¶
Todos los endpoints aceptan cualquiera de las dos formas:
Authorization: Bearer <JWT>(token de empresa)- Headers legacy
rut/X-Company-Token
Sin empresa resoluble → 401.
Endpoints de reservas¶
GET /api/Reservas/configuracion¶
Llamar al iniciar la integración. Devuelve la política del tenant:
{
"momentoReserva": "CHECKOUT", // CHECKOUT | CARRO | NINGUNO
"ttlWebMinutos": 15,
"ttlNotaVentaMinutos": 1440,
"ttlMaximoMinutos": 4320,
"bodegaEcommerce": 0, // 0 = comparte con mostrador
"buffer": 0,
"bufferEsPorcentaje": false
}
momentoReserva indica cuándo debe reservar la tienda (al agregar al carro, al
entrar al checkout, o nunca). Es una instrucción para el integrador: la API no lo
impone, así que respetarlo es responsabilidad de la tienda.
GET /api/Reservas/disponibilidad?codigos=A001,B002&bodega=1¶
Disponibilidad por producto. codigos es CSV obligatorio; bodega (1–20) se ignora
si el tenant tiene bodega dedicada. Códigos inexistentes simplemente no aparecen.
[
{ "codigo": "A001", "bodega": 1, "stockFisico": 10, "reservado": 2,
"buffer": 1, "atpFisico": 8, "atpPublicable": 7 }
]
Publica siempre atpPublicable. Errores: 400 (sin códigos / bodega fuera de rango).
POST /api/Reservas¶
Crea o reemplaza la reserva del carro. Llamar según momentoReserva.
{
"token": "carro-web-8f3a2b", // requerido, ≤64 chars, id del carro
"bodega": 1, // opcional, default 1; ignorado con bodega dedicada
"ttlMinutos": 15, // opcional; acotado por ttlMaximoMinutos
"origen": "ECOMMERCE", // opcional
"usuario": "cliente@correo.cl", // opcional, trazabilidad
"lineas": [
{ "codigo": "A001", "cantidad": 2 },
{ "codigo": "B002", "cantidad": 1 }
]
}
- 200:
{ exito, token, expiraEn, segundosRestantes, lineas[] } - 409 stock insuficiente (no se reservó nada):
{ "exito": false, "faltantes": [
{ "codigo": "A001", "solicitado": 2, "disponible": 1, "stockFisico": 3 } ] }
Ante 409, mostrar los faltantes al cliente y reintentar con cantidades menores.
PATCH /api/Reservas/{token}/renovar?ttlMinutos=15¶
Heartbeat mientras el cliente sigue activo. 404 si el carro ya no tiene reservas
activas (venció o se consumió) → volver a reservar con POST /reservas.
No renovar después de confirmar el pedido.
GET /api/Reservas/{token}¶
Estado actual del carro (lista de líneas con estado, expiraEn, idNotaVenta…).
Lista vacía si no existe. Útil para reconstruir el carro al volver el cliente.
DELETE /api/Reservas/{token} · DELETE /api/Reservas/{token}/lineas/{codigo}¶
Liberan todo el carro o una línea. Idempotentes (devuelven liberadas: 0 si no había
nada). Llamar al vaciar el carro; si no se llama, el TTL limpia solo.
GET /api/Reservas/incumplidas?soloNoAcusadas=true · POST /api/Reservas/incumplidas/acusar¶
Cuando el mostrador vende unidades que estaban reservadas (quiebre), la reserva queda
INCUMPLIDA. La tienda hace polling de incumplidas, avisa al cliente antes de
cobrar, y confirma con acusar ({ "ids": [1,2,3] }) para no reprocesarlas.
Del carro al documento¶
POST /api/Documento/notaventa (con tokenReserva)¶
Al confirmar el pedido, crear la nota de venta pasando el token:
{
"tokenReserva": "carro-web-8f3a2b",
"origen": "ECOMMERCE",
"cliente": { "...": "..." },
"items": [ { "codigo": "A001", "cantidad": 2, "...": "..." } ]
}
La reserva pasa a colgar de la nota de venta y toma el TTL largo
(ttlNotaVentaMinutos, default 24 h). No mueve stock. Si el carro ya venció, la
respuesta incluye advertencia: validar disponibilidad de nuevo antes de cobrar.
POST /api/Documento/notaventa/{folio}/documentar¶
Emite el DTE (tipoDte: 39 boleta, 41 exenta, 33 factura, 34 exenta, 52 guía). Es el
único punto donde baja el stock físico (solo si el tenant activó
apiDescuentaInventario) y donde la reserva se marca CONSUMIDA. Errores 409: ya
documentada, en proceso o anulada.
Campos opcionales del request:
codigoClienteReceptor(33/34/52): receptor distinto al cliente de la NV (caso multi-razón-social; ver abajo).traslado(solo 52): datos del traslado según la Res. Ex. SII N°154/2025 (vigente desde el 01-11-2026 por Res. N°52/2026):
{
"tipoDte": 52,
"codigoTerminal": "CAJA01",
"codigoClienteReceptor": 8021,
"traslado": {
"indTraslado": 1, // 1 venta · 2 ventas por efectuar · 3 consignación
// 4 entrega gratuita · 5 traslado interno · 6 otro no venta
// 7 venta exportación · 8 traslado exportación
"direccionDestino": "Av. Siempre Viva 742",
"comunaDestino": "Ñuñoa",
"fechaSalida": "2026-08-10", // yyyy-MM-dd
"horaSalida": "16:30",
"fechaLlegada": "2026-08-11", // obligatoria si el traslado dura más de un día
"rutTransportista": "77111222-3",
"rutChofer": "12345678-9",
"nombreChofer": "Juan Pérez",
"patente": "ABCD12",
"patenteCarro": ""
}
}
Qué hace la API si se omite el bloque traslado (defaults seguros que cumplen los
obligatorios de la resolución): indTraslado = 1 (venta), destino = domicilio del
cliente de la nota de venta, leyenda con la fecha/hora de inicio del traslado en las
observaciones (mecanismo transitorio del resolutivo 3°d), y si no se informa patente,
la constancia expresa "Patente del vehículo no conocida al momento de la emisión"
(resolutivo 1°c). Chofer/transportista/patentes deben informarse cuando se conocen.
⚠️ fechaSalida/horaSalida/fechaLlegada y patenteCarro son tags del formato
actualizado del SII: se emiten en el XML solo si se informan (opt-in), para no
afectar la validación de schema mientras el SII termina de habilitar el formato.
POST /api/Documento/notaventa/{folio}/anular¶
Cancelación del pedido: libera las reservas asociadas. No "repone" stock porque nunca lo descontó.
Casos según la estructura de la empresa¶
Caso 1: Una sola razón social (el flujo estándar)¶
La tienda y OpenTPV son la misma empresa. El flujo es el descrito arriba: reserva → nota de venta con los datos del cliente final → documentar como boleta (39) o factura (33) al mismo cliente. Nada más que decidir.
Caso 2: Varias razones sociales (e-commerce con ERP propio)¶
Estructura típica: la razón social del e-commerce (con su propio ERP) vende al cliente final y emite su boleta; el stock vive en una o más razones sociales que usan OpenTPV. La operación entre empresas es: la RS OpenTPV le vende a la RS e-commerce, y la mercadería viaja directo al cliente final (dropship).
Flujo por cada OpenTPV que aporta stock:
1. Reserva por API contra la bodega OpenTPV (igual que el caso 1)
2. Nota de venta a nombre del CLIENTE FINAL (el pedido, con su trazabilidad)
3. Documentar con tipoDte 52 (guía de despacho) y
codigoClienteReceptor = código del cliente "RS e-commerce" en OpenTPV:
→ la guía sale a nombre de la RS e-commerce (receptor tributario)
→ el domicilio del cliente final queda como DESTINO del traslado en la guía
→ baja el stock y consume la reserva
4. Fin de mes, en el ERP: facturación masiva de guías → una factura a la
RS e-commerce que referencia todas las guías del período (sin mover stock)
Reglas de este caso:
- Una guía por pedido, no consolidada por día/semana. La guía ampara un traslado físico concreto; cada paquete que viaja al cliente final necesita la suya. La consolidación va en la factura mensual, que referencia N guías (es el patrón estándar de los ERP: delivery por embarque, factura colectiva).
- La RS e-commerce debe existir como cliente en OpenTPV (
codigoClienteReceptor); la guía queda asociada a ella, que es lo que permite la facturación masiva. codigoClienteReceptorsolo aplica a tipos 33, 34 y 52; las boletas no admiten receptor distinto al de la nota de venta.- Ajustar el TTL de pedido confirmado (
ttlReservaNvMin) al ciclo real de despacho (p.ej. 2880 min = 2 días); si la reserva vence antes de emitir la guía, el mostrador podría vender ese stock. - El motivo de traslado de la operación entre razones sociales conviene validarlo con el contador del cliente.
Configuración (ERP → Opciones → E-Commerce)¶
| Opción | Default | Qué hace |
|---|---|---|
| API descuenta stock al emitir | apagado | Compuerta global. Activarla es un cambio de comportamiento para integradores existentes (el ERP pide confirmación). |
| Reservar en | CHECKOUT | CARRO / CHECKOUT / NINGUNO |
| Duración reserva (min) | 15 | TTL del carro |
| Reserva pedido confirmado (min) | 1440 | TTL tras asociar a nota de venta |
| Bodega web | 0 | 0 = compartida; 1–20 = dedicada |
| Stock a no publicar (colchón) | 0 | Unidades o % ; solo con bodega compartida |
ttlReservaMaxMin |
4320 | Techo del TTL que puede pedir el integrador. Solo editable por SQL. |
Requisitos: migración sql/89.reservas_stock.sql aplicada (el sistema igual crea las
columnas en caliente como red de seguridad). El barrido (Cronjob:BarridoReservas en
appsettings de la API) hace dos cosas: vence reservas caducadas y reconcilia
consumos (si una nota de venta ya tiene documento emitido pero su reserva quedó
activa por un fallo transitorio, la consume). La correctitud no depende del cron
(cada operación de reservas ejecuta el mismo barrido de forma perezosa), pero con
e-commerce activo conviene habilitarlo para que la reconciliación corra aunque la
tienda esté quieta.
Errores comunes del integrador¶
- Publicar
atpFisicoostockFisicoen vez deatpPublicable→ sobreventa del colchón. - No reenviar el
POST /reservascompleto al cambiar el carro (el token reemplaza, no suma). - Seguir renovando el token después de confirmar el pedido.
- Cobrar sin revisar
advertenciade la nota de venta ni el polling deincumplidas. - Asumir que anular repone stock (nunca se descontó) o que reservar lo descuenta.