Authentication

How Management API requests are authenticated with a personal access token.

The Authorization header

Every request carries a personal access token as a bearer credential:

Authorization: Bearer tf-pat_…
RuleDetail
Header onlySend the token in Authorization. It is never accepted as a query parameter or a cookie, and x-api-key is not read on this surface.
Personal access tokens onlyProject API keys and portal session tokens are rejected with 401 credential_type_mismatch.
HTTPS onlyAlways call https://portal.<domain>/api/v1/mgmt — the portal host, not the API host.
export TF_PAT="tf-pat_…"   # from your secret manager

curl https://portal.<domain>/api/v1/mgmt/org \
  -H "Authorization: Bearer $TF_PAT"

Credentials never cross surfaces

Inference is on the API host (https://api.<domain>/v1); management is on the portal host under /api/v1/mgmt. Each credential is rejected at the other surface, and each case gets an error that names the fix:

You sentToResult
A project API keyhttps://portal.<domain>/api/v1/mgmt/*401 credential_type_mismatch — "this endpoint needs a personal access token; API keys authenticate inference only"
A personal access tokenhttps://api.<domain>/v1/* inference routes401 credential_type_mismatch — "this endpoint needs a project API key; personal access tokens authenticate the Management API only"
A personal access tokenThe portal's own API (everything on the portal host outside /api/v1/mgmt)A plain 401 — personal access tokens are not portal credentials
# A personal access token on the inference API is rejected before any model is called:
curl https://api.<domain>/v1/chat/completions \
  -H "Authorization: Bearer $TF_PAT" \
  -H "Content-Type: application/json" \
  -d '{"model": "openai/gpt-4o-mini", "messages": [{"role": "user", "content": "hi"}]}'
# → 401 {"error": {"code": "credential_type_mismatch", ...}}

Inference authentication is documented in Authentication.

How a request is checked

Checks run in this order; the first failure is returned.

SituationStatuserror.codeerror.type
No Authorization header, or not Bearer401missing_credentialauthentication_error
Bearer is a project API key, a portal session token, or any other value that is not a personal access token401credential_type_mismatchauthentication_error
Unknown token; owner or organization no longer exists401invalid_credentialauthentication_error
Token past its expires_at401token_expiredauthentication_error
Token revoked; owner disabled; organization suspended401token_revokedauthentication_error
The token could not be checked because of an internal error500internal_error — retryapi_error
Per-token rate limit reached429rate_limit_exceeded (+ Retry-After)rate_limit_error
Token valid, but missing the scope or the creator lacks the right403insufficient_scopepermission_error

The last row is the intersection rule. Only a request that passes the credential checks is counted against the rate limit and carries the X-RateLimit-* headers; a 401 never does.

{
  "error": {
    "message": "this personal access token has expired",
    "type": "authentication_error",
    "code": "token_expired",
    "param": null,
    "action": "Mint a new token in Settings → API Tokens and replace it in your integration.",
    "request_id": "req_e2443998c57f4bf28e50f4288406380d"
  }
}

Note

Do not retry a 401. None of these errors clears on its own — each needs a different credential, a new token, or an Admin's action. (A 500 internal_error while verifying the token is the exception: retry it.)

Portal scoping

A token works only on the portal where it was created; a token from any other Token Factory portal is unknown here (401 invalid_credential), exactly as a random string would be. Keep one secret per portal in your CI system.

Non-enumeration

The credential layer never tells you why an unknown value is unknown. A typo, a token from another portal, and a token whose organization was deleted all return the same generic 401 invalid_credential.

The same rule holds for resources: every query is bound to the token's organization, no parameter accepts another organization's id, and an object that belongs to another organization returns 404 not_found_error — exactly like one that does not exist. A 403 would confirm the id is real.

Request ids

Every response — success or error — carries an X-Request-Id header, and every error body carries the same value as error.request_id. Log it, and quote it in support requests.