Webhooks
Un webhook es una URL de tu sistema a la que Ruta le hace un POST firmado cuando pasa algo con una entrega de la empresa, venga de la API, el panel, la app, un CSV, el formulario de un comercio o una integración.
Registrar un endpoint
En Configuración → Desarrolladores → Avisos a tu sistema (webhooks) (un administrador):
- URL:
https://…(en un entorno local de pruebas tambiénhttp://127.0.0.1ohttp://localhost). Hasta 5 por empresa. - Eventos: los que querés recibir (abajo). Si no elegís, recibís
delivery.createdydelivery.status_changed, como en la primera versión. - Comercio (opcional): sólo las entregas de ese comercio. Útil para darle a cada tienda su propio endpoint.
- Tipo: Real (entregas reales, «producción») o De prueba (las del entorno de prueba).
El secreto de firma (whsec_…) se muestra una sola vez. Podés cambiar los eventos después con Cambiar eventos, pausar y reactivar el webhook y ver el registro de avisos.
Eventos
| Evento | Cuándo | Datos extra en data |
|---|---|---|
delivery.created |
Se creó una entrega. | — |
delivery.status_changed |
Cambió el estado o el intento (genérico; se conserva por compatibilidad). | reason sólo en un inicio deshecho (abajo) |
delivery.assigned |
Se asignó a un repartidor. | reason sólo en un inicio deshecho (abajo) |
delivery.picked_up |
El repartidor retiró el paquete. | place (BASE o SHIPPER), pickedUpAt |
delivery.out_for_delivery |
Salió a entregar. | — |
delivery.delivered |
Se entregó. | collectedAmount, collectionMethod, currency |
delivery.failed |
No se pudo entregar. | (failureReasonCode ya viene siempre) |
delivery.cancelled |
Se canceló. | — |
delivery.retry_scheduled |
Se programó otro intento tras un fallo. | (attemptNumber + 1) |
delivery.proof_added |
Se guardó el comprobante. | proofId, recordedAt |
delivery.collection_settled |
El courier marcó el cobro en efectivo como rendido: su repartidor ya le entregó el dinero. | collectedAmount, collectionMethod, currency, collectedAt, settledAt |
webhook.test |
Lo pediste con Enviar evento de prueba. | endpointId, mode, message |
Un mismo cambio puede generar dos avisos: por ejemplo, al entregar se emiten delivery.status_changed y delivery.delivered. Suscribite a uno de los dos estilos.
El aviso
POST /webhooks/ruta HTTP/1.1
Content-Type: application/json
User-Agent: Ruta-Webhooks/1
Ruta-Event-Id: 3b0f7c3e-8a51-4a43-9f39-0d4c6b1f2a10
Ruta-Event-Type: delivery.delivered
Ruta-Signature: t=1791651734,v1=5f2c…(64 hex)
{"id":"3b0f7c3e-8a51-4a43-9f39-0d4c6b1f2a10","type":"delivery.delivered","createdAt":"2026-10-10T15:20:00.000000Z","test":false,
"data":{"deliveryId":"6f1c…","status":"DELIVERED","previousStatus":"OUT_FOR_DELIVERY","attemptNumber":1,"failureReasonCode":null,
"externalReference":"1042","shipperId":null,"collectedAmount":"1250.00","collectionMethod":"CASH","currency":"UYU"}}datatrae siempredeliveryId,status,previousStatus,attemptNumber,failureReasonCode,externalReferenceyshipperId, más los datos extra del evento. Nunca datos del destinatario: consultáGET /api/v1/deliveries/{id}si los necesitás.reason: "START_UNDONE"llega endelivery.status_changedydelivery.assignedcuando la entrega volvió deOUT_FOR_DELIVERYaASSIGNEDcon el mismoattemptNumber: el repartidor o la oficina deshizo «Iniciar entrega» («Volver a asignada»). Sigue con el mismo repartidor; no es una asignación nueva. En los demás avisosreasonno viene.currencyes la moneda decollectedAmount: la del cobro, salvo que el repartidor haya cobrado en la otra moneda (entonces dice en cuál cobró).testestruepara eventos del entorno de prueba y parawebhook.test.- El orden de las claves del JSON no está garantizado: verificá la firma sobre los bytes crudos que recibiste, antes de parsear.
Verificar la firma
v1 es el HMAC-SHA256, en hexadecimal, del texto "<t>.<cuerpo crudo>" con tu secreto. Rechazá avisos con t a más de 5 minutos de tu reloj y compará en tiempo constante.
// Node.js 18+ (Express con express.raw({ type: 'application/json' }))
import crypto from 'node:crypto'
export function verifyRuta(secret, header, rawBody, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(',').map(part => part.trim().split('=')))
const t = Number(parts.t)
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false
const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest('hex')
const given = Buffer.from(parts.v1 ?? '', 'utf8')
return given.length === expected.length && crypto.timingSafeEqual(given, Buffer.from(expected, 'utf8'))
}# Python 3
import hashlib, hmac, time
def verify_ruta(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
parts = dict(part.strip().split("=", 1) for part in header.split(","))
t = int(parts.get("t", "0"))
if abs(time.time() - t) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))<?php // PHP 8 (por ejemplo, un plugin de WooCommerce)
function verify_ruta(string $secret, string $header, string $rawBody, int $tolerance = 300): bool {
$parts = [];
foreach (explode(',', $header) as $part) {
[$k, $v] = array_map('trim', explode('=', $part, 2)) + [null, null];
$parts[$k] = $v;
}
$t = (int) ($parts['t'] ?? 0);
if (abs(time() - $t) > $tolerance) return false;
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
return hash_equals($expected, $parts['v1'] ?? '');
}
// $rawBody = file_get_contents('php://input'); $header = $_SERVER['HTTP_RUTA_SIGNATURE'];Respuesta y reintentos
- Respondé 2xx en menos de 10 segundos y procesá en segundo plano. No seguimos redirecciones.
- Cualquier otra respuesta, timeout o error de conexión se reintenta con espera creciente: 30 s, 2,5 min, 12,5 min, ~1 h, ~5 h y luego cada 6 h, hasta 8 intentos. Después el aviso queda «Falló» y un administrador puede reenviarlo desde el registro.
- Tras 20 fallos seguidos el webhook se pausa; reactivalo desde el panel cuando tu sistema vuelva a responder. Los avisos pendientes siguen donde quedaron.
Duplicados y orden
- La entrega es «al menos una vez»: un reintento o un reenvío llevan el mismo
Ruta-Event-Idy el mismo cuerpo. Guardá los ids procesados y descartá repetidos. - Los avisos de una misma entrega llegan en orden: el siguiente espera mientras el anterior esté pendiente.
Probar el endpoint
Enviar evento de prueba (o POST /api/account/webhooks/{id}/test con la sesión de un administrador) encola un webhook.test firmado igual que cualquier aviso, sin crear ninguna entrega ni consumir cupo. Hay uno por vez por endpoint; el registro muestra cómo salió.