API reference

The customer-facing endpoints, from onboarding through authorization, step-up and outcomes, with the credential each one needs, its parameters, and an example request and response. All requests are HTTPS and exchange JSON.

Introduction

The Mandate Labs API is organized around the object hierarchy Client → Program → Principal → Agent → Mandate → Authorization. If you haven't yet, read Core concepts for how those objects nest and why.

All endpoints share a single base URL and are versioned under /api/v1:

https://api.mandatelabs.ai/api/v1

Requests and responses are application/json. Monetary amounts are decimal strings or numbers (e.g. "42.99"); send strings to avoid floating-point rounding. Decimal amounts come back as strings on single-resource responses (for example "daily_amount_used": "128.40") and as numbers inside paginated lists. IDs are prefixed by object type (cli_, prg_, prn_, agt_, mnd_, auth_, txn_, wh_). Errors use the envelopes described in Errors & idempotency.

Cursor-paginated lists return a data array and a pagination object, and accept limit (1 to 100, default 25) and cursor. Pass pagination.next_cursor as cursor to fetch the next page.

{
  "data": [ … ],
  "pagination": { "has_more": true, "next_cursor": "eyJjIjoi…", "limit": 25 }
}
One credential, two planes

Your Client API key authenticates both planes. Endpoints marked Client key build your tenant graph. Endpoints marked Runtime act for one of your Principals: add an X-Principal-Id header when your Client has more than one (on /authorize the Principal can also come from the agent). See Acting for a Principal.

Authentication

Every request carries a credential. Which ones an endpoint accepts depends on its plane.

Client API key onboarding + runtime

Pass your Client key in the X-API-Key header. Client keys are issued by Mandate Labs when your organization is provisioned. On the onboarding endpoints (everything that creates Principals, Agents, and Mandates on behalf of your tenant) the key acts for your Client. On the runtime endpoints it acts for one of your Principals, and every read and write is scoped (tenant-isolated) to that Principal.

X-API-Key: mdt_live_…        # live
X-API-Key: mdt_test_…        # sandbox
X-Principal-Id: prn_…        # runtime calls, when your Client has more than one Principal

Sandbox Principal key runtime

Also passed as X-API-Key. In the sandbox you can mint a per-Principal key (see Mint a sandbox key) so a developer can drive the runtime endpoints without your Client key. It always acts for its own Principal and does not authenticate onboarding.

Provisioning

Client provisioning and credential lifecycle are performed by Mandate Labs using an internal operator key. Those endpoints are not part of the public API; your Client and its API key are issued to you during onboarding (book a call).

OAuth2 & environments

Where OAuth2 is enabled, the runtime endpoints also accept a short-lived Bearer token (Authorization: Bearer …) minted at POST /api/v1/oauth/token; onboarding accepts only the Client API key. See Authentication. The environment is implied by the key prefix: mdt_live_ is live, mdt_test_ is sandbox. Sandbox keys behave identically but never touch real rails.

Onboarding

Client-authenticated endpoints that build your tenant graph. The core invariant: a Principal is one stable prn_ that owns many Agents: create the Principal once, then add agents to it. All create endpoints accept an Idempotency-Key header (see idempotency).

Create a full onboarding bundle

POST /api/v1/onboard

Atomically creates a Principal, its first Agent, and a Mandate in one call. Client key Ideal for quickstart and sandbox; use the decoupled endpoints below to add more agents later. Returns 201 (or 202 when the Principal needs platform verification).

Body paramTypeReqNotes
principalobjectyesA full Principal registration object (see Register a Principal).
agentobjectyesname (required), plus optional capabilities, program_id, external_agent_ref, metadata.
mandateobjectyesrails + limits (see Create a Mandate).
curl -X POST https://api.mandatelabs.ai/api/v1/onboard \
  -H "X-API-Key: mdt_live_…" -H "Content-Type: application/json" \
  -d '{
    "principal": {
      "principal_kind": "merchant",
      "program_ids": ["prg_…"],
      "client_customer_ref": "acme-001",
      "profile": { "display_name": "Acme Corp", "email": "[email protected]", "country": "US" },
      "verification": {
        "type": "KYB", "mode": "client_attested",
        "attestation": {
          "attested_by": "[email protected]",
          "scope": "Tier-2 CDD",
          "statement": "Acme performed CDD on this merchant",
          "evidence_ref": "case-123"
        }
      }
    },
    "agent": { "name": "Shopping Assistant v1", "capabilities": ["PURCHASE"] },
    "mandate": {
      "rails": ["card"],
      "limits": { "max_amount_per_transaction": 500, "max_daily_amount": 2000, "currency": "USD" }
    }
  }'
{
  "principal": {
    "id": "prn_…",
    "client_id": "cli_…",
    "principal_kind": "merchant",
    "client_customer_ref": "acme-001",
    "verification_status": "VERIFIED",
    "verification_mode": "client_attested",
    "risk_tier": "LOW",
    "programs": ["prg_…"],
    "agents": [],
    "verification_session": null,
    "created_at": "2026-06-27T18:04:11Z"
  },
  "agent": {
    "id": "agt_…",
    "principal_id": "prn_…",
    "program_id": "prg_…",
    "did": "did:key:z6Mk…",
    "name": "Shopping Assistant v1",
    "status": "ACTIVE",
    "trust_level": "REGISTERED",
    "kya_score": null
  },
  "mandate": {
    "id": "mnd_…",
    "agent_id": "agt_…",
    "program_id": "prg_…",
    "status": "ACTIVE",
    "rails": ["card"],
    "limits": { "max_amount_per_transaction": 500.0, "max_daily_amount": 2000.0, "currency": "USD" },
    "mandate_hash": "sha256:…",
    "valid_until": "2027-06-27T18:04:11Z"
  }
}

Register a Principal

POST /api/v1/principals

Registers a Principal under the Client and enrolls it into one or more Programs. Client key Returns 201 for client_attested (verified immediately) or 202 for platform_provider (verification pending; the response carries a verification_session).

Body paramTypeReqNotes
principal_kindstringyes"personal" or "merchant".
program_idsstring[]yes≥1 Program the Client owns; the Principal is enrolled in each.
profileobjectyesdisplay_name, email (required); optional phone (E.164), country.
verificationobjectyestype (KYC/KYB), mode (client_attested/platform_provider), and attestation (required for client_attested).
client_customer_refstringnoYour own reference; unique per Client.
step_upobjectnomethod (sms/email) + optional phone.
metadataobjectnoArbitrary key/value pairs.
curl -X POST https://api.mandatelabs.ai/api/v1/principals \
  -H "X-API-Key: mdt_live_…" -H "Content-Type: application/json" \
  -d '{
    "principal_kind": "merchant",
    "program_ids": ["prg_…"],
    "client_customer_ref": "acme-001",
    "profile": { "display_name": "Acme Corp", "email": "[email protected]", "country": "US" },
    "verification": {
      "type": "KYB", "mode": "client_attested",
      "attestation": { "attested_by": "[email protected]", "scope": "Tier-2 CDD",
        "statement": "Acme performed CDD on this merchant", "evidence_ref": "case-123" }
    }
  }'
{
  "id": "prn_…",
  "client_id": "cli_…",
  "principal_kind": "merchant",
  "client_customer_ref": "acme-001",
  "verification_status": "VERIFIED",
  "verification_mode": "client_attested",
  "risk_tier": "LOW",
  "programs": ["prg_…"],
  "agents": [],
  "verification_session": null,
  "created_at": "2026-06-27T18:04:11Z"
}

Get a Principal

GET /api/v1/principals/{principal_id}

Returns the acting Principal's own record. Runtime You can only read the acting Principal; requesting another prn_ returns 403.

curl https://api.mandatelabs.ai/api/v1/principals/prn_… \
  -H "X-API-Key: mdt_live_…"
{
  "id": "prn_…",
  "principal_kind": "merchant",
  "verification_status": "VERIFIED",
  "risk_tier": "LOW",
  "client_customer_ref": "acme-001"
}

Enroll a Principal into another Program

POST /api/v1/principals/{principal_id}/programs

Adds an existing Principal to an additional Program (many-to-many). Client key Idempotent: re-enrolling is a safe no-op. Returns the full list of enrolled program IDs.

Body: program_id (string, required): a Program the Client owns.

curl -X POST https://api.mandatelabs.ai/api/v1/principals/prn_…/programs \
  -H "X-API-Key: mdt_live_…" -H "Content-Type: application/json" \
  -d '{ "program_id": "prg_crypto_…" }'
{
  "principal_id": "prn_…",
  "programs": ["prg_…", "prg_crypto_…"]
}

Add an Agent under a Principal

POST /api/v1/principals/{principal_id}/agents

Registers a new Agent under an existing Principal; call once per agent. Client key This is how you honor the one-identity-many-agents invariant. If the Principal is enrolled in exactly one Program the agent inherits it; otherwise pass program_id.

Body paramTypeReqNotes
namestringyesDisplay name for the agent.
program_idstringnoRequired only when the Principal is enrolled in 0 or >1 Programs.
capabilitiesstring[]noDeclared capabilities (defaults to ["PURCHASE"]).
external_agent_refstringnoYour own reference for the agent.
metadataobjectnoArbitrary key/value pairs.
curl -X POST https://api.mandatelabs.ai/api/v1/principals/prn_…/agents \
  -H "X-API-Key: mdt_live_…" -H "Content-Type: application/json" \
  -d '{ "name": "Shopping Assistant v2", "capabilities": ["PURCHASE"] }'
{
  "id": "agt_…",
  "principal_id": "prn_…",
  "client_id": "cli_…",
  "program_id": "prg_…",
  "did": "did:key:z6Mk…",
  "name": "Shopping Assistant v2",
  "status": "ACTIVE",
  "trust_level": "REGISTERED",
  "kya_score": null,
  "created_at": "2026-06-27T18:06:02Z"
}

List a Principal's Agents

GET /api/v1/principals/{principal_id}/agents

Lists every Agent registered under the Principal. Client key

curl https://api.mandatelabs.ai/api/v1/principals/prn_…/agents \
  -H "X-API-Key: mdt_live_…"
{
  "principal_id": "prn_…",
  "agents": [
    { "id": "agt_…", "name": "Shopping Assistant v1", "status": "ACTIVE", "trust_level": "VERIFIED", "kya_score": 0.81 },
    { "id": "agt_…", "name": "Shopping Assistant v2", "status": "ACTIVE", "trust_level": "REGISTERED", "kya_score": null }
  ],
  "count": 2
}

Create a Mandate for an Agent

POST /api/v1/agents/{agent_id}/mandates

Grants spending authority to an Agent: card, crypto, or both. Client key When crypto is in rails, a crypto block is required. Every rail named must be enabled on the agent's Program, otherwise the call fails with 422 rail_not_enabled. Each authorization uses the newest active mandate that covers its rail.

Body paramTypeReqNotes
railsstring[]yesSubset of {"card","crypto"}.
limitsobjectyesmax_amount_per_transaction, max_daily_amount (both >0), currency (ISO 4217).
allowed_mccsstring[]noAllowed Merchant Category Codes.
allowed_countriesstring[]noAllowed countries, ISO 3166-1 alpha-3 ("USA"), the format /authorize sends.
cryptoobjectcond.chains, assets, optional max_transfer (a per-transfer cap; above it an authorization declines with AMOUNT_EXCEEDS_PER_TXN). Required iff crypto ∈ rails.
valid_untildatetimenoExpiry; defaults to ~1 year.
curl -X POST https://api.mandatelabs.ai/api/v1/agents/agt_…/mandates \
  -H "X-API-Key: mdt_live_…" -H "Content-Type: application/json" \
  -d '{
    "rails": ["card"],
    "limits": { "max_amount_per_transaction": 500, "max_daily_amount": 2000, "currency": "USD" },
    "allowed_mccs": ["5411", "5812"],
    "allowed_countries": ["USA"]
  }'
{
  "id": "mnd_…",
  "agent_id": "agt_…",
  "program_id": "prg_…",
  "status": "ACTIVE",
  "rails": ["card"],
  "limits": { "max_amount_per_transaction": 500.0, "max_daily_amount": 2000.0, "currency": "USD" },
  "mandate_hash": "sha256:…",
  "valid_until": "2027-06-27T18:08:40Z",
  "created_at": "2026-06-27T18:08:40Z"
}

Mint a sandbox key for a Principal

POST /api/v1/principals/{principal_id}/sandbox-key

Mints a per-Principal sandbox API key so a freshly onboarded dev Principal can drive the runtime endpoints itself. Client key (sandbox) Sandbox-only; refused (403 sandbox_only) with a live credential. The key is shown once.

curl -X POST https://api.mandatelabs.ai/api/v1/principals/prn_…/sandbox-key \
  -H "X-API-Key: mdt_test_…"
{
  "principal_id": "prn_…",
  "api_key": "mdt_test_…",
  "env": "sandbox",
  "message": "Sandbox key for this Principal — use as X-API-Key. Shown once."
}

Authorization

The runtime decision plane. POST /authorize is the core operation: every transaction an agent attempts is evaluated here against its mandate, velocity limits, and KYA trust score, and recorded as an immutable Authorization. Runtime

Authorize a transaction

POST /api/v1/authorize

Evaluates a transaction and returns a real-time APPROVE / DECLINE / STEP_UP decision with reason codes, KYA score, and a structured risk assessment. Provide at least one of agent_id or agent_did. intent_context is mandatory on every request.

Body paramTypeReqNotes
agent_id / agent_didstringoneIdentify the agent (agt_ id or its DID).
amountdecimalyesTransaction amount, >0. Send a decimal string.
currencystringnoISO 4217, defaults to USD. Must match the mandate currency.
intent_contextobjectyestask_reference (required), plus optional intent_type, reasoning_summary, confidence, alternatives_considered, interaction_depth, session_id, recurrence_number, original_authorization_id. See intent_context.
mccstringif constrained4-digit Merchant Category Code. Send it when the mandate restricts MCCs.
merchant_name / merchant_idstringif constrainedMerchant display name / identifier. Send merchant_id when the mandate has a merchant allowlist.
countrystringif constrainedISO 3166-1 alpha-3 ("USA"); two-letter codes are rejected. Send it when the mandate restricts countries.
capabilitystringnoA capability the agent must hold, e.g. PURCHASE.
railstringnoCARD (default) or CRYPTO; CRYPTO requires a crypto block (see Rails).
cryptoobjectwith CRYPTOchain, asset, counterparty, from_address, value, operation. For USDC, value may not exceed amount and currency must be USD (else 422).
vi_chainobjectnoA Verifiable Intent credential chain (see Verifiable intent).
idempotency_keystringno8–128 chars (letters, digits, _ - : .). A retry with the same key replays the original decision.
client_referencestringnoYour own order or intent reference (≤128 chars). Outcomes can later match on it.
transaction_idstringnoAn existing txn_ id, to thread a retry or re-authorization onto one transaction.
network_rrn, network_stan, auth_code, network_reference, tx_hash, chain_idstring / intnoCard-network or on-chain references you already hold.
runtime_evidenceobjectnoControls performed, tools called and a trace hash, recorded on the decision record. It does not change the decision in this version.
metadataobjectnoFree-form context stored in the audit log.
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": "42.99",
    "currency": "USD",
    "merchant_name": "Whole Foods Market #1042",
    "mcc": "5411",
    "country": "USA",
    "intent_context": {
      "intent_type": "PURCHASE",
      "task_reference": "weekly_grocery_purchase",
      "reasoning_summary": "Weekly organic produce restock; compared 3 nearby grocers",
      "confidence": 0.91,
      "alternatives_considered": 3
    }
  }'
{
  "authorization_id": "auth_…",
  "transaction_id": "txn_…",
  "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, "highest_severity": "NONE"
  },
  "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
}

Reverse an authorization

POST /api/v1/authorize/reverse

Releases the velocity an approved authorization reserved, for a transaction that was cancelled or only partly completed (the ISO 8583 0400/0420 case). Runtime Idempotent: reversing the same authorization again returns already_reversed: true. A declined or stepped-up authorization reserved nothing and returns reversed: false.

Body paramTypeReqNotes
authorization_id / original_idempotency_keystringoneThe authorization to reverse, by id or by the idempotency_key it was sent with.
amountdecimalnoAmount to release. Omit for a full reversal. A partial reversal releases the amount, but the transaction still counts toward count limits.
reasonstringnoUp to 255 characters, stored in the audit log.
curl -X POST https://api.mandatelabs.ai/api/v1/authorize/reverse \
  -H "X-API-Key: mdt_live_…" -H "Content-Type: application/json" \
  -d '{ "authorization_id": "auth_…", "reason": "Order cancelled by merchant" }'
{
  "authorization_id": "auth_…",
  "reversed": true,
  "released_amount": 42.99,
  "partial": false,
  "already_reversed": false,
  "message": "Full reversal"
}

Get an authorization log entry

GET /api/v1/authorize/log/{authorization_id}

Retrieves a single authorization decision from the audit log. Runtime Tenant-scoped: only the owning Principal's authorizations are visible.

curl https://api.mandatelabs.ai/api/v1/authorize/log/auth_… \
  -H "X-API-Key: mdt_live_…"
{
  "id": "auth_…",
  "agent_id": "agt_…",
  "mandate_id": "mnd_…",
  "amount": "42.99",
  "currency": "USD",
  "mcc": "5411",
  "merchant_id": null,
  "country": "USA",
  "decision": "APPROVE",
  "reason_codes": [],
  "kya_score": 0.87,
  "trust_level": "VERIFIED",
  "processing_time_ms": 8.4,
  "created_at": "2026-06-27T18:10:55Z"
}

List an agent's authorization history

GET /api/v1/authorize/log/agent/{agent_id}

Returns the agent's authorization history, newest first, paginated. Runtime

Query: limit (1 to 100, default 25) and cursor. Each item has the same fields as a single log entry.

curl https://api.mandatelabs.ai/api/v1/authorize/log/agent/agt_…?limit=2 \
  -H "X-API-Key: mdt_live_…"
{
  "data": [
    { "id": "auth_…", "agent_id": "agt_…", "decision": "APPROVE", "amount": 42.99, "currency": "USD", "reason_codes": [], "created_at": "2026-06-27T18:10:55+00:00", … },
    { "id": "auth_…", "agent_id": "agt_…", "decision": "STEP_UP", "amount": 4800.0, "currency": "USD", "reason_codes": ["INTENT_ANOMALY_STEP_UP"], "created_at": "2026-06-27T17:55:01+00:00", … }
  ],
  "pagination": { "has_more": true, "next_cursor": "eyJjIjoi…", "limit": 2 }
}

Step-up

When /authorize returns STEP_UP, a challenge stays open for 5 minutes. Record the principal's answer against the original authorization_id; there is no flag to resubmit on /authorize. How the challenge reaches the principal is covered in the step-up flow. Runtime

List pending challenges

GET /api/v1/authorize/step-up/pending

Challenges not yet answered, newest first. Runtime The list can include challenges past their expires_at; those can no longer be confirmed.

Query: limit (1 to 100, default 25) and cursor.

curl https://api.mandatelabs.ai/api/v1/authorize/step-up/pending \
  -H "X-API-Key: mdt_live_…"
{
  "data": [
    {
      "authorization_id": "auth_…",
      "agent_id": "agt_…",
      "amount": "250.00",
      "currency": "USD",
      "merchant_name": "Northwind Electronics",
      "reason_codes": ["INTENT_ANOMALY_STEP_UP"],
      "expires_at": "2026-06-27T18:15:55+00:00",
      "created_at": "2026-06-27T18:10:55+00:00"
    }
  ],
  "pagination": { "has_more": false, "next_cursor": null, "limit": 25 }
}

Confirm a challenge

POST /api/v1/authorize/step-up/{authorization_id}/confirm

The principal approved the transaction. Runtime No body. The challenge must still be pending and unexpired; otherwise the call returns 400.

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": "2026-06-27T18:12:40+00:00"
}

Deny a challenge

POST /api/v1/authorize/step-up/{authorization_id}/deny

The principal rejected the transaction. The authorization's decision becomes DECLINE in the audit log. Runtime

Body (optional): reason (up to 500 characters).

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" }'
{
  "success": true,
  "authorization_id": "auth_…",
  "denied_by": "[email protected]",
  "new_decision": "DECLINE"
}

Verify a one-time code

POST /api/v1/authorize/step-up/{authorization_id}/verify-otp

Checks the 6-digit code sent to the principal by email or SMS. A correct code confirms the challenge and returns the same body as Confirm; a wrong or expired code returns 400. Runtime

Body: code (string, 6 digits, required).

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

Agents

Manage and query agents under the authenticated Principal. Runtime Note: in onboarding you add agents via /principals/{id}/agents; these endpoints operate on the runtime plane.

Register an agent

POST /api/v1/agents

Registers a new agent for the acting Principal. Runtime If principal_id is supplied it must match the acting Principal.

Body paramTypeReqNotes
display_namestringyesAgent display name.
capabilitiesstring[]yes≥1 of PURCHASE, SUBSCRIPTION, REFUND_REQUEST, BALANCE_INQUIRY, TRANSFER.
didstringnoW3C DID; a did:key is auto-generated if omitted.
public_key_jwk + registration_proofobject / stringnoAgent key + proof of possession (required for Verifiable Intent).
curl -X POST https://api.mandatelabs.ai/api/v1/agents \
  -H "X-API-Key: mdt_live_…" -H "Content-Type: application/json" \
  -d '{ "display_name": "Procurement Bot", "capabilities": ["PURCHASE"] }'
{
  "id": "agt_…",
  "did": "did:key:z6Mk…",
  "did_method": "key",
  "display_name": "Procurement Bot",
  "principal_id": "prn_…",
  "status": "ACTIVE",
  "trust_level": "REGISTERED",
  "capabilities": ["PURCHASE"],
  "total_transactions": 0,
  "decline_rate": 0.0,
  "created_at": "2026-06-27T18:12:00Z"
}

List agents

GET /api/v1/agents

Lists agents belonging to the authenticated Principal. Runtime

Query: status, trust_level, page (≥1), page_size (1–100).

curl "https://api.mandatelabs.ai/api/v1/agents?trust_level=VERIFIED&page_size=50" \
  -H "X-API-Key: mdt_live_…"
{
  "agents": [ { "id": "agt_…", "display_name": "Procurement Bot", "trust_level": "VERIFIED", "status": "ACTIVE" } ],
  "total": 1,
  "page": 1,
  "page_size": 50
}

Get an agent

GET /api/v1/agents/{agent_id}

Fetches a single agent (must belong to the authenticated Principal). Runtime

curl https://api.mandatelabs.ai/api/v1/agents/agt_… \
  -H "X-API-Key: mdt_live_…"
{
  "id": "agt_…",
  "display_name": "Procurement Bot",
  "trust_level": "VERIFIED",
  "status": "ACTIVE",
  "capabilities": ["PURCHASE"],
  "total_transactions": 214,
  "decline_rate": 0.0327
}

Get agent metrics

GET /api/v1/agents/{agent_id}/metrics

Returns behavioral metrics and trust-promotion status for an agent. Runtime

curl https://api.mandatelabs.ai/api/v1/agents/agt_…/metrics \
  -H "X-API-Key: mdt_live_…"
{
  "agent_id": "agt_…",
  "trust_level": "VERIFIED",
  "total_transactions": 214,
  "successful_transactions": 207,
  "declined_transactions": 7,
  "decline_rate": 0.0327,
  "dispute_deflection_rate": 0.98,
  "trust_promotion_eligible": false,
  "next_trust_level": "TRUSTED",
  "promotion_criteria": {
    "successful_transactions": { "required": 500, "actual": 207, "met": false },
    "account_age_hours": { "required": 720, "actual": 1630.5, "met": true },
    "decline_rate": { "required": "<2%", "actual": "3.27%", "met": false },
    "dispute_deflection_rate": { "required": ">90%", "actual": "98.00%", "met": true }
  }
}

Update agent status

PATCH /api/v1/agents/{agent_id}/status

Suspends, reactivates, or revokes an agent. Runtime

Body: status (new AgentStatus) and reason (1–500 chars), both required.

curl -X PATCH https://api.mandatelabs.ai/api/v1/agents/agt_…/status \
  -H "X-API-Key: mdt_live_…" -H "Content-Type: application/json" \
  -d '{ "status": "SUSPENDED", "reason": "Manual review pending" }'
{
  "id": "agt_…",
  "status": "SUSPENDED",
  "trust_level": "VERIFIED",
  "display_name": "Procurement Bot"
}

Mandates

Create and query spending mandates on the runtime plane. Runtime The agent a mandate targets must belong to the authenticated Principal.

Create a mandate

POST /api/v1/mandates

Creates a new spending mandate for an agent (AP2-compatible). Runtime

Body paramTypeReqNotes
agent_idstringyesTarget agent (owned by the Principal).
valid_untildatetimeyesMandate expiry.
max_amount_per_transactiondecimalyes>0, 2 decimal places.
currencystringnoISO 4217, defaults to USD.
max_daily_amount / max_monthly_amountdecimalnoVelocity caps.
allowed_mccs / blocked_mccsstring[]noMCC allow/block lists.
allowed_countries / blocked_countriesstring[]noCountry allow/block lists, ISO 3166-1 alpha-3.
allowed_chains / allowed_assets / allowed_counterpartiesstring[]noCrypto-rail constraints.
curl -X POST https://api.mandatelabs.ai/api/v1/mandates \
  -H "X-API-Key: mdt_live_…" -H "Content-Type: application/json" \
  -d '{
    "agent_id": "agt_…",
    "valid_until": "2027-01-01T00:00:00Z",
    "max_amount_per_transaction": "500.00",
    "max_daily_amount": "2000.00",
    "currency": "USD",
    "allowed_mccs": ["5411", "5812"]
  }'
{
  "id": "mnd_…",
  "agent_id": "agt_…",
  "principal_id": "prn_…",
  "status": "ACTIVE",
  "valid_from": "2026-06-27T18:14:30Z",
  "valid_until": "2027-01-01T00:00:00Z",
  "max_amount_per_transaction": "500.00",
  "currency": "USD",
  "max_daily_amount": "2000.00",
  "allowed_mccs": ["5411", "5812"],
  "mandate_hash": "sha256:…",
  "created_at": "2026-06-27T18:14:30Z"
}

Get a mandate

GET /api/v1/mandates/{mandate_id}

Fetches a mandate (ownership is verified through its agent). Runtime

curl https://api.mandatelabs.ai/api/v1/mandates/mnd_… \
  -H "X-API-Key: mdt_live_…"
{
  "id": "mnd_…",
  "agent_id": "agt_…",
  "status": "ACTIVE",
  "max_amount_per_transaction": "500.00",
  "currency": "USD",
  "valid_until": "2027-01-01T00:00:00Z"
}

List an agent's mandates

GET /api/v1/mandates/agent/{agent_id}

Lists all mandates for a given agent. Runtime

Query: status (optional MandateStatus filter).

curl https://api.mandatelabs.ai/api/v1/mandates/agent/agt_… \
  -H "X-API-Key: mdt_live_…"
{
  "mandates": [ { "id": "mnd_…", "status": "ACTIVE", "max_amount_per_transaction": "500.00", "currency": "USD" } ],
  "total": 1
}

Revoke a mandate

DELETE /api/v1/mandates/{mandate_id}

Revokes an active mandate. Runtime The mandate is kept for the audit trail with status REVOKED, and the response returns it. Authorizations that relied on it decline with NO_ACTIVE_MANDATE unless the agent holds another active mandate.

curl -X DELETE https://api.mandatelabs.ai/api/v1/mandates/mnd_… \
  -H "X-API-Key: mdt_live_…"
{
  "id": "mnd_…",
  "agent_id": "agt_…",
  "status": "REVOKED",
  "max_amount_per_transaction": "500.00",
  "currency": "USD",
  "valid_until": "2027-01-01T00:00:00Z"
}

Outcomes

Outcomes record what actually happened after a decision: settlement, a chargeback, confirmed fraud, a principal confirming or disowning a transaction. They arrive days or weeks later and close the loop on the decision record. Runtime

Record an outcome

POST /api/v1/outcomes

Links an outcome to a past authorization. Runtime Returns 201. Idempotent per authorization and outcome type: resending returns the existing outcome instead of writing a second one. A settlement re-report is reconciled by settled_at, so the latest settlement event wins regardless of arrival order.

Body paramTypeReqNotes
authorization_idstringone ofThe authorization this outcome belongs to.
network_rrn / tx_hash / client_referencestringone ofMatch the authorization by a reference you hold instead of its id. For client_reference, the most recent matching authorization is used.
outcome_typestringyesSETTLED, CLEARED, CHARGEBACK, CONFIRMED_FRAUD, DISPUTE_RESOLVED, PRINCIPAL_CONFIRMED_UNAUTHORIZED, PRINCIPAL_CONFIRMED_LEGITIMATE, MERCHANT_COMPLAINT, or FALSE_POSITIVE_REPORTED.
reported_atdatetimeyesWhen the outcome was reported.
reported_bystringyesPRINCIPAL, ISSUER, MERCHANT, or INTERNAL.
settled_amount, settled_currency, settled_atdecimal, string, datetimenoFor SETTLED and CLEARED: the settled value from your clearing file and the settlement event time. Send settled_at so a later correction can supersede an earlier report.
evidenceobjectnoDispute details, chargeback reason codes, or other supporting evidence.
notes / external_reference_idstringnoFree text (up to 5,000 characters) and your own case id (up to 255).
network_stan, auth_code, network_reference, chain_idstring / intnoFurther references. auth_code, network_reference and chain_id are recorded on the authorization where it has none yet.
curl -X POST https://api.mandatelabs.ai/api/v1/outcomes \
  -H "X-API-Key: mdt_live_…" -H "Content-Type: application/json" \
  -d '{
    "authorization_id": "auth_…",
    "outcome_type": "CHARGEBACK",
    "reported_at": "2026-07-19T14:02:00Z",
    "reported_by": "ISSUER",
    "evidence": { "reason_code": "10.4" },
    "external_reference_id": "CB-88213"
  }'
{
  "id": "out_…",
  "authorization_id": "auth_…",
  "tenant_id": "prn_…",
  "outcome_type": "CHARGEBACK",
  "reported_at": "2026-07-19T14:02:00+00:00",
  "reported_by": "ISSUER",
  "evidence": { "reason_code": "10.4" },
  "notes": null,
  "external_reference_id": "CB-88213",
  "settled_amount": null,
  "settled_currency": null,
  "settled_at": null,
  "created_at": "2026-07-19T14:02:03+00:00"
}

Record outcomes in bulk

POST /api/v1/outcomes/batch

Feeds up to 500 outcomes from a clearing, settlement or fraud file in one call. Runtime The request is validated as a whole first: a row missing outcome_type, reported_at or reported_by, or with an over-length field, rejects the whole call with 422. After that, each row is matched and written on its own, so a row whose authorization is not found or whose values are not accepted fails alone. Returns 200 with a summary and a result per item: created, updated (a newer settlement report), duplicate, not_found, or invalid.

Body: outcomes: an array of the objects accepted by Record an outcome.

curl -X POST https://api.mandatelabs.ai/api/v1/outcomes/batch \
  -H "X-API-Key: mdt_live_…" -H "Content-Type: application/json" \
  -d '{
    "outcomes": [
      { "network_rrn": "612345678901", "outcome_type": "SETTLED", "reported_at": "2026-06-28T06:00:00Z",
        "reported_by": "ISSUER", "settled_amount": "42.99", "settled_currency": "USD", "settled_at": "2026-06-28T02:14:00Z" },
      { "authorization_id": "auth_…", "outcome_type": "PRINCIPAL_CONFIRMED_LEGITIMATE",
        "reported_at": "2026-06-28T06:00:00Z", "reported_by": "PRINCIPAL" }
    ]
  }'
{
  "total": 2, "created": 1, "updated": 0, "duplicate": 0, "not_found": 1, "invalid": 0,
  "results": [
    { "index": 0, "status": "not_found", "id": null, "authorization_id": null, "error": "…" },
    { "index": 1, "status": "created", "id": "out_…", "authorization_id": "auth_…", "error": null }
  ]
}

List outcomes for an authorization

GET /api/v1/outcomes/{authorization_id}

Every outcome recorded against an authorization, newest first. Runtime Paginated with limit and cursor; each item has the fields returned by Record an outcome.

curl https://api.mandatelabs.ai/api/v1/outcomes/auth_… \
  -H "X-API-Key: mdt_live_…"

Crypto settlements

For an approved CRYPTO authorization: report the broadcast transfer, then follow it to SETTLED. A settled transfer also records a SETTLED outcome. See Rails for the full crypto flow. Runtime

Report a settlement

POST /api/v1/crypto/settlements

Registers the transaction hash of the EIP-3009 transfer your wallet or facilitator broadcast, and checks it on-chain right away. Runtime Idempotent on tx_hash. The transfer becomes SETTLED only once it has the required confirmations; a background watcher keeps checking until then. Returns 400 if the authorization is not yours or was not approved, and 403 when the crypto rail is not enabled in the environment.

Body paramTypeReqNotes
authorization_idstringyesThe approved crypto authorization.
tx_hashstringyes0x followed by 64 hex characters.
chainstringnoDefaults to base.
from_address / to_addressstringnoEVM addresses of payer and recipient.
value_rawstringnoTransferred amount in the token's base units.
curl -X POST https://api.mandatelabs.ai/api/v1/crypto/settlements \
  -H "X-API-Key: mdt_live_…" -H "Content-Type: application/json" \
  -d '{ "authorization_id": "auth_…", "tx_hash": "0x9f3c…", "chain": "base" }'
{
  "id": "stl_…",
  "authorization_id": "auth_…",
  "chain": "base",
  "tx_hash": "0x9f3c…",
  "status": "PENDING",
  "confirmations": 1,
  "block_number": 31877402,
  "settled_at": null,
  "created_at": "2026-06-27T12:01:14Z"
}

status moves from SUBMITTED (reported, not yet seen on-chain) to PENDING (mined, awaiting confirmations) to SETTLED, or to FAILED if the transaction reverted.

Get settlement status

GET /api/v1/crypto/settlements/{authorization_id}

Every settlement reported for an authorization, oldest first, as an array of the objects above. Runtime

curl https://api.mandatelabs.ai/api/v1/crypto/settlements/auth_… \
  -H "X-API-Key: mdt_live_…"

Webhooks

Register HTTPS endpoints to receive real-time events. Runtime Webhook URLs must be public HTTPS endpoints; the signing secret (used to verify X-Mandate-Signature) is returned once on create. See the Webhooks guide for payloads and signature verification.

Register a webhook

POST /api/v1/webhooks

Creates a webhook subscription and returns its signing secret (shown only once). Runtime

Body paramTypeReqNotes
urlstringyesPublic HTTPS endpoint (≤2048 chars).
event_typesstring[]yes≥1 of gate.fired, kya.zone.red, kya.zone.critical, session.terminate, authorization.decline, trust.promotion, step_up.created, *. With fraud triage enabled, also fraud.triage.suspected_created, fraud.triage.resolved, fraud.pattern.agto_suspected.
descriptionstringno≤255 chars.
curl -X POST https://api.mandatelabs.ai/api/v1/webhooks \
  -H "X-API-Key: mdt_live_…" -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/mandate-ai",
    "event_types": ["gate.fired", "authorization.decline", "trust.promotion"],
    "description": "Production alerting webhook"
  }'
{
  "webhook": {
    "id": "wh_…",
    "url": "https://your-app.com/webhooks/mandate-ai",
    "event_types": ["gate.fired", "authorization.decline", "trust.promotion"],
    "active": true,
    "consecutive_failures": 0,
    "created_at": "2026-06-27T18:16:10Z"
  },
  "signing_secret": "whsec_…"
}

List webhooks

GET /api/v1/webhooks

Lists all webhooks for the authenticated Principal (paginated). Runtime

curl https://api.mandatelabs.ai/api/v1/webhooks \
  -H "X-API-Key: mdt_live_…"
{
  "data": [ { "id": "wh_…", "url": "https://your-app.com/webhooks/mandate-ai", "event_types": ["gate.fired"], "active": true, "last_status_code": 200, … } ],
  "pagination": { "has_more": false, "next_cursor": null, "limit": 25 }
}

Get a webhook

GET /api/v1/webhooks/{webhook_id}

Fetches a single webhook by ID. Runtime

curl https://api.mandatelabs.ai/api/v1/webhooks/wh_… \
  -H "X-API-Key: mdt_live_…"
{
  "id": "wh_…",
  "url": "https://your-app.com/webhooks/mandate-ai",
  "event_types": ["gate.fired", "authorization.decline"],
  "active": true,
  "last_delivery_at": "2026-06-27T18:15:00Z",
  "last_status_code": 200,
  "consecutive_failures": 0
}

Update a webhook

PATCH /api/v1/webhooks/{webhook_id}

Updates a webhook's URL, events, active flag, or description. Runtime Re-enabling (active: true) resets the failure counter.

Body (all optional): url, event_types, active, description.

curl -X PATCH https://api.mandatelabs.ai/api/v1/webhooks/wh_… \
  -H "X-API-Key: mdt_live_…" -H "Content-Type: application/json" \
  -d '{ "event_types": ["*"], "active": true }'
{
  "id": "wh_…",
  "url": "https://your-app.com/webhooks/mandate-ai",
  "event_types": ["*"],
  "active": true,
  "consecutive_failures": 0
}

Send a test event

POST /api/v1/webhooks/{webhook_id}/test

Dispatches a sample payload to the webhook so you can verify delivery, signature checking and routing in your handler. Runtime The payload includes "test": true. The webhook must be active.

Query: event_type (defaults to gate.fired).

curl -X POST "https://api.mandatelabs.ai/api/v1/webhooks/wh_…/test?event_type=authorization.decline" \
  -H "X-API-Key: mdt_live_…"
{
  "status": "sent",
  "event_type": "authorization.decline",
  "message": "Test 'authorization.decline' event dispatched to https://your-app.com/webhooks/mandate-ai."
}

Delete a webhook

DELETE /api/v1/webhooks/{webhook_id}

Permanently deletes a webhook. Runtime Returns 204 No Content.

curl -X DELETE https://api.mandatelabs.ai/api/v1/webhooks/wh_… \
  -H "X-API-Key: mdt_live_…"
# → 204 No Content

List webhook events

GET /api/v1/webhooks/events

Returns every event type in the catalog with a description and a sample payload. Samples are illustrative; the Webhooks reference lists the exact fields each event carries. No auth required

curl https://api.mandatelabs.ai/api/v1/webhooks/events
{
  "event_types": [
    {
      "type": "gate.fired",
      "description": "Intent Anomaly Gate triggered — suspicious behavioral shift detected in agent session.",
      "sample_payload": { "agent_id": "agent_sample_01", "decision": "STEP_UP", "anomaly_score": 0.62 }
    }
  ],
  "wildcard": "Use '*' when registering to receive all event types."
}

Thresholds

Tune your own gate, risk, and trust-promotion thresholds. Runtime Reads return the fully-resolved values (your overrides merged over the platform defaults). Updates are validated for logical consistency (e.g. trust zones must stay ordered green > amber > red).

Get resolved thresholds

GET /api/v1/thresholds

Returns effective thresholds along with which fields you've customized vs. defaults. Runtime

curl https://api.mandatelabs.ai/api/v1/thresholds \
  -H "X-API-Key: mdt_live_…"
{
  "thresholds": { "authorize_min_kya_score": 0.15, "authorize_step_up_threshold": 0.6, "cts_green_threshold": 0.7, "risk_score_hard_decline": 0.85, … },
  "overrides": { "authorize_step_up_threshold": 0.6 },
  "platform_defaults": { "authorize_min_kya_score": 0.15, "authorize_step_up_threshold": 0.4, "cts_green_threshold": 0.7, "risk_score_hard_decline": 0.85, … }
}

Update thresholds

PUT /api/v1/thresholds

Sets issuer-specific overrides; include only the fields you want to change; set a field to null to revert it to default. Runtime Validates cross-field consistency.

Body: any subset of threshold fields, e.g. authorize_step_up_threshold, cts_green_threshold, risk_score_hard_decline, verified_min_successful_txns.

curl -X PUT https://api.mandatelabs.ai/api/v1/thresholds \
  -H "X-API-Key: mdt_live_…" -H "Content-Type: application/json" \
  -d '{ "authorize_step_up_threshold": 0.6, "risk_score_hard_decline": 0.9 }'
{
  "status": "updated",
  "fields_changed": ["authorize_step_up_threshold", "risk_score_hard_decline"],
  "effective_thresholds": { "authorize_step_up_threshold": 0.6, "risk_score_hard_decline": 0.9, … }
}

Reset thresholds

DELETE /api/v1/thresholds

Deletes all your overrides, reverting every threshold to the platform default. Runtime

curl -X DELETE https://api.mandatelabs.ai/api/v1/thresholds \
  -H "X-API-Key: mdt_live_…"
{
  "status": "reset",
  "message": "All thresholds reverted to platform defaults"
}

Next steps