Webhooks
Updated
Subscribe to Asqav events and receive them as signed HTTP POSTs to your endpoint. Available on all tiers.
Create a webhook
The secret is generated server-side and returned exactly once. Store it. Use it to verify every incoming request.
curl -X POST https://api.asqav.com/api/v1/alerts/webhooks \
-H "X-API-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/asqav","events":["*"]}'
Response:
{
"id": "6f1c2b9e-...",
"url": "https://example.com/asqav",
"secret": "whsec_...",
"events": ["*"],
"created_at": "2026-04-19T12:00:00"
}
Choose the events
A webhook gets every alert type, written ["*"], or only the types you list. ["*"] also covers types added later; a list does not. In the dashboard, open Settings, then Webhooks, and pick All events or Selected events when you create or edit a webhook.
| Type | Sent when |
|---|---|
policy_violation | An action broke a policy that alerts, or an agent was suspended or quarantined after repeated violations. |
rate_limit_exceeded | An agent went over a rate limit set for one of its actions. |
behavioral_drift | An agent's activity moved far from its usual baseline. |
anomaly_detected | A metric crossed the threshold of one of your alert rules. |
incident_created | An incident was opened, by hand or from an alert. |
escalation_triggered | An incident moved to the next level of its escalation chain. |
signature_limit_approaching | The organization used 80% of its monthly signatures. |
key_access_log_refused | A signing key access could not join the tamper-evident log and waits for review. |
GET /api/v1/alerts/webhooks/event-types returns this list with the label and group the dashboard shows. Asqav stores the types in this order with repeats removed. An empty list, or one that names an unknown type, is refused with 422.
Change a webhook's events
curl -X PATCH https://api.asqav.com/api/v1/alerts/webhooks/6f1c2b9e-... \
-H "X-API-Key: $ASQAV_API_KEY" \
-H "Content-Type: application/json" \
-d '{"events":["policy_violation","incident_created"]}'
The response is the updated webhook, without its secret. The next alert follows the new list. The URL cannot change; create a new webhook for a new URL. API keys need the alerts:write scope, as for create and delete.
Delivery format
Every delivery has two headers and a JSON body:
| Header | Value |
|---|---|
X-Asqav-Signature | sha256=<hex> |
X-Asqav-Timestamp | Unix seconds when the event was sent |
The signature is HMAC-SHA256 over <timestamp>.<body> where body is the compact JSON with sorted keys. For ten minutes after a rotation, X-Asqav-Signature-Previous carries the same signature made with the old secret.
A generic endpoint receives the alert as JSON. type names the event; alert_type holds the same value for receivers built before type existed.
{
"type": "policy_violation",
"alert_id": "alt_...",
"alert_type": "policy_violation",
"severity": "medium",
"title": "Policy violated: Review sensitive payments",
"description": "...",
"org_id": "...",
"agent_id": "...",
"created_at": "2026-04-19T12:00:00",
"metadata": {}
}
Verification
Verify by recomputing HMAC-SHA256 over timestamp + '.' + body_compact_sorted with your webhook secret. Your receiver MUST parse the JSON body then re-serialize with json.dumps(body, sort_keys=True, separators=(',', ':')) before computing HMAC. Asqav's backend normalizes this way, and variations in whitespace will break verification.
Verify a delivery
import hmac, hashlib, json, time
def verify(secret, headers, raw_body, tolerance=300):
ts = headers["X-Asqav-Timestamp"]
sig = headers["X-Asqav-Signature"].split("=", 1)[1]
if abs(time.time() - int(ts)) > tolerance:
raise ValueError("stale webhook")
body = json.dumps(json.loads(raw_body), separators=(",", ":"), sort_keys=True)
expected = hmac.new(secret.encode(), f"{ts}.{body}".encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
raise ValueError("bad signature")
Rotate the secret
If a secret leaks, rotate it. The response returns a new secret once.
curl -X POST https://api.asqav.com/api/v1/alerts/webhooks/6f1c2b9e-.../rotate \
-H "X-API-Key: sk_live_..."
If the URL points to Slack, Discord, or Microsoft Teams, Asqav formats the payload for that channel automatically. For generic endpoints you receive the raw event JSON.
References
- RFC 2104: HMAC, IETF