Skip to main content

Webhooks

Webhooks deliver exploitation signals and risk tier changes to your server in real time. When a signal is detected, a risk tier changes, or a batch operation completes, Kora Sentinel sends an HTTP POST to your configured endpoint.

Setup

Configure webhooks in your dashboard settings to receive all events for your tenant.

Payload format

All webhook payloads follow the same structure:

{
"id": "evt_abc123",
"eventType": "signal.detected",
"resourceType": "signal",
"resourceId": "sig_def456",
"tenantId": "your-tenant-uuid",
"timestamp": "2026-04-07T14:31:00Z",
"data": {
"signalId": "sig_def456",
"elderProfileId": "prof_ghi789",
"signalCategory": "GRADUAL_DRAINING",
"signalCode": "DRAIN_VELOCITY_INCREASE",
"severity": "HIGH",
"confidence": 0.87,
"ersBefore": 32.5,
"ersAfter": 48.2,
"tierChanged": true,
"newTier": "ELEVATED"
}
}

Signature verification

Every webhook includes an X-Signature header containing an HMAC-SHA256 hash of the request body. Always verify this signature before processing the payload.

const crypto = require("crypto");

function verifyWebhookSignature(body, signature, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(typeof body === "string" ? body : JSON.stringify(body))
.digest("hex");

return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}

// In your Express handler
app.post("/webhooks/sentinel", express.json(), (req, res) => {
const signature = req.headers["x-signature"];
if (!verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: "Invalid signature" });
}

// Process the webhook...
res.status(200).json({ received: true });
});
warning

Always use timing-safe comparison functions (crypto.timingSafeEqual, hmac.compare_digest, hmac.Equal) to prevent timing attacks.

Event types

EventDescriptionTrigger
signal.detectedNew exploitation signal detectedEvent processing detects a baseline deviation
signal.confirmedSignal confirmed by reviewerPUT /signals/{id}/review with status CONFIRMED
signal.dismissedSignal dismissed by reviewerPUT /signals/{id}/review with status DISMISSED
profile.tier_changedElder profile risk tier changedERS recalculation crosses a tier boundary
profile.createdNew elder profile enrolledPOST /profiles
profile.status_changedProfile monitoring status changedPUT /profiles/{id}/status
ers.calculatedERS recalculated for a profileSignal detection or on-demand ERS calculation
batch.completedBatch operation completedEnrollment scan, baseline recalculation, or draining scan finishes
batch.failedBatch operation failedBatch processing encounters a fatal error

Example payloads

signal.detected

{
"id": "evt_abc123",
"eventType": "signal.detected",
"resourceType": "signal",
"resourceId": "sig_def456",
"tenantId": "your-tenant-uuid",
"timestamp": "2026-04-07T14:31:00Z",
"data": {
"signalId": "sig_def456",
"elderProfileId": "prof_ghi789",
"customerName": "Margaret Johnson",
"signalCategory": "GRADUAL_DRAINING",
"signalCode": "DRAIN_VELOCITY_INCREASE",
"signalName": "Transaction Velocity Increase",
"severity": "HIGH",
"confidence": 0.87,
"score": 18.5,
"baselineValue": 45000.0,
"currentValue": 135000.0,
"deviationFactor": 3.0,
"ersBefore": 32.5,
"ersAfter": 48.2,
"tierChanged": true,
"newTier": "ELEVATED"
}
}

profile.tier_changed

{
"id": "evt_xyz789",
"eventType": "profile.tier_changed",
"resourceType": "profile",
"resourceId": "prof_ghi789",
"tenantId": "your-tenant-uuid",
"timestamp": "2026-04-07T14:31:00Z",
"data": {
"elderProfileId": "prof_ghi789",
"customerName": "Margaret Johnson",
"previousTier": "WATCH",
"newTier": "ELEVATED",
"previousERS": 32.5,
"newERS": 48.2,
"ersTrend": "WORSENING",
"activeSignals": 5,
"criticalSignals": 1
}
}

batch.completed

{
"id": "evt_batch001",
"eventType": "batch.completed",
"resourceType": "batch_run",
"resourceId": "run_abc123",
"tenantId": "your-tenant-uuid",
"timestamp": "2026-04-07T15:00:00Z",
"data": {
"runId": "run_abc123",
"runType": "ENROLLMENT_SCAN",
"profilesProcessed": 1245,
"signalsGenerated": 87,
"alertsCreated": 12,
"errorsCount": 0,
"durationSeconds": 45
}
}

Retry behavior

Failed deliveries (non-2xx response or timeout) are retried up to 5 times with exponential backoff:

AttemptDelay
11 minute
25 minutes
330 minutes
42 hours
524 hours

After 3 consecutive failures, you'll receive an email notification. Failed deliveries can also be manually retried from the webhook logs in your dashboard.

Best practices

  • Return 200 quickly — Process webhooks asynchronously. Return a 200 status immediately, then process the event in a background job.
  • Handle duplicates — Use the event id field for idempotency. The same event may be delivered more than once.
  • Verify signatures — Always verify the X-Signature header before trusting the payload.
  • Log payloads — Store raw webhook payloads for debugging and audit trails.
  • Use HTTPS — Webhook endpoints must use HTTPS in production.
  • Prioritize by severity — Route CRITICAL severity signals to immediate-response queues.