Advanced
Webhooks
Audital delivers real-time event notifications to your HTTPS endpoint. Webhooks are the recommended way to react to audit events, chain integrity failures, and shadow AI detections without polling the API.
Available Event Types
| Event Type | Severity | Description |
|---|---|---|
| audit.created | INFO | A new audit event was written to the chain |
| audit.high_severity | HIGH | An audit event with severity HIGH or CRITICAL was created |
| chain.integrity_failure | CRITICAL | The hash chain failed integrity verification |
| chain.verified | INFO | Routine chain verification passed (optional — high volume) |
| shadow_ai.detected | HIGH | Unregistered AI usage detected in the organisation |
| shadow_ai.resolved | INFO | A shadow AI detection was resolved |
| evidence.ready | INFO | An evidence package has finished generating and is ready to download |
| evidence.expired | INFO | An evidence package download URL has expired |
| alert.triggered | MEDIUM | An Audital policy alert was triggered for a model |
| model.registered | INFO | A new AI model was registered with Audital |
| integration.disconnected | MEDIUM | An external integration lost connection |
Registering a Webhook
Create a webhook endpoint via the API or from Settings → Webhooks → Add endpoint.
curl -X POST https://api.audital.ai/v1/webhooks \
-H "Authorization: Bearer ak_live_xxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.example.com/webhooks/audital",
"events": [
"audit.created",
"chain.integrity_failure",
"shadow_ai.detected",
"evidence.ready",
"alert.triggered"
],
"description": "Production compliance webhook",
"active": true
}'{
"id": "wh_01HZABCDEF1234567890ABCD",
"url": "https://your-app.example.com/webhooks/audital",
"events": ["audit.created", "chain.integrity_failure", "shadow_ai.detected", "evidence.ready", "alert.triggered"],
"signingSecret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"active": true,
"createdAt": "2026-03-02T15:00:00.000Z"
}Signing secret
Store the signingSecret securely — it is only shown once. You will use it to verify the HMAC-SHA256 signature on incoming webhook requests.
Payload Schema
All webhook payloads share a common envelope. The data field contains the event-specific object.
{
"id": "we_01HZABCDEF1234567890ABCD",
"type": "audit.created",
"apiVersion": "2024-04-10",
"created": 1741000000,
"livemode": true,
"organisationId": "org_xyz789",
"data": {
"id": "evt_01HZABCDEF1234567890ABCD",
"chainPosition": 4822,
"blockHash": "sha256:9f86d081884c7d659a2feaa0c55ad015a3bf4f1b",
"timestamp": "2026-03-02T14:22:11.003Z",
"eventType": "INFERENCE",
"severity": "INFO",
"modelId": "mdl_abc123",
"verified": true
}
}Envelope fields
| Field | Type | Description |
|---|---|---|
| id | string | Unique webhook event ID (we_ prefix) |
| type | string | Event type (e.g. audit.created) |
| apiVersion | string | API version that produced the event |
| created | integer | Unix timestamp (seconds) of event creation |
| livemode | boolean | false for test-mode events |
| organisationId | string | Your organisation ID |
| data | object | The event-specific payload object |
Signature Verification
Every webhook request includes an X-Audital-Signature header with an HMAC-SHA256 signature. Always verify this before processing the payload.
Header format: t=TIMESTAMP,v1=HMAC_HEX
Node.js / Express
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
// The X-Audital-Signature header format: t=TIMESTAMP,v1=HMAC_HEX
const [tPart, v1Part] = signature.split(',');
const timestamp = tPart.split('=')[1];
const providedHmac = v1Part.split('=')[1];
// Prevent replay attacks: reject if timestamp is >5 minutes old
const age = Math.abs(Date.now() / 1000 - parseInt(timestamp));
if (age > 300) {
throw new Error('Webhook timestamp too old — possible replay attack');
}
// Signed payload: "TIMESTAMP.RAW_BODY"
const signedPayload = `${timestamp}.${payload}`;
const expectedHmac = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Constant-time comparison
if (!crypto.timingSafeEqual(
Buffer.from(expectedHmac, 'hex'),
Buffer.from(providedHmac, 'hex')
)) {
throw new Error('Invalid webhook signature');
}
return JSON.parse(payload);
}
// Express.js example
app.post('/webhooks/audital', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-audital-signature'];
const secret = process.env.AUDITAL_WEBHOOK_SECRET;
let event;
try {
event = verifyWebhookSignature(req.body, sig, secret);
} catch (err) {
console.error('Webhook verification failed:', err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
// Handle the event
switch (event.type) {
case 'audit.created':
console.log('New audit event:', event.data.id);
break;
case 'chain.integrity_failure':
console.error('CRITICAL: Chain integrity breach!', event.data);
break;
case 'shadow_ai.detected':
console.warn('Shadow AI detected:', event.data.details.aiProvider);
break;
case 'evidence.ready':
console.log('Evidence package ready:', event.data.id);
break;
default:
console.log('Unhandled event type:', event.type);
}
res.json({ received: true });
});Python / Flask
import hmac
import hashlib
import time
import json
from flask import Flask, request, jsonify, abort
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
def verify_signature(payload: bytes, signature: str, secret: str) -> dict:
parts = {p.split('=')[0]: p.split('=')[1] for p in signature.split(',')}
timestamp = parts.get('t', '')
provided_hmac = parts.get('v1', '')
# Reject stale payloads
if abs(time.time() - int(timestamp)) > 300:
raise ValueError("Webhook timestamp too old")
signed_payload = f"{timestamp}.{payload.decode('utf-8')}"
expected_hmac = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected_hmac, provided_hmac):
raise ValueError("Invalid HMAC signature")
return json.loads(payload)
@app.route('/webhooks/audital', methods=['POST'])
def handle_webhook():
sig = request.headers.get('X-Audital-Signature', '')
try:
event = verify_signature(request.data, sig, WEBHOOK_SECRET)
except ValueError as e:
abort(400, str(e))
if event['type'] == 'chain.integrity_failure':
# Page on-call immediately
trigger_pagerduty(event['data'])
return jsonify(received=True)Retry Logic
Audital retries failed deliveries using an exponential backoff schedule. A delivery is considered failed if your endpoint returns a non-2xx status or does not respond within 30 seconds.
# Audital retry schedule for failed webhook deliveries
# An endpoint is considered failed if it returns:
# - Any non-2xx HTTP status
# - No response within 30 seconds
# Retry attempts:
# Attempt 1: Immediately (t+0s)
# Attempt 2: After 60s (t+1min)
# Attempt 3: After 300s (t+5min)
# Attempt 4: After 1800s (t+30min)
# Attempt 5: After 10800s (t+3h)
#
# After 5 failed attempts, the webhook delivery is marked FAILED
# and a notification is sent to the account owner's email.
#
# Webhook endpoints that fail >50% of deliveries over 7 days
# are automatically disabled to prevent resource exhaustion.
# You will receive an email notification before this happens.5
Max attempts
3 hours
Max delay
30 seconds
Timeout per attempt
Best Practices
- Always verify the HMAC-SHA256 signature before processing any webhook payload.
- Respond with HTTP 200 immediately, then process the event asynchronously to avoid timeouts.
- Store the raw request body before parsing JSON — HMAC verification requires the unmodified bytes.
- Implement idempotency using the webhook event id (we_ prefix) to handle retried deliveries safely.
- Monitor your webhook endpoint success rate in Settings → Webhooks → Delivery history.
- For chain.integrity_failure events, trigger your incident response process immediately — do not batch these.