Webhooks

Receive real-time event notifications when important things happen — gate firings, trust zone changes, authorization decisions, and more.

Overview

Webhooks deliver event notifications to your server via HTTP POST requests. Each delivery includes a cryptographic signature so you can verify the payload originated from Mandate AI. Webhooks are delivered at most once per event, with automatic retries on failure.

Common use cases include alerting on declined transactions, monitoring agent trust degradation, triggering human review workflows on step-up decisions, and logging all authorization activity to your own systems.

Event types

Subscribe to specific event types or use * to receive all events.

Event Type Trigger
gate.fired A safety gate triggered during an authorization evaluation
cts.red An agent's cognitive trust score dropped into the RED zone (0.25 - 0.49)
cts.critical An agent's cognitive trust score dropped into the CRITICAL zone (below 0.25)
session.terminate An agent session was terminated due to repeated failures or critical trust
authorization.decline A transaction was declined by the authorization engine
trust.promotion An agent was promoted to a higher trust tier (e.g., new -> verified -> trusted)
* Wildcard — subscribe to all event types including future additions

Setting up

Register a webhook endpoint and receive a one-time signing secret. Store the secret securely — it cannot be retrieved again.

from mandate_ai_sdk import MandateAI
from mandate_ai_sdk.models import WebhookCreate

client = MandateAI(api_key="mdt_test_abc123...")

webhook = client.webhooks.create(WebhookCreate(
    url="https://your-app.com/webhooks/mandate-ai",
    event_types=["gate.fired", "cts.red", "cts.critical", "authorization.decline"],
    description="Production alerting",
))

print(f"ID: {webhook.id}")
print(f"Secret: {webhook.signing_secret}")  # whsec_... — save securely!
import { MandateAI } from "@mandate-ai/sdk";

const client = new MandateAI({ apiKey: "mdt_test_abc123..." });

const webhook = await client.webhooks.create({
  url: "https://your-app.com/webhooks/mandate-ai",
  event_types: ["gate.fired", "cts.red", "cts.critical", "authorization.decline"],
  description: "Production alerting",
});

console.log(`ID: ${webhook.id}`);
console.log(`Secret: ${webhook.signing_secret}`); // whsec_... — save securely!
curl -X POST https://mandateai-elwri.ondigitalocean.app/api/v1/webhooks \
  -H "X-API-Key: mdt_test_abc123..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/mandate-ai",
    "event_types": ["gate.fired", "cts.red", "cts.critical", "authorization.decline"],
    "description": "Production alerting"
  }'
One-time secret

The signing_secret (format: whsec_*) is only returned at creation time. If you lose it, delete the webhook and create a new one.

Payload format

All webhook deliveries use the same envelope format with event-specific data in the data field.

gate.fired

{
  "event_type": "gate.fired",
  "event_id": "evt_8f2k3m9x",
  "timestamp": "2026-05-25T14:32:01.234Z",
  "data": {
    "agent_id": "agent_abc123",
    "authorization_id": "auth_7f3k9x2m",
    "gate": "intent_anomaly",
    "decision": "DECLINE",
    "amount": "4999.99",
    "currency": "USD",
    "merchant": "Unknown Electronics Ltd",
    "trust_score": 0.43,
    "risk_score": 0.78
  }
}

cts.red / cts.critical

{
  "event_type": "cts.red",
  "event_id": "evt_9g4l2n8y",
  "timestamp": "2026-05-25T14:35:12.567Z",
  "data": {
    "agent_id": "agent_abc123",
    "previous_zone": "AMBER",
    "current_zone": "RED",
    "trust_score": 0.38,
    "cognitive_limit_multiplier": 0.4,
    "dimensions": {
      "context_coherence": 0.31,
      "reasoning_quality": 0.42,
      "mandate_compliance": 0.55,
      "behavioral_consistency": 0.29,
      "temporal_stability": 0.41
    }
  }
}

authorization.decline

{
  "event_type": "authorization.decline",
  "event_id": "evt_3h7m5p1z",
  "timestamp": "2026-05-25T14:40:22.891Z",
  "data": {
    "agent_id": "agent_abc123",
    "authorization_id": "auth_2x8k4m6n",
    "amount": "750.00",
    "currency": "USD",
    "merchant": "High-End Electronics",
    "gates_fired": ["mandate_exceeded", "cognitive_decline"],
    "trust_score": 0.38,
    "risk_score": 0.65,
    "recommendations": ["reduce_amount", "verify_intent"]
  }
}

session.terminate

{
  "event_type": "session.terminate",
  "event_id": "evt_5j9n1q3w",
  "timestamp": "2026-05-25T14:45:33.012Z",
  "data": {
    "agent_id": "agent_abc123",
    "session_id": "sess_daily_ops_20260525",
    "reason": "consecutive_denial_limit",
    "consecutive_denials": 5,
    "final_trust_score": 0.19
  }
}

trust.promotion

{
  "event_type": "trust.promotion",
  "event_id": "evt_1k2l3m4n",
  "timestamp": "2026-05-25T15:00:00.000Z",
  "data": {
    "agent_id": "agent_abc123",
    "previous_tier": "new",
    "new_tier": "verified",
    "trust_score": 0.89,
    "successful_txns": 52,
    "age_hours": 96
  }
}

Signature verification

Every webhook delivery includes an X-Mandate-Signature header containing an HMAC-SHA256 signature of the request body. Always verify this signature before processing the payload.

import hmac
import hashlib

def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
    """Verify the X-Mandate-Signature header.

    Args:
        payload: Raw request body bytes
        signature: Value of X-Mandate-Signature header
        secret: Your webhook signing secret (whsec_...)
    """
    expected = hmac.new(
        secret.encode("utf-8"),
        payload,
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected, signature)


# Flask example
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = "whsec_your_secret_here"

@app.route("/webhooks/mandate-ai", methods=["POST"])
def handle_webhook():
    signature = request.headers.get("X-Mandate-Signature", "")
    if not verify_webhook_signature(request.data, signature, WEBHOOK_SECRET):
        return jsonify({"error": "invalid signature"}), 401

    event = request.json
    print(f"Received: {event['event_type']} for agent {event['data']['agent_id']}")

    # Process the event...
    return jsonify({"received": True}), 200
import crypto from "crypto";
import express from "express";

function verifyWebhookSignature(payload: Buffer, signature: string, secret: string): boolean {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(signature, "hex")
  );
}

const app = express();
const WEBHOOK_SECRET = "whsec_your_secret_here";

app.post("/webhooks/mandate-ai", express.raw({ type: "application/json" }), (req, res) => {
  const signature = req.headers["x-mandate-signature"] as string;

  if (!verifyWebhookSignature(req.body, signature, WEBHOOK_SECRET)) {
    return res.status(401).json({ error: "invalid signature" });
  }

  const event = JSON.parse(req.body.toString());
  console.log(`Received: ${event.event_type} for agent ${event.data.agent_id}`);

  // Process the event...
  res.status(200).json({ received: true });
});
Important

Always use constant-time comparison (like hmac.compare_digest or crypto.timingSafeEqual) when verifying signatures to prevent timing attacks.

Retry policy

Mandate AI retries failed webhook deliveries with exponential backoff. Your endpoint must respond with a 2xx status code within 10 seconds to be considered successful.

Behavior Details
Retry attempts Up to 5 retries with exponential backoff (1s, 5s, 30s, 2m, 15m)
Timeout Your endpoint must respond within 10 seconds
Success codes Any 2xx HTTP status code (200, 201, 202, 204)
Failure codes Any non-2xx status code or timeout triggers a retry
Auto-disable After 50 consecutive failures, the webhook is automatically disabled
Re-enable Use client.webhooks.update(id, active=True) to re-enable after fixing the endpoint
Idempotency

Due to retries, your webhook handler may receive the same event more than once. Use the event_id field to deduplicate — store processed event IDs and skip any you have already handled.

Testing webhooks

Send a test event to verify your endpoint is configured correctly:

# Send a test ping to your webhook endpoint
result = client.webhooks.test(webhook_id="wh_abc123")
print(f"Test result: {result}")  # {"status": "delivered", "response_code": 200}
// Send a test ping to your webhook endpoint
const result = await client.webhooks.test("wh_abc123");
console.log(`Test result: ${JSON.stringify(result)}`); // {"status":"delivered","response_code":200}

Next steps