Un webhook invia le decisioni bot e bot_high di Bot Protection a un tuo endpoint: l'inoltro a un canale di chat, una coda di ticket, la tua revisione antifrode. Il tuo server continua a ricevere ogni decisione da /v1/decide come prima; il webhook ne è una copia, inviata dopo la risposta, e non fa mai aspettare una decisione.
Imposta il tuo endpoint
Nella pagina Configurazione della dashboard, sotto Webhook, un proprietario scrive l'indirizzo dell'endpoint e sceglie cosa riceve:
- Ogni decisione delle classi scelte,
bot,bot_higho entrambe, in lotti fino a 100, pochi secondi dopo la decisione. - Un riepilogo ogni 5, 15 o 60 minuti: quante decisioni
botebot_highci sono state.
L'indirizzo è https://, sulla porta 443, con un nome come hooks.example.com, mai un indirizzo IP. Korzen non invia nulla a un indirizzo di una rete privata, qualunque sia l'indirizzo del nome, e lo verifica a ogni consegna.
La prima volta che un endpoint viene salvato, Configurazione mostra il suo segreto di firma, kz_whs_ e 64 caratteri, una sola volta: copialo nella configurazione del tuo endpoint. Crea un nuovo segreto lo sostituisce, mostrato anch'esso una sola volta; per 24 ore il vecchio firma ancora ogni consegna insieme al nuovo, così il tuo endpoint continua a verificare mentre aggiorni la sua copia. Invia un test invia ora una consegna test firmata e mostra cosa ha risposto il tuo endpoint.
Il payload
Ogni consegna è un POST con un corpo JSON. Un lotto di decisioni:
{
"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
}
Ogni decisione è at, il momento in cui è stata presa, e questi membri della risposta che /v1/decide ha dato al tuo server: request_id, class, bot_score, human_score, reasons, action, intended_action, mode, step_up, token_required, identity_limited, seen_before, crawler, decoy e own_server. Nient'altro: mai il nome di una regola, il context che hai inviato o il link della pagina di challenge. dropped conta le decisioni per cui questo lotto non aveva spazio.
Un riepilogo:
{"type": "summary", "id": "…", "from": "2026-10-28T12:00:00Z", "to": "2026-10-28T12:15:00Z", "bot": 412, "bot_high": 37}
Un test: {"type": "test", "id": "…", "at": "…"}. Leggi prima type: può arrivare un tipo nuovo, e a ognuno può essere aggiunto un campo.
id è lo stesso a ogni tentativo di una consegna: se l'hai già gestita, rispondi 2xx e scartala. Le consegne possono arrivare fuori ordine.
Cosa promette una consegna
- Almeno una volta, una volta in coda. Una consegna in coda viene tentata finché il tuo endpoint risponde
2xxo viene messo in pausa, quindi la stessa consegna può arrivare due volte: scarta la ripetizione in base al suoid. - Best effort prima di allora. Ogni server di Korzen tiene una decisione fino a 10 secondi prima di metterla in coda, e mette in coda tutto ciò che tiene quando si ferma per un deploy. Un server che va in crash perde ciò che teneva, e le decisioni oltre lo spazio di un lotto o il limite orario del tuo piano sono contate in
dropped, mai inviate. - Il webhook è una copia, non il registro. Ogni decisione resta nella pagina Decisioni della dashboard, che anche gli strumenti
list_decisionseget_decisiondel server MCP leggono: cerca lì una decisione con il suorequest_id.
Verifica la firma
Ogni consegna porta Korzen-Signature: t=<secondi Unix>,v1=<hex>: l'HMAC-SHA256, con il tuo segreto di firma, di t, un punto e il corpo esattamente come ricevuto. Calcolalo sui byte grezzi, prima di qualsiasi parsing JSON, confronta in tempo costante e rifiuta un t a più di 5 minuti dal tuo orologio, così una consegna registrata una volta non può essere rinviata più tardi. Per 24 ore dopo un nuovo segreto segue un secondo v1, con il vecchio: accetta una consegna quando un qualsiasi v1 corrisponde.
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)
Risposta e nuovi tentativi
Rispondi con un qualsiasi 2xx entro 5 secondi: il corpo non viene letto. Qualsiasi altra cosa, anche un redirect, o nessuna risposta in tempo, è un errore, e la consegna viene ritentata 30 secondi dopo, poi ogni volta dopo il doppio del tempo, 8 tentativi in circa un'ora. Fai il lavoro lento dopo aver risposto.
Finché il tuo endpoint fallisce, le nuove decisioni aspettano, fino a 100, e le altre sono contate in dropped. Un endpoint che fallisce per un'ora viene messo in pausa: non gli viene inviato nulla, le decisioni nel frattempo non vengono inviate, e ogni proprietario riceve un'email. Configurazione mostra lo stato o l'errore dell'ultima consegna; Riprendi torna a inviare dalla prossima decisione.
Limiti
Ogni piano invia fino a un certo numero di decisioni all'ora: la prova gratuita 1.000, Starter 10.000, Pro 50.000, Scale 200.000. Oltre, le decisioni sono contate nel dropped del lotto successivo, e l'ora seguente riparte da zero. Un riepilogo viene sempre inviato. Un webhook non conta nelle tue decisioni del mese.