Ir al contenido

Integraciones: su sistema conectado con Llevu

En «Integraciones» (en el menú, debajo de «Empresa») se crean las llaves con las que el sistema de su empresa (su ERP, su tienda en línea, su sistema de facturación) crea pedidos en Llevu sin cargarlos a mano y consulta cómo van, y los webhooks con los que Llevu le avisa a su sistema lo que pasa. La ven y la usan los dueños y los administradores.

Toque «Crear una llave», escriba quién la va a usar («ERP», «Tienda en línea») y toque «Crear la llave». La llave aparece una sola vez: cópiela con «Copiar la llave» y péguela en la configuración de su sistema. Llevu no la guarda (solo una huella para reconocerla), así que no se puede volver a ver: si se pierde, revóquela y cree otra.

Quien tenga la llave puede crear, ver y cancelar pedidos de su empresa. No la mande por chat ni por correo. Use una llave por sistema: si uno deja de usarse, revoca la suya sin tocar las demás.

En la lista se ve cada llave con su nombre, cuándo se creó, cuándo se usó por última vez y si está vigente. Lo que crea una llave queda en la Actividad a su nombre («ERP (integración)»).

Toque «Revocar» en su fila y confirme. Desde la llamada siguiente, el sistema que la usa ya no puede crear pedidos. No se puede deshacer: si hace falta, se crea otra (puede tener el mismo nombre).

En «Avisos a su sistema (webhooks)», toque «Agregar un webhook», escriba la dirección de su sistema (empieza con https:// y tiene que ser pública) y marque qué avisar: un pedido cambió de estado (pasó a una ruta, volvió a pendiente, se entregó, no se entregó o se canceló), una ruta salió o una ruta terminó. Toque «Agregar». Aparece el secreto del webhook una sola vez: cópielo y guárdelo en su sistema, que lo usa para verificar que cada aviso viene de Llevu. Si se pierde, «Cambiar el secreto» genera otro (el anterior deja de valer en el acto). Una empresa tiene hasta 5 webhooks activos.

Cada webhook dice si está activo, cuántos avisos esperan un reintento y cuántos no se pudieron entregar. «Mandar una prueba» le manda un aviso de prueba; «Ver los últimos avisos» muestra los 50 más recientes con su estado (entregado, pendiente o no se entregó), cuántos intentos llevó y por qué falló; uno que no se entregó se puede «Reenviar». «Pausar» deja de mandarle avisos sin borrarlo; «Reanudar» vuelve. Si su sistema no responde con un 2xx, Llevu reintenta a los 1, 5 y 30 minutos, a las 2 y a las 6 horas, y después lo da por no entregado. Los avisos se guardan 30 días. Crear, cambiar o pausar un webhook queda en la Actividad.

La dirección base es la del panel con /api/v1/integraciones/ (por ejemplo, https://app.llevu.com/api/v1/integraciones/). Toda llamada lleva la llave en la cabecera Authorization: Bearer <llave> y JSON.

Llamada Qué hace
GET yo/ Probar la conexión: responde la empresa, el nombre de la llave y sus límites
POST pedidos/ Crear un pedido (201) o devolver el que ya tiene ese código (200)
POST pedidos/lote/ Hasta 500 pedidos en una llamada ({"pedidos": [...]}), cada uno como POST pedidos/
GET pedidos/?cambiados_desde=… Los pedidos que cambiaron, en el orden en que cambiaron, en páginas
GET pedidos/<código>/ El pedido con ese código: su estado y en qué ruta va
GET pedidos/<código>/comprobante/ El comprobante en PDF, cuando el pedido se cerró
POST pedidos/<código>/cancelar/ Cancelarlo mientras no esté entregado ni cerrado
GET rutas/?fecha=AAAA-MM-DD Las rutas de ese día (hoy, si no se dice) con sus paradas en orden
GET rutas/<número>/recorrido/ Por dónde pasó el vehículo de esa ruta

El cuerpo de POST pedidos/:

Campo Obligatorio Qué es
codigo_externo Sí El código del pedido en su sistema (sin «/»). Con el mismo código no se crea otro: un reenvío devuelve el que ya existe
cliente.codigo_externo Sí El código del cliente en su sistema: el próximo pedido del mismo cliente lo reusa
cliente.nombre Sí Se usa la primera vez; después, lo que su empresa corrija en Llevu se respeta
cliente.telefono, cliente.correo No Solo la primera vez
direccion.texto Sí, salvo con direccion_codigo Como se dice en la región. Si el cliente ya tiene esa dirección (mismo texto), se reusa
direccion_codigo No El código de una dirección del cliente en Llevu (ver Clientes): el pedido va a esa. Si no existe y viene direccion, se crea con ese código; si es de otro cliente, es error
direccion.referencia, direccion.latitud, direccion.longitud No Con latitud y longitud, la dirección nueva queda con su pin
tipo No entrega (por defecto) o recogida
prioridad No baja, normal o alta; si no viene, la del cliente
fecha, ventana_desde, ventana_hasta No El día (por defecto, hoy) y el horario de entrega; sin horario, el de la dirección si lo tiene
recibe_nombre, recibe_telefono, recibe_correo No Quién recibe
descripcion, bultos, peso_kg, volumen_m3 No Qué lleva
monto_a_cobrar, moneda, metodo_de_cobro No El cobro contra entrega, con su moneda (CRC, USD…)
notas_para_el_conductor No Lo que ve el conductor en la app
campos_propios No {clave: valor} de los campos propios de su empresa

Un ejemplo:

Ventana de terminal
curl -X POST https://app.llevu.com/api/v1/integraciones/pedidos/ \
-H "Authorization: Bearer <llave>" -H "Content-Type: application/json" \
-d '{"codigo_externo": "SO-1001", "cliente": {"codigo_externo": "C-1", "nombre": "Ferretería La Esquina"}, "direccion": {"texto": "San Pedro, 200 m al norte de la iglesia"}, "bultos": 3, "monto_a_cobrar": "12500.00", "moneda": "CRC"}'

La respuesta: {"id", "numero", "codigo_externo", "estado", "fecha", "ruta", "creado"}. numero es el número del pedido en Llevu; ruta, el número de su ruta (o null); creado es false si ese código ya tenía su pedido. Los estados: pendiente, en_ruta, entregado, fallido y cancelado.

Los errores llegan con {"error": {"codigo", "mensaje", "detalles"}}: 401 sin_llave (falta, no vale o está revocada), 422 datos_invalidos (el campo en detalles), 404 sin_pedido, 422 pedido_cerrado (ya no se puede cancelar) y 429 si pasa de 300 llamadas por minuto con la misma llave. Si una llamada se corta, repítala igual: con el mismo código no se duplica el pedido.

El lote. POST pedidos/lote/ responde un resultado por pedido, en el mismo orden: creado, existente (ese código ya tenía su pedido) o error (con los errores por campo). Uno con error no frena a los demás, y repetir la llamada entera es seguro: lo que ya se creó vuelve como existente. Más de 500 pedidos es un 422 de toda la llamada.

Lo que cambió. GET pedidos/?cambiados_desde=2026-11-08T14:00:00-06:00 trae los pedidos que cambiaron después de ese momento (también fecha=AAAA-MM-DD para los de un día), del que cambió antes al que cambió último, de a 100 (por_pagina hasta 200). Cada pedido trae su estado, prioridad, cliente y dirección con sus códigos, la ruta y su orden, actualizado_en y, si se cerró, el resultado (si se entregó, a qué hora, quién recibió, el motivo y el cobro) y si tiene comprobante. Si la respuesta trae siguiente, pida la página que sigue con cursor=<siguiente>: no se salta ni repite pedidos aunque cambien otros mientras tanto. Para no perder nada, guarde el actualizado_en del último pedido que procesó y úselo como cambiados_desde la próxima vez.

Los avisos. Cada aviso es un POST con JSON a la dirección del webhook, con estas cabeceras: X-Llevu-Evento (pedido.estado, ruta.iniciada, ruta.terminada o prueba), X-Llevu-Entrega (el id de la entrega: un reintento lleva el mismo) y X-Llevu-Firma: t=<segundos>,v1=<firma>. El cuerpo trae id (el del aviso), evento, version, ocurrio_en y el pedido (como en la consulta, más comprobante con la ruta para bajarlo) o la ruta (con sus paradas). Para verificarlo, calcule HMAC-SHA256 con el secreto sobre t, un punto y el cuerpo tal como llegó, y compárelo con v1; descarte los que tengan un t de hace más de 5 minutos. Responda un 2xx apenas lo reciba (en menos de 10 segundos) y procese después: Llevu no sigue redirecciones y puede mandar el mismo aviso más de una vez (use id para no procesarlo dos veces).

import hashlib, hmac, time
def es_de_llevu(secreto: str, cabecera: str, cuerpo: bytes) -> bool:
partes = dict(p.split("=", 1) for p in cabecera.split(","))
esperada = hmac.new(secreto.encode(), f"{partes['t']}.".encode() + cuerpo, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperada, partes["v1"]) and abs(time.time() - int(partes["t"])) < 300