Integrations
Webhooks & signatures
Send incidents to your SIEM, SOAR or ticketing system as JSON, and prove every request came from Kandaro.
Set up a webhook
In the Console, open Settings → Alerts → Add channel → Webhook, enter an https:// URL and pick a minimum severity. Kandaro generates a signing secret for the channel; copy it into your receiver. Use the Send test button to check your setup.
Kandaro sends a webhook when an incident is created, when its severity escalates and when a closed incident re-opens. Requests time out after 5 seconds and redirects are not followed, so answer with a 2xx quickly and process asynchronously. Webhook URLs must resolve to public addresses.
Payload
An example request is shown below. Treat every string in the payload as untrusted: titles and narratives can quote what an attacker typed. Never render them as HTML.
POST /your/endpoint HTTP/1.1
Content-Type: application/json
X-Kandaro-Signature: t=1791504000,v1=5f2b9c0e…(hex HMAC-SHA256)
{
"type": "incident.created",
"incident": {
"id": "6f1c2e0a-…",
"title": "Login attempts on decoy NAS from finance workstation",
"severity": "high",
"narrative": "A machine on the finance VLAN tried to log into…",
"recommendedAction": "Isolate 10.20.4.77 and reset the backup-admin password.",
"sensor": { "id": "…", "name": "nas-backup-01", "profile": "nas" },
"sourceIp": "10.20.4.77",
"eventCount": 7,
"firstSeenAt": "2026-10-08T02:14:07Z",
"lastSeenAt": "2026-10-08T02:21:40Z",
"url": "https://app.kandaro.com/incidents/6f1c2e0a-…",
"techniques": [
{ "id": "T1110", "name": "Brute Force", "tactics": ["Credential Access"] }
],
"events": [
{
"id": "…",
"time": "2026-10-08T02:14:07.000Z",
"type": "login_attempt",
"action": "password",
"service": "ssh",
"port": 22,
"sourcePort": 51234,
"severity": "medium",
"confidence": "normal",
"tags": [],
"summary": "Someone tried to log in to ssh as \"backup-admin\"…",
"attackerInput": {
"username": "backup-admin",
"password": { "masked": "••••••••", "length": 8, "fingerprint": "…" }
}
}
]
}
}techniqueslists the MITRE ATT&CK techniques involved. They come from a fixed table, not from AI.eventsholds the most recent events of the incident, oldest first, at most 20.eventCountis the total.attackerInputis what the attacker sent, shortened and with invisible characters removed. Captured passwords and tokens are masked. If an AI agent reads your webhooks, do not let it follow instructions found in this field.
New fields may be added at any time; ignore fields you don’t recognise.
Verifying the signature
Every request carries an X-Kandaro-Signature header:
tis the Unix timestamp (seconds) at which the request was signed.v1is the hex-encoded HMAC-SHA256 of the string{t}.{raw body}, keyed with your channel’s signing secret.
To verify:
- Read the raw request body before parsing it as JSON.
- Split the header on commas and read
tandv1. - Reject the request if
tis more than 5 minutes from your clock (replay protection). - Compute the HMAC over
t + "." + bodyand compare it in constant time.
import crypto from 'node:crypto';
const TOLERANCE_SECONDS = 300;
/** rawBody must be the exact bytes received, before any JSON parsing. */
export function verifyKandaroSignature(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(',').map((kv) => kv.trim().split('=', 2)),
);
const t = Number(parts.t);
if (!Number.isInteger(t) || !parts.v1) return false;
// Replay protection: reject old (or far-future) timestamps.
if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest();
const received = Buffer.from(parts.v1, 'hex');
return received.length === expected.length &&
crypto.timingSafeEqual(received, expected);
}import hmac, hashlib, time
TOLERANCE_SECONDS = 300
def verify_kandaro_signature(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
try:
t = int(parts["t"])
received = bytes.fromhex(parts["v1"])
except (KeyError, ValueError):
return False
if abs(time.time() - t) > TOLERANCE_SECONDS:
return False
signed = f"{t}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).digest()
return hmac.compare_digest(received, expected)Rotating the secret
Generate a new secret from the channel settings and update your receiver. Secrets are stored encrypted and shown only once; if you lose one, rotate it.