ClouisleClouisle

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

RoleDescription
ownerTeam owner, exactly one per team. Can change member roles, transfer ownership, and update the team; cannot leave directly — ownership must be transferred first
adminTeam 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)
memberRegular member. The default role; operations follow global permissions
viewerRead-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:

PermissionOperations covered
team:readView team details, view members, leave the team (self-removal)
team:updateUpdate team name, description, avatar
team:manageAdd/remove members, change member roles, transfer ownership
admin:team:readList all teams in the admin panel
admin:team:createCreate a team in the admin panel
admin:team:deleteDelete a team in the admin panel

The check order (check_team_permission) is the same for every platform endpoint:

  1. The team must exist, otherwise 404 / 4004.
  2. Superusers pass immediately and skip the remaining checks.
  3. The caller must be a team member, otherwise 403 / 3002.
  4. For management actions (team:update, team:manage, or an endpoint that asks explicitly) the role must be owner or admin, otherwise 403 / 3003.
  5. A viewer role may only perform read-only permissions, otherwise 403 / 3000.
  6. 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

MethodPathPurposePermission / role
GET/api/v1/teams/myList the teams the caller belongs to, with rolesAny authenticated user
GET/api/v1/teams/{team_id}Team details including the member listteam:read
PUT/api/v1/teams/{team_id}Update team infoteam:update + owner/admin
POST/api/v1/teams/{team_id}/membersAdd a memberteam:manage + owner/admin
PUT/api/v1/teams/{team_id}/members/{user_id}Change a member roleteam: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}/leaveLeave the teamteam:read
POST/api/v1/teams/{team_id}/transfer-ownershipTransfer ownershipteam:manage + owner (or superuser)

Admin routes

MethodPathPurposePermission
GET/api/v1/admin/teamsList all teams with paginationadmin:team:read
POST/api/v1/admin/teamsCreate a teamadmin:team:create
DELETE/api/v1/admin/teams/{team_id}Delete a teamadmin: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/my

No 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
ParameterTypeDefaultDescription
pageinteger1Page number
page_sizeinteger50Items per page (note the admin default is 50, not the platform-wide 20)
searchstring—Case-insensitive match on team name or description

Get Team Details

GET /api/v1/teams/{team_id}
Path parameterTypeRequired
team_idstring (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.

FieldTypeDescription
namestringTeam name, globally unique
descriptionstringTeam description
avatar_urlstringTeam 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}/members

Requires the team:manage permission code and an owner/admin role.

FieldTypeRequiredDefaultDescription
user_idstring (UUID)One of the two—Target user UUID
identifierstringOne of the two—Exact username match, or exact email match (case-insensitive)
rolestringNomemberMember 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:

  1. The target user must exist, otherwise HTTP 404 / 4001.
  2. The target user must not already be a member, otherwise HTTP 400 / 5103 (ALREADY_TEAM_MEMBER).
  3. The role cannot be owner: that returns HTTP 400 / 5206 (CANNOT_ADD_AS_OWNER). Ownership changes only through the transfer endpoint.
  4. To add someone as admin, the target user must already hold the global team:manage permission, otherwise HTTP 403 / 3000 (the response includes a permission field 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 parameterTypeRequired
team_idstring (UUID)Yes
user_idstring (UUID)Yes

Request body:

{ "role": "admin" }

role is required. Constraints:

  • Only the owner (or a superuser) can change roles; an admin caller gets HTTP 403 / 3004 (TEAM_OWNER_REQUIRED).
  • The owner's own role cannot be changed (HTTP 400 / 5207, CANNOT_CHANGE_OWNER_ROLE).
  • A member cannot be promoted directly to owner (HTTP 400 / 5208, CANNOT_PROMOTE_TO_OWNER) — use ownership transfer instead.
  • Setting the role to admin requires the target user to already hold the global team:manage permission, otherwise HTTP 403 / 3000.
  • A missing target user returns 404 / 4001; a missing membership returns 404 / 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 parameterTypeRequired
team_idstring (UUID)Yes
user_idstring (UUID)Yes

Authorization depends on whether the target is the caller:

  • Self-removal (user_id equals the caller): only team:read is needed; this is equivalent to leaving.
  • Removing someone else: requires team:manage and an owner/admin role.

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}/leave

Requires team:read (regular members and viewer roles can call it).

  • A non-member gets HTTP 404 / 3002 (NOT_TEAM_MEMBER).
  • The owner cannot leave: HTTP 400 / 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:

  1. The caller needs the team:manage permission code, an owner/admin role, and additionally the owner role itself (superusers bypass only the owner role requirement).
  2. The user referenced by new_owner_id must already be a member of this team, otherwise HTTP 404 / 4005.
  3. The new owner must hold the global team:manage permission, otherwise HTTP 403 / 3000.
  4. 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/teams

Requires admin:team:create.

FieldTypeRequiredDescription
namestringYesTeam name, globally unique
descriptionstringNoDescription
avatar_urlstringNoAvatar 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:

MethodPathPurposePermission
GET/api/v1/teams/{team_id}/modelsList authorized models (optional model_type filter)Superuser or team member
POST/api/v1/teams/{team_id}/modelsAuthorize one model and set its quotasSuperuser only
PUT/api/v1/teams/{team_id}/models/{model_id}Change quotas and enabled stateSuperuser only
DELETE/api/v1/teams/{team_id}/models/{model_id}Revoke one model authorizationSuperuser only
POST/api/v1/teams/{team_id}/models/batchAuthorize several models at onceSuperuser only
DELETE/api/v1/teams/{team_id}/models/batchRevoke several authorizations at onceSuperuser only
GET/api/v1/teams/{team_id}/available-modelsList authorized and enabled models for callers to pick fromSuperuser or team member
GET/api/v1/teams/{team_id}/models/quotaRead per-model quotas and usageSuperuser 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

HTTPCodeScenario
4001001Request validation failed (neither user_id nor identifier provided)
4033000Missing permission code, a viewer attempting a write, or the target user lacking team:manage
4033002Not a team member
4033003Team owner or admin role required
4033004Team owner role required (changing roles, transferring ownership)
4043002Not a team member on POST /teams/{team_id}/leave
4044001Target user not found
4044004Team not found
4044005Team membership not found (including a transfer target who is not a member)
4005102Team name already exists
4005103User is already a team member
4005205The default team cannot be deleted
4005206Cannot add a member directly as owner
4005207Cannot change the owner's role
4005208Cannot promote directly to owner (including transferring to the current owner)
4005209Cannot remove the owner
4005210The 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}/members does not exist; members come only from the GET /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_default is 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}`));

How is this guide?

On this page