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_…| Rule | Detail |
|---|---|
| Header only | Send 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 only | Project API keys and portal session tokens are rejected with 401 credential_type_mismatch. |
| HTTPS only | Always 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 sent | To | Result |
|---|---|---|
| A project API key | https://portal.<domain>/api/v1/mgmt/* | 401 credential_type_mismatch — "this endpoint needs a personal access token; API keys authenticate inference only" |
| A personal access token | https://api.<domain>/v1/* inference routes | 401 credential_type_mismatch — "this endpoint needs a project API key; personal access tokens authenticate the Management API only" |
| A personal access token | The 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.
| Situation | Status | error.code | error.type |
|---|---|---|---|
No Authorization header, or not Bearer | 401 | missing_credential | authentication_error |
| Bearer is a project API key, a portal session token, or any other value that is not a personal access token | 401 | credential_type_mismatch | authentication_error |
| Unknown token; owner or organization no longer exists | 401 | invalid_credential | authentication_error |
Token past its expires_at | 401 | token_expired | authentication_error |
| Token revoked; owner disabled; organization suspended | 401 | token_revoked | authentication_error |
| The token could not be checked because of an internal error | 500 | internal_error — retry | api_error |
| Per-token rate limit reached | 429 | rate_limit_exceeded (+ Retry-After) | rate_limit_error |
| Token valid, but missing the scope or the creator lacks the right | 403 | insufficient_scope | permission_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.