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 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 core event types, plus three fraud triage events that are delivered only when fraud triage is enabled for your account. 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 with each authorization while an agent's behavioral trust zone is RED (per-transaction limit 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 with each authorization while an agent's behavioral trust zone is CRITICAL (per-transaction limit at 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).

Fraud triage events

Delivered only when fraud triage is enabled for your account. These are labeling notifications: they tell you a transaction pattern is under review or has been resolved, and your team decides whether and how to surface them to end users. They do not carry the shared base fields.

fraud.triage.suspected_created triage

Fires when a suspected-fraud triage case is opened: by a declined or stepped-up transaction, by a reviewer, or by an adverse outcome reported later for an approved transaction.

{
  "id": "evt_…", "event": "fraud.triage.suspected_created", "timestamp": "…",
  "data": {
    "case_id": "trg_…",
    "decision": "STEP_UP",
    "agent_id": "agt_…",
    "typology_candidates": ["A5"]
  }
}

Fields: case_id, decision, agent_id, typology_candidates (string[]). A case opened manually carries case_id, source ("manual") and agent_id instead, and a case opened from a reported outcome carries case_id, status, via ("matured_outcome") and outcome_type.

fraud.triage.resolved triage

Fires when a triage case is resolved, either by a reviewer or by an adverse outcome (such as confirmed fraud) reported later for an approved transaction.

{
  "id": "evt_…", "event": "fraud.triage.resolved", "timestamp": "…",
  "data": {
    "case_id": "trg_…",
    "status": "CONFIRMED_FRAUD",
    "typology_id": "A5"
  }
}

Fields: case_id, status, and typology_id when a reviewer resolves the case. A case resolved by a matured outcome carries case_id, status, via ("matured_outcome") and outcome_type.

fraud.pattern.agto_suspected pattern

Fires when repeated suspected cases point to a possible agent or account takeover, for one agent or across a Principal's agents.

{
  "id": "evt_…", "event": "fraud.pattern.agto_suspected", "timestamp": "…",
  "data": {
    "scope": "agent",
    "agent_id": "agt_…",
    "principal_id": "prn_…",
    "agent_cases_72h": 3,
    "principal_cases_7d": 3,
    "recommended_action": "suspend_agent",
    "severity": "high"
  }
}

Fields: scope (agent|principal), agent_id, principal_id, agent_cases_72h, principal_cases_7d, recommended_action (suspend_agent|step_up_all), severity.

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, HTTP 408, 423, 425 or 429, or any 5xx) is retried in-process with exponential backoff + jitter, up to 5 attempts (~1s, 2s, 4s, 8s between tries). Any other 4xx is a permanent reject (e.g. 400/401/404/410): delivery 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. In rare recovery cases an event can be re-sent under a new id; if you must act exactly once, also key on event plus data.authorization_id.
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": "wh_…", "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.