Aona API & Webhooks
Read your security events over REST and receive them as signed webhooks. Both surfaces use the OCSF (Open Cybersecurity Schema Framework) shape so Sentinel, Splunk, and Amazon Security Lake can ingest them natively. Aona-specific fields are preserved in enrichments[].
Quickstart
Create a key in the dashboard at /api-keys with the events:read scope, then:
curl -H "X-Api-Key: aona_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
https://api.aona.ai/v1/eventsThe response is keyset-paginated: pass the returned next_cursor as ?cursor= on the next request. Cursors are opaque and signed, don't construct them by hand.
Verify webhooks, Node.js
Aona signs every webhook with HMAC-SHA256. Verify before parsing the JSON body:
import { createHmac, timingSafeEqual } from "node:crypto";
/**
* Verify an Aona webhook delivery.
*
* const ok = verifyAonaWebhook({
* header: req.headers["x-aona-signature"],
* body: rawRequestBody, // string, before JSON.parse
* secret: process.env.AONA_WEBHOOK_SECRET,
* previousSecret: process.env.AONA_WEBHOOK_SECRET_PREVIOUS, // optional during rotation
* toleranceSeconds: 300,
* });
*/
export function verifyAonaWebhook({
header,
body,
secret,
previousSecret,
toleranceSeconds = 300,
}) {
if (typeof header !== "string") return false;
// Parse "t=<unix>,v1=<hex>"
let t = null, v1 = null;
for (const part of header.split(",")) {
const [k, v] = part.split("=");
if (k === "t") t = Number(v);
else if (k === "v1") v1 = v;
}
if (!t || !v1 || !/^[0-9a-f]{64}$/i.test(v1)) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSeconds) return false;
const payload = `${t}.${body}`;
const tryVerify = (s) => {
const expected = createHmac("sha256", s).update(payload).digest("hex");
try {
return timingSafeEqual(Buffer.from(v1, "hex"), Buffer.from(expected, "hex"));
} catch {
return false;
}
};
if (tryVerify(secret)) return true;
if (previousSecret && tryVerify(previousSecret)) return true;
return false;
}Verify webhooks, Python
import hmac
import hashlib
import time
def verify_aona_webhook(header: str, body: str, secret: str,
previous_secret: str | None = None,
tolerance_seconds: int = 300) -> bool:
"""Verify an Aona webhook delivery.
Pass the raw request body (bytes-or-string before JSON.parse) — the
signature is computed over the wire payload, not the parsed dict.
"""
if not isinstance(header, str):
return False
parsed = dict(part.split("=", 1) for part in header.split(",") if "=" in part)
try:
t = int(parsed["t"])
v1 = parsed["v1"]
except (KeyError, ValueError):
return False
if not (len(v1) == 64 and all(c in "0123456789abcdefABCDEF" for c in v1)):
return False
if abs(int(time.time()) - t) > tolerance_seconds:
return False
payload = f"{t}.{body}".encode("utf-8")
def _try(s: str) -> bool:
expected = hmac.new(s.encode("utf-8"), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(v1.lower(), expected.lower())
if _try(secret):
return True
if previous_secret and _try(previous_secret):
return True
return FalseSecret rotation (zero-downtime)
When you rotate a webhook's signing secret in the dashboard, the previous secret remains valid for 24 hours. During that window, both secrets verify successfully. The recommended pattern is shown in the verifier code above: try the current secret first, then fall back to the previous one if you have it. Update your environment to the new secret and roll the previous one out at your convenience.
Replay protection
The t= component of X-Aona-Signature is the unix-second timestamp at which we signed the payload. The verifier rejects deliveries where |now - t| > toleranceSeconds (default 5 minutes). Don't relax this without reason, it's the only thing protecting you from someone replaying a captured payload.
Idempotency
Every delivery includes X-Aona-Delivery: <uuid>. If a delivery times out and we retry, we send the same id. Dedupe on it for at-least-once safety, receivers that store it in a small bounded cache will never act on the same event twice.
OCSF event reference
Aona events conform to OCSF class_uid 6005 (Application Activity / Web Resources Activity). Aona-native fields appear inside enrichments[] with explicit types.
{
"id": "01J5e2-uuid-...",
"version": "2026-05-23",
"class_uid": 6005,
"category_name": "Application Activity",
"activity_name": "Access",
"time": "2026-05-22T14:08:12.314Z",
"actor": { "user": { "uid": "f6c1...-uuid" } },
"app": { "name": "Aona", "uid": "chat-gpt-platform-uuid" },
"type_name": "policy_violation",
"disposition": "BLOCK",
"metadata": {
"product": { "name": "Aona Workforce AI Security" },
"tenant_uid": "your-business-uuid"
},
"enrichments": [
{ "name": "data_security_risk", "value": 0.83, "type": "numeric" },
{ "name": "data_privacy_risk", "value": 0.41, "type": "numeric" },
{ "name": "policy_id", "value": "policy-uuid", "type": "uid" },
{ "name": "action", "value": "BLOCK", "type": "string" },
{ "name": "chat_id", "value": "abc-123", "type": "string" }
]
}For the full schema, see the OpenAPI spec (machine-readable JSON at /v1/docs-json).
Status codes & rate limits
200, success.400, bad request (invalid cursor, malformed query parameter).401, missing, revoked, expired, or invalidX-Api-Key. We deliberately return the same body for all four cases.403, your plan does not include API access, or your key is missing a required scope.404, event not found (or belongs to another tenant, we don't distinguish).429, rate limit exceeded. InspectX-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset, andRetry-After.
Security notes
- HTTPS only. We won't deliver to
http://URLs. - Loopback and private IPs are rejected at create-time AND at delivery-time (DNS pinning) to prevent SSRF.
- Self-signed TLS certificates fail verification, use a real cert.
- Webhook payloads contain metadata only. Prompt content, redaction text, and other user content never leave Aona via the API or webhooks.
- Cert verification, no redirects, 10s timeout, 1 MiB response cap.