LogirAI Webhooks — API Reference (v1)
Esta guía describe el contrato actual de los webhooks que LogirAI envía a tu sistema. Si un comportamiento no aparece acá, asumí que no está soportado en v1.
1. Resumen
Cuando ocurren eventos en la operación logística, LogirAI envía notificaciones HTTP POST a la URL que nos hayas registrado.
- Fire-and-forget: 1 evento → 1
POST. Sin reintentos. - Autenticación: header
Authorization: Bearer <secret>. - Sin filtros por evento en v1: tu URL recibe todos los eventos disponibles para tu company.
- Una sola URL activa por cliente en v1. Si necesitás múltiples destinos, contactá a operaciones.
2. Onboarding
El alta y baja de webhooks se gestiona manualmente con el equipo de LogirAI (no hay self-service en v1).
Para activar webhooks, enviá:
- URL de destino — debe ser
https://...(HTTP plano se rechaza). - Contacto técnico — para coordinar el secret.
LogirAI te devolverá un secret que vas a recibir en cada request en el header Authorization: Bearer <secret>. Guardalo de forma segura.
Para rotar el secret o desactivar la suscripción: contactá a operaciones. La rotación es inmediata (no hay grace period: el siguiente evento usa el secret nuevo).
3. Estructura del request
Método y URL
POST <tu-url-registrada>Headers
| Header | Valor |
|---|---|
Authorization | Bearer <secret> |
Content-Type | application/json |
User-Agent | LogirAI-Webhook/1.0 |
X-Logir-Delivery-Id | UUID único del envío (formato evt_<uuid-v4>) |
X-Logir-Event | tipo del evento (e.g. guide.delivered) |
No enviamos firma HMAC. La autenticación es exclusivamente el
Bearertoken.
Timeout
LogirAI espera respuesta hasta 10 segundos. Pasado ese tiempo el request se aborta y el evento se considera fallido (sin reintento).
Body — envelope
Todos los eventos comparten esta estructura externa:
{
"id": "evt_550e8400-e29b-41d4-a716-446655440000",
"type": "guide.delivered",
"version": "1.0",
"occurred_at": "2026-04-28T14:32:11.123Z",
"company": {
"code": "ACME",
"country": "CL"
},
"data": { ... }
}| Campo | Tipo | Descripción |
|---|---|---|
id | string | Identificador único del envío. Coincide con el header X-Logir-Delivery-Id. Usalo para deduplicar. |
type | string | Uno de los valores del catálogo (§5). |
version | string | "1.0" en v1. Si cambiamos el shape, la versión se incrementa. |
occurred_at | ISO 8601 (UTC) | Timestamp en el que se construyó el envelope. Para el timestamp del hecho de negocio, ver el campo dentro de data (e.g. delivered_at). |
company.code | string | Código de tu company. Puede venir vacío ("") en casos excepcionales de degradación. |
company.country | string | "CL" o "AR". Puede venir vacío en el mismo caso. |
data | object | Payload específico del evento. Ver §5. |
4. Lo que tu sistema debe hacer
Respuesta esperada
Devolver 2xx rápido. LogirAI no inspecciona el contenido del response, solo el status code (lo registramos para auditoría).
Tu endpoint debería:
- Validar el header
Authorizationcomparando contra el secret con comparación timing-safe. - Persistir o encolar el evento.
- Devolver 2xx (idealmente en < 1 s).
No proceses lógica pesada en línea — recibí, persistí, devolvé 200. Tareas largas van a una cola.
Idempotencia
Deduplicá por envelope.id (o el header X-Logir-Delivery-Id).
- En operación normal, no enviamos duplicados.
- El evento
guide.deliveredtiene garantía explícita de single-emit del lado de LogirAI. - El resto de los eventos pueden duplicarse en escenarios excepcionales (e.g. operario re-ejecuta una acción). Tu cliente debe ser tolerante a duplicados.
Autenticación
Comparación timing-safe del secret. Ejemplo en Node.js:
const crypto = require('crypto');
function isAuthorized(req, expectedSecret) {
const header = req.headers.authorization || '';
const m = header.match(/^Bearer\s+(.+)$/);
if (!m) return false;
const a = Buffer.from(m[1]);
const b = Buffer.from(expectedSecret);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Nunca incluyas el secret en logs ni respuestas.
Orden y latencia
- No garantizamos orden entre eventos. Si tu lógica depende de orden, usá
occurred_ato los timestamps dentro dedata. - No hay rate limit por destino: si LogirAI emite muchos eventos seguidos, te llegan en paralelo (limitado solo por nuestra concurrencia interna).
5. Catálogo de eventos
El catálogo está cerrado en 18 tipos, todos activos.
type | Estado v1 | Descripción |
|---|---|---|
mawb.created | ✅ Activo | Pre-alerta procesada, MAWB y guías creadas en LogirAI. |
mawb.customs_process_started | ✅ Activo | Se generó el archivo aduanero (AIDA en AR, ATREX en AR). |
mawb.flight_arrived | ✅ Activo | El vuelo arribó al aeropuerto destino (ATA confirmado). |
mawb.incident.created | ✅ Activo | Se registró una incidencia sobre un MAWB. |
mawb.incident.resolved | ✅ Activo | Una incidencia sobre un MAWB fue resuelta. |
guide.presumed_retention | ✅ Activo | Cierre de MAWB con HAWBs faltantes en escaneo — se presume retenida. |
guide.held_at_customs | ✅ Activo | Aduana retuvo la guía. |
guide.customs_released | ✅ Activo | Aduana liberó la guía (haya tenido o no holds previos). |
guide.picked_up_by_courier | ✅ Activo | El courier de last-mile recogió la guía del CD. |
guide.delivered | ✅ Activo | Entrega confirmada (PoD del operario o del courier). |
guide.critical_annotation_added | ✅ Activo | Se registró una anotación crítica sobre la guía (cualquier tipo bloqueante para despacho: retención aduanera, presunción de abandono, documentación faltante, etc.). |
guide.scanned_in_customs | ✅ Activo | La guía fue escaneada al llegar a la bodega de aduana. Se emite en lotes, un evento por guía. |
dispatch.cancelled | ✅ Activo | Se anuló una hoja de ruta y sus guías volvieron atrás. Un evento por empresa presente en el despacho. |
guide.damaged | ✅ Activo | Se registró daño físico sobre la carga de la guía. |
guide.requires_documentation | ✅ Activo | La guía requiere documentación adicional para poder liberarse (permisos ISP, SAG, SEREMI, DGMN, DIFROL, SIPRO, GICONA, SERNAPESCA). |
guide.arrived_at_destination_cd | ✅ Activo | La guía llegó al centro de distribución de destino. |
guide.assigned_to_courier | ✅ Activo | La guía fue asignada a un courier de última milla. |
guide.scanned_by_courier | ✅ Activo | El courier escaneó la guía en su recorrido. |
Payloads por evento (campo data)
mawb.created
{
"mawb": "020-12345678",
"flight_number": "LA800",
"origin_airport": "MIA",
"destination_airport": "SCL",
"scheduled_arrival": "2026-04-29T08:00:00Z",
"guides_count": 47
}flight_number, origin_airport, destination_airport, scheduled_arrival pueden ser null si la pre-alerta no incluía esa información.
guide.scanned_in_customs
{
"guide_number": "DRL-2026-00001",
"mawb": "020-12345678",
"manifest_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"scanned_at": "2026-04-29T14:03:11Z",
"country": "CL"
}dispatch.cancelled
{
"dispatch_id": "d1e2f3a4-b5c6-7890-abcd-ef1234567890",
"dispatch_number": "HR-2026-0042",
"cancelled_at": "2026-04-30T18:20:00Z",
"cancelled_by_user_id": "u1234567-89ab-cdef-0123-456789abcdef",
"reason": "El camión no pasó la inspección",
"total_guides": 2,
"guides": [
{ "guide_id": "g1", "hawb": "DRL-2026-00001", "manifest_id": "m1", "reverted_to_status_code": 2 }
],
"mawb_ids_affected": ["m1"],
"batches_reopened": 1,
"bigboxes_reopened": 0
}Un evento por empresa representada en el despacho: guides sólo lleva las guías de esa empresa.
mawb.customs_process_started
{
"mawb": "020-12345678",
"process_id": "uuid-del-manifest",
"txt_url": null,
"started_at": "2026-04-28T14:32:11.123Z"
}
txt_urles siemprenullen v1: el archivo aduanero (AIDA/ATREX) no se publica en una URL pública.
mawb.flight_arrived
{
"mawb": "020-12345678",
"ata": "2026-04-29T08:14:00Z",
"destination_airport": "SCL",
"guides_count": 47
}mawb.incident.created
Se dispara de forma síncrona al completar el request HTTP de creación de la incidencia (en el mismo ciclo de request, antes de devolver respuesta al cliente).
Campos de data:
| Campo | Tipo | Descripción |
|---|---|---|
manifest_id | UUID | ID interno del MAWB en LogirAI. |
mawb_number | string | Número de guía maestra (e.g. 906-12345678). |
incidence_id | bigint | ID único de la incidencia. Usalo como clave de deduplicación. |
type_code | string | Código cerrado del tipo de incidencia (ver valores abajo). |
type_label | string | Descripción legible del tipo, en español. |
comment | string | null | Comentario libre del operador. Puede ser null. |
created_by | UUID | ID del usuario que registró la incidencia. |
created_at | ISO 8601 (UTC) | Timestamp de creación. |
is_blocking | boolean | true si el tipo de incidencia bloquea el despacho del MAWB. |
files | array | Archivos adjuntos. Puede ser []. Ver detalle abajo. |
type_code — valores posibles (12 valores cerrados):
CARGO_NOT_ARRIVED, DAMAGED_BOXES_AND_LABELS, MISSING_PIECES, EXCESS_PIECES, WEIGHT_DISCREPANCY, DOCUMENTATION_ISSUE, CUSTOMS_HOLD, PROHIBITED_ITEM, REPACKAGING_REQUIRED, TEMPERATURE_EXCURSION, SECURITY_INCIDENT, OTHER
files[] — cada elemento:
| Campo | Tipo | Descripción |
|---|---|---|
url | string | URL firmada con TTL de 1 hora. Descargá el archivo inmediatamente; la URL expira. |
name | string | Nombre original del archivo. |
{
"manifest_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"mawb_number": "906-12345678",
"incidence_id": 4821,
"type_code": "DAMAGED_BOXES_AND_LABELS",
"type_label": "Cajas y etiquetas dañadas",
"comment": "Pallet 3 llegó con signos de humedad. 4 cajas con etiquetas ilegibles.",
"created_by": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"created_at": "2026-05-01T14:32:11.123Z",
"is_blocking": false,
"files": [
{
"url": "https://storage.logir.ai/signed/foto-pallet-3.jpg?token=abc123&expires=1746364331",
"name": "foto-pallet-3.jpg"
}
]
}Notas operacionales:
- Emisión síncrona: si tu endpoint está caído, el evento se pierde (sin reintentos, ver §6).
- Cada emisión genera 1 registro en
manifest_incidence_notification_logcon statussentofailed. - En paralelo, LogirAI envía un email de notificación via Resend a los destinatarios configurados en la company — independiente del webhook.
- Las URLs de
files[].urlexpiran en 1 hora. Si necesitás persistir los archivos, descargalos al recibir el evento.
mawb.incident.resolved
Se dispara de forma síncrona al completar el request HTTP de resolución de la incidencia.
Campos de data:
| Campo | Tipo | Descripción |
|---|---|---|
manifest_id | UUID | ID interno del MAWB en LogirAI. |
mawb_number | string | Número de guía maestra. |
incidence_id | bigint | ID de la incidencia resuelta. Correlacioná con el incidence_id de mawb.incident.created. |
type_code | string | Código del tipo de incidencia original. |
type_label | string | Descripción legible del tipo, en español. |
resolved_by | UUID | ID del usuario que marcó la incidencia como resuelta. |
resolved_at | ISO 8601 (UTC) | Timestamp de resolución. |
resolution_notes | string | null | Notas de cierre ingresadas por el operador. Puede ser null. |
is_blocking | boolean | Refleja si el tipo de incidencia era bloqueante. Útil para saber si el despacho puede reanudarse. |
{
"manifest_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"mawb_number": "906-12345678",
"incidence_id": 4821,
"type_code": "DAMAGED_BOXES_AND_LABELS",
"type_label": "Cajas y etiquetas dañadas",
"resolved_by": "c89de012-34ab-5678-cdef-901234567890",
"resolved_at": "2026-05-01T17:15:42.000Z",
"resolution_notes": "Se re-etiquetaron las 4 cajas afectadas. Mercadería apta para despacho.",
"is_blocking": false
}Notas operacionales:
- Emisión síncrona dentro del request de resolución (mismo patrón que
mawb.incident.created). - Genera 1 registro en
manifest_incidence_notification_log. - Email de notificación vía Resend en paralelo, independiente del webhook.
- No hay archivos adjuntos en el evento de resolución.
guide.presumed_retention
{
"guide_number": "ABC123",
"mawb": "020-12345678",
"html_url": null,
"presumed_at": "2026-04-28T14:32:11.123Z"
}
html_urles siemprenullen v1.
guide.held_at_customs
{
"guide_number": "ABC123",
"mawb": "020-12345678",
"hold_reason": "D",
"hold_detail": "Falta DUI",
"held_at": "2026-04-28T14:32:11.123Z"
}hold_reason: código aduanero canónico (string corto, e.g.D,R,DF).hold_detail: razón libre escrita por el operador. Puede sernull.
guide.customs_released
{
"guide_number": "ABC123",
"mawb": "020-12345678",
"released_at": "2026-04-28T14:32:11.123Z",
"had_previous_holds": true
}had_previous_holds: true si la guía estuvo retenida antes de liberarse.
guide.picked_up_by_courier
{
"guide_number": "ABC123",
"mawb": "020-12345678",
"picked_up_at": "2026-04-28T14:32:11.123Z"
}guide.delivered
{
"guide_number": "ABC123",
"mawb": "020-12345678",
"delivered_at": "2026-04-28T14:32:11.123Z",
"pod_signatory": "Juan Pérez",
"pod_image_url": "https://storage.../pod.jpg"
}pod_signatory y pod_image_url pueden ser null (algunos couriers no devuelven foto/firma).
guide.critical_annotation_added
Se dispara fire-and-forget (no bloquea el request HTTP de creación de la anotación) cada vez que se registra sobre una guía una anotación de un tipo marcado como bloqueante para despacho (annotation_types.is_blocking_for_dispatch = true). Cubre los 4 paths internos de creación de anotaciones: endpoint /api/annotations (alta unitaria), endpoint /api/annotations/bulk (alta masiva, emite una vez por guía), cron de presunción de abandono, y anotaciones legacy generadas por acciones de aduana (hold/release).
Campos de data:
| Campo | Tipo | Descripción |
|---|---|---|
guide_id | UUID | ID interno de la guía en LogirAI. |
annotation_id | UUID | ID único de la anotación. Usalo como clave de deduplicación a nivel de anotación. |
type_code | string | Código del tipo de anotación (ver tipos críticos abajo). |
hold_id | UUID | ID del hold asociado a la anotación. Las anotaciones críticas siempre generan un hold. |
{
"guide_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"annotation_id": "9f8e7d6c-5b4a-3210-9876-543210fedcba",
"type_code": "withheld_physical",
"hold_id": "11223344-5566-7788-99aa-bbccddeeff00"
}type_code — tipos críticos actualmente activos:
withheld_physical, withheld_documentary_declaration, withheld_documentary_customs, withheld_unpaid_taxes, sag_import_permit, required_documentation, presumption_abandonment, customs_retained_legacy
Notas operacionales:
- Co-emisión con eventos de aduana: cuando una anotación crítica se origina en una acción de aduana (hold o release), LogirAI emite dos eventos diferentes sobre la misma operación física: el evento de dominio (
guide.held_at_customsoguide.customs_released) yguide.critical_annotation_added. Son eventos distintos, no duplicados. Si solo te interesa uno, filtralo portypeen tu lado. - Bulk: una alta masiva con N guías afectadas emite N eventos
guide.critical_annotation_added(uno por guía), no un solo evento agregado. - Idempotency: si reintentás un alta de anotación con la misma
Idempotency-Keyy la anotación ya existe (replay), no se re-emite el evento. - Fire-and-forget: si el emisor falla (timeout, 5xx), la anotación igual se persiste correctamente — la emisión del webhook no bloquea ni rompe la operación de negocio.
- Routing per-company: el evento solo llega al webhook configurado para la company dueña de la guía. La anotación crítica de una guía de Lion360 va al webhook de Lion360, no al de otras companies.
6. Garantías y limitaciones
Garantizamos
https://obligatorio en la URL de destino.- Single-emit de
guide.delivered: aunque dos paths internos (scan operario + webhook courier) marquen entrega concurrente, sólo se emite un evento. - Auditoría interna de cada intento (éxito o fallo) para soporte y debugging.
NO garantizamos
- No hay reintentos. Un fallo (timeout, 5xx, 4xx, conexión rechazada) es definitivo.
- No hay firma HMAC. La autenticación es exclusivamente el
Bearersecret. - No hay garantía de orden entre eventos.
- No hay deduplicación exactly-once salvo
guide.delivered. Tu cliente debe deduplicar porid. - No hay backpressure: si tu endpoint está caído, los
POSTigual se disparan y vencen por timeout.
7. Recomendaciones
- Respondé 2xx rápido (< 1 s). Procesamiento pesado en cola.
- Deduplicá por
envelope.idsiempre. - Validá
Authorizationcon timing-safe compare. - Tolerá campos
nullen payloads — los marcamos como nullable cuando aplica. - Tolerá campos extra: si en v1.x agregamos campos al
data, no incrementamosversion. Bumps deversionse reservan para cambios breaking. - Monitoreá el endpoint: si tu URL devuelve 5xx por una ventana larga, vas a perder eventos (no los reintentamos).
- Pedí re-envío manual a operaciones si detectás un gap. Tenemos auditoría completa de cada intento.