# Workspaces API

> Manage workspaces, members, and invites. Workspaces are called teams in the API: endpoints live under /teams and requests carry teamid. Every API key belongs to a workspace, and apps, tasks, secrets, ...

**URL:** https://inference.sh/docs/api/rest/teams
**Last updated:** 2026-10-11

---

Manage workspaces, members, and invites. Workspaces are called teams in the API: endpoints live under `/teams` and requests carry `team_id`. Every API key belongs to a workspace, and apps, tasks, secrets, and billing are scoped to it.

Requires API key scopes **`teams:read`** (list, get, members, invites) or **`teams:write`** (create, update, delete, member changes, invite management). Create keys in [settings → workspace → api keys](https://app.inference.sh/settings/team/keys).

→ [Authentication](/docs/api/authentication) · [Entitlements](/docs/api/rest/entitlements): plan limits per workspace

---

## Get current user

`GET /me`

Returns the authenticated user and the **current workspace** (`team`) resolved from the API key (or guest context). Requires a valid API key.

### Response

```json
{
  "user": {
    "id": "usr_abc123",
    "email": "you@example.com",
    "name": "you",
    "full_name": "Alex Example",
    "default_team_id": "team_personal",
    "avatar_url": "https://...",
    "metadata": {
      "completed_onboarding": true,
      "use_case": "agents",
      "signup_source": "github",
      "terms_accepted_at": "2026-01-15T10:30:00Z",
      "terms_version": "2026-01"
    }
  },
  "team": {
    "id": "team_xyz",
    "type": "team",
    "name": "Acme",
    "username": "acme",
    "setup_completed": true,
    "status": "active"
  },
  "org": {
    "id": "org_abc",
    "slug": "acme",
    "name": "Acme Inc",
    "avatar_url": "https://..."
  },
  "team_view": {
    "team_id": "team_xyz",
    "kind": "org_member",
    "governance": {
      "billing": { "by": "org", "team_id": "org_abc" },
      "policy": { "by": "org", "team_id": "org_abc" }
    },
    "org": {
      "id": "org_abc",
      "name": "Acme Inc",
      "slug": "acme",
      "avatar_url": "https://...",
      "can": ["manage_org", "manage_billing", "view_billing"]
    },
    "can": ["edit_profile", "manage_members", "view_billing"]
  },
  "personal_team_id": "team_personal",
  "needs_username": false
}
```

`team` is omitted when no workspace context is attached to the request.

`org` is present when the current workspace belongs to an organization. `org.is_admin` is **not** set on `/me` — use `team_view` (or `GET /teams/{id}/view`) for what the caller may do.

`team_view` is the same object as [`GET /teams/{id}/view`](#workspace-settings-view): workspace kind, who governs billing and usage policy, and the capability strings the API enforces on settings routes. Omitted when there is no workspace context.

The top-level JSON object is typed as **`MeResponse`** in the JavaScript, Python, and Swift SDKs (`team` is a full **`TeamDTO`**, not the slimmer relation object from `GET /teams`). See [Python SDK — `GET /me`](/docs/api/sdk-python#generated-types-inferenceshtypes) and [JavaScript SDK — generated types](/docs/api/sdk-javascript#generated-types-for-rest-integrations).

| Field | Description |
|-------|-------------|
| `personal_team_id` | The caller's personal workspace ID. Omitted or empty for **managed accounts**, which have no personal workspace. |
| `needs_username` | `true` when the personal workspace has not finished setup (username not chosen yet). Always `false` for managed accounts. These fields describe the **personal** workspace, not the session workspace in `team` — for example, someone who accepts an invite may land in the inviter's workspace while `needs_username` is still `true` until they complete setup on their personal workspace. |

When `needs_username` is `true`, pick a username with [`GET /teams/check-username`](#check-username-availability), then call [`POST /teams/{personal_team_id}/complete-setup`](#complete-setup) (use the `personal_team_id` value from this response as `{id}`).

The nested `user.metadata` object (`UserMetadataDTO`) tracks onboarding and consent state:

| Field | Description |
|-------|-------------|
| `completed_onboarding` | Whether the user finished workspace onboarding |
| `use_case`, `use_case_reason`, `use_case_privacy` | Onboarding survey answers |
| `signup_source` | How the account was created (e.g. `github`, `google`) |
| `terms_accepted_at` | ISO timestamp when the user accepted the current terms (optional) |
| `terms_version` | Terms version string the user accepted |

**JavaScript SDK:**

```typescript
// me() returns Response<MeResponse>; the body is on `data`
const { data: { user, team, team_view, org, personal_team_id, needs_username } } =
  await client.teams.me();
// team is TeamDTO; use team_view.can for settings gates
```

**Example:**

```bash
curl https://api.inference.sh/me \
  -H "Authorization: Bearer inf_your_key"
```

---

## Survey

Customer-discovery and open feedback submitted from the CLI (`belt feedback`) or other clients. Structured prompts are driven by active bounty programs named `survey-<question_id>`; the CLI reads question text and options from the platform form `infsh/<question_id>` (first question in the schema). When programs cannot be loaded, the CLI uses built-in fallback wording for the classic IDs below.

### List your responses

`GET /me/survey`

Returns survey answers for the authenticated user. The response lists every structured answer first (`discovery`, `use_case`, `need`, `testimonial`, and legacy `feedback`), then the most recent freeform `open:*` rows, up to 100 rows in total. Open feedback never pushes structured answers off the list. The CLI uses this list to decide which onboarding questions are still pending.

**Response:**

```json
{
  "responses": [
    {
      "id": "srv_abc123",
      "question_id": "discovery",
      "response": "github",
      "agent": "cursor",
      "source": "cli",
      "context": "belt login, belt app run, belt task get abc123"
    }
  ]
}
```

### Submit a response

`POST /me/survey`

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `question_id` | string | Yes | `discovery`, `use_case`, `need`, `testimonial`, `feedback` (legacy), or `open` (freeform) |
| `response` | string | Yes | Answer text |
| `agent` | string | No | Agent runtime that submitted the answer (e.g. `cursor`, `claude-code`) |
| `source` | string | No | Client identifier (CLI sends `cli`) |
| `context` | string | No | Recent commands that led to submission. The CLI sends the last five belt commands (comma-separated) from `~/.belt/history`. Other clients may send a page path or other freeform string. |

Each `question_id` is the platform form `infsh/<question_id>`, and the form's submit policy decides what a repeat answer does. The structured questions (`discovery`, `use_case`, `need`, `testimonial`) take **one answer per user**: a second answer returns **409** `already_submitted`. The legacy `feedback` question is closed and returns **409** `form_closed`. Open feedback (`open`) takes any number of answers. Which questions the CLI asks depends on bounty configuration (`survey-<id>` programs); it walks them in program order.

Under the hood, each structured answer is also stored as a form submission to `infsh/<question_id>`. For general form APIs (create forms, submit JSON answers, list submissions), see [Forms API](/docs/api/rest/forms).

**Rewards:** `POST /me/survey` records the answer only; it grants nothing, and neither does `belt feedback`. The response may still include `granted_amount` and `reward_blocked_reason` for older clients; they are always `0` and empty. Credits come from the matching `survey-<question_id>` bounty program through [`POST /me/bounty`](#claim-a-bounty). The seeded `survey-*` programs use `proof_type: text`, which `belt bounty claim` does not handle, so claim them with a direct `POST /me/bounty` call. A program with `proof_type: form` and `proof_form: infsh/<question_id>` takes the `response.id` from this call as `proof_id`.

Fill-in placeholder text (for example `your answer here` or `<your answer>`, copied from CLI help) is rejected with **422** `validation_error` and nothing is stored.

**Example:**

```bash
curl -X POST https://api.inference.sh/me/survey \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "question_id": "discovery",
    "response": "github",
    "source": "cli",
    "context": "belt login, belt app run, belt task get abc123"
  }'
```

**Response:**

```json
{
  "response": {
    "id": "srv_abc123",
    "question_id": "discovery",
    "response": "github",
    "source": "cli",
    "context": "belt login, belt app run, belt task get abc123"
  }
}
```

Claim the matching bounty (when configured):

```bash
curl -X POST https://api.inference.sh/me/bounty \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "bounty_id": "survey-discovery",
    "proof_id": "discovery",
    "source": "cli"
  }'
```

For a `proof_type: text` program, `proof_id` is a key you choose (one claim per key and program). For a `proof_type: form` program, use the `response.id` from `POST /me/survey`.

**Errors:**

| HTTP | Code | Cause |
|------|------|-------|
| `400` | `validation_error` | `unknown question: <id>`: no `infsh/<id>` form |
| `409` | `already_submitted` | You already answered this one-answer question |
| `409` | `form_closed` | The question no longer takes answers (legacy `feedback`) |
| `422` | `validation_error` | The answer fails the question's schema (empty or too short), or is fill-in placeholder text |

→ [CLI setup — Feedback](/docs/extend/cli-setup#feedback-cli)

---

## Bounties

Incentive programs that grant credits when you complete qualifying actions (for example, deploying a useful app or answering survey questions). Requires authentication.

### List active programs

`GET /bounties`

Returns active bounty programs sorted by `notice_priority` (ascending). Each program includes its stable `name` (used as `bounty_id` when claiming), reward amount, proof requirements, and CLI notice configuration.

The belt CLI calls this endpoint to populate stderr bounty notices. Responses are cached locally for 6 hours (`~/.belt/cache/bounty-programs.json`); admin edits to notice fields propagate on the next cache refresh without a CLI release. Programs with `proof_type: form` surface in the notice block and in `belt feedback` as `belt bounty claim <program>` prompts until the bounty is claimed; `survey-<id>` programs stay on the single-question `belt feedback` path. Reward amounts are per program and may change; `belt bounty list` shows the live values. See [Forms API](/docs/api/rest/forms) and [CLI setup — Feedback](/docs/extend/cli-setup#feedback-cli).

**Response** — array of `BountyProgramDTO` objects:

| Field | Description |
|-------|-------------|
| `name` | Program ID (for example `app-builder`, `survey-discovery`) |
| `description` | Rules shown to users and agents |
| `amount_microcents` | Reward per claim, in microcents |
| `grant_type` | Credit grant type (`bounty` or `survey`) |
| `expiry_days` | Days until granted credits expire |
| `max_per_user` | Total claims per user (`0` = unlimited). Independent of `max_per_day` — surveys default to `1`. |
| `max_per_day` | Daily claim cap (`0` = no daily cap, unique proof only) |
| `proof_type` | Required proof: `app`, `form`, `text`, or `none` |
| `proof_form` | When `proof_type` is `form`, the form ref (`namespace/name`) the submission must answer |
| `requires_payment_method` | When `true`, claims return **402** until the team saves a payment method (no claim row is stored, so you can retry after adding a card) |
| `status` | `active`, `paused`, or `ended` |
| `notice_text` | Message shown in CLI stderr notices |
| `notice_cooldown_hours` | Hours between notice displays |
| `notice_priority` | Lower values surface first when multiple notices are due |

The default **`app-builder`** program (seeded on new deployments) pays **$2** per claim (`amount_microcents`: `200000000`), with **`max_per_day`: `1`** (one successful claim per calendar day) and unlimited total claims across different apps (`max_per_user`: `0`). CLI stderr notices use the program's `notice_text` (for example, pointing at `belt bounty claim app-builder <namespace/app>`).

### Claim a bounty

`POST /me/bounty`

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `bounty_id` | string | Yes | Program `name` (for example `app-builder`) |
| `proof_id` | string | Yes | Proof identifier — see below by `proof_type` |
| `agent` | string | No | Agent runtime that submitted the claim (CLI sends detected runtime) |
| `source` | string | No | Client identifier (CLI sends `cli`) |

| `proof_type` | `proof_id` meaning | Stored `proof_ref` |
|--------------|-------------------|---------------------|
| `app` | App ID you own with a deployed version | App `namespace/name` |
| `form` | Your submission `id` to the program's `proof_form` | That form's `namespace/name` |
| `text`, `none` | Freeform proof key (for example a question slug) | Same as `proof_id` |

For `proof_type: app`, `resource_id` in the response is the app ID. For `proof_type: form`, `resource_id` is the form ID and `proof_id` is the submission ID.

**Response:**

```json
{
  "submission": {
    "id": "bsm_abc123",
    "bounty_id": "app-builder",
    "resource_id": "01abc123def456",
    "proof_id": "01abc123def456",
    "proof_ref": "you/my-app",
    "agent": "cursor",
    "source": "cli"
  },
  "granted_amount": 200000000
}
```

| `submission` field | Description |
|--------------------|-------------|
| `resource_id` | Linked resource ID when applicable — for `proof_type: app`, the app ID |
| `proof_id` | Stable proof key — app ID, form submission ID, or text key |
| `proof_ref` | Human-readable reference (`namespace/name` for apps and form proofs) |

`granted_amount` is in microcents (`200000000` = $2.00 for the default `app-builder` reward). Omitted or `0` when no credits were granted (for example, duplicate claim).

**Errors:**

| HTTP | Code | Message | Cause |
|------|------|---------|-------|
| `400` | `validation_error` | `proof_id is required` | Missing `proof_id` |
| `400` | `validation_error` | `app must be deployed before claiming a bounty` | App has no deployed version |
| `400` | `validation_error` | `submission … answers …; bounty … takes a submission to …` | Form submission is not for this program's `proof_form` |
| `402` | `payment_method_required` | | The program sets `requires_payment_method` and the team has no saved card; nothing is recorded |
| `403` | `forbidden` | `form submission … is not yours` | `proof_id` names another user's form submission |
| `404` | `not_found` | `unknown bounty: <id>` | No program with that name |
| `404` | `not_found` | `app not found: <id>` | App does not exist or is not yours |
| `404` | `not_found` | `form submission not found: …` | Unknown submission id |
| `409` | `conflict` | `bounty program <id> is not active` | Program paused or ended |
| `409` | `conflict` | `daily bounty limit reached — try again tomorrow` | `max_per_day` reached |
| `409` | `already_claimed` | `bounty already claimed for this proof` | Same `proof_id` already claimed for this program |
| `409` | `already_claimed` | `bounty limit reached — you've already claimed this N time(s)` | `max_per_user` reached |

When the team has no saved payment method and the program sets `requires_payment_method`, the API returns **402** with code `payment_method_required` and `meta.billing_page` set to `/settings/team/billing/payments`. The claim is rejected and **not** stored, so you can call `POST /me/bounty` again with the same `proof_id` after adding a card.

**Example:**

```bash
curl -X POST https://api.inference.sh/me/bounty \
  -H "Authorization: Bearer inf_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "bounty_id": "app-builder",
    "proof_id": "01abc123def456",
    "source": "cli"
  }'
```

Survey rewards use the same bounty system (`bounty_id` = `survey-<question_id>`) but are claimed explicitly via `POST /me/bounty` after the answer is stored — see [Survey](#survey) and [Forms API](/docs/api/rest/forms).

Bounty program administration (creating programs, pausing, viewing claims) is internal and not exposed on the public REST API.

**SDKs:** No `client.bounties` namespace yet in either SDK. Import `BountyProgramDTO`, `SubmitBountyRequest`, and `SubmitBountyResponse` from `inferencesh.types` (Python) or `@inferencesh/sdk` (JavaScript) when calling these endpoints directly — see [Python SDK — generated bounty types](/docs/api/sdk-python#generated-types-inferenceshtypes) and [JavaScript SDK — generated bounty types](/docs/api/sdk-javascript#generated-types-for-rest-integrations).

→ [CLI setup — App bounties](/docs/extend/cli-setup#app-bounties-cli) · [Billing — credit grants](/docs/api/rest/billing#credit-grants)

---

## Data export

Request a ZIP archive of your account and workspace data (GDPR Articles 15 and 20: right of access and data portability). Requires authentication as the account owner (session token or API key).

### Request export

`POST /me/export`

Starts an asynchronous export job. Returns **202 Accepted** with the export ID and initial status.

**Response:**

```json
{
  "id": "uex_abc123",
  "status": "pending"
}
```

If you already have a pending or processing export from the last 24 hours, the API returns that existing job instead of starting a duplicate.

### Get export status

`GET /me/export/{id}`

Poll until `status` is `completed` or `failed`. When complete, the response includes a presigned `download_url` (valid for one hour).

| Field | Description |
|-------|-------------|
| `id` | Export job ID |
| `status` | `pending`, `processing`, `completed`, or `failed` |
| `download_url` | Presigned URL to the ZIP (when `completed`) |
| `completed_at` | ISO timestamp when the archive was ready |
| `expires_at` | When the download link expires (seven days after completion) |
| `error` | Failure message (when `failed`) |

**Example:**

```bash
# Start export
curl -X POST https://api.inference.sh/me/export \
  -H "Authorization: Bearer $(belt auth token)"

# Poll for download URL
curl https://api.inference.sh/me/export/uex_abc123 \
  -H "Authorization: Bearer $(belt auth token)"
```

The ZIP contains JSON files for your user profile, sessions (tokens redacted), API keys (masked), workspace resources (apps, agents, flows, knowledge, projects, chats, tasks, files, secrets, integrations, triggers), billing, notifications, and survey responses. Processing may take several minutes; the server enforces a 10-minute timeout per job.

---

## List workspaces

`GET /teams`

Returns all workspaces the authenticated user belongs to (personal workspace, shared workspaces, and organization workspaces you administer).

Requires `teams:read`.

### Response

Array of workspace objects:

| Field | Description |
|-------|-------------|
| `id` | Workspace ID |
| `type` | `personal`, `team`, `org`, or `system`. See [Workspace types](#workspace-types). |
| `name` | Display name |
| `username` | Unique workspace username (used in namespaces and `belt auth switch`) |
| `avatar_url` | Avatar URL |
| `setup_completed` | Whether the workspace finished onboarding (username chosen) |
| `status` | Workspace status |
| `email` | Contact email (when set) |
| `org_id` | Org this workspace belongs to. An `org` workspace points at itself. Omitted for standalone workspaces. |
| `max_concurrency` | Platform-managed field (usually `0`). Your workspace's concurrent task limit comes from the `concurrency` entitlement on your subscription plan. See [Entitlements: concurrent task limits](/docs/api/rest/entitlements#concurrent-task-limits) |

### Workspace types

| `type` | Description |
|--------|-------------|
| `personal` | Personal workspace, one per account |
| `team` | Shared workspace for collaborators. When attached to an org, `org_id` is the org's ID. |
| `org` | An organization's own workspace. Created with the org, with the same ID as the org, and `org_id` points at itself. Its members are the org's admins, so it has owners and admins only. Holds org billing and org-scope credentials. |
| `system` | Internal platform workspace |

**Example:**

```bash
curl https://api.inference.sh/teams \
  -H "Authorization: Bearer inf_your_key"
```

**JavaScript SDK:** `client.teams.list()`

---

## Get workspace

`GET /teams/{id}`

Returns a single workspace by ID. Requires `teams:read` and membership (or admin access).

**JavaScript SDK:** `client.teams.get(teamId)`

---

## Workspace settings view

`GET /teams/{id}/view`

Returns how the **authenticated caller** sees a workspace in settings: workspace kind, billing/policy governance, optional parent organization summary, and the **`can`** capability list the API uses to gate `POST`/`PUT`/`DELETE` routes under `/teams/{id}` and related org routes.

Requires `teams:read`. A caller with no role on the workspace (directly or through org standing) gets `404`. This is the source of truth for UI permission checks — clients should not re-derive capabilities from roles alone.

### Response (`TeamViewDTO`)

| Field | Description |
|-------|-------------|
| `team_id` | Workspace ID (same as path parameter) |
| `kind` | Caller-relative kind: `personal`, `team` (standalone shared workspace), `org_member` (workspace inside an org), or `org` (the organization's own workspace) |
| `governance.billing` | Who decides billing: `by` is `self` or `org`; `team_id` is the workspace that holds the balance |
| `governance.policy` | Who decides usage policy: same shape as `governance.billing` |
| `org` | When the workspace belongs to an org: `id`, `name`, `slug`, `avatar_url`, and `can` — capabilities on the **organization workspace** (billing, org admin actions, org credentials). Omitted for standalone workspaces |
| `can` | Capability strings the caller holds **on this workspace** (for example `manage_members`, `manage_billing`, `manage_org`) |

### Capability strings

The `can` arrays use the same identifiers the API evaluates on settings routes:

| Capability | Typical meaning |
|------------|-----------------|
| `edit_profile` | Rename workspace, avatar, username setup |
| `manage_members` | Invite, remove, and change roles for plain **members** (admins and owners) |
| `manage_admins` | Grant, change, or remove **admin** and **owner** roles (owners only; omitted on personal workspaces) |
| `view_members` | Read the member roster |
| `manage_keys` | Workspace API keys: create workspace keys, list and revoke every key of the workspace |
| `create_keys` | Create, list, and revoke your own personal keys. Absent for service accounts |
| `manage_vault` | Vault membership and secrets layout |
| `view_billing` | Read balance, usage, invoices |
| `manage_billing` | Top-ups, payment methods, subscription checkout |
| `view_policy` | Read effective usage policy |
| `manage_policy` | Edit usage policy (org-wide on the org workspace; per-workspace on standalone teams) |
| `manage_org` | Org admin actions (list workspaces, attach/create teams, managed users, org SSO) |
| `manage_sso` | Team SAML connection |
| `archive` | Delete/archive workspace |
| `create_team` | `POST /teams` — create a standalone workspace. Absent on **managed accounts** (use [`POST /orgs/{id}/teams/create`](/docs/api/rest/orgs#create-workspace-in-org) instead) |
| `create_org` | `POST /orgs` — create an organization from a standalone workspace you **own** |
| `connect_org_credential` | Connect org-scoped integrations |
| `connect_platform_credential` | Platform admin: platform-scoped credentials |

On an **org member** workspace, `can` is the capability table for **this** workspace: your roster role, or — when you administer the org — your role on the organization workspace **raised** to that level (org owners act as owners on every member team; org admins as admins, without joining the roster). `org.can` carries rights on the **organization workspace** itself (for example `manage_billing` when you may change org payment settings). Do not use `org.can` to decide admin invites or role changes on a member workspace. Member-team owners and org owners acting through standing see `view_billing` on the member team but not `manage_billing` there when the org pays. Clients should use `can` from this endpoint (or from [`GET /orgs/{id}/teams`](/docs/api/rest/orgs#list-organization-workspaces) per workspace) instead of inferring permissions from membership alone. **Last-owner** checks still count only listed owners on the workspace, not org standing.

**Example:**

```bash
curl "https://api.inference.sh/teams/team_xyz/view" \
  -H "Authorization: Bearer inf_your_key" \
  -H "X-Team-ID: team_xyz"
```

`GET /me` embeds the same payload as `team_view` for the API key's current workspace.

→ [Organizations API](/docs/api/rest/orgs): create orgs, list member workspaces, org admins

---

## Workspace governance rules (usage policy)

A workspace's usage policy is its governance layer in the rules API: `GET`, `PUT`, and `DELETE /teams/{id}/rules`, single-rule routes under `/teams/{id}/rules/{ruleId}`, member policies under `/teams/{id}/members/{userId}/rules`, the blocked-attempts feed, and effective access on `/teams/{id}/rules/access`. Reads need **`view_policy`**; writes need **`manage_policy`**, which in an organization belongs to the org admins. See the [Usage policy API](/docs/api/rest/usage-policy).

---

## Create workspace

`POST /teams`

Requires `teams:write` and the **`create_team`** capability on your current workspace context (see [`GET /teams/{id}/view`](#workspace-settings-view)). Managed accounts do not receive `create_team`; org admins create workspaces inside an org with [`POST /orgs/{id}/teams/create`](/docs/api/rest/orgs#create-workspace-in-org).

### Request

| Field | Required | Description |
|-------|----------|-------------|
| `name` | yes | Display name |
| `username` | yes | Unique workspace username (kebab-case) |
| `email` | no | Contact email |

```json
{
  "name": "Acme Engineering",
  "username": "acme-eng",
  "email": "eng@acme.com"
}
```

Returns the created workspace object. New workspaces may need [Complete setup](#complete-setup) before all features are available.

**JavaScript SDK:** `client.teams.create({ name, username, email })`

---

## Update workspace

`POST /teams/{id}`

Updates workspace metadata. Requires `teams:write` and appropriate role (owner or admin).

**JavaScript SDK:** `client.teams.update(teamId, { name, username, email })`

---

## Delete workspace

`DELETE /teams/{id}`

Permanently deletes a workspace. Requires `teams:write` and owner role. Personal workspaces cannot be deleted.

**JavaScript SDK:** `client.teams.delete(teamId)`

---

## Check username availability

`GET /teams/check-username?username={name}`

Returns an `AvailabilityResponse`. Use before create or rename.

| Query | Required | Description |
|-------|----------|-------------|
| `username` | Yes | Proposed username (normalized to lowercase server-side) |
| `exclude_team_id` | No | Workspace ID to ignore. Use when updating so the workspace's current username does not count as a conflict |

Requires `teams:read`.

**Response:**

| Field | Type | Description |
|-------|------|-------------|
| `value` | string | Normalized username (lowercased) |
| `available` | boolean | `true` if the username can be used |
| `reason` | string | When `available` is `false`: `taken` (username in use) or `reserved` (on the platform reserved-name list). Omitted when available. |

Use `reason` for user-facing messages instead of maintaining a client-side reserved-name list — the server owns the full reserved namespace.

```json
{
  "value": "acme-eng",
  "available": false,
  "reason": "taken"
}
```

**JavaScript SDK:** `client.teams.checkUsername('acme-eng')`

---

## Complete setup

`POST /teams/{id}/complete-setup`

Chooses the workspace username during onboarding. For a new self-serve account, `{id}` is `personal_team_id` from [`GET /me`](#get-current-user) while `needs_username` is `true`.

### Request

```json
{
  "username": "acme"
}
```

Requires `teams:write`.

---

## Members

### List members

`GET /teams/{id}/members`

Returns workspace members with roles. Requires `teams:read`.

| Field | Description |
|-------|-------------|
| `id` | Membership row ID |
| `user_id` | Member user ID |
| `team_id` | Team ID |
| `role` | `owner`, `admin`, or `member` |
| `user` | Nested user profile (`email`, `name`, `avatar_url`) |
| `assignable_roles` | Roles the **authenticated caller** may set on this member, including their current `role`. Populated only when the caller may manage members. Empty or omitted when the caller cannot change anyone (for example, a `member`), when an admin views an `owner` they cannot demote, or when the sole `owner` cannot step down until another owner exists. Only owners may include `owner` in this list. |
| `removable` | Whether the caller may remove this member. `true` only when removal is allowed (the last owner on a team is never removable). Omitted when the caller cannot manage members. |

These fields mirror the same checks enforced by `POST /teams/{id}/members/{userId}/role` and `DELETE /teams/{id}/members/{userId}`.

Use them to drive member-management UIs without reimplementing server rules:

- **Admins** may add, change, and remove plain **members** (`manage_members`). Granting, changing, or removing **admin** or **owner** requires **`manage_admins`** (owners only).
- A team must keep at least one owner. Demoting or removing the last owner returns **400** with code `last_owner`.
- The **organization workspace** accepts `owner` and `admin` only — `member` is not assignable there.

Member mutations require `teams:write` and route-level capability checks (`manage_members` for roster changes; the service enforces `manage_admins` when the target role is admin or owner). Refusals return **403** with code `team_role_required` (often with `meta.capability` set). See [Workspace settings view](#workspace-settings-view) instead of inferring rights from role names alone.

**JavaScript SDK:** `client.teams.getMembers(teamId)` — typed as `TeamMemberDTO` with `assignable_roles` and `removable`.

**Python SDK:** import `TeamMemberDTO` from `inferencesh.types` when calling this endpoint with `httpx` or `requests` (no `client.teams` namespace yet).

### Add member

`POST /teams/{id}/members`

```json
{
  "email": "colleague@example.com",
  "role": "member"
}
```

Requires `teams:write` and **`manage_members`** (and **`manage_admins`** when `role` is `admin` or `owner`). Adds an existing platform user by email, or creates a pending invite when the email is not registered. The `email` value is trimmed and lowercased before lookup; matching is case-insensitive. For Gmail and Googlemail, the API also treats addresses that deliver to the same mailbox as one address when checking for an existing member (see [REST overview: Signup email restrictions](/docs/api/rest/overview#signup-email-restrictions)).

**JavaScript SDK:** `client.teams.addMember(teamId, { email, role })`

### Remove member

`DELETE /teams/{id}/members/{userId}`

Requires `teams:write` and **`manage_members`** (or **`manage_admins`** when removing an admin or owner). Also revokes the removed user's **personal** API keys on that workspace; workspace keys they created keep working. See [API keys — when someone leaves](/docs/org/api-keys#when-someone-leaves).

**JavaScript SDK:** `client.teams.removeMember(teamId, userId)`

### Update member role

`POST /teams/{id}/members/{userId}/role`

```json
{
  "role": "admin"
}
```

Requires `teams:write` and **`manage_members`** / **`manage_admins`** as for [Add member](#add-member).

**JavaScript SDK:** `client.teams.updateMemberRole(teamId, userId, 'admin')`

---

## Invites

### List workspace invites

`GET /teams/{id}/invites`

Returns pending invites for a workspace. Requires `teams:read`.

**JavaScript SDK:** `client.teams.listInvites(teamId)`

### Create invite

`POST /teams/{id}/invites`

```json
{
  "email": "colleague@example.com",
  "role": "member"
}
```

Requires `teams:write`. Pending invites store the invited address trimmed and lowercased. A second invite to the same address replaces the earlier pending invite for that workspace. Invite emails are limited to 3 per hour per address (letter case and `+` aliases count as one address) and 100 per workspace per day; past either limit the API returns **429** `rate_limited`.

**JavaScript SDK:** `client.teams.createInvite(teamId, { email, role })`

### Revoke invite

`DELETE /teams/{id}/invites/{inviteId}`

Requires `teams:write`.

**JavaScript SDK:** `client.teams.revokeInvite(teamId, inviteId)`

### List my pending invites

`GET /invites/me`

Returns invites addressed to the authenticated user's email. Requires `teams:read`.

### Get invite info (public)

`GET /invites/info?token={token}`

Returns a `TeamInviteInfoDTO` for the accept page. **No API key required** — the invite token is the credential.

Response is a `TeamInviteInfoDTO`:

| Field | Description |
|-------|-------------|
| `id` | Invite ID |
| `email` | Address the invite was sent to |
| `role` | Role the invitee receives on accept |
| `status` | Standing **now**: a pending invite past `expires_at` reads `expired`; an invite to a workspace that no longer admits members reads `revoked` |
| `expires_at` | ISO timestamp when a pending invite stops being valid |
| `team` | Workspace summary: `id`, `type` (`personal`, `team`, or `org`), `name`, `avatar_url` |
| `org_name` | Organization name when the workspace is a member team inside an org. Omitted for standalone workspaces and for the org's own workspace |
| `invited_by` | Display name of the inviter, as the invite email names them |

| HTTP status | When | Client behavior |
|-------------|------|-----------------|
| `400` | `token` query parameter is missing | Fix the link — it must include `?token=…` |
| `404` | No invite matches the token | Treat the link as invalid (truncated URL, revoked invite, or wrong token) |
| `5xx` | Server error while loading invite info | The token may still be valid; retry the lookup |

Only `404` means the link does not work; other failures say nothing about whether the invite stands, so the workspace invite page offers **try again** for them.

### Accept invite

`POST /invites/accept`

Requires authentication. Pass the invite token in the JSON body or `token` query parameter:

```json
{
  "token": "invite_token_from_email"
}
```

Returns the new `TeamMemberDTO`.

### Decline invite

`POST /invites/decline`

Pass the same `token` shape. Does not require the invitee to be logged in.

---

## Roles

| Role | Typical permissions |
|------|---------------------|
| `owner` | Full workspace control, billing, delete workspace |
| `admin` | Manage members, settings, and workspace resources |
| `member` | Use workspace resources within plan limits |

Owners and admins can list and manage other members' `private` resources, for example after a member leaves. Pass `include_private: true` on cursor-list requests, or access an individual resource by ID. Both paths are audited. See [REST overview: Workspace admin visibility](/docs/api/rest/overview#workspace-admin-visibility).

---

## Related

- [REST overview](/docs/api/rest/overview): authentication and response format
- [Organizations API](/docs/api/rest/orgs): org CRUD, member workspaces, org admins
- [Entitlements](/docs/api/rest/entitlements): per-workspace plan limits
- [Billing](/docs/api/rest/billing) · [Subscription](/docs/api/rest/subscription): workspace balance and plans
- [SDK overview](/docs/api/sdk/overview): `client.teams` namespace (JavaScript)
