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:

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.

One key, two planes

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.

Used by: the onboarding API (POST /onboard, POST /principals, POST /principals/{id}/agents, POST /agents/{id}/mandates) and the runtime API (POST /authorize, /agents/*, /mandates/*, /webhooks/*, /thresholds, /crypto/settlements).

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.

Used by: POST /authorize, /agents/*, /mandates/*, /webhooks/*, and the other runtime endpoints, in the sandbox.

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.

Feature-flagged: when OAuth2 is not enabled for your environment, /api/v1/oauth/token returns 404 and Bearer tokens are not accepted. The endpoint accepts both application/x-www-form-urlencoded and JSON bodies, and returns errors in the RFC 6749 format (error, error_description).

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:

RequestActing Principal
X-Principal-Id: prn_… headerThat 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 PrincipalThat Principal.
No header on POST /authorize, several PrincipalsThe 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 Principals401. 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:

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
thresholds/api/v1/thresholds: your decision thresholds
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 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.

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.

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 groupCredentialHeader
Onboarding · /onboard, /principals, /agents/{id}/mandatesClient API keyX-API-Key
Authorize · /authorizeClient API key or Bearer token (the Principal comes from the agent, or from X-Principal-Id); sandbox Principal keyX-API-Key / Authorization
Runtime reads and writes · agents, mandates, webhooks, thresholds, step-up, outcomes, crypto settlementsClient API key or Bearer token, plus X-Principal-Id when your Client has more than one Principal; sandbox Principal keyX-API-Key / Authorization, X-Principal-Id
Client lifecycle (provisioning)Mandate Labs (internal)—
Token exchange · /oauth/tokenclient_id + client_secret(form / JSON body)
Health, JWKS, public keys, webhook event catalogNone (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"
}
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