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.