Webhooks
Overview
Webhook delivery infrastructure exists in the gateway and is managed through admin-only routes under /api/v1/admin/webhooks. Those routes are intentionally hidden from the public OpenAPI schema.
Today, webhook management is provisioned operationally rather than through a public self-service API portal.
Event Types
The gateway currently recognizes these event types:
| Event | Meaning |
|---|---|
assessment.completed |
Assessment completion event |
mastery.threshold_crossed |
Mastery threshold crossed |
key.revoked |
API key revoked |
key.rotated |
API key rotated |
budget.exceeded |
Cost budget threshold crossed |
Event Envelope
{
"id": "evt_uuid_here",
"type": "assessment.completed",
"api_version": "2026.03.1",
"created_at": "2026-03-04T12:00:00+00:00",
"data": {
"session_id": "example",
"score": 85.0
}
}
Signing
Each delivery includes:
| Header | Meaning |
|---|---|
X-ANFI-Signature |
sha256=<hex> |
X-ANFI-Timestamp |
Unix timestamp used in the signature |
X-ANFI-Event-Id |
Event UUID |
X-ANFI-Event-Type |
Event type |
Signature input:
Example verification in Python:
import hashlib
import hmac
import time
def verify_webhook(payload_bytes, signing_secret, signature_header, timestamp):
if abs(int(time.time()) - int(timestamp)) > 300:
return False
expected = "sha256=" + hmac.new(
signing_secret.encode(),
f"{timestamp}.".encode() + payload_bytes,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(signature_header, expected)
Retry and Replay
The dispatcher persists every delivery row before the first outbound HTTP call. Failure states use this backoff schedule in minutes:
1, 5, 30, 120, 480, 1440, 2880, 5760
That corresponds to:
- 1 minute
- 5 minutes
- 30 minutes
- 2 hours
- 8 hours
- 24 hours
- 48 hours
- 96 hours
Important implementation note: the first-attempt dispatcher is live, and failed deliveries are marked with their next scheduled attempt. A general retry drainer is not yet exposed as a public workflow, so replay/retry remains an admin-operated process.
Operational Behavior
- signing secrets are generated server-side
- the secret is returned only once at endpoint creation
- list operations return
signing_secret: null - dead-letter rows can be reset to
pendingthrough the admin replay route