Skip to content

api reference

Intended Documentation

API Authentication

How Intended authenticates API requests — credential types, the fail-closed gate, headers, scopes, rate limits, and the exact errors you must handle.

API Authentication#

Every Intended API call passes through a single authentication gate before any route handler runs. The gate resolves a tenant context — which tenant is calling, as which actor, with which permissions — and denies the request if it cannot establish one. There is no anonymous or best-effort path: a request that fails to resolve a verified tenant context is rejected, not allowed through. This is the same fail-closed principle the runtime applies to actions.

The authentication gate
A request passes four sequential checks — resolve credential, match tenant, check permission, protective controls — each with a fail-closed deny branch and HTTP status, before reaching the route handler.

Each stage is fail-closed: if it cannot pass, the request is denied with a specific status rather than continuing.

Credential types#

Intended accepts four credential sources. The gate resolves them in this order; the first one present is used. Most server-to-server integrations use an API key.

CredentialHow you present itTypical caller
API keyAuthorization: Bearer <key> or X-API-Key: <key>Backend services, CI, agents
Auth0 JWTAuthorization: Bearer <jwt>First-party apps mid-migration
Portal sessionx-portal-session-id header or intended_portal_session cookieBrowser / console-proxied calls
Legacy headersx-tenant-id / x-user-id / x-user-roleLocal development only

Warning

Legacy identity headers are disabled in production. They are honored only when the server runs with INTENDED_ALLOW_LEGACY_HEADERS=1 outside production, and browsers can never set them. Never build a production integration on them.

API keys#

Customer API keys are prefixed intended_live_… so you can recognize them at a glance. A legacy mrt_… prefix is still accepted for older staff/backoffice keys, but new customer keys are always minted as intended_live_….

There is no separate sandbox or test key tier — there is one kind of customer key. A key carries an environment label (for example development, staging, or production), but that label is cosmetic: it is metadata for your own bookkeeping and does not gate authorization, billing, data isolation, or any side effect. The same key behaves the same way regardless of the label. An unrecognized prefix is rejected with 401 INVALID_API_KEY.

Keys are stored only as a SHA-256 hash and compared in constant time, so the raw value can never be recovered from Intended. Treat it like any other secret: keep it server-side, inject it from your secrets manager, and rotate it immediately if it is exposed. Scope each key to the minimum permissions it needs (see Scopes).

Authenticating a request#

The same authenticated call to POST /intent, in three forms. In every case the credential travels in the Authorization header and the tenant in x-tenant-id.

bash
curl -X POST https://api.intended.so/intent \
  -H "Authorization: Bearer $INTENDED_API_KEY" \
  -H "x-tenant-id: tenant_acme_prod" \
  -H "Content-Type: application/json" \
  -d '{
    "tenantId": "tenant_acme_prod",
    "actor": { "id": "svc-ci-bot", "type": "service" },
    "targetSystem": "acme/platform-api",
    "proposedAction": "dispatch release workflow",
    "riskContext": {
      "baseRiskScore": 42,
      "policyCompliant": true,
      "requiresPrivilegedAccess": false,
      "touchesProduction": false,
      "containsSensitiveData": false
    }
  }'

The tenant header#

Tenant-scoped routes require the x-tenant-id header. Several routes additionally require the tenantId in the request body or path to match the tenant your credential resolves to. A mismatch is rejected with 403 TENANT_MISMATCH — a hard cross-tenant isolation boundary, not a warning. After the gate verifies the credential, it re-injects the resolved tenant server-side, so a forged x-tenant-id cannot widen access beyond what the credential grants.

Scopes and permissions#

A credential resolves to a permission set, derived from the caller's role and, for API keys, the scopes attached to the key. Each route checks the specific permission it needs:

OperationRequired permission
Submit an intent (POST /intent)intent:create
Run an authority evaluation (POST /authority/evaluate)authority:evaluate
Read policies / draftsauthority:policy:read
Create or deploy policyauthority:policy:write

A key used only to submit intents should not carry policy-write. Missing or insufficient permission returns 403 with the required permission named in the response body; an unresolved or invalid credential returns 401.

Rate limits and protective controls#

The gate also enforces platform- and tenant-protection controls. Build for them rather than discovering them in production:

  • Rate limiting — a global per-IP limit (500 requests/minute) plus a stricter limit on authentication endpoints. Exceeding it returns 429 RATE_LIMITED; honor it with exponential backoff.
  • IP allowlisting — if a tenant configures an allowlist, calls from other addresses return 403 IP_NOT_ALLOWED.
  • MFA and session freshness — privileged portal roles must satisfy MFA (403 MFA_REQUIRED); idle or expired portal sessions return 401 INVALID_PORTAL_SESSION.
  • Request hardening — a 1 MB body limit, URL/header size caps, and input sanitization apply to every request.

Errors you must handle#

Intended is fail-closed: when authentication or authorization cannot be established, the request is denied rather than allowed through. Handle each explicitly.

StatusCodeCauseWhat to do
401UNAUTHORIZEDNo valid credentialCheck the key value and Authorization header
401INVALID_PORTAL_SESSIONIdle or expired sessionRe-authenticate the session
403TENANT_MISMATCHCredential's tenant ≠ requested tenantSend the correct x-tenant-id / body tenantId
403FORBIDDENAuthenticated but lacks the permissionGrant the named scope/role to the key
403IP_NOT_ALLOWEDSource IP not on the tenant allowlistCall from an allowlisted address
403MFA_REQUIREDPrivileged role without MFAComplete MFA on the session
429RATE_LIMITEDRate limit exceededBack off and retry

Tip

Only operational and discovery routes are public and unauthenticated: /health, /ready, and /.well-known/jwks.json. Everything else requires a verified tenant context.

See also#

API Authentication | Intended