Set up your development environment

Get familiar with the Mandate Labs SDKs and start authorizing agent transactions in minutes.

Who this guide is for: agent platforms integrating pre-presentment. If you are an issuer or processor integrating inside the authorization window, start with the issuer integration guide; production issuer deployments are sales-led and certified via MCI.

Overview

Event naming: kya.zone.* are the current event names (API v0.7). The legacy aliases cts.red / cts.critical remain accepted for 12 months under our deprecation policy.

This guide walks you through everything you need to go from zero to authorizing your first AI agent transaction: become a vetted Client, use the onboarding API to create a Principal and Agent, set spending limits via a mandate, and submit a transaction for real-time authorization.

What you'll learn

Sandbox vs. Production

Sandbox keys (mdt_test_*) work identically to production keys (mdt_live_*) but never touch real payment rails. Sandbox keys are provisioned on request; production access is granted to vetted Clients (Book a call).

Get started

There is no self-serve sign-up. Clients are vetted through KYB onboarding before they are provisioned; sandbox keys come with provisioning. To get started, Book a call.

Once provisioned you receive a Client API key (mdt_live_…) and use the onboarding API shown below. All Client-authed requests pass the key in the X-API-Key header against the base URL https://api.mandatelabs.ai.

Onboard a Principal and Agent

This step uses your Client API key (mdt_live_…). The POST /api/v1/onboard bundle creates a Principal, an Agent, and a Mandate in a single call.

SDKs

Official SDKs are in beta; the canonical interface is the HTTP API shown here.

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

resp = httpx.post(
    "https://api.mandatelabs.ai/api/v1/onboard",
    headers={"X-API-Key": "mdt_live_…"},
    json={
        "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"},
        },
    },
)

data = resp.json()
agent_id = data["agent"]["id"]  # agt_…
const resp = await fetch("https://api.mandatelabs.ai/api/v1/onboard", {
  method: "POST",
  headers: { "X-API-Key": "mdt_live_…", "Content-Type": "application/json" },
  body: JSON.stringify({
    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" },
    },
  }),
});

const data = await resp.json();
const agentId = data.agent.id; // agt_…

One Principal can own many Agents. The bundle above created the Principal; to give it more agents, call POST /api/v1/principals/{principal_id}/agents with the prn_ id from the bundle response, once per agent. If you prefer to build step by step instead of using the bundle, register the Principal on its own with POST /api/v1/principals first. It takes the same principal object, including the required verification block.

# Add another agent to the Principal the bundle created (repeat per agent)
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"] }'

# Step-by-step alternative to the bundle: register a Principal on its own
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-002",
    "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" }
    }
  }'
# → 201 Created, "id": "prn_…"

Create a spending mandate

Mandates define what an agent is allowed to spend. Add or update a mandate for an existing agent with POST /api/v1/agents/{agent_id}/mandates, setting per-transaction and daily limits, allowed merchant categories, and allowed countries. Country codes are ISO 3166-1 alpha-3 (USA, not US), the same format /authorize expects.

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"]
  }'
import httpx

resp = httpx.post(
    "https://api.mandatelabs.ai/api/v1/agents/agt_…/mandates",
    headers={"X-API-Key": "mdt_live_…"},
    json={
        "rails": ["card"],
        "limits": {"max_amount_per_transaction": 500, "max_daily_amount": 2000, "currency": "USD"},
        "allowed_mccs": ["5411", "5812"],
        "allowed_countries": ["USA"],
    },
)

mandate = resp.json()
print(mandate["id"])  # mnd_…
const resp = await fetch("https://api.mandatelabs.ai/api/v1/agents/agt_…/mandates", {
  method: "POST",
  headers: { "X-API-Key": "mdt_live_…", "Content-Type": "application/json" },
  body: JSON.stringify({
    rails: ["card"],
    limits: { max_amount_per_transaction: 500, max_daily_amount: 2000, currency: "USD" },
    allowed_mccs: ["5411", "5812"],
    allowed_countries: ["USA"],
  }),
});

const mandate = await resp.json();
console.log(mandate.id); // mnd_…

Authorize a transaction

This is the core operation. Submit a transaction request to POST /api/v1/authorize and receive a real-time decision with the agent's KYA Score, a structured risk assessment, and machine-readable reason codes. Use the agent_id returned when you onboarded the agent above.

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 on price and delivery window",
      "confidence": 0.91,
      "alternatives_considered": 3
    }
  }'
import httpx

resp = httpx.post(
    "https://api.mandatelabs.ai/api/v1/authorize",
    headers={"X-API-Key": "mdt_live_…"},
    json={
        "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 on price and delivery window",
            "confidence": 0.91,
            "alternatives_considered": 3,
        },
    },
)

result = resp.json()
print(result["decision"])        # "APPROVE"
print(result["kya_score"])       # 0.555 for a brand-new REGISTERED agent
print(result["risk_score"])      # agent risk score, 0.0 to 1.0
print(result["reason_codes"])    # [] on approval
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: "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 on price and delivery window",
      confidence: 0.91,
      alternatives_considered: 3,
    },
  }),
});

const result = await resp.json();
console.log(result.decision);      // "APPROVE"
console.log(result.kya_score);     // 0.555 for a brand-new REGISTERED agent
console.log(result.risk_score);    // agent risk score, 0.0 to 1.0
console.log(result.reason_codes);  // [] on approval
Response fields

decision: APPROVE, DECLINE, or STEP_UP. kya_score: the agent's KYA Score at decision time (0 to 1). risk_score: the agent's risk score (0 to 1); risk_assessment carries the indicators behind it. reason_codes and reason_detail: why a transaction was declined or stepped up, most specific first. authorization_id and transaction_id: ids for the audit log and for reversals and outcomes. Fields the API does not recognize are ignored, so a misspelled field (for example merchant instead of merchant_name) is dropped silently. See the full response.

Set up webhooks

Register a webhook endpoint to receive real-time notifications about important events like gate firings, trust zone changes, and declined authorizations. Webhooks belong to a Principal: if your Client credential owns more than one Principal, add an X-Principal-Id: prn_… header to choose which one (see Authentication).

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", "kya.zone.red", "kya.zone.critical", "authorization.decline", "step_up.created", "trust.promotion"],
    "description": "Production alerting webhook"
  }'

# 201 Created: { "webhook": { "id": "wh_…", … }, "signing_secret": "whsec_…" }
# The signing secret is shown only once.
import httpx

resp = httpx.post(
    "https://api.mandatelabs.ai/api/v1/webhooks",
    headers={"X-API-Key": "mdt_live_…"},
    json={
        "url": "https://your-app.com/webhooks/mandate-ai",
        "event_types": ["gate.fired", "kya.zone.red", "kya.zone.critical", "authorization.decline", "step_up.created", "trust.promotion"],
        "description": "Production alerting webhook",
    },
)
data = resp.json()
# Save the signing secret: it is shown only once
print(data["webhook"]["id"], data["signing_secret"])  # wh_…  whsec_…
const resp = await fetch("https://api.mandatelabs.ai/api/v1/webhooks", {
  method: "POST",
  headers: { "X-API-Key": "mdt_live_…", "Content-Type": "application/json" },
  body: JSON.stringify({
    url: "https://your-app.com/webhooks/mandate-ai",
    event_types: ["gate.fired", "kya.zone.red", "kya.zone.critical", "authorization.decline", "step_up.created", "trust.promotion"],
    description: "Production alerting webhook",
  }),
});
const data = await resp.json();
// Save the signing secret: it is shown only once
console.log(data.webhook.id, data.signing_secret); // wh_…  whsec_…

Available event types:

  • gate.fired: the intent anomaly gate flagged a transaction
  • kya.zone.red: an authorization by an agent in the RED zone (limits halved)
  • kya.zone.critical: an authorization by an agent in the CRITICAL zone (limits at 25%)
  • session.terminate: session termination is recommended for the agent
  • authorization.decline: a transaction was declined
  • step_up.created: a transaction needs human confirmation
  • trust.promotion: the agent was promoted to a higher trust level
  • *: subscribe to all event types

Accounts with fraud triage enabled can also subscribe to fraud.triage.suspected_created, fraud.triage.resolved and fraud.pattern.agto_suspected. Payloads for every event are in the Webhooks reference.

Next steps

Now that you can authorize transactions, explore these guides to deepen your integration: