RutaDocs

Webhooks

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én http://127.0.0.1 o http://localhost). Hasta 5 por empresa.
  • Eventos: los que querés recibir (abajo). Si no elegís, recibís delivery.created y delivery.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

HTTP
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"}}
  • data trae siempre deliveryId, status, previousStatus, attemptNumber, failureReasonCode, externalReference y shipperId, 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 en delivery.status_changed y delivery.assigned cuando la entrega volvió de OUT_FOR_DELIVERY a ASSIGNED con el mismo attemptNumber: 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 avisos reason no viene.
  • currency es la moneda de collectedAmount: la del cobro, salvo que el repartidor haya cobrado en la otra moneda (entonces dice en cuál cobró).
  • test es true para eventos del entorno de prueba y para webhook.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.

JavaScript
// 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
# 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 // 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-Id y 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ó.

Ver en Markdown