Authentication & credentials
Your Client API key is the credential for the whole developer surface: it onboards Principals and agents, and it authorizes transactions for one of your Principals. This page covers how the acting Principal is chosen, sandbox keys, OAuth2 bearer tokens, how keys route to sandbox or live, and the controls layered on top: scopes, IP allowlists, rotation, expiry.
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 its Client API key.
- The same Client API key authorizes transactions and manages agents, mandates and webhooks. On these runtime endpoints it acts for one of your Principals (see Acting for a Principal). A short-lived OAuth2 Bearer token can stand in for the key on the runtime endpoints.
- Mandate Labs runs Client lifecycle internally with the Admin master key; integrators never hold or present it.
API keys travel in the X-API-Key header, and the key prefix selects 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.
On the onboarding API the Client API key resolves to your cli_ tenant. On the runtime API it resolves to one of your Principals (prn_). Per-Principal API keys are a sandbox affordance for development: they authenticate the runtime API only, and presenting one 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. It authenticates the onboarding API (creating Principals, enrolling them in Programs, adding agents, granting mandates) and the runtime API (authorizing transactions, reading and managing agents and mandates, configuring webhooks), where it acts for one of your Principals.
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.
Sandbox Principal key X-API-Key
Identifies a single Principal. In the sandbox, a freshly onboarded dev Principal can get its own key from POST /principals/{id}/sandbox-key, so a developer can drive the runtime endpoints without holding your Client API key. It authenticates the runtime API only.
Format: mdt_test_…. The key always acts for its own Principal. Resolution checks the active key first, then any previous key still inside its rotation grace window. Per-Principal keys are not part of the production credential model.
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. Exchange a client_id and client_secret at POST /api/v1/oauth/token (RFC 6749 §4.4, grant_type=client_credentials) for a short-lived Bearer access token, then send Authorization: Bearer <token> to the runtime APIs. The onboarding API accepts only the Client API key.
Mandate Labs issues OAuth credentials to your Client: a client_id of the form moc_live_… or moc_test_… and a mos_… secret, shown once. A token minted from them acts for your Client under the same acting-Principal rules as the Client API key. Tokens last 15 minutes by default and carry the credential's scope and environment, frozen at mint time. A per-Principal key can also be exchanged, with the prn_ id as client_id and the key as client_secret; revoking or expiring that key invalidates its tokens on next use.
Acting for a Principal
Runtime resources (agents, mandates, authorizations, webhooks, thresholds) belong to a Principal. When you call the runtime API with your Client API key or a Client OAuth token, the platform picks the acting Principal in this order:
| Request | Acting Principal |
|---|---|
X-Principal-Id: prn_… header | That Principal, if it belongs to your Client. A Principal outside your Client returns 403, without revealing whether it exists. |
| No header, and your Client has exactly one Principal | That Principal. |
No header on POST /authorize, several Principals | The Principal that owns the agent named in the request. An agent outside your Client is declined with AGENT_NOT_FOUND, the same answer as an agent that does not exist. |
| No header on any other runtime endpoint, several Principals | 401. Send X-Principal-Id. |
A sandbox Principal key always acts for its own Principal; the header is not consulted.
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 |
thresholds | /api/v1/thresholds: your decision thresholds |
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 grace window (24 hours by default). 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.
Per-Principal sandbox keys
In the sandbox, 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 sandbox-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 | Client API key or Bearer token (the Principal comes from the agent, or from X-Principal-Id); sandbox Principal key | X-API-Key / Authorization |
| Runtime reads and writes · agents, mandates, webhooks, thresholds, step-up, outcomes, crypto settlements | Client API key or Bearer token, plus X-Principal-Id when your Client has more than one Principal; sandbox Principal key | X-API-Key / Authorization, X-Principal-Id |
| Client lifecycle (provisioning) | Mandate Labs (internal) | — |
Token exchange · /oauth/token | client_id + client_secret | (form / JSON body) |
| Health, JWKS, public keys, webhook event catalog | 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": { … } }'
Client 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" } }' # The acting Principal is the one that owns agt_…
Client API key · acting for a specific Principal
# Needed on runtime endpoints other than /authorize when your Client has more than one Principal curl https://api.mandatelabs.ai/api/v1/webhooks \ -H "X-API-Key: mdt_live_…" \ -H "X-Principal-Id: prn_…"
OAuth2 · exchange credentials for a Bearer token
curl -X POST https://api.mandatelabs.ai/api/v1/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=client_credentials" \ -d "client_id=moc_live_…" \ -d "client_secret=mos_…" # then call the runtime API with the token (same acting-Principal rules as the Client API key) 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": 900,
"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.