Sincroniza el inventario y las ventas de tu tienda LOGI con tu sitio web, POS o ERP: recibe eventos en tiempo real y consulta el inventario completo cuando lo necesites.
Crear cuenta en LOGI Ir a la guía técnicaLOGI envía a tu URL cada reserva, venta y salida de inventario apenas ocurre.
Cada envío lleva una firma HMAC-SHA256 para que verifiques que viene de LOGI.
Consulta el inventario completo y el historial de eventos con un token de solo lectura.
Esta guía es para quien programa la integración entre LOGI y un sistema externo (sitio web, POS o ERP). Antes de empezar necesitas:
Authorization.
Todo se configura desde el portal de clientes de LOGI, en Configuración → Webhook / Integración POS.
Inicia sesión en el portal de clientes con la cuenta principal. Si administras varias tiendas, selecciona primero la tienda que vas a integrar. Luego abre Configuración → Webhook / Integración POS.
Escribe la URL que va a recibir los eventos y guárdala. Debe usar HTTPS y su dominio debe existir y no apuntar a una IP privada; si no se cumple, el portal rechaza la URL. Puedes cambiarla después sin perder el secreto.
Al guardar la URL por primera vez, LOGI genera el secreto con el que firma cada evento. Se muestra una sola vez: cópialo y configúralo en tu sistema antes de cerrar la ventana.
El botón Probar envía un evento test.ping a tu URL y
muestra el código HTTP que respondió. Sirve para validar la conexión, pero su formato
es distinto al de los eventos reales, que se describen en la
guía técnica.
Si tu sistema va a consultar el inventario con la API POS, genera el token desde la sección Token POS. También se muestra una sola vez, y generar uno nuevo invalida el anterior de inmediato.
Pausa automática. Si tu URL falla 10 veces seguidas, LOGI pausa el envío y el estado cambia a "Pausado automáticamente" (distinto de "Inactivo", que es una desactivación manual). No se pierde nada: cuando tu sistema vuelva a responder, guarda la URL de nuevo (aunque sea la misma) o usa Probar para reactivarlo.
Tu sistema necesita estos tres valores, todos obtenidos desde el portal:
| Valor | Para qué sirve | Notas |
|---|---|---|
| URL del webhook | Dirección donde LOGI entrega los eventos | Se puede actualizar sin perder el secreto |
| Secreto del webhook | Verificar la firma de cada evento | Se muestra una sola vez; si se pierde hay que rotarlo, lo que invalida el anterior |
| Token POS | Autenticar las consultas a la API POS | Se muestra una sola vez; generar uno nuevo invalida el anterior |
El secreto y el token no se pueden recuperar. Si se pierden, genera uno nuevo desde el portal y actualízalo en tu sistema.
Hay cuatro tipos de evento, cada uno con su propio cuerpo JSON. El cuerpo que llega por HTTP es exactamente este objeto, sin envolver en nada más: el tipo y el id del evento viajan en los headers.
Se dispara al reservar inventario para un envío o venta en proceso, antes de que la mercancía salga físicamente.
{
"shipment_id": 4821,
"sale_number": "V-10234",
"lines": [
{ "variant_id": 551, "sku": "CAM-AZUL-M", "barcode": "7701234567890", "product_name": "Camiseta azul talla M", "quantity": 2 }
],
"destination_city": "BOGOTA D.C.",
"destination_department": "CUNDINAMARCA"
}| Campo | Tipo | Descripción |
|---|---|---|
shipment_id | Int | Id interno del envío asociado a esta reserva |
sale_number | String | Número de venta asociada, si la reserva viene de una venta |
lines | Json[] | Listado de productos reservados |
variant_id | Int | Id interno de la variante del producto |
sku | String | null | Código SKU de la variante |
barcode | String | null | Código de barras de la variante |
product_name | String | null | Nombre del producto |
quantity | Int | Cantidad reservada de esa variante: ya está apartada, pero todavía no ha salido del inventario |
destination_city | String | null | Ciudad de destino del envío, si aplica |
destination_department | String | null | Departamento de destino del envío, si aplica |
Se dispara cuando una venta queda confirmada (pago aceptado).
{
"sale_id": 9931,
"amount": 125000.0,
"lines": [
{ "variant_id": 551, "sku": "CAM-AZUL-M", "barcode": "7701234567890", "product_name": "Camiseta azul talla M", "quantity": 2 }
],
"destination_city": "BOGOTA D.C.",
"destination_department": "CUNDINAMARCA"
}| Campo | Tipo | Descripción |
|---|---|---|
sale_id | Int | Id interno de la venta |
amount | Float | Valor total de la venta |
lines | Json[] | Listado de productos vendidos |
variant_id | Int | Id interno de la variante del producto |
sku | String | null | Código SKU de la variante |
barcode | String | null | Código de barras de la variante |
product_name | String | null | Nombre del producto |
quantity | Int | Cantidad vendida de esa variante en esta venta |
destination_city | String | null | Ciudad de destino del envío asociado, si aplica |
destination_department | String | null | Departamento de destino del envío asociado, si aplica |
Se dispara cuando la mercancía efectivamente sale del inventario (alistamiento o confirmación
de la transportadora). origin solo toma dos valores:
"alistamiento" o "carrier".
{
"shipment_id": 4821,
"origin": "alistamiento",
"number_guide": "GU-778812",
"sale_number": "V-10234",
"lines": [
{ "variant_id": 551, "sku": "CAM-AZUL-M", "barcode": "7701234567890", "product_name": "Camiseta azul talla M", "quantity": 2 }
],
"destination_city": "BOGOTA D.C.",
"destination_department": "CUNDINAMARCA"
}| Campo | Tipo | Descripción |
|---|---|---|
shipment_id | Int | Id interno del envío cuya mercancía salió |
origin | String | "alistamiento" (se marcó como alistado en el portal) o "carrier" (la transportadora confirmó la recolección): qué disparó la salida |
number_guide | String | Número de guía de la transportadora, para rastrear el envío físico |
sale_number | String | Número de venta asociada, si aplica |
lines | Json[] | Listado de productos que salieron |
variant_id | Int | Id interno de la variante del producto |
sku | String | null | Código SKU de la variante |
barcode | String | null | Código de barras de la variante |
product_name | String | null | Nombre del producto |
quantity | Int | Cantidad que salió físicamente del inventario |
destination_city | String | null | Ciudad de destino del envío, si aplica |
destination_department | String | null | Departamento de destino del envío, si aplica |
Se dispara cuando alguien saca mercancía manualmente desde el portal (sin que sea una venta ni
un envío), siempre con una sola línea. Como no tiene envío asociado, el destino viene en
null.
{
"product_output_id": 7765,
"shipment_id": null,
"sale_number": null,
"lines": [
{ "variant_id": 551, "sku": "CAM-AZUL-M", "barcode": "7701234567890", "product_name": "Camiseta azul talla M", "quantity": 1 }
],
"destination_city": null,
"destination_department": null
}| Campo | Tipo | Descripción |
|---|---|---|
product_output_id | Int | Id interno del movimiento de salida manual |
shipment_id | null | Siempre null: esta salida no tiene envío asociado |
sale_number | null | Siempre null: esta salida no tiene venta asociada |
lines | Json[] | Listado de productos (siempre una sola línea) |
variant_id | Int | Id interno de la variante del producto |
sku | String | null | Código SKU de la variante |
barcode | String | null | Código de barras de la variante |
product_name | String | null | Nombre del producto |
quantity | Int | Cantidad que salió del inventario por este ajuste manual |
destination_city | null | Siempre null en este evento |
destination_department | null | Siempre null en este evento |
destination_city y destination_department vienen en
null cuando el evento no tiene un envío de destino real (por ejemplo, una
salida manual o una ciudad que no existe en el catálogo). sale_number,
shipment_id y number_guide también pueden venir en
null según el caso, al igual que sku, barcode y
product_name si la variante no tiene esos datos.
| Header | Valor |
|---|---|
Content-Type | application/json |
X-Logi-Event-Id | UUID único de este evento (úsalo para deduplicar) |
X-Logi-Event-Type | Uno de los cuatro tipos de evento |
X-Logi-Timestamp | Fecha y hora ISO-8601 del intento de envío actual. Cambia en cada reintento; no es la hora en que ocurrió el evento |
X-Logi-Signature | Firma HMAC-SHA256 del cuerpo, en hexadecimal |
La firma es HMAC-SHA256(cuerpo_crudo, secreto), codificada en hexadecimal
minúscula. El secreto es el que el portal mostró al crear o rotar el webhook.
Calcula el HMAC sobre los bytes crudos exactos del cuerpo recibido, antes de parsear el JSON. Si vuelves a serializar el objeto, el orden de las llaves puede cambiar y la firma no va a coincidir.
const crypto = require('crypto');
function esFirmaValida(cuerpoCrudo, firmaRecibida, secreto) {
const firmaCalculada = crypto
.createHmac('sha256', secreto)
.update(cuerpoCrudo) // el string/buffer crudo, no el objeto parseado
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(firmaCalculada),
Buffer.from(firmaRecibida)
);
}$firmaCalculada = hash_hmac('sha256', $cuerpoCrudo, $secreto);
$esValida = hash_equals($firmaCalculada, $firmaRecibida);
Usa siempre una comparación de tiempo constante (timingSafeEqual,
hash_equals), nunca === o == directo sobre la firma.
X-Logi-Event-Id.
Si tu sistema no puede recibir eventos (por ejemplo, porque no tiene URL pública), puedes usar la consulta de eventos de la API POS como única vía de sincronización.
X-Logi-Event-Id. Guarda los ids ya
procesados y, si llega uno repetido, descártalo (pero igual responde 200).
outflow.created de un envío puede llegar antes o después que el
sale.created asociado.
GET /pos/v1/snapshot para el estado inicial y el
webhook solo para mantenerte al día: no dependas del webhook para la primera carga.
Dos endpoints de solo lectura, independientes del webhook. No usan la sesión del portal, solo el Token POS.
La URL base de la API y la documentación detallada están disponibles en el portal de clientes, en Configuración → Webhook / Integración POS → Ver documentación. Las rutas de esta guía son relativas a esa URL base.
Autenticación: envía el header
Authorization: Bearer <token> en cada petición.
Límite de peticiones: 60 por minuto, por token (no por IP). Pasado el límite, responde 429.
| Código | Significado |
|---|---|
| 401 | Falta el header Authorization, o el token no existe |
| 403 | El token existe pero fue revocado desde el portal |
| 400 | Algún parámetro (page, after, limit) no es válido |
| 429 | Se superó el límite de 60 peticiones por minuto para ese token |
Todas las respuestas de error devuelven JSON:
{ "status": "NOT OK", "status_code": 401, "status_message": "<detalle>" }Devuelve el inventario completo de la tienda, en páginas de 200 variantes.
Parámetro: page (entero, empieza en 1; por defecto 1). Pedir
una página fuera de rango no es un error: devuelve items: [].
{
"status": "OK",
"status_code": 200,
"as_of": "2026-10-08T15:30:00-05:00",
"last_event_id": 4821,
"page": 1,
"total_pages": 1,
"items": [
{
"variant_id": 551,
"sku": "CAM-AZUL-M",
"barcode": "7701234567890",
"product_name": "Camiseta azul talla M",
"reserved_total": 3,
"stock_reference": 42
}
]
}| Campo | Tipo | Descripción |
|---|---|---|
as_of | String | Fecha y hora ISO-8601 exacta en que se tomó esta foto del inventario |
last_event_id | Int | Id del evento más reciente de la tienda al momento de la consulta. Es el punto de partida para pedir solo lo nuevo con GET /pos/v1/events?after=<last_event_id> |
page / total_pages | Int | Página actual y total de páginas |
items | Json[] | Variantes de esta página |
variant_id | Int | Id interno de la variante |
sku | String | null | Código SKU de la variante |
barcode | String | null | Código de barras de la variante |
product_name | String | null | Nombre del producto |
reserved_total | Int | Cantidad reservada (ventas o envíos en proceso, aún no despachados) |
stock_reference | Int | Cantidad en stock según el registro de LOGI |
Devuelve los eventos de la tienda con id mayor al indicado, en orden ascendente. Es el mismo catálogo de eventos del webhook, consultado por pull en vez de recibido por push.
Parámetros:
after (entero, por defecto 0): solo trae eventos con id mayor a este.limit (entero, por defecto 100, máximo 500): si se pide más de 500, se recorta a 500 sin error.{
"status": "OK",
"status_code": 200,
"next_after": 4821,
"events": [
{
"event_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"type": "outflow.created",
"occurred_at": "2026-10-08T15:12:03-05:00",
"payload": {
"shipment_id": 4821,
"origin": "alistamiento",
"number_guide": "GU-778812",
"sale_number": "V-10234",
"lines": [
{ "variant_id": 551, "sku": "CAM-AZUL-M", "barcode": "7701234567890", "product_name": "Camiseta azul talla M", "quantity": 2 }
]
}
}
]
}| Campo | Tipo | Descripción |
|---|---|---|
next_after | Int | Úsalo como after en la siguiente llamada para paginar sin huecos ni duplicados. Si no hay eventos nuevos, es igual al after enviado |
events | Json[] | Eventos en orden ascendente por id |
event_id | String | UUID del evento: el mismo que llega en X-Logi-Event-Id por webhook |
type | String | Uno de los cuatro tipos de evento |
occurred_at | String | Fecha y hora ISO-8601 en que ocurrió el evento (dato que el webhook no incluye) |
payload | Json | Idéntico al cuerpo que enviaría el webhook para ese mismo evento |
GET /pos/v1/snapshot (todas las páginas)
para tener el inventario completo y guarda el last_event_id de la última
respuesta.
GET /pos/v1/events?after=<último id conocido> y aplica los eventos que
no hayas procesado, comparando por event_id.