Skip to main content

Webhooks

AISafe outbound webhooks deliver real-time notifications when important events occur: assessments completing, findings discovered or changing status, reports ready, monitoring regressions detected, and spend guardrails crossing thresholds. Subscribe to events and AISafe delivers signed HTTP POST requests to your endpoint.

Subscription scopes

You can configure webhook subscriptions at three levels:

  • Org-wide: fires on matching events across your organization. Managed by owners and admins.
  • Per-project: fires only for events whose assessment belongs to a specific project. Managed by any user with project write access.
  • Per-assessment: fires only for events on a specific assessment. Managed by any user with assessment write access.

Scopes combine: a single event matching an org-wide subscription, a project subscription, and an assessment subscription produces three distinct deliveries.

API scope requirements

Webhook endpoints (org-wide, per-project, and per-assessment) require the webhooks:* scope family on your API key:

ActionScope required
List subscriptions, deliveries, and attemptswebhooks:read
Create a subscriptionwebhooks:create
Update, rotate secret, or send test deliverywebhooks:update
Delete a subscriptionwebhooks:delete

Resource-level access follows role-based permissions: org-wide endpoints require admin-or-above, while project and assessment endpoints require write access to the parent resource.

Event catalogue

AISafe delivers the following event types:

EventWhen it fires
assessment.completedAn assessment completes
finding.createdA new finding was recorded
finding.confirmedA finding was confirmed during triage
finding.status_changedA finding's status changed (carries both new and previous status)
finding.sla_breachedA finding is still open past its severity-based remediation SLA window
report.readyA PDF report is available for download
fix_verify.completedA fix verification run completed
pr_review.completedA PR security review was posted
spend.threshold_crossedOrganization spend crossed a configured alert threshold
spend.cap_reachedOrganization or project spend reached a configured cap
monitoring.regressionA fixed vulnerability has regressed

You can send a synthetic webhook.test event to verify connectivity and signature handling.

Per-subscription finding filters

Each subscription can carry optional severity_filter and status_filter lists. If set, finding-shaped events (finding.created, finding.confirmed, finding.status_changed, finding.sla_breached) are delivered only if the finding's severity/status is in the respective filter set. Non-finding events ignore both filters.

These filters let you subscribe a SIEM endpoint to "critical + high, open + confirmed only" without server-side routing rules.

Payload formats

Each subscription carries a payload_format (default aisafe). Supported formats:

  • aisafe: the native JSON envelope
  • splunk_hec: Splunk HTTP Event Collector record
  • microsoft_sentinel: flat record for Sentinel / Log Analytics custom tables
  • cef: ArcSight Common Event Format line (text/plain)

Signing covers the exact delivered bytes, regardless of format. The Content-Type header matches the rendered format, and an X-AISafe-Format header names the chosen format.

Payload envelope

Delivery bodies are deterministic JSON with this envelope:

{
"id": "whdel_...",
"event_id": "event_...",
"type": "assessment.completed",
"created_at": "2026-05-08T00:00:00Z",
"organization_id": "AIS-ORG",
"data": {
"kind": "assessment.completed"
}
}

The data shape is event-specific and exposes public IDs only. Webhook payloads exclude internal database IDs, raw source content, tokens, and credentials.

Signing

AISafe signs webhook deliveries so you can verify authenticity. Each subscription has a whsec_... signing secret (shown once at creation, rotatable on demand). AISafe signs the exact JSON bytes sent over the wire:

signed_payload = "{timestamp}.{raw_body}"
signature = hmac_sha256(secret, signed_payload)

Request headers

HeaderValue
X-AISafe-EventEvent type, e.g. assessment.completed
X-AISafe-DeliveryDelivery ID
X-AISafe-TimestampUnix timestamp used in the signature
X-AISafe-SignatureComma-separated values, e.g. t=1778198400,v1=<hex>
X-AISafe-FormatPayload format used (e.g. aisafe, splunk_hec, cef)

Verifying signatures

Verify the signature on your endpoint before processing the payload:

import hmac
import hashlib
import time

def verify_webhook(headers, raw_body, signing_secret):
signature_header = headers.get("X-AISafe-Signature", "")
timestamp = None
signatures = []

for part in signature_header.split(","):
if part.startswith("t="):
timestamp = part[2:]
elif part.startswith("v1="):
signatures.append(part[3:])

if not timestamp or not signatures:
return False

# Reject stale timestamps (replay protection)
if abs(time.time() - int(timestamp)) > 300:
return False

signed_payload = f"{timestamp}.{raw_body}"
expected = hmac.new(
signing_secret.encode(),
signed_payload.encode(),
hashlib.sha256,
).hexdigest()

return hmac.compare_digest(expected, signatures[0])
const crypto = require("crypto");

function verifyWebhook(headers, rawBody, signingSecret) {
const signatureHeader = headers["x-aisafe-signature"] || "";
let timestamp = null;
const signatures = [];

for (const part of signatureHeader.split(",")) {
if (part.startsWith("t=")) timestamp = part.slice(2);
else if (part.startsWith("v1=")) signatures.push(part.slice(3));
}

if (!timestamp || signatures.length === 0) return false;

// Reject stale timestamps (replay protection)
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) return false;

const signedPayload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac("sha256", signingSecret)
.update(signedPayload)
.digest("hex");

return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatures[0]),
);
}

Secret rotation

After you rotate a signing secret, the X-AISafe-Signature header includes two v1= signatures: one for the new secret and one for the previous secret, for 72 hours. Your endpoint should accept either valid signature during that grace window. After 72 hours, only the new secret's signature is included.

Managing subscriptions

Via the API

EndpointPurpose
GET /api/v1/webhooks/eventsList supported customer-facing events
POST /api/v1/webhooks/subscriptionsCreate a subscription (returns signing secret once)
GET /api/v1/webhooks/subscriptionsList subscriptions
GET /api/v1/webhooks/subscriptions/{id}Read a subscription
PATCH /api/v1/webhooks/subscriptions/{id}Update name, URL, events, or active status
DELETE /api/v1/webhooks/subscriptions/{id}Soft-delete a subscription
POST /api/v1/webhooks/subscriptions/{id}/rotate-secretRotate the signing secret
POST /api/v1/webhooks/subscriptions/{id}/testSend a webhook.test delivery
GET /api/v1/webhooks/deliveriesInspect delivery payload snapshots
GET /api/v1/webhooks/deliveries/{id}/attemptsView attempt logs

Per-project routes mirror these at /api/v1/projects/{id}/webhooks/* and per-assessment routes at /api/v1/assessments/{id}/webhooks/*.

Via the dashboard

You can manage webhook subscriptions from the dashboard:

  • Org-wide: under Settings → Webhooks
  • Per-assessment: under the assessment's Settings → Webhooks tab

Retry policy

The system retries failed deliveries with bounded exponential backoff up to 8 attempts. Success is any 2xx response; timeouts, network errors, and non-2xx responses trigger retries. Terminal failures and per-attempt logs are retained for 30 days.

The system moves subscriptions to failing after repeated terminal delivery failures, auto-disables them after sustained failures, or disables them at once if URL validation becomes unsafe. The system caps payloads at 256 KiB. Oversized payloads count as terminal delivery failures without penalizing the subscription.

The delivery logs, accessible via the API, show delivery attempts and their outcomes. You can inspect the exact payload sent and the response your endpoint returned for each attempt.

Endpoint requirements

Your webhook endpoint must:

  • Use HTTPS (the API rejects HTTP URLs)
  • Be reachable on the public internet (not behind a VPN or firewall)
  • Return a 2xx response within 10 seconds of receiving a delivery

Subscription limits

Each organization can have up to 25 active, disabled, or failing subscriptions. Soft-deleted subscriptions do not count against the limit.