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/events

The 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 False

Secret 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| &gt; 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: &lt;uuid&gt;. 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

Security notes