BerriAI/litellm · error · HTTPException
User={key.user_id} is not a member of the team={team.team_id
Error message
User={key.user_id} is not a member of the team={team.team_id}. Check team members via `/team/info`. What it means
For team-scoped keys, LiteLLM validates that the key's user_id refers to an actual member of that team (looked up via _get_user_in_team against the team's membership table/cached object). If the user is not a member, the key create/update is rejected with 403 and a pointer to /team/info. This keeps spend attribution correct: key spend rolls up to a user who must belong to the team.
Source
Thrown at litellm/proxy/management_endpoints/key_management_endpoints.py:3326
# Check if the key's tpm/rpm limit is less than the team's tpm/rpm limit
if key.tpm_limit is not None:
if team.tpm_limit and key.tpm_limit > team.tpm_limit:
raise HTTPException(
status_code=403,
detail=f"Key={key.token} has a tpm_limit={key.tpm_limit} which is greater than the team's tpm_limit={team.tpm_limit}.",
)
if team.rpm_limit and key.rpm_limit and key.rpm_limit > team.rpm_limit:
raise HTTPException(
status_code=403,
detail=f"Key={key.token} has a rpm_limit={key.rpm_limit} which is greater than the team's rpm_limit={team.rpm_limit}.",
)
# Check if the key's user_id is a member of the team
member_object: Final = _get_user_in_team(team_table=cast(LiteLLM_TeamTableCachedObj, team), user_id=key.user_id)
if key.user_id is not None:
if not member_object:
raise HTTPException(
status_code=403,
detail=f"User={key.user_id} is not a member of the team={team.team_id}. Check team members via `/team/info`.",
)
# Check if the person initiating the change is a Proxy Admin or Team Admin
if (
change_initiated_by.user_role == LitellmUserRoles.PROXY_ADMIN.value
or _is_user_team_admin(
user_api_key_dict=change_initiated_by,
team_obj=team,
)
or TeamMemberPermissionChecks.does_team_member_have_permissions_for_endpoint(
team_member_object=member_object,
team_table=cast(LiteLLM_TeamTableCachedObj, team),
route=KeyManagementRoutes.KEY_UPDATE.value,
)
):
returnView on GitHub (pinned to 77b7c6c40c)
Solutions
- Add the user to the team first: POST /team/member_add with the team_id and the user, then retry the key operation.
- Verify membership with GET /team/info and check the members list for the exact user_id.
- Or drop user_id from the key request if the key need not be bound to a specific user.
Example fix
# before
client.post("/key/generate", json={"team_id": tid, "user_id": uid, "models": [...]})
# after
client.post("/team/member_add", json={"team_id": tid, "member": {"user_id": uid, "role": "user"}})
client.post("/key/generate", json={"team_id": tid, "user_id": uid, "models": [...]}) Defensive patterns
Strategy: validation
Validate before calling
def ensure_user_in_team(client, team_id: str, user_id: str) -> None:
info = client.get("/team/info", params={"team_id": team_id}).json()
members = info.get("team_info", {}).get("members", []) or info.get("members", [])
if user_id not in {m.get("user_id") for m in members}:
client.post("/team/member_add", json={"team_id": team_id, "member": {"user_id": user_id, "role": "user"}}) Try / catch
try:
client.post("/key/generate", json={"team_id": tid, "user_id": uid, ...})
except HTTPError as e:
if e.response.status_code == 403 and "not a member of the team" in e.response.text:
client.post("/team/member_add", json={"team_id": tid, "member": {"user_id": uid, "role": "user"}})
retry_create_key(tid, uid)
else:
raise Prevention
- Model provisioning as: create user -> add to team -> create key, in that order.
- Handle team-removal webhooks by also disabling that user's team keys.
- Store the exact LiteLLM user_id (not just email) in your directory sync.
When it happens
Trigger: POST /key/generate or /key/update with team_id X and user_id of a user who has never been added to team X (or was removed from it); creating a key for a user by email when the membership record uses a different user_id.
Common situations: New employee provisioned a key before the team-member add completed; user was removed from the team during offboarding but automation still refreshes their key; user_id mismatch between your IdP and LiteLLM's user table.
Related errors
- User={change_initiated_by.user_id} is not a Proxy Admin or T
- Not all keys passed in were deleted. This probably means you
- You are not allowed to access this key's info. Your role={us
- Unable to record skill ownership: caller has no identity sco
- Skill not found: {skill_id}
AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18).
Data as JSON: /api/errors/fc12360b3f700f72.
Report an issue: GitHub.