Envíos CaimánEnvíos Caimán

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).

Tu sistema (tienda) Envíos Caimán ────────────────── ───────────── POST /v1/quotes ─────────────► cotiza (precio, distancia, días) POST /v1/deliveries ─────────────► crea el envío y busca repartidor ◄───────────── webhooks firmados en cada estado GET /v1/deliveries/{id} ─────────────► estado + GPS del repartidor enlace /track/{token} ─────────────► tracking público para tu cliente
Pensada para Cuba: rutas por carretera con OSRM auto-hospedado (sin servicios geobloqueados), pagos en efectivo entre cliente y repartidor (la plataforma no intermedia el dinero del envío) y tolerancia total a cortes: si algún componente cae, la API responde igual con datos de respaldo.

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_123
Nunca pongas la API key en el front-end público de tu web o app: las llamadas deben salir de tu servidor. El secret HMAC no viaja nunca por la red — solo se usa para verificar firmas.

Quickstart — 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ódigoSignificado
400JSON inválido o falta un campo requerido
401API key/sesión inválida o ausente
403Tu 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
404Recurso inexistente o de otro socio (no se filtra la existencia)
409Conflicto de estado (p. ej. cancelar un envío ya entregado)
429Demasiados 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.

Consulta 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

POST /v1/quotesAPI key

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
}
El vehículo lo elige el peso y el tamaño de la carga, no tú: si mandas 800 kg, la cotización sube sola a camioneta y el precio la refleja. Los campos vehiculo y vehiculo_clase te dicen con cuál se cotizó, para que puedas explicárselo a tu cliente.
Mínimo facturable: 1 km. Un trayecto más corto se cobra como si fuera de 1 km. Si replicas el cálculo por tu cuenta y comparas, esa es la diferencia que verás en envíos de pocas cuadras: 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

POST /v1/deliveriesAPI key
CampoDescripción
external_idId del pedido en TU sistema; vuelve en cada webhook
verticalreqcomida (mensajería urbana) o logistica (carga)
cargo.tiporeqcomidaCaliente · documento · paquete · fragil · voluminoso · refrigerado
cargo.peso_kgreqPeso en kg de un bulto, no del total
cargo.cantidadBultos 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")
paradasLista de puntos intermedios [{lat, lon, municipio, direccion}]
programado_paraISO 8601: reserva a futuro en vez de buscar ahora
ida_y_vueltatrue cobra la distancia doble (llevar y traer)
paga_envioremitente (default) o destinatario
notasComentarios para el repartidor
webhook_urlURL 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": { … }
}
El código de entrega lo da quien recibe al repartidor: es la prueba de que el paquete llegó a las manos correctas. Con sin_telefono_dest: true el cierre pasa a ser foto + confirmación remota del remitente.

Listar tus envíos

GET /v1/deliveriesAPI key

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

GET /v1/deliveries/{id}API key

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

GET /v1/deliveries/{id}/routeAPI key

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

POST /v1/deliveries/{id}/cancelAPI key

Cancela un envío no entregado. Si ya se entregó responde 409.

Fotos de recogida y entrega

GET /v1/deliveries/{id}/proofAPI key

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

GET /v1/coverage?municipio=Cerropúblico

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));
}
Responde 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

WS /v1/ws/deliveries/{id}?token=…sesión

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

GET /track/{share_token}público

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

programado ──► buscando_repartidor ──► asignado ──► en_camino_a_recogida │ entregado ◄── en_camino_a_entrega ◄── recogido ◄──┘ en cualquier punto antes de entregar: cancelado · fallido
EstadoQué significa
programadoReservado a futuro; se ofrecerá cerca de la fecha
buscando_repartidorOfertado a los repartidores elegibles (caduca a las 24 h)
asignadoUn repartidor lo aceptó
en_camino_a_recogidaVa hacia tu local
recogidoTiene el paquete (con foto de recogida)
en_camino_a_entregaVa hacia el destinatario, GPS en vivo
entregadoCerrado con código OTP (o confirmación remota) + foto
cancelado / fallidoNo 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.

tarifa = base + CUP/km del vehículo × km + energía (L/km × precio litro, o kWh/km × precio kWh) + peso (solo "logistica" y vehículos ligeros) × multiplicador de la provincia de recogida × surge (solo urbano)

Valores por defecto — el operador puede ajustarlos todos desde su panel:

ComponenteValor por defecto
Base80 CUP por viaje
Bicicleta · moto25 · 22 CUP/km
Auto · camioneta35 · 55 CUP/km
Camión · rastra86,03 · 177,88 CUP/km (de fichas de costo reales)
Combustible · electricidad156 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