Agent skill
KnowBe4 Webhooks Skill
Receive and authenticate KnowBe4 webhooks from KSAT (security awareness training) and PhishER. Use when setting up a KnowBe4 webhook endpoint, adding the secret custom header, debugging 401s, or handling phishing, training and PhishER message events such as Phish Failure Removed.
Install this skill
npx skills add hookdeck/webhook-skills --skill knowbe4-webhooks
KnowBe4 sends JSON webhooks from two products: the KSAT console (Platinum, Diamond or SAT Advanced tiers), which posts phishing, training and general events, and PhishER, whose Send to Webhook action posts data about reported messages. Webhooks are not signed. Add a custom header with a secret value to the webhook, and your endpoint authenticates each request by comparing that header with the value you stored.
When to Use This Skill
- How do I receive KnowBe4 webhooks?
- How do I authenticate KSAT or PhishER webhook requests?
- Does KnowBe4 sign webhook payloads? (No: use a secret custom header)
- How do I handle a
Phish Failure Removedevent? - Why does my KnowBe4 webhook endpoint return 401, or why was the webhook deactivated?
Authentication Setup
Add a custom header with a secret value to the KSAT or PhishER webhook.
- The header name is yours to choose. The examples read it from
KNOWBE4_WEBHOOK_HEADER(defaultAuthorization) and the expected value fromKNOWBE4_WEBHOOK_SECRET. KNOWBE4_WEBHOOK_SECRETis the exact, whole header value, including anyBearerprefix you typed. Look the header up case-insensitively and compare the whole value; do not strip or parse aBearerprefix.- Hookdeck's KnowBe4 source does the same (API key verification: header name plus exact whole header value).
- KSAT also has an Authentication method field and PhishER offers No Auth, API Key, Bearer Token or Basic Auth. Basic Auth is not covered by Hookdeck's KnowBe4 source; use the custom header instead.
Verification (core)
const crypto = require('crypto');
// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
const ha = crypto.createHash('sha256').update(String(a)).digest();
const hb = crypto.createHash('sha256').update(String(b)).digest();
return crypto.timingSafeEqual(ha, hb);
}
function verifyKnowbe4Request(headers, config = process.env) {
const expected = config.KNOWBE4_WEBHOOK_SECRET;
if (!expected) return false; // never accept when unconfigured
const name = (config.KNOWBE4_WEBHOOK_HEADER || 'Authorization').toLowerCase();
const received = headers[name]; // Node lowercases header names
return Boolean(received) && safeEqual(received, expected); // whole value, no prefix parsing
}
- Missing header or mismatch: return
401. A body that isn't a JSON object:400. Handled:200. - Read the body raw (
express.raw({ type: '*/*' })) and authenticate before parsing it. - Python:
hmac.compare_digeston bytes (encode both sides; passingstrraises on non-ASCII). - A static shared credential authenticates the sender, not the body, and gives no replay protection (no signature, no timestamp). Use HTTPS and rotate the credential if it leaks.
For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.
Events and Payload
KnowBe4 does not document the body field by field and there is no vendor event field. In KSAT you subscribe to events from three groups, and you can add admin-defined Custom Data key/values to the payload (for example {"System": "KnowBe4"}).
| Source | What you get |
|---|---|
| KSAT: Phishing Events | Events such as Phish Failure Removed (UI label) |
| KSAT: Training Events | Training activity events you tick in the webhook |
| KSAT: General Events | General account events you tick in the webhook |
| PhishER: Send to Webhook | Message data for the data types selected on the webhook |
The examples and tests use a clearly labelled illustrative payload with an event_type string (Phish Failure Removed). Inspect a real delivery, then adapt the handler.
Delivery Behaviour
- KSAT retries a failed request up to three more times, 15 minutes apart.
- A KSAT webhook with more than 1,000 failed responses in an hour is deactivated; set it back to Active in Account Settings once the endpoint is fixed.
- The HTTP method isn't stated; the examples accept
POST.
Environment Variables
KNOWBE4_WEBHOOK_HEADER=Authorization # name of the custom header you added
KNOWBE4_WEBHOOK_SECRET=Bearer your-long-random-secret # exact, whole header value
Local Development
KnowBe4 can't reach localhost. Tunnel with the Hookdeck CLI (no account required):
npx hookdeck-cli listen 3000 knowbe4 --path /webhooks/knowbe4
Use the printed URL as the webhook URL in KSAT or PhishER.
Resources
- references/overview.md - KSAT and PhishER webhooks, events, payload, retries
- references/setup.md - Create the webhook and add the secret header
- references/verification.md - Header check details, gotchas, debugging 401s
- examples/ - Runnable Express, Next.js and FastAPI handlers with tests