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
- Onboard a Principal and Agent with the Client onboarding API
- Create a spending mandate to set an agent's limits
- Authorize a transaction and interpret the response
- Set up webhooks for real-time event notifications
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.
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
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 transactionkya.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 agentauthorization.decline: a transaction was declinedstep_up.created: a transaction needs human confirmationtrust.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: