Kernsignal/API docs

Webhooks

Webhooks

Kernsignal calls your server when an audit finishes, so you do not have to poll.

Set up

  1. Register an HTTPS endpoint. The response includes the signing secret once. Store it.
  2. Create audits with "webhook": true in config. Audits without it send nothing.
  3. Answer each delivery with a 2xx status.
curl https://api.kernsignal.ai/v1/webhook_endpoints \
  -H "Authorization: Bearer $KERNSIGNAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/hooks/kernsignal", "events": ["audit.succeeded", "audit.failed"]}'
Method and pathWhat it does
POST /v1/webhook_endpointsCreate. Body: url (required), events (default all), description. Up to 10 per account.
GET /v1/webhook_endpointsList. Secrets are not returned.
DELETE /v1/webhook_endpoints/{id}Delete. Deliveries still waiting for retry are cancelled.
GET /v1/webhook_endpoints/{id}/deliveriesThe delivery log: status, attempts and the result of each attempt.

Events

audit.succeeded, audit.failed, screen.succeeded, screen.failed. An endpoint receives events for jobs that finish after it was created.

{
  "id": "dlv_...",
  "object": "event",
  "type": "audit.succeeded",
  "created_at": "2026-11-14T10:00:41Z",
  "data": {"object": {"id": "aud_...", "object": "audit", "status": "succeeded", "mode": "live",
                      "created_at": "2026-11-14T10:00:00Z", "finished_at": "2026-11-14T10:00:40Z", "error": null}}
}

The event is thin on purpose: fetch GET /v1/audits/{id} for the result. id is the same on every retry, so use it to ignore duplicates.

Verify the signature

Each request has these headers:

Kernsignal-Signature: t=1700000000,v1=3f5c...
Kernsignal-Event: audit.succeeded
Kernsignal-Delivery: dlv_...

v1 is the hex HMAC-SHA256, keyed with your endpoint secret, of the string {t}.{raw_body}, where t is the number after t= and raw_body is the request body exactly as received. Compute it, compare in constant time, and reject timestamps more than 5 minutes from now. Verify the raw bytes before parsing JSON: re-serialised JSON changes the bytes.

Python

from kernsignal import construct_event, SignatureVerificationError

try:
    event = construct_event(raw_body, request.headers.get("Kernsignal-Signature"), secret)
except SignatureVerificationError:
    ...  # answer 400

Node.js

import { constructEvent } from "./kernsignal.mjs";

const event = constructEvent(rawBody, req.headers["kernsignal-signature"], process.env.KERNSIGNAL_WEBHOOK_SECRET);

Any language

expected = hex(hmac_sha256(secret, t + "." + raw_body))
valid    = constant_time_equal(expected, v1) and abs(now - t) <= 300

Retries

A delivery succeeds on any 2xx answer within 10 seconds. Anything else, including a redirect, a timeout or a connection error, is retried. There are at most 6 attempts, waiting 1 minute, 10 minutes, 1 hour, 4 hours and 18 hours after the previous failure, so the last attempt is about 23 hours after the first. Then the delivery is marked failed; the audit itself is unaffected, and its result is still available from the API. Redirects are not followed.

Where Kernsignal will deliver