Guía para integradores
offline-pos es un punto de venta web para comercios minoristas (kiosco, tienda). Funciona offline primero: una venta queda cerrada y guardada en el navegador aunque no haya red, y viaja al backend cuando vuelve la conexión. Se opera 100% con teclado (y también con mouse).
El POS es estático y genérico: son archivos HTML, JS y CSS sin servidor propio, y no conoce
ningún backend en particular. Se conecta a cualquier sistema (ERP, e-commerce, facturación,
inventario) que implemente el Connector API, un contrato REST/JSON versionado. Esta guía
acompaña a la versión del contrato 4.6.0; el detalle de cada operación está en
connector-api.openapi.yaml.
Probarlo en 5 minutos
El repo incluye un demo-backend: la implementación de referencia del contrato, en Node y SQLite, sin dependencias externas.
Con Node 24 y pnpm instalados, cloná el repo e instalá:
git clone https://github.com/rauldiazsolis/offline-pos.git cd offline-pos pnpm installLevantá el demo-backend (queda en
http://localhost:4000):pnpm backendUsá el demo-backend del último tag del repo (
git checkoutdel tag más nuevo antes de levantarlo): es el que acompaña al POS publicado.Abrí la home del sitio publicado (
https://pos.contax.ar/) y hacé click en Abrir demo en la fila del demo-backend local. El link abre el POS en su canal (/v4/, el del major del contrato que habla el backend), que siempre tiene la última versión del POS para ese major.Chrome te va a pedir permiso de acceso a la red local: la página es
httpsy el backend eshttp://localhost. Aceptalo. Si lo rechazaste: ícono a la izquierda de la dirección → Configuración del sitio → "Acceso a la red local" → Permitir, y recargá la página.
El POS arranca en modo demo, ya conectado, con un catálogo de ejemplo. El navegador de referencia es Chromium (Chrome, Edge); el POS anda en Firefox y Safari, pero se prueba en Chromium.
Cómo entra un comercio: el link de demo y el alta
Un comercio llega al POS por un link de demo que arma el backend:
<pos>/?demo=true&backend=<base URL>&template=<opcional>
backend tiene que ser https:, o http: a localhost, 127.0.0.1 o [::1]. En una terminal
nueva (sin conexión configurada y sin datos), el POS llama a POST /demo-sessions en ese backend: es
el único endpoint sin autenticación. La respuesta trae la API key de la demo, la sucursal, el
punto de venta y la página de alta (onboarding.url y el texto de su botón). El POS prueba la
conexión, la aplica y entra a la venta. Si la terminal ya tiene datos o una conexión real, el POS
primero le muestra al operador qué se pierde y llama a POST /demo-sessions recién si confirma.
Para revocar una demo (por ejemplo, al reiniciar sus datos o tras un tiempo sin uso), el backend
responde 401 a todo request con su API key. El POS en demo lo toma como "la demo terminó": deja de
sincronizar y ofrece empezar una demo nueva, que es otro POST /demo-sessions al mismo backend y con
la misma plantilla. Fuera de una demo, un 401 es una credencial inválida.
Una terminal en demo muestra la marca DEMO y un botón para darse de alta. Ese botón lleva a
onboarding.url con ?return_url=<url del POS>&wipe_key=<token>. Cuando el alta termina, el backend
devuelve al usuario a:
<return_url>#connect=<base64url de JSON>
JSON: { baseUrl, apiKey, branch, pointOfSale, wipeKey? }
La conexión viaja en el fragmento (#…), nunca en la query string: el navegador no manda el
fragmento al servidor que sirve el POS. wipeKey es el wipe_key recibido en la ida, sin modificar:
con él, el POS borra los datos de la demo y aplica la conexión real; sin él, precarga la
configuración para que el operador decida.
Un backend que no ofrece demos no implementa POST /demo-sessions; el operador carga la conexión a
mano en /CONFIG (URL y API key).
Proteger las demos: POST /demo-sessions no lleva autenticación, así que un backend público que
ofrece demos conviene que limite los pedidos por IP (429 con Retry-After y
{ "code": "rate-limited" }) y la cantidad de demos abiertas (503 con
{ "code": "demo-capacity" }). El POS muestra un mensaje claro ("probá de nuevo en 10 minutos") y no
reintenta solo.
Implementar el contrato
Principio central: el backend nunca rechaza el contenido de lo que manda el POS. No hay forma de
que una venta, un cliente o un movimiento de caja sea "rechazado" de forma síncrona: el backend
registra todo y audita, y una inconsistencia se resuelve de su lado o a mano. Para avisarle algo al
humano, el backend manda avisos (notices) en el pull.
Las operaciones:
| Operación | Para qué |
|---|---|
GET /info |
Versión del contrato, estado (ok o maintenance) y capacidades. Liviana: el POS la consulta al probar la conexión, al arrancar y antes de sincronizar. |
POST /sync/push |
Un lote con todos los eventos pendientes de la terminal (ventas, pagos, movimientos de stock y de caja, cobranzas, clientes). Responde un ack de recepción, no de procesamiento. Idempotente por Idempotency-Key: reintentar el mismo lote nunca duplica su efecto. |
POST /sync/pull |
Catálogo, clientes con su saldo, stock, el estado de los lotes enviados y los avisos vigentes. Delta por cursor o foto completa. |
POST /account-holds |
La única operación síncrona: reserva de crédito para una venta a cuenta corriente. |
POST /demo-sessions |
Opcional: arranca una demo (ver la sección anterior). |
POST /portal-links |
Opcional: la URL para abrir el backend desde el POS (capacidad portal, ver más abajo). |
Todo request del POS lleva:
Authorization: Bearer <apiKey>(salvoPOST /demo-sessions). Cómo se emite esa API key lo define cada backend.X-POS-Contract-Version: <versión>. Si el major no es uno que el backend soporta, responde409sin procesar nada; nada se pierde, el lote queda en la terminal hasta que el backend se actualice.
El POS corre en otro origen que el backend: el backend tiene que contestar CORS (incluidos
Authorization, Idempotency-Key y X-POS-Contract-Version en los headers permitidos). Si el
backend corre en la red local o en localhost y el POS está publicado en https, conviene además
contestar el preflight de red privada (Access-Control-Allow-Private-Network: true cuando llega
Access-Control-Request-Private-Network: true), como hace el demo-backend. Si el backend manda
Retry-After (en un 429 o un 503), tiene que exponerlo con
Access-Control-Expose-Headers: Retry-After: si no, el navegador no se lo deja leer al POS.
El detalle de cada operación, sus esquemas y ejemplos está en
connector-api.openapi.yaml.
Mantenimiento
Mientras el backend no puede atender (por ejemplo, migrando su base), GET /info responde 200 con
status: maintenance (y un message opcional), y los demás endpoints responden 503 con
{ "code": "maintenance", "message": "…" } y Retry-After. El POS no pierde nada: sin ack, el lote
sigue en la terminal con su mismo Idempotency-Key. Deja de sincronizar, muestra el mantenimiento en
la barra de estado y vuelve a consultar /info hasta que el backend vuelva a ok. La venta nunca se
bloquea.
Compatibilidad y capacidades
Un backend es compatible con el POS si habla el mismo major y un minor igual o mayor que el piso 4.0.0. Un agregado nuevo del contrato no obliga a todos los backends a actualizarse.
Lo que un backend hace más allá del piso lo declara como capacidad en GET /info
(capabilities):
| Capacidad | Qué habilita |
|---|---|
demo-sessions |
El backend implementa POST /demo-sessions. |
customer-payment-void |
El POS puede anular cobranzas. |
portal |
El POS muestra un comando y un botón para abrir el backend. |
El POS nunca deduce una capacidad de la versión, e ignora una capacidad que no conoce. Las reglas de evolución (campos y valores desconocidos, fotos completas, numeración con huecos) están en la sección "Reglas de evolución" del OpenAPI.
Portal: entrar al backend desde el POS
Con la capacidad portal, el cajero abre el backend en una pestaña nueva con un comando y un botón
del POS. El backend los declara en GET /info:
"capabilities": ["demo-sessions", "portal"],
"portal": { "command": "MINI", "label": "Abrir mini" }
command va en mayúsculas, dígitos y _ (de 2 a 16 caracteres, sin la /); el POS lo muestra como
/MINI y el botón como Abrir mini (/MINI). Al usarlos, el POS pide POST /portal-links con su
API key y abre la url de la respuesta (201 { "url": "…", "expiresAt": "…" }, expiresAt
opcional). El backend decide qué URL devolver según la credencial: un link con autorización (de
un solo uso o de varios) que abre una sesión acotada a esa caja, o su página de login para que entre
un usuario con su cuenta.
- La URL nunca lleva la API key: si lleva autorización, es un token opaco del backend. Se recomienda un solo uso y un vencimiento corto (60 segundos).
- El POS no guarda la URL ni la trae en el pull: la pide cada vez que se usa el comando.
- La sesión y sus permisos los decide el backend; el contrato no define roles.
El demo-backend lo implementa con /PANEL: un link de un solo uso que vence a los 60 segundos y
muestra con qué caja se entró.
Servir el POS desde tu propio servidor
El POS es un build estático. Una versión exacta sale de los
tags del repo: git checkout vX.Y.Z,
pnpm install y pnpm build, y el resultado queda en dist/. Para servirlo en tu infraestructura:
- Serví
dist/como archivos estáticos, en una carpeta propia (por ejemplo/pos/). - Abrila siempre con barra final (
/pos/, no/pos): el build usa rutas relativas, y sin la barra los archivos no cargan. - Con
https(o enlocalhost) el POS trae un service worker: abre sin red y se instala como app. Una versión nueva que sirvas en la misma carpeta llega sola a las terminales, que la aplican con/ACTUALIZAR(nunca con una venta en curso). Sinhttpsanda igual, pero sin red no abre. - No sirvas en la misma carpeta una versión más vieja que la que ya usaron las terminales: una versión vieja no abre una base ya migrada. Para volver atrás, publicá una versión nueva con el arreglo.
- Cada carpeta tiene su propio almacenamiento en el navegador (IndexedDB y
localStorage) y su propio service worker. Cambiar de carpeta o de dominio es una instalación nueva: lo que no se llegó a sincronizar queda en la carpeta anterior. Antes de cambiar, sincronizá.
Aparecer en la home
La home del sitio publicado lista los backends conocidos, cada uno con su contrato, sus capacidades
y su link de demo al canal de su major (o el motivo de la incompatibilidad). Para sumar el tuyo,
abrí un PR a
site/backends.json con
name, url (https) y, si querés, notes.
El backend tiene que ofrecer demos (capacidad demo-sessions): la home se genera todos los días
consultando en vivo POST /demo-sessions y, con esa conexión, GET /info. Si tu backend no
contesta, la home no se publica hasta que vuelva.