Teams API
Manage team details, member roles, ownership transfer, and team model authorization
The Teams API covers three groups of endpoints: platform-side team and member management (/api/v1/teams), admin team management (/api/v1/admin/teams, where creation and deletion live), and team model authorization (/api/v1/teams/{team_id}/models*, writable only by superusers).
No platform-side create/delete team endpoints
The platform routes are only GET /api/v1/teams/my, GET|PUT /api/v1/teams/{team_id}, member management, leave, and transfer-ownership. There is no POST /api/v1/teams and no DELETE /api/v1/teams/{team_id}; creating and deleting teams belongs to the admin panel at POST /api/v1/admin/teams and DELETE /api/v1/admin/teams/{team_id}.
Roles and Permission Model
| Role | Description |
|---|---|
owner | Team owner, exactly one per team. Can change member roles, transfer ownership, and update the team; cannot leave directly — ownership must be transferred first |
admin | Team administrator. Can update the team and add/remove members (changing a role to admin requires the target user to already hold the global team:manage permission) |
member | Regular member. The default role; operations follow global permissions |
viewer | Read-only member. Allowed only read permissions (team:read, agent:read, workflow:read, kb:read, tool:read, conversation:read, and similar); any write operation is rejected |
How permission codes map to operations:
| Permission | Operations covered |
|---|---|
team:read | View team details, view members, leave the team (self-removal) |
team:update | Update team name, description, avatar |
team:manage | Add/remove members, change member roles, transfer ownership |
admin:team:read | List all teams in the admin panel |
admin:team:create | Create a team in the admin panel |
admin:team:delete | Delete a team in the admin panel |
The check order (check_team_permission) is the same for every platform endpoint:
- The team must exist, otherwise
404/4004. - Superusers pass immediately and skip the remaining checks.
- The caller must be a team member, otherwise
403/3002. - For management actions (
team:update,team:manage, or an endpoint that asks explicitly) the role must beowneroradmin, otherwise403/3003. - A
viewerrole may only perform read-only permissions, otherwise403/3000. - Finally the user's global permission code is checked; if missing,
403/3000.
Step 3 also means non-members cannot read team details: even when a team is visible to administrators, a regular user must be a member first. For a full deployment-wide team list, use the admin endpoint.
Base URLs and Authentication
- Platform endpoints:
/api/v1/teams - Admin endpoints:
/api/v1/admin/teams - Team model authorization:
/api/v1/teams/{team_id}/models*
Every team endpoint requires an authenticated JWT user session; API keys are not accepted.
Endpoints
Platform routes
| Method | Path | Purpose | Permission / role |
|---|---|---|---|
| GET | /api/v1/teams/my | List the teams the caller belongs to, with roles | Any authenticated user |
| GET | /api/v1/teams/{team_id} | Team details including the member list | team:read |
| PUT | /api/v1/teams/{team_id} | Update team info | team:update + owner/admin |
| POST | /api/v1/teams/{team_id}/members | Add a member | team:manage + owner/admin |
| PUT | /api/v1/teams/{team_id}/members/{user_id} | Change a member role | team:manage + owner/admin, and the caller must be owner (or superuser) |
| DELETE | /api/v1/teams/{team_id}/members/{user_id} | Remove a member (self-removal supported) | Self-removal needs team:read; removing others needs team:manage + owner/admin |
| POST | /api/v1/teams/{team_id}/leave | Leave the team | team:read |
| POST | /api/v1/teams/{team_id}/transfer-ownership | Transfer ownership | team:manage + owner (or superuser) |
Admin routes
| Method | Path | Purpose | Permission |
|---|---|---|---|
| GET | /api/v1/admin/teams | List all teams with pagination | admin:team:read |
| POST | /api/v1/admin/teams | Create a team | admin:team:create |
| DELETE | /api/v1/admin/teams/{team_id} | Delete a team | admin:team:delete |
There is no GET /admin/teams/{team_id} and no PUT /admin/teams/{team_id}: reading and updating still go through the platform routes (which require team membership).
List My Teams
GET /api/v1/teams/myNo permission code is required. Returns every membership of the caller, in TeamMember record order:
curl -X GET "https://your-domain.com/api/v1/teams/my" \
-H "Authorization: Bearer YOUR_TOKEN"Response (200 OK):
{
"code": 0,
"data": [
{
"id": "8f14e45f-ceea-467a-9d0f-1a2b3c4d5e6f",
"name": "Marketing Team",
"description": "Marketing and content creation team",
"avatar_url": null,
"role": "member",
"joined_at": "2026-01-15T10:00:00Z"
}
],
"msg": "success"
}The body is a plain array, not a paginated envelope. There is no GET /api/v1/teams that lists all teams; for a deployment-wide list use the admin endpoint:
GET /api/v1/admin/teams?page=1&page_size=50&search=marketing| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
page_size | integer | 50 | Items per page (note the admin default is 50, not the platform-wide 20) |
search | string | — | Case-insensitive match on team name or description |
Get Team Details
GET /api/v1/teams/{team_id}| Path parameter | Type | Required |
|---|---|---|
team_id | string (UUID) | Yes |
Response (200 OK):
{
"code": 0,
"data": {
"id": "8f14e45f-ceea-467a-9d0f-1a2b3c4d5e6f",
"name": "Marketing Team",
"description": "Marketing and content creation team",
"avatar_url": null,
"is_default": false,
"owner": {
"id": "3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c",
"username": "alice",
"email": "alice@example.com",
"avatar_url": "https://example.com/avatars/alice.png"
},
"created_at": "2026-01-15T10:00:00Z",
"updated_at": "2026-02-11T15:30:00Z",
"members": [
{
"id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e",
"user_id": "3f2a1b0c-9d8e-4f7a-8b6c-5d4e3f2a1b0c",
"username": "alice",
"email": "alice@example.com",
"avatar_url": "https://example.com/avatars/alice.png",
"role": "owner",
"joined_at": "2026-01-15T10:00:00Z"
}
]
},
"msg": "success"
}There is no standalone member-list endpoint: the complete member list is returned only by this response. members is not paginated, so large teams need client-side slicing.
Update a Team
PUT /api/v1/teams/{team_id}Requires the team:update permission code and an owner or admin role. All fields are optional; send only what you want to change.
| Field | Type | Description |
|---|---|---|
name | string | Team name, globally unique |
description | string | Team description |
avatar_url | string | Team avatar URL |
curl -X PUT "https://your-domain.com/api/v1/teams/8f14e45f-ceea-467a-9d0f-1a2b3c4d5e6f" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Growth Team"}'The response returns the updated team object (with owner, without members). A duplicate name returns HTTP 400 / 5102 (TEAM_NAME_EXISTS).
Only fields explicitly present in the request body are applied: passing "description": null counts as “not provided” and is skipped, so you cannot currently clear a description or avatar this way.
Add a Team Member
POST /api/v1/teams/{team_id}/membersRequires the team:manage permission code and an owner/admin role.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
user_id | string (UUID) | One of the two | — | Target user UUID |
identifier | string | One of the two | — | Exact username match, or exact email match (case-insensitive) |
role | string | No | member | Member role: member, admin, or viewer |
Provide at least one of user_id or identifier; providing neither returns HTTP 400 / 1001.
curl -X POST "https://your-domain.com/api/v1/teams/8f14e45f-ceea-467a-9d0f-1a2b3c4d5e6f/members" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"identifier": "carol@example.com", "role": "member"}'Four business rules apply:
- The target user must exist, otherwise HTTP
404/4001. - The target user must not already be a member, otherwise HTTP
400/5103(ALREADY_TEAM_MEMBER). - The role cannot be
owner: that returns HTTP400/5206(CANNOT_ADD_AS_OWNER). Ownership changes only through the transfer endpoint. - To add someone as
admin, the target user must already hold the globalteam:managepermission, otherwise HTTP403/3000(the response includes apermissionfield naming the missing code).
Response (200 OK): the new membership record (id, user_id, username, email, avatar_url, role, joined_at). Both the added user and the whole team receive an in-app notification.
Change a Member Role
PUT /api/v1/teams/{team_id}/members/{user_id}| Path parameter | Type | Required |
|---|---|---|
team_id | string (UUID) | Yes |
user_id | string (UUID) | Yes |
Request body:
{ "role": "admin" }role is required. Constraints:
- Only the
owner(or a superuser) can change roles; anadmincaller gets HTTP403/3004(TEAM_OWNER_REQUIRED). - The
owner's own role cannot be changed (HTTP400/5207,CANNOT_CHANGE_OWNER_ROLE). - A member cannot be promoted directly to
owner(HTTP400/5208,CANNOT_PROMOTE_TO_OWNER) — use ownership transfer instead. - Setting the role to
adminrequires the target user to already hold the globalteam:managepermission, otherwise HTTP403/3000. - A missing target user returns
404/4001; a missing membership returns404/4005.
The response returns the updated membership record with the new role, and the affected user is notified.
Remove a Team Member
DELETE /api/v1/teams/{team_id}/members/{user_id}| Path parameter | Type | Required |
|---|---|---|
team_id | string (UUID) | Yes |
user_id | string (UUID) | Yes |
Authorization depends on whether the target is the caller:
- Self-removal (
user_idequals the caller): onlyteam:readis needed; this is equivalent to leaving. - Removing someone else: requires
team:manageand anowner/adminrole.
The owner cannot be removed (HTTP 400 / 5209, CANNOT_REMOVE_OWNER). A missing target user returns 404 / 4001; a missing membership returns 404 / 4005.
Response (200 OK):
{
"code": 0,
"data": { "user_id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e" },
"msg": "Team member removed successfully"
}Both the removed user and the team receive a notification.
Leave a Team
POST /api/v1/teams/{team_id}/leaveRequires team:read (regular members and viewer roles can call it).
- A non-member gets HTTP
404/3002(NOT_TEAM_MEMBER). - The
ownercannot leave: HTTP400/5210(OWNER_CANNOT_LEAVE). Transfer ownership first.
Response (200 OK):
{ "code": 0, "data": { "team_id": "8f14e45f-..." }, "msg": "You have left the team" }Transfer Ownership
POST /api/v1/teams/{team_id}/transfer-ownership?new_owner_id=<new owner UUID>new_owner_id is a query parameter, not a body field. Sending it in a JSON body is ignored and returns 422 (missing required query parameter new_owner_id).
curl -X POST "https://your-domain.com/api/v1/teams/8f14e45f-ceea-467a-9d0f-1a2b3c4d5e6f/transfer-ownership?new_owner_id=b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e" \
-H "Authorization: Bearer YOUR_TOKEN"Constraints:
- The caller needs the
team:managepermission code, anowner/adminrole, and additionally theownerrole itself (superusers bypass only theownerrole requirement). - The user referenced by
new_owner_idmust already be a member of this team, otherwise HTTP404/4005. - The new owner must hold the global
team:managepermission, otherwise HTTP403/3000. - Ownership cannot be transferred to the current owner, which returns HTTP
400/5208(CANNOT_PROMOTE_TO_OWNER).
On success the new owner's role becomes owner, the previous owner is demoted to admin, and team.owner is updated. Both users receive a notification. The response returns the updated team object.
Can a superuser make a member without any team:manage permission the owner? No — rule 3 also applies to the incoming owner, so even a superuser cannot bypass the new owner's permission requirement.
Admin Panel: Create and Delete Teams
Create a team
POST /api/v1/admin/teamsRequires admin:team:create.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Team name, globally unique |
description | string | No | Description |
avatar_url | string | No | Avatar URL |
The creator becomes the team's owner and an owner membership record is written, so a new team immediately has exactly one owner. A duplicate team name returns HTTP 400 / 5102.
Delete a team
DELETE /api/v1/admin/teams/{team_id}Requires admin:team:delete. A missing team returns 404 / 4004; the default team (is_default = true) cannot be deleted and returns HTTP 400 / 5205 (CANNOT_DELETE_DEFAULT_TEAM).
data in the response is the deleted team object (returned for audit echo), not null.
Deleting a team also removes its memberships and related authorizations. Resources inside the team — Agents, workflows, knowledge bases — are not transferred automatically, so migrate or archive them first.
Team Model Authorization
Which models a team may use is decided by team model authorization records, and read and write access differ:
| Method | Path | Purpose | Permission |
|---|---|---|---|
| GET | /api/v1/teams/{team_id}/models | List authorized models (optional model_type filter) | Superuser or team member |
| POST | /api/v1/teams/{team_id}/models | Authorize one model and set its quotas | Superuser only |
| PUT | /api/v1/teams/{team_id}/models/{model_id} | Change quotas and enabled state | Superuser only |
| DELETE | /api/v1/teams/{team_id}/models/{model_id} | Revoke one model authorization | Superuser only |
| POST | /api/v1/teams/{team_id}/models/batch | Authorize several models at once | Superuser only |
| DELETE | /api/v1/teams/{team_id}/models/batch | Revoke several authorizations at once | Superuser only |
| GET | /api/v1/teams/{team_id}/available-models | List authorized and enabled models for callers to pick from | Superuser or team member |
| GET | /api/v1/teams/{team_id}/models/quota | Read per-model quotas and usage | Superuser or team member |
For non-superusers the three read endpoints require team membership but no specific permission code. Lists are returned by descending priority, so higher-priority authorizations come first.
Field definitions, quota math, and batch request bodies are documented in the Team Models API.
Error Codes
| HTTP | Code | Scenario |
|---|---|---|
400 | 1001 | Request validation failed (neither user_id nor identifier provided) |
403 | 3000 | Missing permission code, a viewer attempting a write, or the target user lacking team:manage |
403 | 3002 | Not a team member |
403 | 3003 | Team owner or admin role required |
403 | 3004 | Team owner role required (changing roles, transferring ownership) |
404 | 3002 | Not a team member on POST /teams/{team_id}/leave |
404 | 4001 | Target user not found |
404 | 4004 | Team not found |
404 | 4005 | Team membership not found (including a transfer target who is not a member) |
400 | 5102 | Team name already exists |
400 | 5103 | User is already a team member |
400 | 5205 | The default team cannot be deleted |
400 | 5206 | Cannot add a member directly as owner |
400 | 5207 | Cannot change the owner's role |
400 | 5208 | Cannot promote directly to owner (including transferring to the current owner) |
400 | 5209 | Cannot remove the owner |
400 | 5210 | The owner must transfer ownership before leaving |
Not Available Today
These are current-version boundaries; design clients around them:
- No standalone member list endpoint:
GET /teams/{team_id}/membersdoes not exist; members come only from theGET /teams/{team_id}response. - No team statistics endpoint: per-team usage comes from the model quota endpoint or admin statistics.
- No platform-side create/delete team: only
/api/v1/admin/*can do it. is_defaultis read-only and cannot be changed through the API.
Code Examples
Python
import os
import requests
BASE = "https://your-domain.com/api/v1"
TOKEN = os.environ["USER_TOKEN"]
HEADERS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
def list_my_teams() -> list[dict]:
"""List the teams you belong to, with your role in each."""
result = requests.get(f"{BASE}/teams/my", headers=HEADERS).json()
if result["code"] != 0:
raise RuntimeError(f"{result['code']} {result['msg']}")
return result["data"]
def add_member(team_id: str, identifier: str, role: str = "member") -> dict:
"""Add a member by exact email match."""
response = requests.post(
f"{BASE}/teams/{team_id}/members",
headers=HEADERS,
json={"identifier": identifier, "role": role},
)
result = response.json()
if result["code"] != 0:
raise RuntimeError(f"{result['code']} {result['msg']}")
return result["data"]
def transfer_ownership(team_id: str, new_owner_id: str) -> dict:
"""Transfer ownership (new_owner_id is a query parameter)."""
response = requests.post(
f"{BASE}/teams/{team_id}/transfer-ownership",
headers=HEADERS,
params={"new_owner_id": new_owner_id},
)
result = response.json()
if result["code"] != 0:
raise RuntimeError(f"{result['code']} {result['msg']}")
return result["data"]
# Usage
for team in list_my_teams():
print(f"{team['name']} — my role: {team['role']}")JavaScript
const BASE = 'https://your-domain.com/api/v1';
async function listMyTeams(token) {
const response = await fetch(`${BASE}/teams/my`, {
headers: { Authorization: `Bearer ${token}` },
});
const result = await response.json();
if (result.code !== 0) throw new Error(`${result.code} ${result.msg}`);
return result.data;
}
async function addMember(token, teamId, identifier, role = 'member') {
const response = await fetch(`${BASE}/teams/${teamId}/members`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ identifier, role }),
});
const result = await response.json();
if (result.code !== 0) throw new Error(`${result.code} ${result.msg}`);
return result.data;
}
async function leaveTeam(token, teamId) {
const response = await fetch(`${BASE}/teams/${teamId}/leave`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
});
const result = await response.json();
if (result.code !== 0) throw new Error(`${result.code} ${result.msg}`);
return result.data;
}
// Usage
const teams = await listMyTeams(process.env.USER_TOKEN);
teams.forEach((team) => console.log(`${team.name} — my role: ${team.role}`));Related Documentation
- Team Models API — authorization fields, quotas, and batch operations
- Users API — creating users and assigning roles as an administrator
- API Errors and Retries — unified handling for
3002-3004and5206-5210
How is this guide?