{"record":{"id":"021d01acb6f45497","repo":"odysseus-dev/odysseus","slug":"api-token-is-not-scoped-for-chat-021d01","errorCode":null,"errorMessage":"API token is not scoped for chat","messagePattern":"API token is not scoped for chat","errorType":"http","errorClass":"HTTPException","httpStatus":403,"severity":"error","filePath":"routes/webhook/webhook_routes.py","lineNumber":243,"sourceCode":"                if model_lower.startswith(prefix):\n                    return KNOWN_PROVIDERS[prov]\n        return None\n\n    class SyncChatRequest(BaseModel):\n        message: str = Field(..., max_length=MAX_MESSAGE_LEN)\n        model: Optional[str] = Field(None, max_length=200)\n        session: Optional[str] = Field(None, max_length=100)\n        api_key: Optional[str] = Field(None, max_length=256)\n        base_url: Optional[str] = Field(None, max_length=MAX_URL_LEN)\n        provider: Optional[str] = Field(None, max_length=50)\n\n    @router.post(\"/v1/chat\")\n    async def sync_chat(request: Request, body: SyncChatRequest):\n        if not getattr(request.state, \"api_token\", False):\n            raise HTTPException(403, \"This endpoint requires an API token\")\n        scopes = set(getattr(request.state, \"api_token_scopes\", []) or [])\n        if \"chat\" not in scopes:\n            raise HTTPException(403, \"API token is not scoped for chat\")\n        token_owner = getattr(request.state, \"api_token_owner\", None)\n\n        from core.models import ChatMessage\n        from src.llm_core import llm_call_async\n        from src.endpoint_resolver import build_chat_url, build_headers, build_models_url, normalize_base\n\n        message = body.message.strip()\n        if not message:\n            raise HTTPException(400, \"Message is required\")\n\n        session_id = body.session\n        sess = None\n\n        # --- Case 1: Resume an existing session ---\n        if session_id and session_manager:\n            try:\n                sess = session_manager.get_session(session_id)\n            except (KeyError, Exception):","sourceCodeStart":225,"sourceCodeEnd":261,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/webhook/webhook_routes.py#L225-L261","documentation":"HTTP 403 from POST /v1/chat: a valid API token was presented but its scope set does not include 'chat'. Scopes are attached by the token middleware onto request.state.api_token_scopes; tokens are minted with a restricted scope list and this endpoint requires the chat scope.","triggerScenarios":"Calling /v1/chat with a token created for webhook-delivery-only or admin scopes; using a token whose scopes string was edited at creation time to omit 'chat'.","commonSituations":"Reusing a narrowly-scoped automation token for a new chat integration; creating a token before the chat feature existed and not regenerating it.","solutions":["Create a new API token that includes the 'chat' scope (or regenerate the existing one with chat added)","Verify the token's scopes via the admin token endpoint before wiring it into automation"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"# at setup time: verify the token carries the chat scope\nme = requests.get(f\"{base}/api/keys/me\", headers=h).json()\nassert \"chat\" in me.get(\"scopes\", []), \"token lacks chat scope\"","typeGuard":null,"tryCatchPattern":"if resp.status_code == 403 and 'scoped for chat' in detail:\n    regenerate_token_with_scopes([\"chat\"])  # then update the stored credential","preventionTips":["Create dedicated tokens per integration with exactly the scopes it needs","Re-check scopes after regenerating or editing a token"],"tags":["auth","scopes","api-token","http-403"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}