AntivioenIniciar sesión

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

POST /webhooks/antivio HTTP/1.1
Content-Type: application/json
User-Agent: Antivio-Webhooks/1.0 (+https://antivio.app/developers/webhooks)
webhook-id: evt_0193a6b2c4d87c1e9f0a1b2c3d4e5f60
webhook-timestamp: 1760716800
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
webhook-event: shipment.delivered
{
  "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.

Node.js

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

Python

import base64, hashlib, hmac, time

def verify_antivio(secret: str, headers, raw_body: bytes) -> bool:
    try:
        msg_id, timestamp = headers["webhook-id"], headers["webhook-timestamp"]
        signatures = headers["webhook-signature"]
        sent_at = int(timestamp)
    except (KeyError, TypeError, ValueError):
        return False
    if abs(time.time() - sent_at) > 300:
        return False
    key = base64.b64decode(secret.removeprefix("whsec_"))
    digest = hmac.new(key, f"{msg_id}.{timestamp}.".encode() + raw_body, hashlib.sha256).digest()
    expected = "v1," + base64.b64encode(digest).decode()
    return any(hmac.compare_digest(sig, expected) for sig in signatures.split(" "))

PHP

function verify_antivio(string $secret, array $headers, string $rawBody): bool {
    $id = $headers['webhook-id'] ?? '';
    $timestamp = $headers['webhook-timestamp'] ?? '';
    $signatures = $headers['webhook-signature'] ?? '';
    if ($id === '' || $signatures === '' || !ctype_digit($timestamp)) return false;
    if (abs(time() - (int) $timestamp) > 300) return false;
    $key = base64_decode(substr($secret, strlen('whsec_')));
    $expected = 'v1,' . base64_encode(hash_hmac('sha256', "$id.$timestamp.$rawBody", $key, true));
    foreach (explode(' ', $signatures) as $signature) {
        if (hash_equals($expected, $signature)) return true;
    }
    return false;
}
// $rawBody = file_get_contents('php://input');

Go

func VerifyAntivio(secret string, h http.Header, rawBody []byte) bool {
	id, timestamp := h.Get("webhook-id"), h.Get("webhook-timestamp")
	ts, err := strconv.ParseInt(timestamp, 10, 64)
	if err != nil || math.Abs(float64(time.Now().Unix()-ts)) > 300 {
		return false
	}
	key, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, "whsec_"))
	if err != nil {
		return false
	}
	mac := hmac.New(sha256.New, key)
	mac.Write([]byte(id + "." + timestamp + "."))
	mac.Write(rawBody)
	expected := []byte("v1," + base64.StdEncoding.EncodeToString(mac.Sum(nil)))
	for _, signature := range strings.Split(h.Get("webhook-signature"), " ") {
		if hmac.Equal([]byte(signature), expected) {
			return true
		}
	}
	return false
}

C#

using System.Security.Cryptography;
using System.Text;

static bool VerifyAntivio(string secret, string id, string timestamp, string signatures, byte[] rawBody)
{
    if (!long.TryParse(timestamp, out var ts)
        || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - ts) > 300) return false;
    var key = Convert.FromBase64String(secret["whsec_".Length..]);
    using var hmac = new HMACSHA256(key);
    var signed = Encoding.UTF8.GetBytes($"{id}.{timestamp}.").Concat(rawBody).ToArray();
    var expected = Encoding.UTF8.GetBytes("v1," + Convert.ToBase64String(hmac.ComputeHash(signed)));
    return signatures.Split(' ').Any(s =>
        CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(s), expected));
}

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.

Todas las guías