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" }'
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 }); });
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 |
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}