Inicio / Desarrolladores / Webhook e Integración POS
Guía técnica

Webhook e Integración POS

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écnica

Webhook saliente

LOGI envía a tu URL cada reserva, venta y salida de inventario apenas ocurre.

Eventos firmados

Cada envío lleva una firma HMAC-SHA256 para que verifiques que viene de LOGI.

API POS

Consulta el inventario completo y el historial de eventos con un token de solo lectura.

Primeros pasos

Requisitos

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:

  • Una cuenta en LOGI con tu tienda creada. Si aún no la tienes, créala aquí.
  • Acceso con la cuenta principal de la tienda. La configuración de webhook e integración POS no está disponible para usuarios auxiliares.
  • Para el webhook: una URL pública con HTTPS, de un dominio que exista y no apunte a una IP privada, con máximo 255 caracteres, que responda con un código 2xx en menos de 3 segundos.
  • Para la API POS: que tu sistema pueda hacer peticiones HTTPS enviando un header Authorization.
Primeros pasos

Configuración en el portal

Todo se configura desde el portal de clientes de LOGI, en Configuración → Webhook / Integración POS.

  1. Ingresa y abre la pantalla de integración

    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.

    Menú Configuración con la opción Webhook / Integración POS
    Menú Configuración → Webhook / Integración POS.
  2. Registra la URL de tu sistema

    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.

    Formulario para registrar la URL del webhook
    Registro de la URL del webhook.
  3. Guarda 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.

    Ventana que muestra el secreto del webhook una sola vez
    El secreto se muestra una única vez.
  4. Prueba la conexión

    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.

    Resultado exitoso de la prueba de conexión
    Resultado de la prueba de conexión.
  5. Genera el Token POS (opcional)

    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.

    Sección Token POS del portal
    Generación del Token POS.

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.

Primeros pasos

Valores de integración

Tu sistema necesita estos tres valores, todos obtenidos desde el portal:

ValorPara qué sirveNotas
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.

Webhook saliente

Eventos y payload

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.

reservation.created

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"
}
CampoTipoDescripción
shipment_idIntId interno del envío asociado a esta reserva
sale_numberStringNúmero de venta asociada, si la reserva viene de una venta
linesJson[]Listado de productos reservados
variant_idIntId interno de la variante del producto
skuString | nullCódigo SKU de la variante
barcodeString | nullCódigo de barras de la variante
product_nameString | nullNombre del producto
quantityIntCantidad reservada de esa variante: ya está apartada, pero todavía no ha salido del inventario
destination_cityString | nullCiudad de destino del envío, si aplica
destination_departmentString | nullDepartamento de destino del envío, si aplica

sale.created

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"
}
CampoTipoDescripción
sale_idIntId interno de la venta
amountFloatValor total de la venta
linesJson[]Listado de productos vendidos
variant_idIntId interno de la variante del producto
skuString | nullCódigo SKU de la variante
barcodeString | nullCódigo de barras de la variante
product_nameString | nullNombre del producto
quantityIntCantidad vendida de esa variante en esta venta
destination_cityString | nullCiudad de destino del envío asociado, si aplica
destination_departmentString | nullDepartamento de destino del envío asociado, si aplica

outflow.created

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"
}
CampoTipoDescripción
shipment_idIntId interno del envío cuya mercancía salió
originString"alistamiento" (se marcó como alistado en el portal) o "carrier" (la transportadora confirmó la recolección): qué disparó la salida
number_guideStringNúmero de guía de la transportadora, para rastrear el envío físico
sale_numberStringNúmero de venta asociada, si aplica
linesJson[]Listado de productos que salieron
variant_idIntId interno de la variante del producto
skuString | nullCódigo SKU de la variante
barcodeString | nullCódigo de barras de la variante
product_nameString | nullNombre del producto
quantityIntCantidad que salió físicamente del inventario
destination_cityString | nullCiudad de destino del envío, si aplica
destination_departmentString | nullDepartamento de destino del envío, si aplica

outflow.manual

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
}
CampoTipoDescripción
product_output_idIntId interno del movimiento de salida manual
shipment_idnullSiempre null: esta salida no tiene envío asociado
sale_numbernullSiempre null: esta salida no tiene venta asociada
linesJson[]Listado de productos (siempre una sola línea)
variant_idIntId interno de la variante del producto
skuString | nullCódigo SKU de la variante
barcodeString | nullCódigo de barras de la variante
product_nameString | nullNombre del producto
quantityIntCantidad que salió del inventario por este ajuste manual
destination_citynullSiempre null en este evento
destination_departmentnullSiempre 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.

Webhook saliente

Headers y firma

HeaderValor
Content-Typeapplication/json
X-Logi-Event-IdUUID único de este evento (úsalo para deduplicar)
X-Logi-Event-TypeUno de los cuatro tipos de evento
X-Logi-TimestampFecha 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-SignatureFirma HMAC-SHA256 del cuerpo, en hexadecimal

Cómo verificar la firma

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.

Node.js
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)
  );
}
PHP
$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.

Webhook saliente

Reintentos y expiración

  1. Envío inmediato: apenas ocurre el evento, se intenta entregar una vez, con un tiempo de espera de 3 segundos.
  2. Si falla (tiempo de espera, error de red o respuesta distinta de 2xx), el evento queda pendiente y se reintenta automáticamente cada 15 minutos.
  3. Expiración a las 48 horas, contadas desde que ocurrió el evento (no desde el primer intento). Si en ese plazo no se pudo entregar, se marca como vencido y no se vuelve a intentar.
  4. Sin espera progresiva: todos los reintentos esperan lo mismo, 15 minutos.
  5. Pausa automática tras 10 fallos seguidos. LOGI deja de enviar eventos a esa URL hasta que se reactive desde el portal (guardando la URL o con una prueba exitosa), lo que también reinicia el contador de fallos.
  6. Un mismo evento nunca se envía dos veces en paralelo, pero aun así el receptor debe ser idempotente por 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.

Webhook saliente

Buenas prácticas para el receptor

  • Responde rápido, con 2xx, antes de procesar nada pesado. LOGI espera máximo 3 segundos; si tu procesamiento es lento, encólalo y responde 200 de inmediato.
  • Verifica la firma antes de confiar en el contenido. Rechaza sin procesar cualquier petición cuya firma no coincida.
  • Sé idempotente por X-Logi-Event-Id. Guarda los ids ya procesados y, si llega uno repetido, descártalo (pero igual responde 200).
  • No asumas un orden de llegada entre eventos de distintos tipos. Por ejemplo, el outflow.created de un envío puede llegar antes o después que el sale.created asociado.
  • Usa 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.
API POS

Autenticación, límites y errores

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ódigos de error

CódigoSignificado
401Falta el header Authorization, o el token no existe
403El token existe pero fue revocado desde el portal
400Algún parámetro (page, after, limit) no es válido
429Se 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>" }
API POS

Inventario completo

GET/pos/v1/snapshot

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: [].

Respuesta de ejemplo · GET /pos/v1/snapshot?page=1
{
  "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
    }
  ]
}
CampoTipoDescripción
as_ofStringFecha y hora ISO-8601 exacta en que se tomó esta foto del inventario
last_event_idIntId 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_pagesIntPágina actual y total de páginas
itemsJson[]Variantes de esta página
variant_idIntId interno de la variante
skuString | nullCódigo SKU de la variante
barcodeString | nullCódigo de barras de la variante
product_nameString | nullNombre del producto
reserved_totalIntCantidad reservada (ventas o envíos en proceso, aún no despachados)
stock_referenceIntCantidad en stock según el registro de LOGI
API POS

Consulta de eventos

GET/pos/v1/events

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.
Respuesta de ejemplo · GET /pos/v1/events?after=4800&limit=100
{
  "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 }
        ]
      }
    }
  ]
}
CampoTipoDescripción
next_afterIntÚsalo como after en la siguiente llamada para paginar sin huecos ni duplicados. Si no hay eventos nuevos, es igual al after enviado
eventsJson[]Eventos en orden ascendente por id
event_idStringUUID del evento: el mismo que llega en X-Logi-Event-Id por webhook
typeStringUno de los cuatro tipos de evento
occurred_atStringFecha y hora ISO-8601 en que ocurrió el evento (dato que el webhook no incluye)
payloadJsonIdéntico al cuerpo que enviaría el webhook para ese mismo evento
Sincronización

Flujo recomendado

  1. Carga inicial: pide GET /pos/v1/snapshot (todas las páginas) para tener el inventario completo y guarda el last_event_id de la última respuesta.
  2. Tiempo real: aplica cada evento que llegue por webhook (verificando su firma) sobre ese estado local.
  3. Reconciliación periódica: por si se perdió algún webhook (por ejemplo, si tu servidor estuvo caído), llama GET /pos/v1/events?after=<último id conocido> y aplica los eventos que no hayas procesado, comparando por event_id.
  4. Si pasa mucho tiempo sin sincronizar (días), es más simple y confiable pedir de nuevo un snapshot completo que reconstruir el historial de eventos.