Webhooks
Webhooks
Kernsignal calls your server when an audit finishes, so you do not have to poll.
Set up
- Register an HTTPS endpoint. The response includes the signing
secretonce. Store it. - Create audits with
"webhook": trueinconfig. Audits without it send nothing. - 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 path | What it does |
|---|---|
POST /v1/webhook_endpoints | Create. Body: url (required), events (default all), description. Up to 10 per account. |
GET /v1/webhook_endpoints | List. Secrets are not returned. |
DELETE /v1/webhook_endpoints/{id} | Delete. Deliveries still waiting for retry are cancelled. |
GET /v1/webhook_endpoints/{id}/deliveries | The 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
- The URL must be
https, with a public hostname (not an IP address, notlocalhost) and no username or password. - Every address the hostname resolves to must be public. Addresses in private, loopback, link-local and similar ranges are refused when you register the endpoint and again on every delivery. Kernsignal connects to the address it checked.
- To test locally, expose your server through an HTTPS tunnel.