Un webhook envía las decisiones bot y bot_high de Bot Protection a un endpoint tuyo: el reenvío a un canal de chat, una cola de tickets, tu propia revisión de fraude. Tu servidor sigue recibiendo cada decisión de /v1/decide como antes; el webhook es una copia, enviada después de la respuesta, y nunca hace esperar a una decisión.
Define tu endpoint
En la página Configuración del panel, en Webhook, un propietario escribe la dirección del endpoint y elige qué recibe:
- Cada decisión de las clases elegidas,
bot,bot_higho ambas, en lotes de hasta 100, segundos después de la decisión. - Un resumen cada 5, 15 o 60 minutos: cuántas decisiones
botybot_highhubo.
La dirección es https://, en el puerto 443, con un nombre como hooks.example.com, nunca una dirección IP. Korzen no envía nada a una dirección de una red privada, sea cual sea la dirección del nombre, y lo comprueba en cada entrega.
La primera vez que se guarda un endpoint, Configuración muestra su secreto de firma, kz_whs_ y 64 caracteres, una sola vez: cópialo en la configuración de tu endpoint. Crear un nuevo secreto lo reemplaza, también mostrado una sola vez; durante 24 horas el anterior sigue firmando cada entrega junto a él, para que tu endpoint siga verificando mientras actualizas su copia. Enviar una prueba envía ahora una entrega test firmada y muestra lo que respondió tu endpoint.
El payload
Cada entrega es un POST con un cuerpo JSON. Un lote de decisiones:
{
"type": "decisions",
"id": "0193a5f0-7c1e-7d2a-9b4e-3f1a2c9d8e01",
"decisions": [
{
"at": "2026-10-28T12:00:00.123Z",
"request_id": "…",
"class": "bot_high",
"action": "block",
"intended_action": "block",
"bot_score": 97,
"mode": "block",
"reasons": ["behaviour", "inconsistency"]
}
],
"dropped": 0
}
Cada decisión es at, el momento en que se tomó, y estos miembros de la respuesta que /v1/decide dio a tu servidor: request_id, class, bot_score, human_score, reasons, action, intended_action, mode, step_up, token_required, identity_limited, seen_before, crawler, decoy y own_server. Nada más: nunca el nombre de una regla, el context que enviaste ni el enlace de la página de desafío. dropped cuenta las decisiones para las que este lote no tuvo espacio.
Un resumen:
{"type": "summary", "id": "…", "from": "2026-10-28T12:00:00Z", "to": "2026-10-28T12:15:00Z", "bot": 412, "bot_high": 37}
Una prueba: {"type": "test", "id": "…", "at": "…"}. Lee type primero: puede llegar un tipo nuevo, y se puede añadir un campo a cualquiera de ellos.
id es el mismo en cada intento de una entrega: si ya la procesaste, responde 2xx y descártala. Las entregas pueden llegar desordenadas.
Lo que promete una entrega
- Al menos una vez, una vez encolada. Una entrega encolada se intenta hasta que tu endpoint responde
2xxo queda en pausa, así que la misma entrega puede llegar dos veces: descarta la repetición por suid. - Mejor esfuerzo antes de eso. Cada servidor de Korzen guarda una decisión hasta 10 segundos antes de encolarla, y encola todo lo que guarda cuando se detiene para un deploy. Un servidor que se cae pierde lo que guardaba, y las decisiones que superan el espacio de un lote o el límite por hora de tu plan se cuentan en
dropped, nunca se envían. - El webhook es una copia, no el registro. Cada decisión queda en la página Decisiones del panel, que las herramientas
list_decisionsyget_decisiondel servidor MCP también leen: busca allí una decisión por surequest_id.
Comprueba la firma
Cada entrega lleva Korzen-Signature: t=<segundos Unix>,v1=<hex>: el HMAC-SHA256, con tu secreto de firma, de t, un punto y el cuerpo tal como llegó. Calcúlalo sobre los bytes en bruto, antes de cualquier parsing de JSON, compara en tiempo constante y rechaza un t a más de 5 minutos de tu reloj, para que una entrega grabada una vez no pueda reenviarse después. Durante 24 horas después de un nuevo secreto, sigue un segundo v1, con el anterior: acepta una entrega cuando cualquier v1 coincide.
import { createHmac, timingSafeEqual } from "node:crypto"; // raw: the body as received (a Buffer); header: Korzen-Signature function verify(raw, header, secret) { const parts = header.split(",").map((p) => p.split("=")); const t = Number(parts.find(([k]) => k === "t")?.[1]); if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > 300) return false; const mac = createHmac("sha256", secret).update(`${t}.`).update(raw).digest(); return parts.some(([k, v]) => { const got = Buffer.from(k === "v1" ? v ?? "" : "", "hex"); return got.length === mac.length && timingSafeEqual(got, mac); }); }
import hashlib, hmac, time def verify(raw: bytes, header: str, secret: str) -> bool: parts = [p.partition("=")[::2] for p in header.split(",")] t = next((v for k, v in parts if k == "t"), "") if not t.isdigit() or abs(time.time() - int(t)) > 300: return False mac = hmac.new(secret.encode(), f"{t}.".encode() + raw, hashlib.sha256).hexdigest() return any(k == "v1" and hmac.compare_digest(mac, v) for k, v in parts)
Respuesta y reintentos
Responde cualquier 2xx en 5 segundos: el cuerpo no se lee. Cualquier otra cosa, también una redirección, o ninguna respuesta a tiempo, es un fallo, y la entrega se reintenta 30 segundos después, y luego tras el doble de tiempo cada vez, 8 intentos en una hora aproximadamente. Haz el trabajo lento después de responder.
Mientras tu endpoint falla, las decisiones nuevas esperan, hasta 100, y el resto se cuenta en dropped. Un endpoint que falla durante una hora queda en pausa: no se le envía nada, las decisiones de mientras tanto no se envían, y cada propietario recibe un email. Configuración muestra el estado o el error de la última entrega; Reanudar vuelve a enviar desde la próxima decisión.
Límites
Cada plan envía hasta un número de decisiones por hora: la prueba gratuita 1.000, Starter 10.000, Pro 50.000, Scale 200.000. Pasado ese número, las decisiones se cuentan en el dropped del siguiente lote, y la hora siguiente empieza de cero. Un resumen se envía siempre. Un webhook no cuenta para tus decisiones del mes.