odysseus-dev/odysseus · error · HTTPException
API token missing required scope: {required}
Error message
API token missing required scope: {required} What it means
Raised as HTTP 403 by _scope_owner when a request authenticated with an API token lacks every one of the scopes in the route's allowed set (no intersection). API-token callers are authorized purely by scopes, unlike cookie-session callers who go through require_user. The message names the exact scopes that would have been accepted.
Source
Thrown at routes/codex_routes.py:91
return result
finally:
request.state.current_user = orig
if orig_api_token is None:
try:
delattr(request.state, "api_token")
except AttributeError:
pass
else:
request.state.api_token = orig_api_token
def _scope_owner(request: Request, allowed: set[str]) -> str:
"""Return the data owner if the caller is allowed for this Codex action."""
if getattr(request.state, "api_token", False):
scopes = set(getattr(request.state, "api_token_scopes", []) or [])
if not scopes.intersection(allowed):
required = " or ".join(sorted(allowed))
raise HTTPException(403, f"API token missing required scope: {required}")
owner = getattr(request.state, "api_token_owner", None)
if not owner:
raise HTTPException(403, "API token has no owner")
return owner
return require_user(request)
def _scope_owner_all(request: Request, required: set[str]) -> str:
"""Return owner only when an API token has every required scope."""
if getattr(request.state, "api_token", False):
scopes = set(getattr(request.state, "api_token_scopes", []) or [])
missing = required - scopes
if missing:
raise HTTPException(403, f"API token missing required scope: {' and '.join(sorted(missing))}")
owner = getattr(request.state, "api_token_owner", None)
if not owner:
raise HTTPException(403, "API token has no owner")
return ownerView on GitHub (pinned to f9235ebbf1)
Solutions
- Read the message — it lists the accepted scopes; mint a new API token that includes one of them (via the API token management endpoint).
- Inspect the token's current scopes via the token introspection/management endpoint to confirm what it carries.
- If scopes were renamed in an upgrade, re-issue the token with the new scope names.
- Alternatively call the route with a cookie session instead of the API token.
Example fix
# before curl -H 'Authorization: Bearer $TOKEN' https://host/api/codex/emails # 403 # after TOKEN=$(create_token --scope email:read) curl -H "Authorization: Bearer $TOKEN" https://host/api/codex/emails
Defensive patterns
Strategy: validation
Validate before calling
const resp = await fetch('/api/tokens/introspect', {headers: auth});
const {scopes} = await resp.json();
const needed = ['email:read']; // from route docs
if (!needed.some(s => scopes.includes(s))) {
throw new Error(`Token lacks scope; has [${scopes}] needs one of [${needed}]`);
} Type guard
function tokenAllows(tokenScopes: string[], required: string[]): boolean {
return required.some(s => tokenScopes.includes(s));
} Try / catch
try { r = await codexListEmails() } catch (e) { if (e.status === 403 && e.detail.startsWith('API token missing required scope')) { await mintTokenWithScopes(parseScopes(e.detail)); retry once } else throw } Prevention
- Introspect token scopes at startup of any script/plugin and fail fast with a clear message.
- Keep scope constants in one shared place so clients and provisioning agree on names.
- Treat 403 scope messages as machine-readable: parse the required list out of the detail text.
When it happens
Trigger: Calling a /api/codex/* endpoint (e.g. GET /api/codex/emails) with a token whose scope list does not include any of EMAIL_READ_SCOPES (or the route's specific set); using a read-only token against a write route; typo in a custom token's scope string.
Common situations: Token minted with a minimal scope set for a different integration; scopes renamed between app versions so old tokens no longer match; using a token created for the cookbook on email routes.
Related errors
- API token missing required scope: {' and '.join(sorted(missi
- API token owner mismatch
- API token requires chat scope
- API token has no owner
- API token is not scoped for chat
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/fb07679c1269ccf1.
Report an issue: GitHub.