Korzen
Bot Protection
En esta documentación

Webhooks

Cada decisión de bot, o un resumen cada pocos minutos, enviada a tu endpoint y firmada con HMAC-SHA256: el payload, la firma, los reintentos y los límites.

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_high o ambas, en lotes de hasta 100, segundos después de la decisión.
  • Un resumen cada 5, 15 o 60 minutos: cuántas decisiones bot y bot_high hubo.

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 2xx o queda en pausa, así que la misma entrega puede llegar dos veces: descarta la repetición por su id.
  • 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_decisions y get_decision del servidor MCP también leen: busca allí una decisión por su request_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.