RutaDocs

API

Entregas

Todas las respuestas llevan Cache-Control: no-store. Los cuerpos son objetos JSON cerrados: una clave desconocida o repetida se rechaza con 400. Los parámetros de consulta también son cerrados.

Crear una entrega

POST /api/v1/deliveries · permiso deliveries:write · encabezado Idempotency-Key obligatorio (idempotencia).

Campo Obligatorio Regla
customerName sí 1–120 caracteres, sin caracteres de control.
address sí 1–240 caracteres; admite saltos de línea. Ruta no geocodifica la dirección.
recipientPhone no E.164, por ejemplo +59899123456. También acepta un celular uruguayo sin prefijo (099123456) y lo guarda como +59899123456; las respuestas lo devuelven siempre en E.164.
recipientEmail no Email de quien recibe, para los avisos por email del courier (y como alternativa si el WhatsApp no llega). Hasta 254 caracteres; se guarda sin espacios y en minúsculas. Un email mal formado responde 400.
instructions no Hasta 280 caracteres.
externalReference no El número de tu pedido: 1–64 caracteres A-Z a-z 0-9 _ . : # / -, empezando por letra o dígito. Único por empresa y comercio.
scheduledDate, windowStart, windowEnd no Programación: YYYY-MM-DD y HH:mm en la hora de la empresa. Requiere que el courier la tenga habilitada.
amountToCollect no Cobro contra entrega, texto decimal con punto ("1250.00"), en la moneda de currency.
currency no Moneda del cobro: "UYU" (pesos uruguayos, el valor si no lo mandás) o "USD" (dólares). Con "USD" hace falta amountToCollect. Ruta nunca convierte entre monedas: el repartidor cobra y da el vuelto en esa moneda. Otro valor responde 400 INVALID_CURRENCY.
shipperId no Id de un comercio activo de la empresa. Con una clave de comercio, se usa el suyo.
zoneId no Id de una zona activa (ver zonas).
destinationLat, destinationLng no Números (grados decimales), los dos o ninguno: el punto exacto de la puerta. Tiene que estar en Uruguay (latitud −35,2 a −30,0, longitud −58,6 a −53,0); si no, 400 VALIDATION_FAILED. Sin punto, y con la búsqueda de direcciones activa en la cuenta, Ruta puede ponerle uno aproximado a los pocos minutos, sólo si encuentra la dirección sin dudas; nunca reemplaza un punto que mandaste. Ver Direcciones y puntos en el mapa.

Todos los campos son texto o null, salvo las coordenadas, que son números. Las reglas son exactamente las del alta en el panel: plan, cuenta en solo lectura, programación, cobro, comercio y zona.

Nota

Cupo mensual. Si el courier ya usó las entregas de su plan este mes, el pedido no se rechaza: se crea igual (201) con quotaHeld: true y queda retenido, sin repartidor, hasta que el courier tenga cupo (el 1.º del mes o al subir de plan). Entonces sale solo y quotaHeld pasa a false. No tenés que reintentar nada.

Respuesta 201 Created con Location: /api/v1/deliveries/{id} e Idempotent-Replayed: false:

JSON
{
  "id": "6f1c1d0e-2f5e-4bb5-9c1a-1c1b1f0e9a77",
  "status": "CREATED",
  "customerName": "Ana Pérez",
  "address": "Av. Italia 1234, apto 5",
  "recipientPhone": "+59899123456",
  "instructions": "Timbre 5",
  "trackingUrl": "https://<tu-courier>/track/0b6d…",
  "scheduledDate": null, "windowStart": null, "windowEnd": null, "scheduleTimeZone": null,
  "amountToCollect": "1250.00",
  "shipperId": null, "zoneId": null,
  "destinationLat": null, "destinationLng": null,
  "attemptNumber": 1, "failureReasonCode": null,
  "createdAt": "2026-10-10T15:04:05.123456Z",
  "startedAt": null, "deliveredAt": null, "cancelledAt": null, "failedAt": null,
  "externalReference": "1042",
  "collectedAmount": null, "collectionMethod": null, "collectedAt": null, "settledAt": null,
  "test": false,
  "recipientEmail": null,
  "quotaHeld": false, "quotaHeldAt": null,
  "currency": "UYU",
  "collectedCurrency": null
}

Compartí trackingUrl con el destinatario: es la misma página de seguimiento que usa el panel.

Si externalReference ya está en otra entrega de la empresa y el mismo comercio, la respuesta es 409 EXTERNAL_REFERENCE_TAKEN y no se consume cupo. Dos comercios distintos pueden tener el mismo número de pedido.

Crear en lote

POST /api/v1/deliveries/batch · permiso deliveries:write. Hasta 100 entregas por llamada (cuerpo de hasta 512 KiB), cada una con su propia idempotencyKey dentro del objeto:

Terminal
curl -X POST "$RUTA_API/api/v1/deliveries/batch" \
  -H "Authorization: Bearer $RUTA_API_KEY" -H "Content-Type: application/json" \
  -d '{"deliveries": [
        {"idempotencyKey": "pedido-1042", "customerName": "Ana Pérez", "address": "Av. Italia 1234", "externalReference": "1042"},
        {"idempotencyKey": "pedido-1043", "customerName": "Bruno Díaz", "address": "Rivera 2020", "externalReference": "1043"}
      ]}'

Cada ítem se crea por separado, en orden: uno que falla no deshace a los demás. La respuesta es 200 con un resultado por ítem:

JSON
{
  "results": [
    {"index": 0, "status": 201, "idempotencyKey": "pedido-1042", "replayed": false, "delivery": {"id": "…"}, "error": null},
    {"index": 1, "status": 409, "idempotencyKey": "pedido-1043", "replayed": false, "delivery": null,
     "error": {"code": "EXTERNAL_REFERENCE_TAKEN", "detail": "External reference already used by another delivery"}}
  ],
  "created": 1,
  "failed": 1
}

Repetir el lote completo con las mismas claves es seguro: los ítems ya creados vuelven con replayed: true. Si la plataforma falla a mitad (5xx), los ítems anteriores quedaron creados; reintentá el lote igual.

Consultar y listar

Terminal
curl "$RUTA_API/api/v1/deliveries/$ID" -H "Authorization: Bearer $RUTA_API_KEY"
curl "$RUTA_API/api/v1/deliveries?limit=50" -H "Authorization: Bearer $RUTA_API_KEY"
curl "$RUTA_API/api/v1/deliveries?limit=50&cursor=<nextCursor>" -H "Authorization: Bearer $RUTA_API_KEY"
curl "$RUTA_API/api/v1/deliveries?externalReference=1042" -H "Authorization: Bearer $RUTA_API_KEY"

Permiso deliveries:read. El listado devuelve {"deliveries": [...], "nextCursor": "..." | null}, de la entrega más nueva a la más vieja. limit va de 1 a 100 (50 por defecto); seguí nextCursor hasta recibir null. Un id de otra empresa (o de otro comercio, con una clave de comercio), inexistente o mal formado responde igual: 404 NOT_FOUND.

Corregir antes de que salga

PATCH /api/v1/deliveries/{id} · permiso deliveries:write. Sólo mientras la entrega está CREATED o ASSIGNED; después responde 409 DELIVERY_STATE_INVALID.

Campo Regla
customerName, address Ausente o null conserva el valor; vacío es 400.
recipientPhone, recipientEmail, instructions, amountToCollect, zoneId, externalReference Ausente o null conserva; "" borra; un valor lo reemplaza.
currency Ausente o null conserva la moneda; "UYU" o "USD" la cambia (con "USD" la entrega tiene que tener monto). Borrar el monto vuelve a pesos.
destinationLat, destinationLng Si enviás alguna, el par reemplaza el punto; las dos en null lo borran.

La programación y el comercio no se cambian por esta vía. Un cuerpo vacío es 400.

Terminal
curl -X PATCH "$RUTA_API/api/v1/deliveries/6f1c1d0e-2f5e-4bb5-9c1a-1c1b1f0e9a77" \
  -H "Authorization: Bearer $RUTA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
       "address": "Av. Italia 1234, apto 7",
       "instructions": ""
     }'
JavaScript
// Node.js 18+ (fetch incluido)
const response = await fetch(`${process.env.RUTA_API}/api/v1/deliveries/6f1c1d0e-2f5e-4bb5-9c1a-1c1b1f0e9a77`, {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.RUTA_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "address": "Av. Italia 1234, apto 7",
    "instructions": ""
  }),
})
if (!response.ok) throw new Error(`Ruta respondió ${response.status}`)
const delivery = await response.json()
Python
# Python 3 con requests (pip install requests)
import os

import requests

response = requests.patch(
    f"{os.environ['RUTA_API']}/api/v1/deliveries/6f1c1d0e-2f5e-4bb5-9c1a-1c1b1f0e9a77",
    headers={
        "Authorization": f"Bearer {os.environ['RUTA_API_KEY']}",
    },
    json={
        "address": "Av. Italia 1234, apto 7",
        "instructions": "",
    },
    timeout=30,
)
response.raise_for_status()
delivery = response.json()
PHP
<?php // PHP 8 con la extensión curl
$ch = curl_init(getenv('RUTA_API') . '/api/v1/deliveries/6f1c1d0e-2f5e-4bb5-9c1a-1c1b1f0e9a77');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST => 'PATCH',
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('RUTA_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'address' => 'Av. Italia 1234, apto 7',
        'instructions' => '',
    ], JSON_UNESCAPED_UNICODE),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 30,
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if ($status >= 400) throw new RuntimeException("Ruta respondió $status");
$delivery = json_decode($body, true);

Cancelar

POST /api/v1/deliveries/{id}/cancel · permiso deliveries:write · sin cuerpo. Sólo CREATED o ASSIGNED (si ya salió, 409 DELIVERY_STATE_INVALID). Es idempotente: cancelar una entrega ya cancelada responde 200 con la misma entrega y el mismo cancelledAt.

Estados

status Significa
CREATED Cargada, sin repartidor.
ASSIGNED Asignada a un repartidor.
OUT_FOR_DELIVERY El repartidor salió a entregar. Si la inició por error puede volver a ASSIGNED con el mismo attemptNumber (eventos delivery.status_changed y delivery.assigned).
DELIVERED Entregada. Si había cobro, collectedAmount, collectionMethod y collectedAt dicen lo declarado.
FAILED No se pudo entregar (failureReasonCode). Puede reintentarse: vuelve a CREATED o ASSIGNED con attemptNumber + 1.
CANCELLED Cancelada.

Ver el ciclo de vida completo, con el evento de cada paso, y cada campo de la respuesta.

settledAt se completa cuando el courier rinde el cobro al comercio. La API nunca muestra la identidad ni la ubicación del repartidor, ni notas internas.

Zonas

GET /api/v1/zones · permiso deliveries:read → {"zones": [{"id": "…", "name": "Centro", "areas": "Ciudad Vieja, Cordón"}]}: las zonas activas de la empresa, para elegir zoneId; areas son los barrios que cubre, tal como los escribió el courier (puede ser vacío).

Ver en Markdown