Zaroz
Guía de la API de Units

Webhooks

Un webhook es el mismo flujo de eventos, empujado a tu servidor. Las rutas de webhooks son solo para personas (un usuario con sesión o un token personal con acceso de gestión); un principal de servicio no puede llamarlas.

Un Workload puede tener hasta 5 webhooks. Cada uno tiene una URL https en un host público, un filtro event_types (una lista, o todos) y un estado: active, failing o disabled. Al crearlo se devuelve {webhook, secret}: el secreto de firma, whsec_ y 43 caracteres, se muestra una sola vez (y de nuevo solo con rotate-secret). Un webhook nuevo empieza con los eventos posteriores al momento de crearlo. disabled_reason es failing_72h si se desactivó por fallos repetidos, y null en otro caso.

RutaHace
GET, POST .../units/webhookslistar, crear
GET, PATCH, DELETE .../units/webhooks/{webhook_id}leer, cambiar, borrar
POST .../units/webhooks/{webhook_id}/rotate-secretsecreto nuevo; el anterior sigue firmando 24 horas
POST .../units/webhooks/{webhook_id}/testenvía un webhook.ping
POST .../units/webhooks/{webhook_id}/enablereactiva un webhook deshabilitado
GET .../units/webhooks/{webhook_id}/deliveriesintentos, del más reciente al más antiguo: hora, rango de eventos (from_seq, to_seq), código de estado, latencia, error, los primeros 4 KiB de tu respuesta, próximo intento (30 días). limit de 1 a 100 (por defecto 50), before pagina hacia atrás
GET .../units/webhooks/metricspor webhook en las últimas window_hours (1 a 720, por defecto 24): succeeded, failed, success_rate, latencia media, p95 y máxima

test responde con el propio intento (estado, latencia, error) y está limitado a 6 por minuto y webhook. Funciona también con un webhook deshabilitado y no mueve el cursor. Cambiar url o event_types con PATCH sigue entregando desde el mismo cursor; event_types: [] significa todos los tipos.

curl

TerminalCode
W="$ZAROZ_API/projects/$PROJECT/service-instances/$SERVICE/units/webhooks" curl -sS -X POST "$W" -H "$AUTH" -H "Content-Type: application/json" \ -d '{"url": "https://hooks.example.com/zaroz", "event_types": ["unit.exited", "unit.failed"]}' curl -sS -X POST "$W/<webhook.id de la respuesta de crear>/test" -H "$AUTH" # sends webhook.ping

TypeScript

TypeScriptCode
const W = `${UNITS}/webhooks`; const hook = await call("POST", W, { url: "https://hooks.example.com/zaroz", event_types: ["unit.exited", "unit.failed"] }); console.log(hook.body.secret); // whsec_...: shown once await call("POST", `${W}/${hook.body.webhook.id}/test`); // sends webhook.ping

Python

Code
W = f"{UNITS}/webhooks" _, hook = call("POST", W, {"url": "https://hooks.example.com/zaroz", "event_types": ["unit.exited", "unit.failed"]}) print(hook["secret"]) # whsec_...: shown once call("POST", f"{W}/{hook['webhook']['id']}/test") # sends webhook.ping

Qué recibes

Un POST de {webhook_id, delivery_id, events: [...]}: como máximo 50 eventos o 256 KiB, en orden de seq, al menos una vez. El siguiente lote solo se envía tras tu 2xx (en 10 segundos), así que la promesa es el orden. Deduplica por seq.

Cabeceras: Zaroz-Signature: v1=<hex>, Zaroz-Timestamp (segundos Unix), Zaroz-Webhook-Id, Zaroz-Delivery-Id.

Verificar la firma

La firma es HMAC-SHA256(secreto, "{timestamp}.{cuerpo en bruto}") en hexadecimal. Durante una rotación la cabecera lleva dos valores v1= separados por coma: acepta cualquiera. Rechaza una marca de tiempo a más de 5 minutos. Verifica los bytes en bruto que recibiste, no un cuerpo reserializado.

curl

TerminalCode
# the signed string is "<Zaroz-Timestamp>.<raw body>"; compare against each v1= value printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$WHSEC" -hex

TypeScript

TypeScriptCode
import { createHmac, timingSafeEqual } from "node:crypto"; export function verify(secret: string, timestamp: string, rawBody: string, header: string): boolean { if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // refuse > 5 min off const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex"); return header.split(",").some((part) => { const sig = part.trim().replace(/^v1=/, ""); return sig.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected)); }); }

Python

Code
import hashlib, hmac, time def verify(secret: str, timestamp: str, raw_body: bytes, header: str) -> bool: if abs(time.time() - int(timestamp)) > 300: # refuse more than 5 minutes off return False signed = timestamp.encode() + b"." + raw_body expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() return any(hmac.compare_digest(p.strip().removeprefix("v1="), expected) for p in header.split(","))

Reintentos y desactivación

Las entregas fallidas se reintentan a los 5 s, 30 s, 2 min, 10 min, 30 min, 1 h y después cada hora. Un webhook que falla durante 72 horas pasa a disabled (failing_72h) y se avisa a los propietarios de la organización; su cursor se conserva. Tras enable, la entrega continúa desde el cursor si los eventos aún se conservan, o desde el evento más antiguo conservado, precedido de un evento events.gap {from_seq, to_seq} para que sepas que te perdiste algunos.

A qué se niega a llamar la plataforma

El destino se resuelve una vez por intento y se comprueba cada dirección: se rechazan loopback, privadas, link-local, CGNAT, multicast, unique-local IPv6 y los rangos propios de la plataforma, En crear o actualizar la respuesta es un rechazo invalid-field-value con address_not_allowed: ... en details; después, al entregar, el registro de entregas muestra address_not_allowed como error. Los demás valores de error son dns, connect, timeout, http_status, redirect_not_followed, invalid_url y transport. Un sexto webhook se rechaza con 409 business-rule-violation (too_many_webhooks en details). No se siguen redirecciones. Solo se leen los primeros 4 KiB de tu respuesta.

Last modified on