Navigate docs

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

  1. You register a subscription via the register_webhook MCP tool, specifying the destination URL and which event types to receive.
  2. When a matching event occurs, AgentLattice enqueues a delivery.
  3. A background worker signs the payload with HMAC-SHA256 using the subscription's secret, then POSTs it to your URL.
  4. If delivery fails, AgentLattice retries with exponential backoff (up to 3 attempts).
  5. 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.