API de Envíos Caimán
Domicilio como servicio para Cuba. Tu tienda, restaurante o sistema mantiene su catálogo y sus pedidos; nosotros resolvemos el transporte completo: matching de repartidor, GPS en vivo, tracking público y prueba de entrega.
Introducción
La API es REST + JSON sobre HTTPS. Todas las rutas cuelgan de /v1. Los importes van siempre en CUP, las coordenadas en grados decimales (WGS84) y las fechas en ISO 8601 UTC (2026-07-22T18:30:00Z).
Autenticación
Cada negocio recibe una API key (pk_…) y un secret (sk_…) para verificar webhooks. La key va en cada petición, en una de estas dos cabeceras:
X-Api-Key: pk_demo_123
# o equivalente:
Authorization: Bearer pk_demo_123Quickstart — tu primer envío en 5 minutos
1. Cotiza (opcional, para mostrar el precio antes de confirmar):
curl -X POST https://api.envioscaiman.com/v1/quotes \
-H "Content-Type: application/json" -H "X-Api-Key: pk_demo_123" \
-d '{
"vertical": "comida",
"cargo": { "tipo": "comidaCaliente", "peso_kg": 1.5,
"dimensiones_cm": { "largo": 35, "ancho": 35, "alto": 8 } },
"recogida": { "lat": 23.1367, "lon": -82.3674,
"municipio": "Centro Habana", "provincia": "La Habana" },
"entrega": { "lat": 23.1069, "lon": -82.3819,
"municipio": "Cerro", "provincia": "La Habana" }
}'2. Crea el envío (mismo cuerpo + direcciones y contacto):
curl -X POST https://api.envioscaiman.com/v1/deliveries \
-H "Content-Type: application/json" -H "X-Api-Key: pk_demo_123" \
-d '{
"external_id": "PEDIDO-123",
"vertical": "comida",
"cargo": { "tipo": "comidaCaliente", "peso_kg": 1.5,
"dimensiones_cm": { "largo": 35, "ancho": 35, "alto": 8 } },
"recogida": { "lat": 23.1367, "lon": -82.3674,
"municipio": "Centro Habana", "provincia": "La Habana",
"direccion": "Calle 23 #456", "referencia": "La pizzería" },
"entrega": { "lat": 23.1069, "lon": -82.3819,
"municipio": "Cerro", "provincia": "La Habana",
"direccion": "Calle 10 #12", "referencia": "Edificio azul, apto 3B",
"contacto": { "nombre": "Cliente", "telefono": "+53 5XXXXXXX" } },
"webhook_url": "https://tu-tienda.com/hooks/domicilio"
}'3. Guarda el delivery_id y el share_token de la respuesta. Comparte /track/{share_token} con tu cliente y escucha los webhooks. Listo.
Errores
Los errores devuelven un JSON con un mensaje en español listo para registrar o mostrar:
{ "error": "API key inválida o ausente" }| Código | Significado |
|---|---|
| 400 | JSON inválido o falta un campo requerido |
| 401 | API key/sesión inválida o ausente |
| 403 | Tu cuenta no puede crear envíos ahora mismo: agotaste los envíos de prueba y falta verificar el negocio, o la suscripción está vencida. El mensaje dice cuál de los dos |
| 404 | Recurso inexistente o de otro socio (no se filtra la existencia) |
| 409 | Conflicto de estado (p. ej. cancelar un envío ya entregado) |
| 429 | Demasiados intentos (p. ej. login) |
Idempotencia: reintenta POST /v1/deliveries con la cabecera Idempotency-Key: <clave-única> y el mismo cuerpo: si ya se creó, devolvemos el envío existente en vez de duplicarlo.
Antes de integrar: verificación
Una cuenta nueva puede crear 2 envíos de prueba sin verificar, para que compruebes que la integración funciona. A partir de ahí hay que enviar los documentos del negocio desde el panel; mientras no se aprueben, POST /v1/deliveries responde 403.
GET /v1/partner antes de empezar: el campo bloqueo te dice si puedes crear envíos ahora mismo (null = sí) y pedidos_prueba_restantes cuántas pruebas te quedan. Es mejor comprobarlo al arrancar tu integración que descubrirlo con un 403 en mitad de un pedido real.Cotizar sin crear
Mismo cuerpo que crear (sin direcciones ni contacto). Responde:
{
"tarifa_estimada_cup": 301.0, // precio final sugerido (con surge si aplica)
"tarifa_base_cup": 301.0, // sin surge
"desglose": {
"base_cup": 80.0, // arranque del viaje
"distancia_cup": 215.0, // CUP/km del vehículo × km (ver "Tarifa")
"energia_cup": 0.0, // combustible o electricidad del viaje
"peso_cup": 6.0 // solo "logistica" y vehículos ligeros
},
"vehiculo": "bicicleta", // vehículo mínimo que exige esta carga
"vehiculo_clase": 1, // 1 = el más pequeño de la flota
"multiplicador_provincia": 1.0, // ajuste por provincia de recogida
"surge": 1.0, // multiplicador por demanda (urbano)
"precio_alto": false,
"nacional": false, // true si cruza de provincia
"entrega_dias_min": 2, // solo en nacionales
"entrega_dias_max": 3,
"hay_repartidores": true,
"repartidores_elegibles": 4,
"distancia_viaje_km": 4.7, // por carretera real (OSRM)
"duracion_min": 8, // solo si OSRM respondió
"ruta_real": true
}vehiculo y vehiculo_clase te dicen con cuál se cotizó, para que puedas explicárselo a tu cliente.distancia_viaje_km puede decir 0.4 y aun así el importe corresponder a 1 km.No es un redondeo arbitrario: el costo de un reparto no es proporcional a la distancia. Llegar, aparcar, encontrar el portal y entregar pesa igual en 300 m que en 1 km. Sin ese suelo, los envíos muy cortos salen por debajo de lo que cuesta hacerlos, ningún repartidor los acepta y el pedido se queda dando vueltas hasta caducar — que para tu cliente es peor que pagar el mínimo.
En ida y vuelta el mínimo se aplica por trayecto: un viaje de 400 m de ida y vuelta factura 2 km, porque son dos salidas reales.
Crear un envío
| Campo | Descripción |
|---|---|
| external_id | Id del pedido en TU sistema; vuelve en cada webhook |
| verticalreq | comida (mensajería urbana) o logistica (carga) |
| cargo.tiporeq | comidaCaliente · documento · paquete · fragil · voluminoso · refrigerado |
| cargo.peso_kgreq | Peso en kg de un bulto, no del total |
| cargo.cantidad | Bultos iguales. Por defecto 1. El peso y el volumen se multiplican por esta cifra; el largo no (lo que tiene que caber en el vehículo es una pieza, no la pila) |
| cargo.dimensiones_cm | {largo, ancho, alto} de un bulto |
| recogida / entregareq | {lat, lon, municipio, provincia, direccion, referencia, contacto{nombre, telefono}}. La referencia es clave en Cuba ("edificio azul, al lado de la farmacia") |
| paradas | Lista de puntos intermedios [{lat, lon, municipio, direccion}] |
| programado_para | ISO 8601: reserva a futuro en vez de buscar ahora |
| ida_y_vuelta | true cobra la distancia doble (llevar y traer) |
| paga_envio | remitente (default) o destinatario |
| notas | Comentarios para el repartidor |
| webhook_url | URL para los eventos de ESTE envío (si no, la global del socio) |
Respuesta 201 (resumen):
{
"delivery_id": "dlv_abc123",
"external_id": "PEDIDO-123",
"estado": "buscando_repartidor",
"tarifa_cup": 197.0,
"comision_pct": 0.15, // comisión de plataforma (informativa)
"comision_cup": 30.0,
"ganancia_repartidor_cup": 167.0,
"codigo_entrega": "4821", // OTP que cierra la entrega
"share_token": "trk_xxx", // para /track/{token} público
"eta_recogida_min": 7,
"duracion_viaje_min": 8,
"recogida": { … }, "entrega": { … }
}sin_telefono_dest: true el cierre pasa a ser foto + confirmación remota del remitente.Listar tus envíos
Todos los envíos de tu negocio, del más nuevo al más viejo: { "deliveries": [ … ] } con el mismo formato del detalle.
Consultar un envío
Estado actual + posición GPS del repartidor (ubicacion: {lat, lon}), distancia a la entrega, prueba de entrega, calificación y propina.
Geometría de la ruta
Puntos [lat, lon] de la ruta por calles (OSRM), listos para pintar en un mapa: { "puntos": [[23.137, -82.362], …] }. Lista vacía si el motor de rutas no está disponible.
Cancelar
Cancela un envío no entregado. Si ya se entregó responde 409.
Fotos de recogida y entrega
El repartidor fotografía el paquete al recogerlo y al entregarlo. Es la evidencia si tu cliente reclama que no le llegó o que llegó mal.
{
"foto_recogida": "data:image/jpeg;base64,...", // null si aún no recogió
"recogida_ts": "2026-07-28T19:12:04Z",
"foto": "data:image/jpeg;base64,...", // la de entrega
"prueba_ts": "2026-07-28T19:41:22Z"
}Las imágenes van en base64 y pesan. En GET /v1/deliveries tienes los indicadores prueba_recogida y prueba_entrega (booleanos): pide las fotos solo cuando existan y cuando de verdad las vayas a mostrar.
Zonas cubiertas
Consulta si operamos en un municipio y con qué verticales, para validar el checkout de tu tienda antes de cotizar.
Webhooks
En cada cambio de estado hacemos POST a tu webhook_url con reintentos. El cuerpo:
{
"delivery_id": "dlv_abc123",
"external_id": "PEDIDO-123",
"estado": "recogido",
"repartidor": "Ana",
"timestamp": "2026-07-22T14:32:10Z"
}Verificar la firma HMAC
Cada webhook lleva X-Signature: sha256=<hex> — el HMAC-SHA256 del cuerpo crudo con tu secret. Verifícalo SIEMPRE:
# Python
import hmac, hashlib
def firma_valida(cuerpo_crudo: bytes, cabecera: str, secret: str) -> bool:
esperada = 'sha256=' + hmac.new(
secret.encode(), cuerpo_crudo, hashlib.sha256).hexdigest()
return hmac.compare_digest(esperada, cabecera)// Node.js
const crypto = require('crypto');
function firmaValida(cuerpoCrudo, cabecera, secret) {
const esperada = 'sha256=' + crypto.createHmac('sha256', secret)
.update(cuerpoCrudo).digest('hex');
return crypto.timingSafeEqual(Buffer.from(esperada), Buffer.from(cabecera));
}2xx rápido (< 5 s) y procesa en segundo plano. Un webhook puede llegar más de una vez: usa delivery_id + estado como clave de deduplicación.WebSocket de seguimiento
Push en tiempo real del envío completo (estado + GPS) en cada cambio. Lo usa la app del cliente; disponible también para integraciones que prefieran socket en vez de polling.
Página pública de tracking
Página web ligera para el destinatario final: ve al repartidor en vivo sin cuenta ni app. Compártela por WhatsApp/SMS desde tu sistema. La versión JSON está en /v1/track/{share_token} (vista limitada, sin datos sensibles).
Ciclo de vida de estados
| Estado | Qué significa |
|---|---|
| programado | Reservado a futuro; se ofrecerá cerca de la fecha |
| buscando_repartidor | Ofertado a los repartidores elegibles (caduca a las 24 h) |
| asignado | Un repartidor lo aceptó |
| en_camino_a_recogida | Va hacia tu local |
| recogido | Tiene el paquete (con foto de recogida) |
| en_camino_a_entrega | Va hacia el destinatario, GPS en vivo |
| entregado | Cerrado con código OTP (o confirmación remota) + foto |
| cancelado / fallido | No se completó |
Cómo se calcula la tarifa
El precio sale del costo real del vehículo que hace falta para la carga, con la misma estructura que una ficha de costo (Res. 44/2021 MFP): un CUP por kilómetro propio de cada vehículo —que cubre desgaste, salario, depreciación y margen— y la energía aparte, según su norma de consumo. Por eso un eléctrico sale más barato que su equivalente de gasolina.
Valores por defecto — el operador puede ajustarlos todos desde su panel:
| Componente | Valor por defecto |
|---|---|
| Base | 80 CUP por viaje |
| Bicicleta · moto | 25 · 22 CUP/km |
| Auto · camioneta | 35 · 55 CUP/km |
| Camión · rastra | 86,03 · 177,88 CUP/km (de fichas de costo reales) |
| Combustible · electricidad | 156 CUP/L · 1,6 CUP/kWh, multiplicados por la norma de consumo del vehículo (p. ej. 0,20 L/km un camión) |
Peso (solo logistica) | 3 CUP/kg — ×2 si es nacional. Solo en vehículos ligeros: de auto para arriba la capacidad ya la cobra el propio vehículo |
| Provincia | ×1.0 por defecto; ajustable por provincia de recogida (en nacionales se aplica el mayor de origen y destino) |
| Surge (solo urbano) | ×1.0 a ×2.0 según la demanda de la zona (por municipio de recogida); nunca aplica a nacionales |
La clase de vehículo la decide la carga: peso, volumen y dimensión mayor. La flota va, de menor a mayor capacidad, de bicicleta y bicimoto a camión y rastra, pasando por triciclos (de pedales y eléctricos), moto, auto y camioneta. Un envío dentro de la misma provincia no está limitado a moto: si la carga lo pide, sube sola de categoría. Un envío entre provincias nunca baja de auto.
Un envío nacional (recogida y entrega en provincias distintas) exige un transportista con viajes nacionales, no lleva surge, y la cotización incluye entrega_dias_min/max (~600 km/día + margen de coordinación). El pago es en efectivo directo cliente→repartidor; la plataforma cobra su comisión (15% por defecto) al repartidor al entregar.
Envíos Caimán · API v1