Authentication & credentials

Mandate Labs has three developer credential planes — the Client that onboards, the Principal that transacts, and OAuth2 for short-lived tokens — plus an internal operator key Mandate Labs uses to provision Clients. This page covers each developer plane, how keys route to sandbox or live, and the controls — scopes, IP allowlists, rotation, expiry — layered on top.

Overview

Every request to the API is authenticated. Which credential you present depends on who is acting and what they are doing:

The two tenant planes use the same header — X-API-Key — and the same prefix convention to select the environment. (X-Admin-Key is the internal operator header Mandate Labs uses for Client provisioning; it is not part of the developer-facing surface.) Keys are never stored in the clear: the server stores only a SHA-256 hash and compares hashes on every request.

Client key vs Principal key

Both ride on X-API-Key, but they resolve to different actors. A Client key resolves to a cli_ (the tenant) and is accepted by the onboarding API. A Principal key resolves to a single prn_ and is accepted by the transacting APIs. Presenting a Client key to /authorize, or a Principal key to onboarding, fails authentication.

The credential planes

Client API key X-API-Key

Identifies the Client — the contracting organization and tenant boundary. This is the key issued when your Client is provisioned, and it is the credential for the onboarding API: creating Principals, enrolling them in Programs, adding agents, and granting mandates.

Format: mdt_live_… (production) or mdt_test_… (sandbox). The key is owned at the Client level and resolves to the owning cli_; the credential record itself carries an id (acr_), its scopes, IP allowlist, and expiry.

Used by: POST /onboard, POST /principals, POST /principals/{id}/agents, POST /agents/{id}/mandates, and the rest of the Client-authenticated onboarding surface.

Principal API key X-API-Key

Identifies a single Principal — the verified person or merchant whose agents transact. This is the credential for the runtime APIs: authorizing transactions, reading and managing agents and mandates, and configuring webhooks.

Format: same mdt_live_… / mdt_test_… prefix convention. Resolution checks the active key first, then any previous key still inside its rotation grace window, then — when client tenancy is enabled — a Client credential that owns exactly one Principal.

Used by: POST /authorize, /agents/*, /mandates/*, /webhooks/*, /crypto/settlements, and other per-Principal endpoints.

Admin master key X-Admin-Key

An internal operator credential held by Mandate Labs to provision and manage Clients. It is not issued to integrators and is shown here only to complete the model.

There is no self-serve sign-up; Mandate Labs vets and provisions each Client, then issues its Client API key during onboarding.

OAuth2 client-credentials Bearer

A standards-based alternative to sending a long-lived key on every call. A client exchanges client_id + client_secret at POST /oauth/token (RFC 6749 §4.4, grant_type=client_credentials) for a short-lived Bearer access token, then sends Authorization: Bearer <token> to the runtime APIs.

In the current phase the client_id is the Principal id and the client_secret is its API key. The minted token freezes the key's scope and environment, and inherits the same post-auth checks — a revoked or expired key invalidates its tokens on next use.

Feature-flagged: when OAuth2 is not enabled, /oauth/token returns 404 so the surface stays closed. The endpoint accepts both application/x-www-form-urlencoded and JSON bodies.

Sandbox vs live routing

There is no separate hostname or header for the sandbox. The key prefix alone selects the environment, and the platform routes the request to the matching database:

PrefixEnvironmentRoutes to
mdt_test_…SandboxSandbox database
mdt_live_…ProductionProduction database
mdt_… (legacy, no env segment)ProductionProduction database (backward compatible)

This means a single integration can target either environment just by swapping the key. Sandbox and live data are fully isolated — a mdt_test_ key never sees production records, and OAuth tokens carry the environment they were minted in.

Keys are shown once

Every issue and rotate response returns the raw key exactly once and stores only its hash. If you lose it, you cannot recover it — issue a new credential or rotate. Treat keys as secrets: never embed a mdt_live_ key in client-side code or a public repo.

Scopes

A credential can be restricted to a subset of the API with scopes — a comma-separated list set when the key is issued. An empty scope or * means full access; otherwise the request's path must map to one of the allowed scopes or it is rejected with 403.

Scopes are resolved by URL prefix. A key scoped authorize can reach any path under /api/v1/authorize but nothing else. Common scopes:

ScopeGrants access to
authorize/api/v1/authorize — the authorization engine
agents/api/v1/agents — agent registry and management
mandates/api/v1/mandates — mandate lifecycle
webhooks/api/v1/webhooks — webhook endpoints
principals/api/v1/principals — onboarding (Client plane)
risk / metrics / outcomesRead surfaces for risk signals, metrics, and outcomes
vault / casesPayment-vault and case-management surfaces

Issue least-privilege credentials: a service that only authorizes transactions should carry the authorize scope and nothing more, so a leaked key cannot manage agents or read unrelated data. The scope is enforced on every request, and for OAuth2 the scope is frozen into the access token at mint time.

IP allowlists

A credential can additionally be bound to a set of source IPs. If an allowlist is configured, requests from any other origin are rejected with 403 before the request is processed.

The check is proxy-aware: the real client IP is resolved through the trusted-proxy chain, so a caller cannot spoof its way past the allowlist by setting its own X-Forwarded-For header. Leave the allowlist empty (the default) for unrestricted origins; set it to lock a production key to your egress IPs.

Rotation, expiry & revocation

Your Client credential has a full lifecycle, all managed by Mandate Labs on your behalf.

Issue

Your Client credential is issued to you during onboarding, and issuance is additive — new keys don't invalidate existing ones. Each key can carry scopes, an IP allowlist, an environment (live or sandbox), and an expiry. The raw key is shown once.

Rotate with a grace window

Rotation is zero-downtime. Mandate Labs issues a new key while the previous key stays valid as a previous key for a short grace window. During that window both keys authenticate, so you can roll the new key out everywhere before the old one stops working. Once the grace period lapses, the previous key is no longer accepted.

Rotate without an outage

Roll out the new key while the old one still works, then let the grace window expire. The previous-key check runs only after the current-key lookup misses, so live traffic is never interrupted mid-rotation.

Expiry

A credential issued with an expiry stops authenticating after that date — requests then fail with 401. Expiry is enforced on every request and, for OAuth2, also bounds how long a derived access token can outlive the underlying key.

Revoke

If a key is compromised, Mandate Labs revokes it immediately on request. Revocation is irreversible — to restore access, a new credential is issued.

Sandbox playground keys

For the sandbox playground, a freshly onboarded dev Principal can mint its own sandbox key via POST /principals/{id}/sandbox-key (sandbox credentials only). This lets the dev drive the per-Principal endpoints (authorize, mandates, reads) without ever exposing the Client's master credential. It is a playground-only affordance — per-Principal keys are not part of the production credential model.

Which auth each API group needs

API groupCredentialHeader
Onboarding — /onboard, /principals, /agents/{id}/mandatesClient API keyX-API-Key
Authorize — /authorizePrincipal API key (or Bearer)X-API-Key / Authorization
Agents & mandates (runtime reads/writes)Principal API key (or Bearer)X-API-Key / Authorization
Webhooks managementPrincipal API keyX-API-Key
Crypto settlements — /crypto/settlementsPrincipal API keyX-API-Key
Client lifecycle (provisioning)Mandate Labs (internal)
Token exchange — /oauth/tokenclient_id + client_secret(form / JSON body)
Health, JWKS, public keysNone (public)

Header reference

The exact request shape for each plane.

Client API key — onboarding

curl -X POST https://api.mandatelabs.ai/api/v1/onboard \
  -H "X-API-Key: mdt_live_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: onboard-acme-001" \
  -d '{ "principal": { … }, "agent": { … }, "mandate": { … } }'

Principal API key — authorize

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.00,
    "currency": "USD",
    "mcc": "5812",
    "intent_context": { "task_reference": "Lunch order for the team" }
  }'

OAuth2 — exchange credentials for a Bearer token

curl -X POST https://api.mandatelabs.ai/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=prn_…" \
  -d "client_secret=mdt_live_…"

# then call the runtime API with the token
curl -X POST https://api.mandatelabs.ai/api/v1/authorize \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{ "agent_id": "agt_…", "amount": 42.00, "intent_context": { "task_reference": "…" } }'
{
  "access_token": "eyJ…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "authorize,agents"
}
Auth failures are uniform

A missing key, an unknown key, and an id/secret mismatch all return the same 401 with no hint about which half was wrong — there is no oracle to probe. Scope and IP-allowlist failures return 403. See Errors & idempotency for the full envelope.

Next steps