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:
| Action | Scope required |
|---|---|
| List subscriptions, deliveries, and attempts | webhooks:read |
| Create a subscription | webhooks:create |
| Update, rotate secret, or send test delivery | webhooks:update |
| Delete a subscription | webhooks: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:
| Event | When it fires |
|---|---|
assessment.completed | An assessment completes |
finding.created | A new finding was recorded |
finding.confirmed | A finding was confirmed during triage |
finding.status_changed | A finding's status changed (carries both new and previous status) |
finding.sla_breached | A finding is still open past its severity-based remediation SLA window |
report.ready | A PDF report is available for download |
fix_verify.completed | A fix verification run completed |
pr_review.completed | A PR security review was posted |
spend.threshold_crossed | Organization spend crossed a configured alert threshold |
spend.cap_reached | Organization or project spend reached a configured cap |
monitoring.regression | A 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 envelopesplunk_hec: Splunk HTTP Event Collector recordmicrosoft_sentinel: flat record for Sentinel / Log Analytics custom tablescef: 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
| Header | Value |
|---|---|
X-AISafe-Event | Event type, e.g. assessment.completed |
X-AISafe-Delivery | Delivery ID |
X-AISafe-Timestamp | Unix timestamp used in the signature |
X-AISafe-Signature | Comma-separated values, e.g. t=1778198400,v1=<hex> |
X-AISafe-Format | Payload 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
| Endpoint | Purpose |
|---|---|
GET /api/v1/webhooks/events | List supported customer-facing events |
POST /api/v1/webhooks/subscriptions | Create a subscription (returns signing secret once) |
GET /api/v1/webhooks/subscriptions | List 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-secret | Rotate the signing secret |
POST /api/v1/webhooks/subscriptions/{id}/test | Send a webhook.test delivery |
GET /api/v1/webhooks/deliveries | Inspect delivery payload snapshots |
GET /api/v1/webhooks/deliveries/{id}/attempts | View 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.
Related
- API conventions: base URLs, pagination, rate limits
- Authentication: API keys for managing subscriptions
- Integrations: Slack: alternative notification channel