Webhooks: eventos y verificación de la firma
Cómo recibir en tu sistema un aviso firmado cuando se calcula un score, cambia alguien en monitoreo o se registra una decisión, y cómo verificar la firma.
Actualizado el
Qué es un webhook de detect.ar
Es un aviso que detect.ar le manda a tu sistema cuando pasa algo en tu espacio de trabajo: se guardó un score nuevo o el monitoreo detectó un cambio. Tu sistema recibe un POST con JSON, firmado con un secreto que solo conocen detect.ar y vos.
Son avisos salientes que se configuran desde la app. No son una API para pedirle consultas o scores a detect.ar desde tu sistema.
Los webhooks no dependen del plan. Los eventos salen de los perfiles de Personas y del monitoreo, que sí necesitan un plan pago.
Agregar un endpoint
Endpoints
Solo URLs https públicas. Hasta 5 por workspace.
https://sistema.ejemplo.com.ar/webhooks/detectar
HabilitadoScore calculado, Alerta de monitoreo
Últimas entregas (2)
- EntregadoAlerta de monitoreoHTTP 20403/10/2026 09:40
- ReintentandoScore calculadoHTTP 503, 2 intentos03/10/2026 09:38
Paso 1: Abrí la sección Webhooks
Está en Ajustes, sección Webhooks. Solo los administradores del espacio de trabajo pueden configurarla.
Paso 2: Cargá la URL y los eventos
Con Agregar endpoint se abre un formulario con la URL y la lista de Eventos. La URL tiene que empezar con https:// y ser accesible desde internet. Hay que elegir al menos un evento. Cada espacio puede tener hasta 5 endpoints.
Paso 3: Guardá el secreto
Al agregarlo, la app muestra el secreto del endpoint, que empieza con whsec_. Se muestra una sola vez. Copialo y guardalo en tu sistema antes de cerrar con Listo, lo guardé.
Paso 4: Probalo
Probar manda un aviso de prueba firmado, con el evento webhook.test, y te dice qué respondió tu endpoint. Hay un tope de 10 pruebas por hora por espacio de trabajo. La prueba no se reintenta.
Endpoint agregado
Guardá este secreto en tu sistema: lo usás para verificar la firma de cada aviso.
No lo vamos a volver a mostrar. Si lo perdés, rotá el secreto para generar uno nuevo.
Cada endpoint de la lista tiene estas acciones:
- Interruptor
- Habilita o deshabilita el endpoint sin borrarlo. Deshabilitado, no recibe avisos.
- Probar
- Manda el aviso de prueba.
- Rotar secreto
- Genera un secreto nuevo y lo muestra una sola vez. El anterior deja de servir al instante: hasta que cargues el nuevo en tu sistema, la verificación de la firma va a fallar.
- Eliminar
- El endpoint deja de recibir avisos y se borra su registro de entregas. No se puede deshacer.
- Últimas entregas
- El evento, el estado (Entregado, Reintentando o Falló), el código HTTP que respondió tu sistema, la cantidad de intentos y la fecha. No muestra el contenido del aviso.
Eventos
| Evento | Cuándo se emite |
|---|---|
| subject.scored | Se guardó un score nuevo de una persona, a mano o por el monitoreo. En el formulario figura como «Score calculado». No se emite cuando el perfil muestra un score que ya estaba calculado. |
| subject.alert | El monitoreo detectó un cambio. Sale uno por alerta. En el formulario figura como «Alerta de monitoreo». |
| decision.recorded | Alguien de tu equipo registró una decisión en el perfil de una persona o de una empresa. En el formulario figura como «Decisión registrada». |
| webhook.test | Solo cuando usás el botón Probar. |
Formato del aviso
Cada aviso es un POST con Content-Type: application/json y tres headers propios: la firma, el nombre del evento y el identificador de la entrega.
POST /tu-ruta HTTP/1.1
Content-Type: application/json
X-Detectar-Signature: t=<unix>,v1=<hex>
X-Detectar-Event: subject.alert
X-Detectar-Delivery: <id de la entrega>El cuerpo tiene siempre la misma forma: id, event, createdAt, workspaceId y data, que cambia según el evento.
{
"id": "evt_0b0e6c1e-5a52-4d0e-9a59-0d2a4c7e1f11",
"event": "subject.alert",
"createdAt": "2026-10-03T12:40:00.000Z",
"workspaceId": "…",
"data": {
"alertId": "…",
"subjectId": "…",
"type": "situacion_bcra",
"severity": "alta",
"title": "Situación en el BCRA: 1 → 3",
"previous": "1",
"current": "3",
"detail": {
"source": "Central de Deudores del BCRA",
"signalKey": "bcra.peor_situacion",
"observedAt": "2026-10-03 12:40:00",
"previousComputedAt": "2026-09-05T12:40:00.000Z",
"direction": "empeora"
},
"createdAt": "2026-10-03T12:40:00.000Z"
}
}{ "subjectId": "…", "scoreId": "…", "score": 72, "band": "alto", "confidence": 0.82,
"modelVersion": "2026-10.2", "computedAt": "2026-10-03T12:40:00.000Z" }{ "decisionId": "…", "subjectId": "…", "decision": "aprobar_condicionado",
"policyOutcome": "rechazar", "override": true }{ "message": "Evento de prueba de detect.ar" }- Ningún aviso lleva el documento ni el nombre de la persona. La persona se identifica por subjectId, su identificador interno en tu espacio de trabajo. Es el mismo que aparece en la dirección de su perfil en la app.
- En subject.scored, score es null y band es "insuficiente" cuando no hay datos suficientes. band es "bloqueado" cuando hay una coincidencia confirmada en listas de sanciones.
- En decision.recorded, decision es "aprobar", "aprobar_condicionado", "revisar" o "rechazar". policyOutcome es lo que recomendaba la política de decisión activa, con los mismos valores, o null si no había una. override es true cuando la decisión quedó registrada como excepción.
Verificar la firma
El header X-Detectar-Signature trae dos valores: t, la hora del envío en segundos Unix, y v1, la firma. v1 es el HMAC-SHA256, en hexadecimal, del texto "<t>.<cuerpo>" con el secreto del endpoint.
- Calculalo sobre el cuerpo crudo, tal como llegó, antes de convertir el JSON. Si tu framework lo convierte y lo vuelve a escribir, la firma no va a coincidir.
- Rechazá las firmas con más de 5 minutos de antigüedad. Así nadie puede reenviarte un aviso viejo.
- Compará las firmas en tiempo constante.
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const SECRET = process.env.DETECTAR_WEBHOOK_SECRET; // whsec_…
const TOLERANCE_S = 300; // 5 minutos
function verify(header, rawBody) {
const parts = Object.fromEntries((header ?? "").split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!Number.isInteger(t) || !parts.v1) return false;
if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_S) return false;
const expected = createHmac("sha256", SECRET).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
return a.length === b.length && timingSafeEqual(a, b);
}
const app = express();
// express.raw: el cuerpo llega como Buffer, sin tocar.
app.post("/webhooks/detectar", express.raw({ type: "application/json" }), (req, res) => {
const raw = req.body.toString("utf8");
if (!verify(req.get("X-Detectar-Signature"), raw)) return res.status(401).end();
const event = JSON.parse(raw);
// Responder 2xx rápido y procesar después; deduplicar por event.id.
res.status(204).end();
});Entrega y reintentos
- Qué cuenta como entregado
- Una respuesta 2xx de tu sistema. Cualquier otra respuesta, o ninguna, cuenta como fallo.
- Tiempo de espera
- 10 segundos. Respondé 2xx enseguida y procesá el aviso después.
- Redirecciones
- No se siguen. Una respuesta 3xx cuenta como fallo.
- Reintentos
- Hasta 5 intentos en total. El primero es inmediato. Entre uno y otro hay una espera mínima de 1 minuto, 5 minutos, 30 minutos y 2 horas. Son esperas mínimas: el reintento puede salir más tarde. Después del quinto intento la entrega queda como fallida y no se vuelve a enviar.
- Deshabilitación por fallos
- 20 entregas fallidas seguidas deshabilitan el endpoint y dejan una alerta en Monitoreo. Una entrega exitosa pone el contador en cero. Al habilitarlo de nuevo desde Ajustes, el contador también vuelve a cero.
Estos destinos se rechazan al crear el endpoint y de nuevo en cada envío:
- URLs con http://.
- URLs con usuario y contraseña.
- localhost, direcciones IP privadas o internas y servicios de metadata de proveedores de nube.
detect.ar no escribe en sus registros el secreto ni el contenido de los avisos. Lo que se guarda de cada entrega es lo que ves en Últimas entregas.