offline-pos 0.5.0 · Markdown · OpenAPI · llms.txt · Backends y canales

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.

  1. Con Node 24 y pnpm instalados, cloná el repo e instalá:

    git clone https://github.com/rauldiazsolis/offline-pos.git
    cd offline-pos
    pnpm install
    
  2. Levantá el demo-backend (queda en http://localhost:4000):

    pnpm backend
    

    Usá el demo-backend del último tag del repo (git checkout del tag más nuevo antes de levantarlo): es el que acompaña al POS publicado.

  3. 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.

  4. Chrome te va a pedir permiso de acceso a la red local: la página es https y el backend es http://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:

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.

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:

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.