> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Teams: create, staff, and route conversations to them

> Create a team, manage its members, and target it with the assign_team routing action — the operational walkthrough from POST /api/v1/teams through to empty-team safe fallback, permissions, and curl.

A **team** is a named group of members in your organization — org-scoped, not tied to any one channel. Two shipped surfaces consume the same roster: the inbox's **Assign to team** picker and the routing-rule action `assign_team`, which spreads inbound conversations across the group instead of leaving them floating in an unassigned bucket. The concept and full endpoint reference live in [Roles, teams, and permissions](/concepts/roles-teams-permissions); this page is the operational walkthrough from create-team through to wiring the `assign_team` routing rule.

Everything here is **tenant-owned**: team definitions, member rosters, and the rules that target them live in your organization's tenant schema and take effect on the next inbound item. None of these controls change how outbound calls or messages are placed — outbound MT voice and SMS continue to exit exclusively through the Devotel wholesale softswitch.

**Base path:** `/api/v1/teams`

**Authentication:** Clerk session (`Authorization: Bearer <token>`) or API key (`X-API-Key`).

***

## 1. What a team is

A team is one row in your organization's tenant schema: a `name` (unique per organization, up to 120 characters), an optional `description` (up to 500), and an `active` flag. Members are separate rows that link an existing organization user to the team — the team never creates a user; invite the person to your organization first from **Settings → Team**, then attach their user id.

Both consumers share the roster, so the membership you manage on one screen is the pool the routing action picks from and the group a supervisor samples:

* The dashboard's **Voice → Teams** page lists every team in the organization with its name, description, active state, and member count, and opens the create / edit / member dialogs.
* The inbox's **Assign to team** picker reads the same list for manual assignment.
* The `assign_team` routing action picks one active member of the roster at assignment time.
* Supervisor **Team Listen** (Voice → Monitoring) reuses the roster for group QA sampling.

## 2. Create a team

From the dashboard, open **Voice → Teams** and click **Create team**. Enter a routing-meaningful name (the routing rule references the team by it — "Billing support" reads better in a rule list than "team\_1"), an optional description, and set the **active** flag. Save; the team appears in the list sorted by name.

Over the API:

```bash cURL theme={null}
# Create
curl -X POST "https://api.orbit.devotel.io/api/v1/teams" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Billing support", "description": "Billing and payments queue", "active": true }'

# Update (rename / redescribe / toggle active)
curl -X PATCH "https://api.orbit.devotel.io/api/v1/teams/team_abc123" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Billing and payments", "active": true }'

# Delete (cascades the member rows with it)
curl -X DELETE "https://api.orbit.devotel.io/api/v1/teams/team_abc123" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

Create returns `201` with the new team; the shape is `{ id, name, description, active, created_at, updated_at }`. Team names are unique per organization — a duplicate name returns `409 CONFLICT`. Each organization is capped at 200 teams; over the cap, creation returns `409 LIMIT_EXCEEDED`. Deleting a team returns `204` and removes its member rows with it; a routing rule that still targets the deleted team falls back to the safe behavior in Section 5, so re-point or delete the rule before you delete the team.

## 3. Manage membership

Members link an organization user id to the team. The user must already belong to your org (invite from **Settings → Team** first). Adding the same user twice returns `409 CONFLICT`; removing returns `204`.

```bash cURL theme={null}
# Add a member
curl -X POST "https://api.orbit.devotel.io/api/v1/teams/team_abc123/members" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "user_id": "user_2bFzq9W" }'

# List members (keyset pagination)
curl "https://api.orbit.devotel.io/api/v1/teams/team_abc123/members?limit=25" \
  -H "X-API-Key: dv_live_sk_your_key_here"

# Remove a member
curl -X DELETE "https://api.orbit.devotel.io/api/v1/teams/team_abc123/members/user_2bFzq9W" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

Member listing is keyset-paginated over `(created_at, id)` — read the opaque `cursor` from the response's pagination metadata to fetch the next page, up to 100 rows per page. The response also carries a `total` count that stops at 1,000 and flags `total_capped: true` at the ceiling (render "1,000+"). Each member row carries `last_assigned_at`, the timestamp the routing picker uses for round-robin fairness (Section 4). The roster is additive across teams — removing a user from one team does not affect their membership in any other.

## 4. Wire the routing rule

Rules evaluate in priority order and AND-combine their conditions; the full condition/action reference is in [Omnichannel queue routing](/guides/omnichannel-queue-routing). From the inbox routing-rules builder or the API, create a rule whose action is `assign_team` with the team as its target:

```bash cURL theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/inbox/routing-rules" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Billing emails → Billing support team",
    "priority": 10,
    "conditions": [
      { "kind": "channel", "value": "email" },
      { "kind": "contact_attribute", "key": "topic", "operator": "equals", "value": "billing" }
    ],
    "actions": [
      { "kind": "assign_team", "target_id": "team_abc123" }
    ]
  }'
```

When the rule fires, the platform picks one **active** member of the team by a load-balanced ordering:

1. fewest currently-open conversations assigned to them (open-count ascending),
2. tie-break on the last-assigned time, oldest first (a member never picked yet wins), then
3. user id, for a deterministic order on perfect ties.

The conversation is then assigned to that picked user — a real user id, so the inbox assignee, filters, and reporting all render it as a person, exactly like a manual assignment — and the member's `last_assigned_at` bumps so the next assignment favors the rest of the roster. If your organization configured per-channel concurrency caps (Settings → Team → Channel concurrency caps), members already at their ceiling on the conversation's channel are skipped; if every active member is at cap, the conversation is left unassigned rather than overshooting. The `assign_team` action only considers teams whose `active` flag is on — a rule pointing at a deactivated team falls through to the safe behavior in Section 5.

## 5. Empty-team behavior

If the rule targets a team with **no active members** — the team was deactivated, every member was removed, or the team no longer exists — the action refuses to mis-assign. The conversation is left unassigned and a system-authored internal note is posted on its timeline saying the team-based assignment could not resolve. Fix the root cause and the next inbound item routes normally: reactivate the team, staff it, or re-point the rule at a team that has members.

## 6. Permissions

* **Reads** — `GET /api/v1/teams` and `GET /api/v1/teams/{id}/members` admit any member of the organization. The inbox's Assign to team picker and the Team Listen group list work for agents and supervisors, not just admins.
* **Writes** — create, update, delete, and membership changes require the `team:invite` grant. On the built-in roles that means owner or admin; on a custom role, the role must include the "Invite team members" capability — the check honours the custom role's grant set strictly, so a custom role without the grant is denied even for a built-in-looking caller.

Every create, update, and membership change is written to your audit log (`teams.created` / `teams.updated` / `teams.deleted` / `teams.member_added` / `teams.member_removed`).

## 7. Worked end-to-end example

Create a team, staff two members, and route inbound billing email to it:

```bash cURL theme={null}
# 1. Create the team — capture the returned id (e.g. team_hj3kQ9)
curl -X POST "https://api.orbit.devotel.io/api/v1/teams" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Billing support", "description": "Billing and payments queue" }'

# 2. Add members (repeat per user id)
curl -X POST "https://api.orbit.devotel.io/api/v1/teams/team_hj3kQ9/members" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "user_id": "user_2bFzq9W" }'
curl -X POST "https://api.orbit.devotel.io/api/v1/teams/team_hj3kQ9/members" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "user_id": "user_7kLmA2c" }'

# 3. Create the routing rule
curl -X POST "https://api.orbit.devotel.io/api/v1/inbox/routing-rules" \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Billing emails → Billing support team",
    "priority": 10,
    "conditions": [
      { "kind": "channel", "value": "email" },
      { "kind": "contact_attribute", "key": "topic", "operator": "equals", "value": "billing" }
    ],
    "actions": [
      { "kind": "assign_team", "target_id": "team_hj3kQ9" }
    ]
  }'

# 4. Verify the roster and pagination
curl "https://api.orbit.devotel.io/api/v1/teams/team_hj3kQ9/members?limit=25" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

From here on, an inbound email whose contact carries `topic: billing` assigns to the least-loaded active member of the Billing support team, and the round-robin marker keeps the pool spread evenly.

## Related guides

* [Roles, teams, and permissions](/concepts/roles-teams-permissions) — the endpoint and permission reference this page builds on.
* [Omnichannel queue routing](/guides/omnichannel-queue-routing) — the routing-rules engine, conditions, and actions (including `assign_team`).
* [Voice teams: Team Listen and the voice console](/guides/voice-teams) — the dashboard's Voice → Teams shell plus supervisor group sampling, which reuses this roster.
* [Inbox setup](/guides/inbox-setup) — wire the digital channels whose conversations your rules then route.
* [Audit log](/guides/audit-log) — review the team lifecycle entries this page's mutations emit.
