Webhooks reference

Every event Mandate can deliver, the exact payload of each, how to verify a delivery, and how delivery behaves on success and on failure. This is a companion to the live API reference — use it to build and test your receiver against a fixed structure.

1. The delivery envelope

Every webhook — regardless of event type — is an HTTP POST with a JSON body wrapped in the same envelope. Event-specific fields always live under data.

// HTTP POST to your registered URL
{
  "id":        "evt_4f9c1e8a7b6d4f2c9e1a3b5c7d9f0a2b",   // unique per delivery — de-dupe on this
  "event":     "authorization.decline",                  // the event type
  "timestamp": "2026-06-10T22:41:07.512938+00:00",        // ISO 8601 UTC, when sent
  "data":      { /* event-specific fields — see §3 */ }
}
Envelope fieldTypeNotes
idstringUnique per delivery (evt_ + 32 hex). Use it as your idempotency key: store the ids you have processed and ignore any repeats.
eventstringOne of the seven event types in §3. Also sent in the X-Mandate-Event header.
timestampstringISO 8601 UTC at the moment of dispatch.
dataobjectEvent-specific payload. Field set depends on event.

Request headers

HeaderValue
Content-Typeapplication/json
X-Mandate-EventThe event type, e.g. kya.zone.critical
X-Mandate-Signaturesha256=<hex> — HMAC-SHA256 of the raw body (see §2)
X-Mandate-Delivery-Attempt1-based attempt number. 1 on first try, increments on each retry. The envelope id is identical across attempts.
User-AgentMandateAI-Webhook/1.0

2. Verifying a webhook

Each delivery is signed with the signing_secret returned once when you created the webhook. The signature is HMAC-SHA256 over the raw request body, hex-encoded, prefixed with sha256=. Compute the same HMAC and compare in constant time. Reject any request whose signature does not match.

# Python
import hmac, hashlib

def verify(raw_body: bytes, header_sig: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header_sig)
Sign over the raw bytes

Verify before parsing JSON — re-serializing the body can change byte ordering and break the signature. The platform signs the exact body it sends (keys serialized in sorted order).

3. Event catalog & payloads

There are seven event types. Subscribe to a subset, or to * for all. Most events fire from the authorization pipeline and share a common base set of fields inside data; event-specific fields are added on top.

Shared base fields (present in data for all authorization-pipeline events)

FieldTypeDescription
authorization_idstringThe authorization this event relates to (auth_ prefix).
agent_idstringThe agent that transacted (agt_ prefix).
decisionstringAPPROVE | DECLINE | STEP_UP.
amountstringTransaction amount, decimal string (e.g. "120.00").
currencystringISO 4217 (e.g. "USD").
processing_time_msnumberEngine decision time in milliseconds.

Exception: trust.promotion does not use the base set — its data fields are listed with the event below.

authorization.decline decline

Fires when the engine returns a DECLINE. The reason codes name exactly why.

{
  "id": "evt_…", "event": "authorization.decline", "timestamp": "…",
  "data": {
    "authorization_id": "auth_1781111323760_13a96cd71daa0fc5",
    "agent_id": "agt_1781050696426_c442906049e6617f",
    "decision": "DECLINE",
    "amount": "800.00",
    "currency": "USD",
    "processing_time_ms": 73.44,
    "reason_codes": ["AMOUNT_EXCEEDS_PER_TXN"],
    "reason_detail": "Amount 800 exceeds per-txn limit 500.00"
  }
}

Extra fields: reason_codes (string[]), reason_detail (string). Common reason codes include AMOUNT_EXCEEDS_PER_TXN, MCC_NOT_ALLOWED, COUNTRY_NOT_ALLOWED, DAILY_COUNT_EXCEEDED, DAILY_AMOUNT_EXCEEDED, NO_ACTIVE_MANDATE, AGENT_SUSPENDED, KYA_SCORE_TOO_LOW, INTENT_ANOMALY_DETECTED, RISK_SCORE_TOO_HIGH, SYSTEM_UNAVAILABLE, VI_VERIFICATION_FAILED, VI_CONSTRAINT_VIOLATION, VI_AGENT_KEY_MISMATCH.

step_up.created step-up

Fires when the engine returns STEP_UP — the transaction needs out-of-band human confirmation before it can proceed.

{
  "id": "evt_…", "event": "step_up.created", "timestamp": "…",
  "data": {
    "authorization_id": "auth_…", "agent_id": "agt_…",
    "decision": "STEP_UP", "amount": "250.00", "currency": "USD",
    "processing_time_ms": 61.2,
    "reason_codes": ["INTENT_ANOMALY_STEP_UP"],
    "reason_detail": "Step-up verification required"
  }
}

Extra fields: reason_codes (string[]), reason_detail (string).

gate.fired behavioral

Fires when the Intent Anomaly Gate detects a behavioral deviation (e.g. a prompt-injection pattern) on a transaction.

{
  "id": "evt_…", "event": "gate.fired", "timestamp": "…",
  "data": {
    "authorization_id": "auth_…", "agent_id": "agt_…",
    "decision": "DECLINE", "amount": "45.00", "currency": "USD",
    "processing_time_ms": 63.1,
    "anomaly_score": 0.2286
  }
}

Extra field: anomaly_score (number, 0.0–1.0).

kya.zone.red behavioral trust

Fires when an agent's behavioral trust zone enters RED (limits tightened to ~50%). Legacy alias: cts.red — accepted for subscription during the deprecation window; subscribers registered under the alias receive the event under that name.

{
  "id": "evt_…", "event": "kya.zone.red", "timestamp": "…",
  "data": {
    "authorization_id": "auth_…", "agent_id": "agt_…",
    "decision": "APPROVE", "amount": "90.00", "currency": "USD",
    "processing_time_ms": 70.6,
    "cognitive_limit_multiplier": 0.50,
    "zone": "RED"
  }
}

Extra fields: cognitive_limit_multiplier (number, wire field name), zone ("RED").

kya.zone.critical behavioral trust

Fires when an agent's behavioral trust zone enters CRITICAL (limits ~25%; session termination is also recommended — see next event). Legacy alias: cts.critical.

{
  "id": "evt_…", "event": "kya.zone.critical", "timestamp": "…",
  "data": {
    "authorization_id": "auth_…", "agent_id": "agt_…",
    "decision": "DECLINE", "amount": "150.00", "currency": "USD",
    "processing_time_ms": 66.0,
    "cognitive_limit_multiplier": 0.25,
    "zone": "CRITICAL"
  }
}

Extra fields: cognitive_limit_multiplier (number, wire field name), zone ("CRITICAL").

session.terminate behavioral trust

Fires when the behavioral trust layer recommends terminating the agent's session (CRITICAL zone). Treat as a signal to stop routing further transactions from this agent session.

{
  "id": "evt_…", "event": "session.terminate", "timestamp": "…",
  "data": {
    "authorization_id": "auth_…", "agent_id": "agt_…",
    "decision": "DECLINE", "amount": "150.00", "currency": "USD",
    "processing_time_ms": 66.0,
    "cognitive_limit_multiplier": 0.25,
    "reason": "Trust score in CRITICAL zone; session termination recommended"
  }
}

Extra fields: cognitive_limit_multiplier (number, wire field name), reason (string).

trust.promotion lifecycle

Fires when an agent is auto-promoted to a higher trust level (e.g. REGISTERED → VERIFIED). Note: this event does NOT carry the shared base fields — its data is listed below.

{
  "id": "evt_…", "event": "trust.promotion", "timestamp": "…",
  "data": {
    "agent_id": "agt_…",
    "authorization_id": "auth_…",
    "new_trust_level": "VERIFIED",
    "kya_score": 0.83
  }
}

Fields: agent_id (string), authorization_id (string), new_trust_level (REGISTERED|VERIFIED|TRUSTED), kya_score (number).

4. Delivery, success, and failure behavior

AspectBehavior
TransportHTTPS POST, JSON body, ~10s total timeout (5s connect).
SuccessAny 2xx response. The webhook's failure counter resets; last_status_code and last_delivery_at are recorded.
FailureAny non-2xx response, timeout, connection error, or TLS error. The consecutive-failure counter increments and last_status_code is recorded.
RetriesAt-least-once. A transient failure (network error, timeout, 408, 429, or any 5xx) is retried in-process with exponential backoff + jitter — up to 5 attempts (~1s, 2s, 4s, 8s between tries). A permanent reject (any other 4xx, e.g. 400/401/404/410) stops immediately and is not retried.
IdempotencyEvery retry of an event reuses the same envelope id and signature — byte-identical. De-duplicate on id so a retry never double-processes.
Failure countingAn event counts as one failure only after all retries are exhausted — not once per attempt.
Auto-disableAfter 10 consecutive failed events, the webhook is automatically deactivated (active=false) and stops receiving events until re-enabled via the API.
OrderingNot guaranteed. Use timestamp and your own state, not arrival order.
SSRF protectionDelivery to private/internal IP ranges is blocked; your URL must resolve to a public, HTTPS address.
At-least-once, not exactly-once

Retries mean you may receive the same event more than once — always de-duplicate on the envelope id. And because retries are bounded (~15s total), a receiver down longer than that will still miss events. Keep a reconciliation job that periodically reads authorization history via the API to fill any gaps. Webhooks are for low-latency reaction; the API remains the system of record.

5. Receiver requirements & best practices

  1. Return 2xx fast. Acknowledge within the timeout, then process asynchronously. Slow handlers count as failures.
  2. Verify the signature on the raw body before trusting anything (§2). Reject mismatches with 401.
  3. De-duplicate on id. Store processed event ids; ignore repeats. Handlers must be idempotent.
  4. Tolerate unknown fields. New optional fields may be added to data without notice; do not hard-fail on extras.
  5. Reconcile. Poll the authorization API on a schedule to catch any events a downtime window dropped.
  6. Re-enable after auto-disable. If your endpoint was down long enough to trigger auto-disable, re-activate the webhook via PATCH once healthy.

6. Managing webhooks (API)

Method & pathPurpose
POST /api/v1/webhooksRegister a URL + event_types. Returns the webhook and the signing_secret (shown once).
GET /api/v1/webhooksList your registered webhooks.
GET /api/v1/webhooks/{id}Retrieve one webhook (incl. delivery stats).
PATCH /api/v1/webhooks/{id}Update URL, event_types, or re-activate after auto-disable.
DELETE /api/v1/webhooks/{id}Remove a webhook.
POST /api/v1/webhooks/{id}/testSend a test delivery to verify your receiver.
GET /api/v1/webhooks/eventsList all event types with sample payloads.

Create example

# POST /api/v1/webhooks   (X-API-Key: your key)
{
  "url": "https://your-app.example.com/hooks/mandate",
  "event_types": ["authorization.decline", "step_up.created", "kya.zone.critical"],
  "description": "Prod risk alerts"
}

# 201 Created — signing_secret is returned ONCE
{
  "webhook": { "id": "whk_…", "url": "…", "event_types": […], "active": true },
  "signing_secret": "whsec_…  // save this — not shown again"
}

Subscribe to "*" in event_types to receive every event.

This document is a companion, not the contract

Field availability, reason codes, and event types are versioned in the live API. When they differ, the API reference and your account's API version govern. Generated against Mandate API v0.7.