La API pública de OrigenOS: 16 operaciones sobre la carta, las comandas, las ventas, los clientes, el stock y la contabilidad del comercio, más webhooks salientes firmados. Autenticación por API key, respuestas JSON, mensajes de error en castellano.
OrigenOS corre el mostrador: la carta, las comandas, la caja, el stock y los comprobantes fiscales. La API v1 es la puerta para que otro sistema tuyo —un ERP, un tablero de BI, una app de fidelidad, el bot de compras del proveedor, la web de pedidos que hiciste vos— lea esos datos y cargue pedidos, sin que nadie tenga que copiar números a mano de una pantalla a una planilla.
Es una API REST común y silvestre: pedís por HTTPS con una API key en el header, te contesta JSON. Cada key pertenece a un comercio y solo ve los datos de ese comercio: además del filtro en cada consulta, el aislamiento lo impone la base de datos con row-level security. Las fechas van y vuelven en ISO-8601, los importes son números en la moneda del comercio y los precios ya vienen con IVA adentro (como en el ticket).
Lo que la API no hace es tan importante como lo que hace: no crea ventas, no emite comprobantes fiscales, no mueve stock, no toca puntos de fidelidad ni asientos contables. Todo eso se calcula en el cierre de caja y en el motor contable, que son el único lugar donde salen bien los totales, el IVA, las promociones y el CFE. Escribir plata por una API paralela es la receta para que el cierre del café no cuadre con el sistema. Por eso solo hay dos caminos de escritura: crear comandas y crear o editar clientes.
02 · Autenticación
Conseguir una API key
Las keys las genera el dueño del comercio desde el panel. Vos, como integrador, no podés crearlas: pedísela y te la pasa por un canal seguro.
PASO 1
Activar el módulo
La API es el add-on API Access + Webhooks. Se activa desde Módulos+ en la cuenta del comercio. Si no está activo, todas las llamadas responden 403 aunque la key exista.
PASO 2
Crear la key
Un usuario administrador entra a Configuración → API & Webhooks (/configuracion/api), le pone un nombre y tilda los scopes que la integración necesita.
PASO 3
Guardarla ya
La key en claro se muestra una sola vez. Después solo queda visible el prefijo (ok_live_a1b2) para identificarla. Si se pierde, se revoca y se crea otra.
Cómo se autentica
La key viaja en el header Authorization con el esquema Bearer. Nada de query params: una key en la URL termina en los logs del proxy, en el historial y en el Referer.
Una key da acceso de lectura —y según los scopes, de escritura— a los datos de un comercio real, incluidos datos personales de sus clientes. Guardala en variables de entorno, nunca en el repo ni en código de frontend: cualquiera que abra el navegador puede leerla. La API está pensada para llamarse desde tu servidor.
El host no elige el comercio
El comercio se resuelve a partir de la key, no del dominio. Usá el host del comercio (https://tu-comercio.origenos.com) por prolijidad y porque es el que te van a dar, pero no intentes cambiar de comercio cambiando el host: una key solo ve su propio comercio, siempre.
03 · Permisos
Los 8 scopes
Cada key lleva una lista de scopes y cada operación exige uno. Si la key no lo tiene, la respuesta es 403 con el nombre del scope que falta. Pedí solo los que uses: una key de lectura para el tablero no tiene por qué poder cargar pedidos.
Scopes disponibles al crear una API key
Scope
Qué habilita
Alcance
products:read
Leer la carta
Productos activos con precio, IVA, categoría y dónde se preparan.
orders:read
Leer comandas
Comandas abiertas y cerradas, con sus ítems.
orders:write
Crear comandas
Cargar pedidos desde afuera. Entran como pendientes de aprobación.
sales:read
Leer ventas
Ventas cerradas con total, IVA, medios de pago y datos del comprobante fiscal.
customers:read
Leer clientes
Ficha de clientes y socios, con sus puntos.
customers:write
Crear y editar clientes
Alta y actualización de clientes desde afuera.
stock:read
Leer stock
Insumos con existencia actual, mínimo y movimientos.
accounting:read
Leer contabilidad
Plan de cuentas y asientos contables.
Por qué no hay escritura de plata ni de fiscal
No existe sales:write ni accounting:write, y no es un olvido: es una decisión de diseño. Una venta se cierra en la caja, que es donde se resuelven totales, IVA, promociones, puntos y comprobante fiscal en un solo lugar. Una API que también pudiera crear ventas tendría una segunda fórmula, y dos fórmulas de plata siempre terminan dando números distintos.
04 · Referencia
Las 16 operaciones
Esto es todo lo que existe hoy en /api/v1. No hay endpoints ocultos ni beta: si no está en esta tabla, no está en la API. Todos los listados devuelven data con los resultados; el bloque de paginación cambia según el recurso (lo explicamos abajo).
Carta
Lo que el comercio vende hoy. Solo lectura: la carta se edita en OrigenOS. Devuelve productos activos y NO archivados.
Operaciones de carta
Método
Ruta
Scope
Qué hace
Parámetros
GET
/api/v1/products
products:read
Lista los productos de la carta, en el orden que les dio el dueño, con precio de mostrador, IVA, categoría y dónde se preparan.
Detalle de una comanda con sus líneas vigentes, las anuladas por separado y el subtotal bruto ya calculado.
—
Ventas
La plata ya cobrada, con su comprobante fiscal. Solo lectura, siempre: los totales, el IVA, las promos y el CFE se calculan en el cierre de caja, que es el único camino canónico.
Operaciones de ventas
Método
Ruta
Scope
Qué hace
Parámetros
GET
/api/v1/sales
sales:read
Lista ventas cerradas con totales, medio de pago dominante y bloque fiscal. Por defecto excluye las anuladas y mira los últimos 30 días.
limit (1–200, default 50), cursor, from / to (default 30 días, máximo 366), locationId, voided (false | true | all)
GET
/api/v1/sales/{id}
sales:read
Detalle de una venta: líneas, desglose real por medio de pago (pago dividido), datos del receptor del CFE y el árbol de notas de crédito.
—
Clientes
La ficha de clientes y socios. Es el otro recurso con escritura, y el que más cuidado pide: son datos personales de gente real.
Operaciones de clientes
Método
Ruta
Scope
Qué hace
Parámetros
GET
/api/v1/customers
customers:read
Lista clientes con búsqueda por nombre, teléfono, email o documento. El documento sale enmascarado.
limit (1–100, default 50), cursor, q (mínimo 2 caracteres), active
POST
/api/v1/customers
customers:write
Da de alta un cliente. El documento lo valida el driver del país del comercio. Email y documento duplicados dan 409.
body: name (obligatorio), phone, email, documentId, birthday, notes, marketingConsent
GET
/api/v1/customers/{id}
customers:read
Ficha completa: documento sin enmascarar, notas, consentimiento de marketing y saldo de puntos (solo lectura).
—
PATCH
/api/v1/customers/{id}
customers:write
Actualización parcial: solo se tocan los campos que mandás. Un null borra el dato; un campo ausente lo deja como está.
body: cualquiera de los campos del alta (al menos uno)
Stock
Insumos y su existencia. Solo lectura: la API no mueve stock. El caso de uso típico es un ERP que pregunta cada tanto qué hay que reponer.
Operaciones de stock
Método
Ruta
Scope
Qué hace
Parámetros
GET
/api/v1/stock
stock:read
Lista insumos con existencia, mínimo, costo por unidad de compra y de uso, valorización y proveedor preferido.
limit (1–200, default 100), cursor, belowMin
GET
/api/v1/stock/{id}/movements
stock:read
Movimientos de un insumo: compras, consumo, mermas, ajustes y despieces, con signo y costo. Incluye la cabecera del insumo.
limit (1–200, default 100), cursor, type, from / to (alias desde / hasta)
Contabilidad
Plan de cuentas y libro diario, para el estudio contable o el BI. Solo lectura: los asientos los genera el motor contable.
Operaciones de contabilidad
Método
Ruta
Scope
Qué hace
Parámetros
GET
/api/v1/accounting/accounts
accounting:read
Plan de cuentas jerárquico. Cada cuenta trae parentId y parentCode para rearmar el árbol del lado tuyo.
limit (default 500, máximo 2000), type, kind, active, country (ISO alpha-2)
GET
/api/v1/accounting/entries
accounting:read
Libro diario del período. Los asientos anulados vienen marcados. Los totales son los persistidos, no se recalculan.
from y to (YYYY-MM-DD, OBLIGATORIOS, máximo 366 días), limit (default 50, máximo 200), cursor, source, voided, accountId
GET
/api/v1/accounting/entries/{id}
accounting:read
Asiento con todas sus líneas. Devuelve los totales persistidos, la suma de las líneas por separado y una bandera de integridad.
—
Rangos de fecha
En todos los listados que aceptan from / to (y sus alias en castellano desde / hasta), from es inclusivo y to es exclusivo. Una fecha suelta como 2026-07-01 se lee como medianoche UTC: si querés cortar por el día local del comercio, mandá el offset (2026-07-01T00:00:00-03:00). No adivinamos el huso horario, y una fecha que no existe —2026-02-31— da 400 en vez de correrse sola a marzo.
En /api/v1/accounting/entries el formato es distinto y más estricto: from y to son obligatorios y solo aceptan YYYY-MM-DD. En contabilidad una fecha ambigua te corre un asiento de mes.
05 · Los dos casos de siempre
Ejemplos completos
Listar comandas
El listado va liviano a propósito: trae la cabecera de cada comanda, sin las líneas. Para el detalle con ítems pegale a /api/v1/orders/{id}.
Los precios no se mandan: los pone el sistema desde la carta, con el precio de mostrador vigente. Vos mandás qué producto y cuánto. La comanda entra con estado PENDING_APPROVAL y aparece en la pantalla de comandas para que alguien la apruebe antes de que vaya a cocina.
El header Idempotency-Key es opcional, pero si no lo mandás y se te vence un timeout, el reintento carga el pedido dos veces y la cocina prepara dos desayunos. Con la clave puesta, el segundo intento devuelve la comanda original con 200, "replayed": true y el header Idempotent-Replay: true — y no vuelve a disparar el webhook, porque no pasó nada nuevo. La clave tiene que tener entre 8 y 200 caracteres ASCII imprimibles sin espacios (un UUID, un ULID o algo como pedido-2026-07-28-0042).
Detalle importante: gana la clave, no el contenido. Si reusás la misma clave con ítems distintos, te devolvemos la comanda vieja en vez de crear la nueva. Es conservador a propósito: ante la duda, no duplicamos comida. Una clave nueva por pedido y listo.
Sobre los ítems anulados
En el detalle de una comanda, items trae solo las líneas vigentes y voidedItems las anuladas, con su motivo. Están separadas porque si van todas juntas, quien suma cantidad × precio cobra de más: la línea que el mozo anuló seguiría sumando. itemsSummary.subtotalGross ya viene calculado con la fórmula del cierre.
Ese subtotal es el bruto de las líneas, no el total del ticket. Los descuentos, las promociones, los puntos y el comprobante fiscal se resuelven recién al cobrar. Por eso la API no expone un "total" de comanda: si lo hiciera, sería un número que no coincide con lo que se cobra.
06 · Recorrer listados
Paginación
Todos los listados paginan por cursor (nunca por offset: con offset, una fila nueva corre todo y te saltea resultados). Pedís una página, te devolvemos un cursor, y se lo pasás tal cual a la request siguiente repitiendo los mismos filtros. Cuando el cursor viene en null, se terminó.
La forma del bloque de paginación no es uniforme entre recursos —te lo decimos de frente para que no te sorprenda:
pagination.nextCursor · range devuelve la ventana realmente aplicada
customers
{ data, nextCursor }
nextCursor, en la raíz (no hay bloque pagination)
accounting/entries
{ data, meta }
meta.nextCursor (+ meta.hasMore)
accounting/accounts
{ data, meta }
No pagina. Subí limit (hasta 2000) y mirá meta.truncated
Dos detalles que ahorran una tarde
El cursor es opaco. En algunos recursos es el id de la última fila y en otros un valor codificado en base64url. No lo interpretes ni lo construyas a mano: copialo tal cual. Un cursor malformado siempre da 400; en carta, comandas, stock y clientes se valida además que corresponda a un registro de tu comercio, así que un id prestado corta con 400 en vez de devolver una página vacía sin explicación.
Un limit inválido no se comporta igual en todos lados. En carta, comandas y stock, un valor fuera de rango da 400 con el nombre del parámetro. En ventas, clientes y contabilidad se recorta en silencio al rango permitido. Mandá siempre un entero dentro del rango y no dependas de ninguno de los dos comportamientos.
07 · Cuando algo sale mal
Códigos de error
Los errores vienen con un campo error en castellano, pensado para que se pueda leer en un log sin traducir nada. Cuando el problema es un parámetro puntual, se agrega param; en los choques de duplicado, field.
Respuestas de error de /api/v1
Código
Cuándo
Body
400
Un parámetro no válido, un body que no parsea, un cursor que no es de tu comercio o un combo en POST /orders.
{ "error": "…", "param": "limit" }
401
Falta el header Authorization, o la key no existe / fue revocada.
{ "error": "API key inválida o revocada" }
403
La key no tiene el scope que pide la operación, o el add-on API Access no está activo en el comercio.
{ "error": "La API key no tiene el scope \"orders:write\"" }
404
El id no existe en tu comercio. Nunca decimos si existe en otro: desde afuera no se puede distinguir.
{ "error": "Orden no encontrada" }
409
Email o documento de cliente duplicado, o una Idempotency-Key ya usada por otra comanda.
{ "error": "Ya hay un cliente de este comercio con ese email.", "field": "email" }
429
Pasaste el límite de caudal de la key. Ojo: este body tiene forma distinta al resto.
Todos los errores usan { "error": "…" } salvo el del límite de caudal, que devuelve { "ok": false, "error": "…", "retryAfterSec": n }. Si tu cliente parsea errores, contemplá los dos formatos.
08 · Eventos salientes
Webhooks
En vez de preguntar cada treinta segundos si pasó algo, dejás una URL https y te avisamos nosotros. Las suscripciones se crean en el mismo panel que las keys (/configuracion/api), eligiendo a qué eventos querés escuchar. Al crearla se muestra un secret (whsec_…) una sola vez: con ese secret se verifica la firma.
Eventos
Eventos a los que se puede suscribir una URL
Evento
Qué significa
Estado
order.created
Se cargó una comanda nueva.
Se emite al crear una comanda por POST /api/v1/orders. El data incluye los ítems.
order.closed
Se cobró y cerró una comanda.
Se emite al cobrar una comanda en la caja. El data trae solo la cabecera, sin ítems: si necesitás el detalle, pedí /api/v1/orders/{id}.
order.cancelled
Se anuló una comanda.
Se puede seleccionar en el panel, pero hoy no lo emite ningún camino del sistema. No construyas lógica esperándolo.
Cómo llega
Un POST con Content-Type: application/json y dos headers propios: X-OrigenOS-Event con el nombre del evento y X-OrigenOS-Signature con la firma. El cuerpo siempre tiene la misma forma: el evento, el dato y un deliveryId —que te sirve para deduplicar si te llega repetido.
La firma es sha256= seguido del HMAC-SHA256 del cuerpo crudo usando el secret de la suscripción como clave. Verificala siempre: sin eso, cualquiera que adivine tu URL puede inventarte pedidos.
Verificaciónjavascript
import crypto from "node:crypto";
// El HMAC se calcula sobre el CUERPO CRUDO, byte por byte. Si lo parseás y lo
// volvés a serializar, la firma no va a coincidir: guardate el raw body.
export function firmaValida(rawBody, headerFirma, secret) {
const esperado =
"sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(esperado, "utf8");
const b = Buffer.from(headerFirma ?? "", "utf8");
// Comparación en tiempo constante: un === filtra el secreto de a poquito.
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Ejemplo con Express (nota el express.raw: NO uses express.json acá).
app.post(
"/hooks/origenos",
express.raw({ type: "application/json" }),
(req, res) => {
if (!firmaValida(req.body, req.get("X-OrigenOS-Signature"), process.env.OS_WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const evento = JSON.parse(req.body.toString("utf8"));
// Respondé 2xx rápido y procesá después: 10 segundos y cortamos.
res.sendStatus(200);
encolar(evento);
},
);
Reintentos
Se considera entregado con cualquier respuesta 2xx. Todo lo demás —incluido un timeout— cuenta como fallo.
Cada intento espera 10 segundos como máximo. Contestá rápido y procesá después: si tardás, lo tomamos por caído y reintentamos.
Hasta 5 intentos en total, con backoff de 1 minuto, 5 minutos, 30 minutos y 2 horas. Después del quinto se abandona la entrega.
Los reintentos los levanta un proceso que corre cada 5 minutos, así que el próximo intento puede caer un poco después del backoff exacto.
La URL tiene que ser https y pública: se bloquean destinos internos (protección contra SSRF). Un localhost o una IP privada no van a recibir nada.
Hacé tu endpoint idempotente. Con reintentos de por medio, un mismo deliveryId puede llegarte más de una vez; guardá cuáles ya procesaste.
09 · Cuotas
Límite de caudal
600 requests cada 5 minutos por API key (unas 2 por segundo sostenidas). El contador es por key, no por IP ni por endpoint: todas las rutas de /api/v1 comparten el mismo balde. Si tenés varias integraciones, dales keys distintas y no se pisan entre ellas.
Al pasarte, la API responde 429 con el header Retry-After en segundos. Los rechazos no cuentan como consumo, así que la ventana no se renueva sola por seguir golpeando: esperá lo que dice Retry-After y reintentá.
Respuesta al pasarsehttp
HTTP/1.1 429 Too Many Requests
Retry-After: 300
{
"ok": false,
"error": "Demasiados intentos. Esperá un momento y volvé a probar.",
"retryAfterSec": 300
}
Cómo no llegar al tope
Sincronizá la carta cada tanto y guardala en caché en tu lado (no cambia todo el día). Para enterarte de comandas nuevas usá webhooks en vez de encuestar el listado. Y al recorrer históricos, subí el limit en lugar de hacer más requests: 200 ventas en una llamada gastan lo mismo que 20.
10 · Spec
OpenAPI
OrigenOS publica el spec OpenAPI 3.1 de esta API en /api/openapi (YAML). Trae las 16 operaciones de /api/v1, los webhooks salientes y los esquemas de todos los recursos. Podés descargarlo con curl y abrirlo en Swagger Editor, Postman o Insomnia, o generar un cliente con él.
Hay un segundo spec, y no es este
/api/openapi?spec=internal sirve un archivo distinto, que documenta otra superficie del producto: el pedido por QR en la mesa, el programa de fidelidad, el webhook de WhatsApp, los procesos programados y el webhook de Stripe. No es la API que estás integrando y no incluye ninguna ruta de /api/v1. Lo dejamos publicado para no romperle el link a quien ya lo tenía.
11 · Sin vueltas
Limitaciones conocidas
Todo esto lo sabemos y lo decimos acá para que no lo descubras a las tres de la tarde de un viernes. Si algo de la lista te bloquea, escribinos: sirve para priorizar.
Los combos no se cargan por API
POST /api/v1/orders rechaza con 400 cualquier ítem que sea un combo, y devuelve sus nombres en el campo combos del error.
Un combo no es una línea: es una cabecera con el precio final más un hijo por cada opción elegida, cada uno con su estación de impresión y su receta. Sin las selecciones del cliente entraría una línea plana, sin hijos y sin descontar recetas —cocina no sabría qué preparar y el comercio cobraría de menos. Preferimos el error explícito antes que la comanda rota en silencio. Los combos se cargan desde el POS. En GET /api/v1/products se los reconoce por isCombo: true y traen el resumen de sus grupos.
No se puede modificar ni cancelar una comanda
Solo hay alta. No existe PATCH ni DELETE sobre /api/v1/orders: agregar ítems, anular líneas, cerrar o anular una comanda se hace desde el POS.
order.cancelled no se dispara
El evento se puede tildar al crear una suscripción, pero hoy ningún camino del sistema lo emite. Los que sí llegan son order.created y order.closed.
No hay endpoint de entregas de webhook
Si tu servidor estuvo caído, no hay forma de listar ni de reenviar las entregas fallidas por API. Los reintentos son automáticos (5 intentos, hasta ~2h 36m de ventana) y después se pierden.
La idempotencia mira la clave, no el body
Reusar una Idempotency-Key con ítems distintos devuelve la comanda original en lugar de crear la nueva, y no avisa que el contenido cambió. Usá una clave nueva por pedido.
Los puntos de fidelidad son solo lectura
El saldo se ve en la ficha del cliente (GET /api/v1/customers/{id}) pero no hay forma de acreditarlos ni canjearlos por API: los mueve únicamente el motor de fidelidad. El listado tampoco los trae —el único número barato de listar sería un espejo que el motor nuevo no actualiza, o sea un dato falso.
Campos de cliente que la API no toca
No se puede activar ni desactivar un cliente (dar de baja tiene un control de cuenta corriente que vive en el sistema), ni tocar fiado, límites de crédito o datos fiscales. Un cliente dado de baja no se lista, no se lee y no se edita: responde 404.
Los campos desconocidos en el body dan 400 en vez de ignorarse, así que un typo se nota enseguida.
Ventanas de consulta acotadas
Ventas: máximo 366 días por consulta y, si no mandás from, se asumen los últimos 30. Asientos contables: from y to obligatorios, tope de 366 días. Para históricos largos, partí en tramos y paginá.
El plan de cuentas no pagina
GET /api/v1/accounting/accounts devuelve hasta limit filas (máximo 2000) y avisa con meta.truncated: true si cortó. No hay cursor: si te truncan, el árbol queda con padres faltantes.
No hay SDKs todavía
Los SDKs oficiales de TypeScript y Python están anunciados pero no publicados. Por ahora, fetch, requests o lo que uses.
No hay entorno de pruebas separado
Todas las keys son ok_live_: apuntan a datos reales. Si vas a probar la creación de comandas, coordiná con el comercio y usá una sucursal o un horario donde no moleste.
Precios de plataformas de delivery
price es siempre el precio de mostrador. Los precios diferenciados de PedidosYa y Rappi no se exponen: son un canal aparte y se administran en el sistema.
Ayuda
¿Te trabaste con algo?
Si un endpoint devuelve algo distinto a lo que dice esta página, es un error nuestro y lo queremos saber. Escribinos con la ruta, los parámetros y el prefijo de la key (nunca la key completa) y lo miramos.