# 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)

```javascript
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/](https://github.com/hookdeck/webhook-skills/tree/main/skills/knowbe4-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/knowbe4-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/knowbe4-webhooks/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

```bash
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):

```bash
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](https://github.com/hookdeck/webhook-skills/blob/main/skills/knowbe4-webhooks/references/overview.md) - KSAT and PhishER webhooks, events, payload, retries
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/knowbe4-webhooks/references/setup.md) - Create the webhook and add the secret header
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/knowbe4-webhooks/references/verification.md) - Header check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/knowbe4-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests