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.
- Node.js
- Python
- Go
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 });
});
import hmac
import hashlib
def verify_webhook_signature(body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(signature, expected)
# In your Flask handler
@app.post("/webhooks/sentinel")
def webhook():
signature = request.headers.get("X-Signature", "")
if not verify_webhook_signature(request.data, signature, WEBHOOK_SECRET):
return jsonify({"error": "Invalid signature"}), 401
# Process the webhook...
return jsonify({"received": True})
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func verifyWebhookSignature(body []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(signature), []byte(expected))
}
// In your HTTP handler
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
signature := r.Header.Get("X-Signature")
if !verifyWebhookSignature(body, signature, webhookSecret) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process the webhook...
w.WriteHeader(http.StatusOK)
}
Always use timing-safe comparison functions (crypto.timingSafeEqual, hmac.compare_digest, hmac.Equal) to prevent timing attacks.
Event types
| Event | Description | Trigger |
|---|---|---|
signal.detected | New exploitation signal detected | Event processing detects a baseline deviation |
signal.confirmed | Signal confirmed by reviewer | PUT /signals/{id}/review with status CONFIRMED |
signal.dismissed | Signal dismissed by reviewer | PUT /signals/{id}/review with status DISMISSED |
profile.tier_changed | Elder profile risk tier changed | ERS recalculation crosses a tier boundary |
profile.created | New elder profile enrolled | POST /profiles |
profile.status_changed | Profile monitoring status changed | PUT /profiles/{id}/status |
ers.calculated | ERS recalculated for a profile | Signal detection or on-demand ERS calculation |
batch.completed | Batch operation completed | Enrollment scan, baseline recalculation, or draining scan finishes |
batch.failed | Batch operation failed | Batch 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:
| Attempt | Delay |
|---|---|
| 1 | 1 minute |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 24 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
idfield for idempotency. The same event may be delivered more than once. - Verify signatures — Always verify the
X-Signatureheader 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
CRITICALseverity signals to immediate-response queues.