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.cu/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 |
| 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.
Cotizar sin crear
Mismo cuerpo que crear (sin direcciones ni contacto). Responde:
{
"tarifa_estimada_cup": 197.0, // precio final sugerido (con surge si aplica)
"tarifa_base_cup": 197.0, // sin surge
"desglose": {
"base_cup": 80.0, // arranque del viaje
"distancia_cup": 117.0, // por tramos de km (ver "Tarifa")
"peso_cup": 0.0 // solo en vertical "logistica"
},
"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
}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 |
| cargo.dimensiones_cm | {largo, ancho, alto} del 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.
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
La distancia se cobra por tramos, como el transporte real (el km urbano vale más que el km de carretera). Valores por defecto — el operador puede ajustarlos desde su panel:
| Componente | Valor por defecto |
|---|---|
| Base | 80 CUP por viaje |
| Km 0–15 (urbano) | 25 CUP/km |
| Km 15–100 (interurbano) | 15 CUP/km |
| Km 100+ (carretera) | 6 CUP/km |
Peso (solo logistica) | 3 CUP/kg — ×2 si el envío es nacional |
| Surge (solo urbano) | ×1.0 a ×2.0 según la demanda de la zona (por municipio de recogida); nunca aplica a nacionales |
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