AntivioenPortal

Webhooks

Antivio avisa a tu servidor cuando algo le pasa a un envío, con un POST HTTPS firmado. Los mensajes siguen la especificación abierta Standard Webhooks: verifícalos con unas pocas líneas de código (abajo) o con cualquier librería de Standard Webhooks. Sin SDK de Antivio.

Endpoints

Agrega endpoints en Configuración → Desarrolladores → Webhooks (comercios: Portal → Desarrolladores) o con la API (POST /v1/webhook-endpoints, permiso webhooks:write).

Eventos

TipoCuándo
shipment.createdse creó un envío (borrador o confirmado)
shipment.status_changedcambió su estado; data.previous_status → data.status
shipment.deliveredse entregó (también se envía como shipment.status_changed)
shipment.exceptionpasó a exception: perdido, dañado o retenido (también como shipment.status_changed)

GET /v1/webhook-event-types los lista.

El mensaje

HTTP
POST /webhooks/antivio HTTP/1.1
Content-Type: application/json
User-Agent: Antivio-Webhooks/1.0 (+https://developers.antivio.app/webhooks)
webhook-id: evt_0193a6b2c4d87c1e9f0a1b2c3d4e5f60
webhook-timestamp: 1760716800
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
webhook-event: shipment.delivered
JSON
{
  "id": "evt_0193a6b2c4d87c1e9f0a1b2c3d4e5f60",
  "type": "shipment.delivered",
  "api_version": "2026-10-01",
  "created_at": "2026-10-17T15:20:00.123Z",
  "tenant_id": "0192f5a0-0000-7000-8000-000000000001",
  "livemode": true,
  "data": {
    "status": "delivered",
    "previous_status": "out_for_delivery",
    "shipment": {
      "id": "0193a6b2-…",
      "tracking_number": "AV2610ABCDE7K",
      "status": "delivered",
      "external_ref": "ORDER-1001",
      "customer_account_id": "…",
      "recipient_name": "Ana Pérez",
      "payment_mode": "cod",
      "cod_amount_minor": 150000,
      "currency": "DOP",
      "parcels": [ … ],
      "…": "the same fields as GET /v1/shipments/{id}"
    }
  }
}

Verifica la firma

Verifica siempre antes de confiar en un mensaje. La firma es HMAC-SHA256 con tu secreto (el base64 después de whsec_) sobre {webhook-id}.{webhook-timestamp}.{cuerpo}, enviada como v1,<base64>. Durante una rotación de secreto la cabecera trae dos firmas separadas por un espacio; acepta el mensaje si una coincide. Rechaza marcas de tiempo a más de 5 minutos de tu reloj (repeticiones).

Usa los bytes crudos del cuerpo tal como llegan, antes de interpretar el JSON.

import crypto from "node:crypto";

export function verifyAntivio(secret, headers, rawBody) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"];
  if (!id || !timestamp || !signatures || !/^\d+$/.test(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const key = Buffer.from(secret.slice("whsec_".length), "base64");
  const expected = Buffer.from(
    "v1," + crypto.createHmac("sha256", key).update(`${id}.${timestamp}.`).update(rawBody).digest("base64"),
  );
  return signatures.split(" ").some((signature) => {
    const candidate = Buffer.from(signature);
    return candidate.length === expected.length && crypto.timingSafeEqual(candidate, expected);
  });
}
// Express: app.post("/webhooks/antivio", express.raw({ type: "application/json" }), (req, res) => {
//   if (!verifyAntivio(process.env.ANTIVIO_WEBHOOK_SECRET, req.headers, req.body)) return res.sendStatus(400);
//   …; res.sendStatus(204);
// });

Vector de prueba (de la especificación Standard Webhooks): secreto whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw, webhook-id msg_p5jXN8AQM9LWM0D4loKWxJek, webhook-timestamp 1614265330, cuerpo {"test": 2432232314} → firma v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE= (desactiva tu control de reloj al probarlo).

Responde rápido, procesa después

Reintentos y desactivación

Todo lo que no sea 2xx (o sin respuesta en 10 s) se reintenta a los 30 s, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h y 24 h: 9 intentos en unos 3 días.

Registro de entregas

GET /v1/webhook-endpoints/{id}/deliveries lista las entregas (filtra por status, pagina con cursor); GET /v1/webhook-deliveries/{id} muestra el mensaje enviado y cada intento: cuándo, cuánto tardó, tu código de estado y el inicio de tu respuesta. El historial se guarda 30 días.

Rota el secreto

POST /v1/webhook-endpoints/{id}/rotate-secret devuelve un secreto nuevo. Durante 24 horas los mensajes se firman con el nuevo y con el anterior, así despliegas el nuevo sin perder mensajes.

Prueba en local

Antivio no puede llegar a localhost. Expón tu servidor local con un túnel (por ejemplo cloudflared tunnel --url http://localhost:3000 o ngrok http 3000) y usa su URL HTTPS, o empieza con un inspector de peticiones como https://webhook.site. Enviar evento de prueba en el panel (o POST /v1/webhook-endpoints/{id}/test) muestra la respuesta de tu servidor al instante.