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 Removed event?
  • 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 (default Authorization) and the expected value from KNOWBE4_WEBHOOK_SECRET.
  • KNOWBE4_WEBHOOK_SECRET is the exact, whole header value, including any Bearer prefix you typed. Look the header up case-insensitively and compare the whole value; do not strip or parse a Bearer prefix.
  • 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_digest on bytes (encode both sides; passing str raises 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"}).

SourceWhat you get
KSAT: Phishing EventsEvents such as Phish Failure Removed (UI label)
KSAT: Training EventsTraining activity events you tick in the webhook
KSAT: General EventsGeneral account events you tick in the webhook
PhishER: Send to WebhookMessage 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 9, 2026

View on GitHub →