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:
- A Client onboarding Principals and agents presents a Client API key.
- A Principal (or an agent acting for it) authorizing transactions presents a Principal API key — or an OAuth2 Bearer token minted from it.
- Mandate Labs runs Client lifecycle internally with the Admin master key; integrators never hold or present it.
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.
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.
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.
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.
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:
| Prefix | Environment | Routes to |
|---|---|---|
mdt_test_… | Sandbox | Sandbox database |
mdt_live_… | Production | Production database |
mdt_… (legacy, no env segment) | Production | Production 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.
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:
| Scope | Grants 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 / outcomes | Read surfaces for risk signals, metrics, and outcomes |
vault / cases | Payment-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.
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 group | Credential | Header |
|---|---|---|
Onboarding — /onboard, /principals, /agents/{id}/mandates | Client API key | X-API-Key |
Authorize — /authorize | Principal API key (or Bearer) | X-API-Key / Authorization |
| Agents & mandates (runtime reads/writes) | Principal API key (or Bearer) | X-API-Key / Authorization |
| Webhooks management | Principal API key | X-API-Key |
Crypto settlements — /crypto/settlements | Principal API key | X-API-Key |
| Client lifecycle (provisioning) | Mandate Labs (internal) | — |
Token exchange — /oauth/token | client_id + client_secret | (form / JSON body) |
| Health, JWKS, public keys | None (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"
}
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.