Webhook Subscriptions
AgentLattice can push governance events to your systems in real time via webhooks. When an agent action is denied, a delegation expires, or an anomaly is detected, AgentLattice delivers a signed HTTP POST to the URL you configure. Your system verifies the signature, processes the event, and responds with a 2xx status code.
Webhook subscriptions are managed through MCP tools, not the REST API. See MCP Integration for setup.
How Webhooks Work
- You register a subscription via the
register_webhookMCP tool, specifying the destination URL and which event types to receive. - When a matching event occurs, AgentLattice enqueues a delivery.
- A background worker signs the payload with HMAC-SHA256 using the subscription's secret, then POSTs it to your URL.
- If delivery fails, AgentLattice retries with exponential backoff (up to 3 attempts).
- After 10 consecutive fully-exhausted failures across deliveries, the subscription is automatically disabled.
Supported Event Types
Subscribe to any combination of these event types:
| Event Type | Fired When |
|---|---|
action.denied |
A policy evaluation denies an agent's action |
action.executed |
An action passes policy evaluation and is auto-executed |
delegation.expired |
A time-bounded delegation reaches its expiry |
delegation.revoked |
A parent agent or operator revokes a delegation |
policy.changed |
A policy is created, updated, or deleted |
anomaly.detected |
The anomaly detection system flags unusual agent behavior |
enforcement.triggered |
A circuit breaker enforcement action (halt, kill) is applied |
Event types are matched exactly as strings. If you subscribe to an event type that does not match any of the values above, deliveries will be created but will never fire because no matching events will occur. There is no server-side validation of event type names at registration time.
Subscribing to Events
Register a webhook subscription using the register_webhook MCP tool:
register_webhook({
name: "prod-security-alerts",
url: "https://yourserver.example.com/al-webhooks",
events: ["action.denied", "anomaly.detected", "enforcement.triggered"],
})
The response includes the subscription ID and a secret. This secret is shown exactly once at registration time. Store it immediately in your secrets manager. If you lose the secret, you must delete the subscription and create a new one. There is no "show secret" or "rotate secret" operation.
Only HTTPS URLs are accepted. Plain HTTP endpoints are rejected at registration time.
Verifying Payloads
Every webhook delivery includes three headers for verification:
| Header | Value | Purpose |
|---|---|---|
X-AL-Signature |
sha256=<hex> |
HMAC-SHA256 of the raw JSON request body |
X-AL-Timestamp |
ISO-8601 string | Delivery timestamp (use for replay protection) |
X-AL-Event-Type |
String | The event type that triggered this delivery |
The signature is computed over the raw JSON body exactly as sent. To verify, compute the HMAC-SHA256 of the raw body using your stored secret, prepend sha256=, and compare to the X-AL-Signature header.
Node.js Verification
import { createHmac, timingSafeEqual } from "crypto";
function verifyWebhook(rawBody: string, signatureHeader: string, secret: string): boolean {
const expected = "sha256=" + createHmac("sha256", secret)
.update(rawBody, "utf8")
.digest("hex");
if (expected.length !== signatureHeader.length) return false;
return timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
Edge / Serverless Verification (Web Crypto API)
async function verifyWebhook(rawBody: string, signatureHeader: string, secret: string): Promise<boolean> {
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"],
);
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(rawBody));
const expected = "sha256=" + Array.from(new Uint8Array(sig))
.map(b => b.toString(16).padStart(2, "0"))
.join("");
return expected === signatureHeader;
}
Python Verification
import hashlib
import hmac
def verify_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header)
Note: the Python function expects raw_body as bytes (the raw request body before any decoding).
Use timing-safe comparison in production to prevent timing attacks. The Node.js example uses timingSafeEqual; Python's hmac.compare_digest is constant-time by default. The Edge/Serverless example above uses plain === string comparison, which is not timing-safe. In edge environments, use a constant-time comparison library or compare byte-by-byte.
Retry and Backoff
If your endpoint returns a non-2xx response or the connection fails, AgentLattice retries:
| Attempt | Delay After Failure |
|---|---|
| 1st (initial) | Immediate |
| 2nd | +1 minute |
| 3rd (final) | +5 minutes |
After the 3rd attempt fails, the delivery is marked as permanently failed. No further retries occur for that specific delivery. Each delivery attempt has a 10-second timeout. Endpoints that take longer to respond are counted as failures.
Auto-Disable Policy
AgentLattice tracks consecutive failures at the subscription level. A "consecutive failure" is a delivery that exhausts all 3 retry attempts without a single successful response.
- After 10 consecutive fully-exhausted failures, the subscription is automatically disabled (
is_active: false). - A single successful delivery at any point resets the failure counter to zero.
- Auto-disabled subscriptions are not deleted. They remain visible when listing webhooks, with
is_active: false. - To re-enable, delete the disabled subscription and create a new one.
Monitor your webhook subscriptions periodically. Auto-disable happens silently. There is no notification when a subscription is disabled.
Security
HTTPS required. Only https:// URLs are accepted at registration time. Plain HTTP endpoints are rejected.
SSRF protection. AgentLattice blocks deliveries to internal and private network targets. The following destinations are rejected at both registration and delivery time:
| Category | Blocked Ranges |
|---|---|
| Private IPv4 | 10.x.x.x, 172.16-31.x.x, 192.168.x.x |
| Loopback | 127.x.x.x, ::1 |
| Link-local | 169.254.x.x, fe80: |
| IPv6 private | fc00:, fd00: |
| Cloud metadata | metadata.google.internal, metadata.internal, localhost |
These checks run before every delivery attempt, not just at registration. A URL that resolves to a private IP after DNS changes will be blocked at delivery time.