AdemWeb S.L. — Especificación técnica — Abril 2026

API de Integración TPV Externo → Odoo

Descripción de los endpoints REST que el TPV externo debe implementar para enviar datos de ventas a Odoo 18.

Introducción

Este documento describe la integración entre el TPV externo y el ERP Odoo 18 gestionado por AdemWeb. La integración es unidireccional: el TPV externo envía los datos de ventas a Odoo. Odoo no envía catálogo, precios ni empleados al TPV externo (al menos en esta primera versión).

ℹ️

Odoo es el sistema contable. El TPV externo es el master de ventas: el pedido se cobra en el TPV y luego se replica a Odoo para generar los asientos contables, el cierre de caja y los informes de gestión.

Resumen de endpoints

MétodoURLDescripción
POST /pos/ext/sessions/open Abrir sesión de caja
POST /pos/ext/sessions/close Cerrar sesión de caja
POST /pos/ext/orders Enviar pedido cobrado

Autenticación

Cada establecimiento tiene un PdV (pos.config) en Odoo con su propia API Key. El software del TPV debe incluirla en la cabecera X-API-Key de todas las peticiones.

Adicionalmente, Odoo usa autenticación de sesión estándar. El software del TPV externo debe autenticarse primero con las credenciales del usuario de servicio, obtener la cookie de sesión y usarla en las llamadas.

Paso 1 — Obtener cookie de sesión

POST https://odoo.ejemplo.com/web/session/authenticate
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "method": "call",
  "params": {
    "db": "nombre_base_datos",
    "login": "tpv_service_user",
    "password": "contraseña_usuario_servicio"
  }
}

La respuesta incluye una cookie session_id. Incluirla en todas las llamadas siguientes.

Paso 2 — Cabeceras de cada llamada

POST /pos/ext/orders HTTP/1.1
Host: odoo.ejemplo.com
Content-Type: application/json
Cookie: session_id=<cookie-de-sesion>
X-API-Key: <api-key-del-establecimiento>
⚠️

Si la X-API-Key es incorrecta o no coincide con el config_id enviado, Odoo responde 403. Nunca exponer la API Key en logs ni en el código fuente.

Formato de las peticiones

Códigos de respuesta

CódigoSignificadoAcción recomendada
200 Éxito
400 Datos inválidos (campo obligatorio ausente, producto no encontrado, etc.) Revisar el body enviado. No reintentar sin corrección.
403 API Key incorrecta o usuario sin permisos Verificar credenciales con AdemWeb.
404 Sesión no encontrada (en close) Comprobar el ext_session_uid enviado.
500 Error interno de Odoo Reintentar con back-off exponencial. Notificar a AdemWeb si persiste.

Todos los errores incluyen un campo "error" en el body con la descripción: {"error": "Descripción del problema"}.

POST /pos/ext/sessions/open

Abre una sesión de caja en Odoo al inicio del turno. Si la sesión con ese ext_session_uid ya existe y está abierta, se devuelve la misma (idempotente).

POST /pos/ext/sessions/open Apertura de turno

Parámetros del body

CampoTipoReq.Descripción
config_code string Recomendado Código estable del PdV configurado en Odoo (p. ej. "SALA_1"). No cambia con restores de BD. Preferible a config_id.
config_id integer Opcional* ID numérico del PdV en Odoo. Usar solo si no se tiene config_code. Puede cambiar tras un restore.
ext_session_uid string (UUID) ✔ Sí Identificador único de la sesión generado por el TPV externo. Se usa para idempotencia.
opening_cash number Opcional Importe en caja al abrir (fondo de caja). Por defecto 0.
date_open string (ISO 8601) Opcional Fecha/hora de apertura en el dispositivo del TPV.

Ejemplo de petición

{
  "config_code": "SALA_1",
  "ext_session_uid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "opening_cash": 150.00,
  "date_open": "2026-05-14T09:00:00"
}

Respuesta exitosa 200

{
  "session_id": 42,
  "name": "TPV Externo/E00001",
  "state": "opened"
}

POST /pos/ext/sessions/close

Cierra la sesión de caja al final del turno. Odoo genera el asiento de cierre de forma automática. Si la sesión ya estaba cerrada, devuelve "already_closed": true sin error (idempotente).

POST /pos/ext/sessions/close Cierre de turno

Parámetros del body

CampoTipoReq.Descripción
config_code string Recomendado Código estable del PdV (preferible a config_id)
config_id integer Opcional* ID numérico del PdV en Odoo
ext_session_uid string (UUID) ✔ Sí El mismo UUID enviado en el open de esta sesión
closing_cash number Opcional Efectivo contado al cierre. Por defecto 0.
date_close string (ISO 8601) Opcional Fecha/hora de cierre en el dispositivo del TPV.

Ejemplo de petición

{
  "config_code": "SALA_1",
  "ext_session_uid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "closing_cash": 187.50,
  "date_close": "2026-05-14T23:30:00"
}

Respuesta exitosa 200

{
  "ok": true,
  "session_id": 42,
  "name": "TPV Externo/E00001"
}

Sesión ya cerrada (idempotente) 200

{
  "ok": true,
  "session_id": 42,
  "name": "TPV Externo/E00001",
  "already_closed": true
}

POST /pos/ext/orders

Envía un pedido ya cobrado a Odoo. Se crea la venta con sus líneas y pagos y queda en estado paid (listo para contabilidad). Si el ext_order_uid ya existe, se devuelve el registro existente sin duplicar.

POST /pos/ext/orders Envío de pedido cobrado

Parámetros del body

CampoTipoReq.Descripción
config_code string Recomendado Código estable del PdV (preferible a config_id)
config_id integer Opcional* ID numérico del PdV en Odoo
ext_order_uid string (UUID) ✔ Sí ID único del pedido en el TPV externo. Se usa para evitar duplicados.
ext_session_uid string (UUID) ✔ Sí UID de la sesión abierta donde se genera este pedido
date_order string (ISO 8601) Opcional Fecha/hora del pedido en el TPV
table_name string Opcional Nombre o número de mesa (p. ej. "Mesa 5", "T3", "12")
waiter_name string Opcional Nombre del camarero que tomó el pedido
lines array ✔ Sí Líneas del pedido (ver tabla siguiente). Mínimo 1 línea válida.
payments array ✔ Sí Formas de pago. Total debe cubrir el importe del pedido.

Estructura de una línea (lines[])

CampoTipoReq.Descripción
product_ref string ✔ Sí* Código interno del producto en Odoo (default_code). Ver sección Productos.
product_name string Opcional Nombre del producto. Se usa como fallback si product_ref no coincide.
qty number ✔ Sí Cantidad vendida. Puede ser decimal (p. ej. peso).
price_unit number ✔ Sí Precio unitario con IVA incluido (precio de venta al público).
discount_pct number Opcional Porcentaje de descuento aplicado (0–100). Por defecto 0.

Estructura de un pago (payments[])

CampoTipoReq.Descripción
method_code string Recomendado Código corto del método de pago acordado con AdemWeb (p. ej. "CASH", "CARD", "BIZUM"). Se configura una sola vez en Odoo. Más fiable que el nombre. Ver sección Métodos de pago.
method_name string Opcional Nombre legible del método. Se usa como fallback si method_code no está configurado o no coincide.
amount number ✔ Sí Importe cobrado con este método. Los métodos con importe 0 se ignoran.

Ejemplo de petición

{
  "config_code": "SALA_1",
  "ext_order_uid": "f7e3a1b0-1234-5678-9abc-def012345678",
  "ext_session_uid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "date_order": "2026-05-14T21:32:00",
  "table_name": "Mesa 5",
  "waiter_name": "Ana García",
  "lines": [
    {
      "product_ref": "CARNE-001",
      "product_name": "Carne a la brasa",
      "qty": 2,
      "price_unit": 14.50,
      "discount_pct": 0
    },
    {
      "product_ref": "BEBIDA-COLA",
      "product_name": "Coca-Cola 33cl",
      "qty": 2,
      "price_unit": 2.50,
      "discount_pct": 0
    }
  ],
  "payments": [
    { "method_code": "CASH", "method_name": "Efectivo", "amount": 20.00 },
    { "method_code": "CARD", "method_name": "Tarjeta",  "amount": 14.00 }
  ]
}

Respuesta exitosa 200

{
  "order_id": 1523,
  "name": "POS/2026/05/0127",
  "state": "paid"
}

Pedido ya existente (idempotente) 200

{
  "order_id": 1523,
  "name": "POS/2026/05/0127",
  "state": "paid",
  "already_existed": true
}

Matching de productos

Odoo busca el producto en este orden de prioridad:

  1. product_ref → busca por default_code (código interno) en productos disponibles en POS. Método recomendado.
  2. product_ref → busca por barcode.
  3. product_name → busca por nombre (case-insensitive).
  4. Auto-creación → si ningún criterio coincide, Odoo crea automáticamente un producto stub con el nombre y precio de la línea del TPV externo.

Ningún pedido se pierde. Si un producto del TPV externo no existe en Odoo, se crea automáticamente en la categoría "TPV Externo - Pendiente" con el nombre, precio e impuesto por defecto de la empresa. El nombre original del TPV externo siempre se conserva en la línea del pedido.

⚠️

Los productos auto-creados aparecen en Odoo bajo la categoría POS "TPV Externo - Pendiente". Tras la puesta en marcha hay que revisar esa categoría y vincular cada producto al artículo correcto del catálogo (o asignarle la cuenta contable y el impuesto adecuados). Esta revisión se recomienda hacer en los primeros días de operación.

💡

Acción recomendada antes de la puesta en marcha: acordar con AdemWeb que cada producto del TPV externo lleve un product_ref que coincida con el default_code en Odoo. Esto evita la auto-creación y garantiza que los impuestos y cuentas contables son correctos desde el primer día. AdemWeb puede exportar el catálogo completo con sus códigos internos.

Matching de métodos de pago

Cada pago puede enviarse con method_code y/o method_name. Odoo aplica la siguiente prioridad:

PrioridadCondiciónEjemplo
1ª — Código externo method_code coincide exactamente con el campo Código TPV Externo configurado en el método de pago de Odoo "BIZUM" → método con código BIZUM
2ª — Tipo de diario method_name es "efectivo"/"cash" y el método tiene diario de tipo Cash "Efectivo" → diario caja
3ª — Tipo de diario method_name es "tarjeta"/"card"/"datáfono" y el método tiene diario de tipo Bank "Tarjeta" → diario banco
4ª — Nombre normalizado Coincidencia parcial sin espacios/guiones, case-insensitive "Uber Eats" coincide con "UberEats"
Fallback Sin coincidencia → primer método del PdV (con aviso en el log) El pago se registra igualmente

Método recomendado: usar method_code. Antes de la puesta en marcha, AdemWeb configura en cada método de pago del PdV el código corto acordado con el proveedor del TPV (p. ej. CASH, CARD, BIZUM, AMEX). Este código es estable y no depende de cómo se llame el método en Odoo.

💡

Acción previa a la puesta en marcha: El proveedor del TPV debe proporcionar a AdemWeb la lista de códigos (method_code) que usará su software para cada forma de pago. AdemWeb los introduce en el campo Código TPV Externo de cada método antes de la primera sincronización.

Mesas

El campo table_name es opcional pero recomendado para restaurantes con distribución de sala. Odoo extrae los dígitos del nombre y busca la mesa con ese número en el suelo vinculado al PdV.

Ejemplos de valores aceptados: "Mesa 5", "T5", "5", "mesa5". Si la mesa no se encuentra, el pedido se crea igualmente sin asignación de mesa.

Flujo completo de un turno

1. [TPV externo] Inicio de turno
       POST /pos/ext/sessions/open  →  session_id: 42

2. [TPV externo] Pedidos durante el turno (uno a uno, en tiempo real o en batch)
       POST /pos/ext/orders  →  order_id: 1520  (pedido 1)
       POST /pos/ext/orders  →  order_id: 1521  (pedido 2)
       POST /pos/ext/orders  →  order_id: 1522  (pedido 3)
       ...

3. [TPV externo] Fin de turno
       POST /pos/ext/sessions/close  →  ok: true

4. [Odoo] Genera automáticamente:
       - Asientos contables de cada venta
       - Movimiento de cierre de caja
       - Informe de sesión (Z)

Idempotencia y reintentos

Todos los endpoints son idempotentes: enviar la misma petición varias veces produce el mismo resultado sin duplicar datos en Odoo.

EndpointClave de idempotenciaSi ya existe
/sessions/open ext_session_uid Devuelve la sesión existente
/sessions/close ext_session_uid Devuelve "already_closed": true
/orders ext_order_uid Devuelve el pedido existente con "already_existed": true

El software del TPV puede reintentar cualquier llamada fallida (por timeout de red, error 500, etc.) sin riesgo de duplicar datos, siempre que use el mismo UID en el reintento.

⚠️

Los errores 400 (datos inválidos) y 403 (auth) no deben reintentarse automáticamente — requieren corrección manual.