Authorization Engine

How POST /api/v1/authorize decides: the pipeline, the request and response fields, reason codes, and the step-up flow.

Overview

Every call to POST /api/v1/authorize runs the Decision Trust Protocol pipeline: seven decision gates followed by two system steps. It returns one of three decisions: APPROVE, DECLINE, or STEP_UP (a human must confirm before the transaction proceeds). Every decision carries machine-readable reason codes, the agent's KYA Score, and a structured risk assessment, and is written to the audit log. The exceptions are an AGENT_NOT_FOUND decline, which has no agent to attach a record to, and the fail-policy answer returned while the engine itself is unavailable.

Engine decision time is 23ms under burst, with a sustained end-to-end P50 under 100ms.

The authorization pipeline

Steps 1 to 7 run in order and any of them can end the evaluation early with a DECLINE or STEP_UP. Steps 8 and 9 are system steps that record the decision and run follow-up work.

1

Agent resolve

Finds the agent by agent_id or agent_did within your tenant. An agent that does not exist and an agent that belongs to another tenant both get the same AGENT_NOT_FOUND decline. Suspended or revoked agents are declined, and if the request names a capability, the agent must hold it.

2

Intent verification

Validates the mandatory intent_context. If the request presents a Verifiable Intent chain in vi_chain, the chain is verified in full, and an invalid chain is a hard decline (see Verifiable intent).

3

Anomaly gate (EDQS)

Compares the stated intent with the agent's behavioral baseline for the session. Scoring starts once there is a baseline: 3 earlier transactions in the same session when intent_context.session_id is sent, otherwise 5 earlier transactions for the agent. With platform defaults, an anomaly score of 0.20 or more steps up and 0.70 or more declines.

4

Mandate enforcement

Checks the agent's active mandate: per-transaction amount, currency, MCC allow and block lists, merchant allowlist, countries, allowed hours and days, and daily and monthly amount and count caps. If the mandate constrains a field that the request leaves out, the transaction is declined and reason_codes include MANDATE_SCOPE_UNVERIFIABLE.

5

Behavioral overlay

Applies the agent's trust-zone multiplier (cognitive_limit_multiplier) to the mandate's per-transaction amount limit: 1.0 in GREEN, 0.75 in AMBER, 0.50 in RED, and 0.25 in CRITICAL. See KYA scoring.

6

Risk scoring

Scores velocity spikes, amount outliers, rapid-fire bursts, and behavioral, geographic and temporal indicators into risk_score and risk_assessment. With platform defaults, a risk score of 0.85 or more (or any critical signal) declines, and 0.60 or more steps up.

7

KYA decision

Computes the agent's KYA Score for this transaction. Below 0.15 the transaction is declined and below 0.40 it steps up (platform defaults, adjustable through the Thresholds API). Otherwise it is approved.

8

Persist and webhooks (system)

Writes the signed decision record to the audit log and fires the matching webhooks (authorization.decline, step_up.created, gate.fired, and trust-zone events).

9

Async post-decision (system)

Runs after the response is returned: trust promotion checks, deeper risk analysis, and delivery of step-up challenges. None of this delays the decision.

Request format

The POST /api/v1/authorize body accepts these fields:

FieldTypeRequiredDescription
agent_idstringOne of the twoAgent id (agt_…).
agent_didstringOne of the twoThe agent's W3C DID, as an alternative to agent_id.
amountdecimalYesGreater than 0. Send a decimal string such as "42.99"; numbers are also accepted.
currencystringNoISO 4217 code. Defaults to USD and must match the mandate currency.
intent_contextobjectYesWhy the agent is transacting. See the table below.
mccstringIf constrained4-digit Merchant Category Code. Required in practice when the mandate restricts MCCs.
merchant_namestringNoMerchant display name (up to 255 characters).
merchant_idstringIf constrainedMerchant identifier (up to 128 characters). Required when the mandate has a merchant allowlist.
countrystringIf constrainedISO 3166-1 alpha-3 code, for example "USA". Two-letter codes are rejected. Required when the mandate restricts countries.
capabilitystringNoA capability the agent must hold for this transaction, for example PURCHASE.
railstringNoCARD (default) or CRYPTO. CRYPTO requires the crypto block; see Rails.
cryptoobjectWith CRYPTOThe on-chain transfer: chain, asset, counterparty, from_address, value, operation. For USDC, value may not exceed amount and currency must be USD (else 422). Not allowed on a card request.
vi_chainobjectNoVerifiable Intent chain: l1, l2, and optionally l3a and l3b.
idempotency_keystringNo8 to 128 characters (letters, digits, _ - : .). A retry with the same key returns the original decision instead of creating a second authorization.
client_referencestringNoYour own order or intent reference (up to 128 characters), stored as sent.
transaction_idstringNoAn existing txn_ id to thread a retry or re-authorization onto. Omit it and the engine mints a new one.
network_rrn, network_stan, auth_code, network_referencestringNoCard network references, if you already hold them.
tx_hash, chain_idstring, integerNoOn-chain references, if already known.
runtime_evidenceobjectNoWhat the agent did before asking: controls performed, tools called, trace hash. Recorded on the decision record; it does not change the decision in this version.
metadataobjectNoFree-form context stored in the audit log.

intent_context

FieldTypeRequiredDescription
intent_typestringNoOne of PURCHASE (default), REPEAT_PURCHASE, RECURRING, COF, REFUND, TRANSFER. Each type is scored on different criteria.
task_referencestringYesThe task or goal this transaction serves (up to 500 characters).
reasoning_summarystringNoThe agent's reasoning (up to 2,000 characters). Expected for PURCHASE, REPEAT_PURCHASE and TRANSFER.
confidencenumberNoSelf-reported confidence, 0 to 1.
alternatives_consideredintegerNoHow many options the agent evaluated before choosing this one.
interaction_depthintegerNoHow many tool calls or steps the agent is into its current task.
session_idstringNoYour orchestration session id (up to 128 characters). Groups transactions for session-level scoring.
recurrence_numberintegerNoFor RECURRING and COF: which occurrence this is.
original_authorization_idstringNoFor REFUND: the original authorization. For COF: the authorization that stored the credential.
Send every field your mandate constrains

If the mandate restricts MCCs, merchants or countries and the request leaves that field out, the transaction is declined and reason_codes include MANDATE_SCOPE_UNVERIFIABLE. The API ignores fields it does not recognize, so a misspelled field (for example merchant instead of merchant_name, or session_id at the top level instead of inside intent_context) is dropped without an error.

Response format

A successful call returns 200 with the decision and every signal that contributed to it:

{
  "authorization_id": "auth_1782561623760_13a96cd71daa0fc5",
  "transaction_id": "txn_1782561623760_4be1f07a2c9d8e11",
  "correlation_key": "5d0c9b3e…",
  "decision": "APPROVE",
  "reason_codes": [],
  "reason_detail": "All checks passed",
  "agent_id": "agt_…",
  "mandate_id": "mnd_…",
  "kya_score": 0.87,
  "trust_level": "VERIFIED",
  "risk_score": 0.12,
  "risk_assessment": {
    "score": 0.12,
    "reason_codes": [],
    "condition_code": "00",
    "velocity_indicator": false,
    "behavioral_indicator": false,
    "geographic_indicator": false,
    "temporal_indicator": false,
    "amount_indicator": false,
    "active_signal_count": 0,
    "critical_signal_count": 0,
    "highest_severity": "NONE",
    "recommendation": "APPROVE",
    "baseline_transactions": 214,
    "baseline_age_hours": 1630.5
  },
  "daily_amount_used": "128.40",
  "daily_count_used": 3,
  "monthly_amount_used": "1840.10",
  "monthly_count_used": 41,
  "intent_context_provided": true,
  "intent_anomaly_score": 0.05,
  "intent_anomaly_triggered": false,
  "intent_anomaly_skipped": false,
  "vi_verified": false,
  "vi_mode": null,
  "cognitive_limit_active": false,
  "cognitive_limit_multiplier": 1.0,
  "session_terminate_recommended": false,
  "attestation": null,
  "processing_time_ms": 8.4
}
FieldDescription
authorization_idId of this decision in the audit log (auth_…).
transaction_idRail-agnostic transaction id (txn_…). Reversals, retries and re-authorizations of the same transaction share it.
correlation_keyGroups structurally similar transactions (same agent, rail, counterparty, MCC and amount band).
decisionAPPROVE, DECLINE or STEP_UP.
reason_codes, reason_detailMachine-readable reasons, most specific first, and a human-readable explanation. See Reason codes.
agent_id, mandate_idThe resolved agent and the mandate that was evaluated.
kya_score, trust_levelThe agent's KYA Score (0 to 1) and trust level at decision time.
risk_score, risk_assessmentThe agent's risk score (0 to 1) and the assessment behind it: up to four reason codes, a condition code (00 to 07), indicator flags and the risk engine's recommendation. The assessment is shaped for DE 48.75 mapping; it is not a scheme-certified score.
daily_amount_used, daily_count_used, monthly_amount_used, monthly_count_usedVelocity already consumed before this transaction. Amounts are decimal strings.
intent_context_providedWhether the request carried intent reasoning. The task_reference counts, so this is true for every valid request.
intent_anomaly_score, intent_anomaly_triggered, intent_anomaly_skippedAnomaly gate output. intent_anomaly_skipped is true when the gate did not score, for example before the session has enough history.
vi_verified, vi_modeWhether a presented Verifiable Intent chain verified, and its mode (immediate or autonomous).
cognitive_limit_active, cognitive_limit_multiplierWhether the trust-zone multiplier is tightening this agent's limits, and its current value.
session_terminate_recommendedTrue when the agent is in the CRITICAL zone. An advisory signal for your orchestration layer; it does not block the current transaction.
attestationCrypto approvals only: the signed transaction-authorization attestation {jws, kid, nonce, valid_after, valid_before, expires_at}. See Rails.
processing_time_msEngine time for this decision, in milliseconds.

Decisions

APPROVE

Every gate passed. Proceed with the payment. On the crypto rail the response also carries a signed attestation that the wallet verifies before signing.

DECLINE

Do not proceed. reason_codes says why, most specific first, and reason_detail explains it in words. Common causes: a mandate limit or scope violation, a high risk score, a KYA Score below the floor, or an intent anomaly. Declines also fire authorization.decline, except when the agent is not found or the engine is unavailable.

STEP_UP

A human must confirm before the transaction proceeds. Mandate Labs opens a challenge that is valid for 5 minutes and fires step_up.created. See the step-up flow below.

Step-up flow

How the challenge reaches the human depends on the Principal's step-up method, set with the step_up block at onboarding or with PUT /api/v1/principals/me/step-up:

MethodWhat happens
MANDATE_DASHBOARD (default)No message is sent. The challenge appears in GET /api/v1/authorize/step-up/pending and in the dashboard; your system collects the answer and calls confirm or deny.
MANDATE_EMAILThe principal receives a one-time code by email and, where email action links are enabled, a single-use approve or deny link. Submit the code with verify-otp.
MANDATE_SMSThe principal receives a one-time code by SMS. Submit it with verify-otp.
ISSUER_3DSYour issuer flow runs the challenge (for example 3-D Secure) and reports the result with confirm or deny.
# The principal approved
curl -X POST https://api.mandatelabs.ai/api/v1/authorize/step-up/auth_…/confirm \
  -H "X-API-Key: mdt_live_…"
# → { "success": true, "authorization_id": "auth_…", "confirmed_by": "[email protected]", "confirmed_at": "…" }

# The principal declined (reason is optional)
curl -X POST https://api.mandatelabs.ai/api/v1/authorize/step-up/auth_…/deny \
  -H "X-API-Key: mdt_live_…" -H "Content-Type: application/json" \
  -d '{ "reason": "Principal did not recognize the purchase" }'

# Check a one-time code delivered by email or SMS
curl -X POST https://api.mandatelabs.ai/api/v1/authorize/step-up/auth_…/verify-otp \
  -H "X-API-Key: mdt_live_…" -H "Content-Type: application/json" \
  -d '{ "code": "482913" }'

# Challenges still waiting for an answer (paginated)
curl https://api.mandatelabs.ai/api/v1/authorize/step-up/pending \
  -H "X-API-Key: mdt_live_…"
Record the answer on the original authorization

There is no step_up_confirmed flag on /authorize: a resubmitted request is evaluated as a new authorization. Confirm or deny the challenge against its authorization_id instead. A denial changes that authorization's decision to DECLINE in the audit log, and after 5 minutes a challenge can no longer be confirmed.

Reason codes

reason_codes lists the reasons for a DECLINE or STEP_UP, most specific first. Codes for limits tightened by the agent's trust zone are listed on the KYA scoring page.

CodeTypical decisionMeaning
AGENT_NOT_FOUNDDECLINENo agent with that id or DID in your tenant. Also returned for agents that belong to another tenant.
AGENT_NOT_ACTIVE, AGENT_SUSPENDED, AGENT_REVOKEDDECLINEThe agent is not in ACTIVE status.
CAPABILITY_NOT_ALLOWEDDECLINEThe agent does not hold the capability named in the request.
VI_VERIFICATION_FAILED, VI_CONSTRAINT_VIOLATION, VI_AGENT_KEY_MISMATCHDECLINEA presented Verifiable Intent chain failed verification. See Verifiable intent.
INTENT_ANOMALY_STEP_UPSTEP_UPThe anomaly gate scored the intent between the step-up and decline thresholds.
INTENT_ANOMALY_DETECTEDDECLINEThe anomaly gate scored the intent at or above the decline threshold.
NO_ACTIVE_MANDATE, MANDATE_EXPIREDDECLINEThe agent has no active, unexpired mandate that covers the request's rail.
AMOUNT_EXCEEDS_PER_TXNDECLINEThe amount is above the mandate's per-transaction limit.
CURRENCY_MISMATCHDECLINEThe request currency differs from the mandate currency.
MCC_NOT_ALLOWED, MCC_BLOCKEDDECLINEThe MCC is outside the mandate's allowlist, or on its blocklist.
MERCHANT_NOT_ALLOWEDDECLINEThe merchant is not on the mandate's merchant allowlist.
COUNTRY_NOT_ALLOWED, COUNTRY_BLOCKEDDECLINEThe country is outside the mandate's allowlist, or on its blocklist. Codes are compared as ISO 3166-1 alpha-3.
MANDATE_SCOPE_UNVERIFIABLEDECLINEThe mandate constrains a field (MCC, merchant or country) that the request did not send. When that field has an allowlist, it follows MCC_NOT_ALLOWED, MERCHANT_NOT_ALLOWED or COUNTRY_NOT_ALLOWED.
OUTSIDE_TIME_WINDOW, DAY_NOT_ALLOWEDDECLINEOutside the mandate's allowed hours or days.
DAILY_AMOUNT_EXCEEDED, DAILY_COUNT_EXCEEDED, MONTHLY_AMOUNT_EXCEEDED, MONTHLY_COUNT_EXCEEDEDDECLINEThe transaction would exceed a velocity cap on the mandate.
CHAIN_NOT_ALLOWED, ASSET_NOT_ALLOWED, COUNTERPARTY_NOT_ALLOWED, WALLET_ADDRESS_MISMATCHDECLINECrypto rail: the chain, asset or recipient is outside the mandate, or the transfer does not come from the agent's bound wallet.
RAIL_NOT_ENABLEDDECLINEThe requested rail is not enabled for the agent's Program, or in this environment.
RISK_SCORE_TOO_HIGHDECLINE or STEP_UPThe agent's risk score is at or above the hard-decline or step-up threshold.
KYA_SCORE_TOO_LOWDECLINE or STEP_UPThe KYA Score is below the decline floor (0.15) or the step-up threshold (0.40), platform defaults.
PEAK_EXPOSURE_STEP_UPSTEP_UPWhen enabled for your account: an established agent is paying a new counterparty an amount that is high for its own history.
PRINCIPAL_VELOCITY_EXCEEDEDDECLINE or STEP_UPWhen enabled for your account: spending across all of the Principal's agents exceeds its aggregate limit.
SYSTEM_UNAVAILABLEPer fail policyA dependency needed for the decision was unavailable. Your fail policy applies (fail closed by default).

Detectors that are still rolling out can add further codes (lowercase, for example structuring_pattern). Log codes you do not recognize rather than failing on them.

Code examples

A complete authorization call with decision handling. Send your credential in the X-API-Key header (see Authentication), then branch on decision:

curl -X POST https://api.mandatelabs.ai/api/v1/authorize \
  -H "X-API-Key: mdt_live_…" -H "Content-Type: application/json" \
  -d '{
    "agent_id": "agt_…",
    "amount": "149.99",
    "currency": "USD",
    "merchant_name": "Amazon Web Services",
    "mcc": "5734",
    "country": "USA",
    "idempotency_key": "ops-20260525-0042",
    "intent_context": {
      "intent_type": "PURCHASE",
      "task_reference": "cloud_infrastructure_scaling",
      "reasoning_summary": "Auto-scaling triggered by a traffic spike; reserved capacity is cheaper than on-demand for the next 30 days",
      "confidence": 0.84,
      "alternatives_considered": 2,
      "session_id": "sess_daily_ops_20260525"
    }
  }'

# Response: "decision" is "APPROVE", "DECLINE" or "STEP_UP",
# with reason_codes, reason_detail, kya_score, risk_score and risk_assessment.
import httpx

resp = httpx.post(
    "https://api.mandatelabs.ai/api/v1/authorize",
    headers={"X-API-Key": "mdt_live_…"},
    json={
        "agent_id": "agt_…",
        "amount": "149.99",
        "currency": "USD",
        "merchant_name": "Amazon Web Services",
        "mcc": "5734",
        "country": "USA",
        "idempotency_key": "ops-20260525-0042",
        "intent_context": {
            "intent_type": "PURCHASE",
            "task_reference": "cloud_infrastructure_scaling",
            "reasoning_summary": "Auto-scaling triggered by a traffic spike; reserved capacity is cheaper than on-demand for the next 30 days",
            "confidence": 0.84,
            "alternatives_considered": 2,
            "session_id": "sess_daily_ops_20260525",
        },
    },
)
result = resp.json()

if result["decision"] == "APPROVE":
    process_payment(result["authorization_id"])
elif result["decision"] == "STEP_UP":
    # A challenge is open for 5 minutes; confirm or deny it once the principal answers
    queue_for_confirmation(result["authorization_id"], result["reason_codes"])
else:  # DECLINE
    log_decline(result["reason_codes"], result["reason_detail"])
const resp = await fetch("https://api.mandatelabs.ai/api/v1/authorize", {
  method: "POST",
  headers: { "X-API-Key": "mdt_live_…", "Content-Type": "application/json" },
  body: JSON.stringify({
    agent_id: "agt_…",
    amount: "149.99",
    currency: "USD",
    merchant_name: "Amazon Web Services",
    mcc: "5734",
    country: "USA",
    idempotency_key: "ops-20260525-0042",
    intent_context: {
      intent_type: "PURCHASE",
      task_reference: "cloud_infrastructure_scaling",
      reasoning_summary: "Auto-scaling triggered by a traffic spike; reserved capacity is cheaper than on-demand for the next 30 days",
      confidence: 0.84,
      alternatives_considered: 2,
      session_id: "sess_daily_ops_20260525",
    },
  }),
});
const result = await resp.json();

if (result.decision === "APPROVE") {
  processPayment(result.authorization_id);
} else if (result.decision === "STEP_UP") {
  // A challenge is open for 5 minutes; confirm or deny it once the principal answers
  queueForConfirmation(result.authorization_id, result.reason_codes);
} else {
  logDecline(result.reason_codes, result.reason_detail);
}

To review past decisions for an agent, query the authorization history (newest first). List responses return a data array and a pagination object; pass pagination.next_cursor as cursor to fetch the next page.

curl "https://api.mandatelabs.ai/api/v1/authorize/log/agent/agt_…?limit=10" \
  -H "X-API-Key: mdt_live_…"

Next steps