Webhooks
Los webhooks avisan a tu backend cuando pasa algo que requiere acción: un evento queda en revisión, hay una coincidencia en listas o un analista decide un caso.
Registrar un endpoint
Con una llave admin:
curl -s "$SAFEGATE_URL/v1/webhooks" \
-H "Authorization: Bearer $SAFEGATE_ADMIN_KEY" -H "Content-Type: application/json" \
-d '{"url": "https://api.tucasino.mx/webhooks/safegate", "events": ["score.review", "score.decline", "case.decided"]}'
La respuesta trae el secret de firma (whsec_…) una sola vez. La URL debe ser https con un host público. Máximo 10 endpoints por entorno. Si omites events, recibes todos.
POST /v1/webhooks/{id}/test envía un evento webhook.test para que pruebes tu receptor.
Eventos
| Tipo | Cuándo | data |
|---|---|---|
score.review, score.decline |
Una evaluación terminó en review / decline | event_id, event_type, score, decision, reasons (códigos), rules_triggered, screening_status, ruleset_version |
screening.match |
Screening con match o potential_match |
event_id, status, lists_version, hits (list, kind, entity_id, score) |
case.decided |
Un analista decidió un caso en el panel | review_id, decision (approve / decline / escalate), case_status, label, event_ids |
subject.erased |
Terminó un borrado de datos | erasure_id, environment, erased_at, counts |
webhook.test |
Prueba manual | endpoint_id |
Los webhooks no llevan datos personales, solo ids, códigos y scores. Cruza con tus registros usando event_id.
Entrega
Hacemos POST de {"id": "whe_…", "type", "created_at", "environment", "data"} con estos encabezados:
| Encabezado | Uso |
|---|---|
Sentinel-Signature |
t=<unix>,v1=<hex>: verifica la firma |
Sentinel-Delivery |
whd_…, igual en todos los reintentos: úsalo para descartar duplicados |
Sentinel-Event |
El tipo de evento |
Cualquier respuesta 2xx en menos de 10 s cuenta como entregada. Si no, reintentamos aprox. a 1 min, 5 min, 30 min, 2 h, 6 h y 12 h; después la entrega queda como dead y puedes reenviarla con POST /v1/webhooks/deliveries/{id}/retry. Las redirecciones no se siguen. La entrega es "al menos una vez": un mismo webhook puede llegar dos veces.
Verificar la firma
La firma es HMAC-SHA256(secret, "<t>.<cuerpo crudo>"). Calcúlala sobre el cuerpo crudo (antes de parsear el JSON) y rechaza t con más de 5 minutos de antigüedad.
Python:
import hashlib, hmac, time
def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts["t"])
mac = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return abs(time.time() - t) <= tolerance and hmac.compare_digest(mac, parts.get("v1", ""))
Node.js:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret, header, rawBody, tolerance = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
const t = Number(parts.t);
const mac = createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest("hex");
const given = Buffer.from(parts.v1 ?? "", "utf8");
const expected = Buffer.from(mac, "utf8");
return Math.abs(Date.now() / 1000 - t) <= tolerance
&& given.length === expected.length && timingSafeEqual(given, expected);
}
Rotar el secreto
POST /v1/webhooks/{id}/rotate-secret regresa un secreto nuevo y aplica de inmediato, también a los reintentos pendientes.
Historial
GET /v1/webhooks/{id}/deliveries?status=dead muestra las entregas fallidas (las últimas 200 por endpoint, durante 14 días), con el último código HTTP y el error.