
# Team API

Your team is everyone who works inside your account besides you — admins, agents and read-only viewers — plus the invitations you have sent and the departments you organise them into. The Team API is the programmatic version of **Settings → Team**: add and remove people, set what each of them can see and do, send and chase invitations, and manage departments.

All endpoints below are relative to the base URL `https://api.youraiconnector.com/v1`. For the dashboard version of everything on this page, see [Team Management](../settings/team-management.md).

---

## Authentication: these endpoints need a signed-in person

**This is the one part of the API that an API key cannot use.** Every `/team` endpoint except the [department](#departments) ones has to be called with a **Firebase ID token** from a signed-in session:

```
Authorization: Bearer <Firebase ID token>
```

Send an API key instead and the request is rejected with a `401`:

```json
{
  "success": false,
  "error_code": 401,
  "error": "This endpoint requires a Firebase ID token (Authorization: Bearer <token>)."
}
```

The reason is that these endpoints decide what to do based on **who is signed in**: your role, the ceiling on what you are allowed to grant someone else, and whether you are currently working inside another account. An API key is an integration, not a person, so there is nobody for those rules to apply to.

In practice that means the Team API is for a first-party app with a logged-in <span data-t="appName">Your AI Connector</span> user (see [Authentication → Firebase ID token](authentication.md#4-firebase-id-token-first-party-only)). A server-to-server integration cannot manage team members — there is no way to mint one of these tokens from outside the app.

> **The exception:** the four [department](#departments) endpoints are ordinary API endpoints. They accept your API key exactly like the rest of the API, as well as a signed-in session.

Every response on this page follows the usual envelope: `success: true` plus the endpoint's fields at the top level, or `success: false` with `error` and `error_code` when something goes wrong.

---

## Roles and permissions

Every team member has one **role**, which sets their default access across 12 areas of the app. You can then override individual areas.

| Role | Value | Summary |
|---|---|---|
| Admin | `admin` | Everything except the owner's billing-level actions. |
| Editor | `editor` | Can create and change things. Shown as **Agent** in the app. |
| Viewer | `viewer` | Read-only. |

Each area is set to one of four levels: `none` (hidden), `view` (read-only), `edit` (create and change), `full` (including deleting).

| Area | Admin | Editor | Viewer |
|---|---|---|---|
| `campaigns` | full | edit | view |
| `contacts` | full | edit | view |
| `messages` | full | edit | view |
| `appointments` | full | edit | view |
| `settings` | edit | view | none |
| `billing` | edit | none | none |
| `team_management` | edit | none | none |
| `analytics` | full | view | view |
| `phone_numbers` | edit | none | none |
| `integrations` | edit | none | none |
| `faqs` | full | edit | view |
| `daily_summaries` | full | view | view |

To depart from the role's defaults, send `permission_overrides` — an array of `{ "area": ..., "level": ... }` objects. Each entry replaces the role's default for that one area; everything you do not list keeps the role default.

```json
"permission_overrides": [
  { "area": "analytics", "level": "full" },
  { "area": "billing", "level": "none" }
]
```

**Who can call these endpoints**

- The **account owner** can always do everything.
- A team member needs `team_management` at `view` to read the roster and the invite list, and at `edit` to add, change, suspend, remove, invite, cancel or resend. Admins have `edit` by default; editors and viewers have `none`, so by default only admins can manage the team.
- **Nobody can grant access above their own.** If you try to give someone a level you do not hold yourself — or to edit, suspend or remove someone whose access is already broader than yours — the request is refused with `403` and a message naming the area.

---

## The team member object

`GET /team/members` returns one of these per member:

| Field | Type | Description |
|---|---|---|
| `member_uid` | string | The member's own user ID. This is the `{memberUid}` in the paths below. |
| `account_owner_uid` | string | The account they are a member of. |
| `member_email` | string | Their email address. |
| `member_display_name` | string | The name shown for them in the app. |
| `role` | string | `admin`, `editor` or `viewer`. |
| `permission_overrides` | array | Their per-area exceptions. `[]` when they are purely on role defaults. |
| `status` | string | `active` or `suspended`. |
| `auto_assign_enabled` | boolean \| null | Whether new contacts can be auto-assigned to them. `null` means never changed, which behaves as `true`. |
| `created_by` | string | Who added them. |
| `created_at` | string \| null | ISO 8601 timestamp. |
| `updated_at` | string \| null | ISO 8601 timestamp. |

Removed members are not returned — the list is active and suspended members only.

> **Visibility limits are write-only here.** `contact_scope`, `contact_scope_axes` and `sub_account_access` (see [Limiting what a member can see](#limiting-what-a-member-can-see)) can be set on create, update and invite, but this endpoint does not return them.

---

## List team members

`GET /team/members`

Returns the roster plus your plan's seat counts, so you can show "3 of 5 seats" and know when inviting is about to be refused.

**cURL**

```bash
curl "https://api.youraiconnector.com/v1/team/members" \
  -H "Authorization: Bearer FIREBASE_ID_TOKEN"
```

**JavaScript**

```javascript
const res = await fetch("https://api.youraiconnector.com/v1/team/members", {
  headers: { Authorization: `Bearer ${idToken}` },
});
const { members, seat_limit, seats_used } = await res.json();
```

**Python**

```python
import requests

res = requests.get(
    "https://api.youraiconnector.com/v1/team/members",
    headers={"Authorization": f"Bearer {id_token}"},
)
data = res.json()
```

**Response**

```json
{
  "success": true,
  "members": [
    {
      "account_owner_uid": "owner_uid_123",
      "member_uid": "uid_alice",
      "member_email": "alice@example.com",
      "member_display_name": "Alice Chen",
      "role": "admin",
      "permission_overrides": [],
      "status": "active",
      "auto_assign_enabled": true,
      "created_by": "owner_uid_123",
      "created_at": "2026-05-01T10:00:00.000Z",
      "updated_at": "2026-06-02T09:15:00.000Z"
    }
  ],
  "seat_limit": 5,
  "seats_used": 3
}
```

`seat_limit` is `null` when your plan has no seat cap. `seats_used` counts **active** members only — suspending or removing someone frees their seat immediately.

---

## Add a team member directly

`POST /team/members`

Puts someone on your team straight away, without an invitation.

> **This sends no email.** Nobody is told they were added, and if they did not already have a <span data-t="appName">Your AI Connector</span> login, the account created for them has **no password**, so they cannot sign in until they reset it. Use [Send an invitation](#send-an-invitation) unless you have your own way of telling the person and getting them signed in.

**Request fields**

| Field | Required | Description |
|---|---|---|
| `email` | Yes | The teammate's email address. |
| `display_name` | Yes | The name shown for them in the app. |
| `role` | Yes | `admin`, `editor` or `viewer`. |
| `permission_overrides` | No | Per-area exceptions to the role's defaults. |
| `contact_scope` | No | `all` or `assigned` — see [Limiting what a member can see](#limiting-what-a-member-can-see). |
| `contact_scope_unassigned` | No | With `assigned`, also let them see contacts nobody owns yet. |
| `contact_scope_axes` | No | Limit them to named agents, channels or departments. |
| `sub_account_access` | No | Agencies only — which client sub-accounts they may open. |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/team/members" \
  -H "Authorization: Bearer FIREBASE_ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "sam@example.com",
    "display_name": "Sam Rivera",
    "role": "editor"
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.youraiconnector.com/v1/team/members", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${idToken}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    email: "sam@example.com",
    display_name: "Sam Rivera",
    role: "editor",
  }),
});
const { member_uid } = await res.json();
```

**Response** — `201 Created`

```json
{
  "success": true,
  "team_member_id": "owner_uid_123_uid_sam",
  "member_uid": "uid_sam",
  "message": "Team member created successfully."
}
```

| Status | When |
|---|---|
| `400` | `email`, `display_name` or `role` is missing, the role is not one of the three, or you tried to add yourself. |
| `403` | You do not have permission to manage the team, or you tried to grant access above your own. |
| `409` | That person is already an active member of your team. |
| `429` | Your plan's team seats are full. |

Adding someone who was previously **suspended or removed** reinstates them rather than failing.

---

## Update a team member

`PATCH /team/members/{memberUid}`

Changes a member's role, permissions, visibility, client access, or whether they take part in automatic contact assignment. Send only the fields you want to change; anything you leave out keeps its current value.

**Request fields**

| Field | Description |
|---|---|
| `role` | `admin`, `editor` or `viewer`. |
| `permission_overrides` | Replaces their whole override list. Send `[]` to put them back on pure role defaults. |
| `status` | Only `active` is accepted, to bring a suspended member back. To suspend someone, use the [suspend endpoint](#suspend-a-team-member). |
| `auto_assign_enabled` | `true` or `false`. |
| `contact_scope` | `all` or `assigned`. |
| `contact_scope_unassigned` | `true` or `false`. |
| `contact_scope_axes` | See [Limiting what a member can see](#limiting-what-a-member-can-see). |
| `sub_account_access` | Agencies only. |

> **This is the only endpoint where `null` means "clear".** Sending `"contact_scope": null`, `"contact_scope_axes": null` or `"sub_account_access": null` removes that limit entirely and puts the member back to seeing everything. On create and invite, `null` simply means "not supplied".

**cURL**

```bash
curl -X PATCH "https://api.youraiconnector.com/v1/team/members/uid_sam" \
  -H "Authorization: Bearer FIREBASE_ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "role": "admin",
    "permission_overrides": [{ "area": "billing", "level": "none" }]
  }'
```

**Response**

```json
{
  "success": true,
  "message": "Team member updated successfully."
}
```

| Status | When |
|---|---|
| `400` | An invalid `status` or `auto_assign_enabled` value, or you tried to reactivate a member who was removed (removed members must be re-invited). |
| `403` | You do not have permission, or the change would edit or create access broader than your own. |
| `404` | No such team member. |

---

## Suspend a team member

`POST /team/members/{memberUid}/suspend`

Suspends someone: they keep their place on the team but lose access. Use this instead of removing when the pause is temporary — bring them back with `PATCH /team/members/{memberUid}` and `{"status": "active"}`.

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/team/members/uid_sam/suspend" \
  -H "Authorization: Bearer FIREBASE_ID_TOKEN"
```

**Response**

```json
{
  "success": true,
  "message": "Team member suspended successfully."
}
```

A suspended member **frees their seat**, so you can invite someone else in their place. Their access ends when their current session token next refreshes, which can take up to an hour — remove them instead if you need it to be immediate.

| Status | When |
|---|---|
| `400` | You tried to suspend the account owner, or a member who is already suspended or removed. |
| `403` | Their access is broader than yours. |
| `404` | No such team member. |

---

## Remove a team member

`DELETE /team/members/{memberUid}`

Removes someone from your team and frees their seat. They are signed out and lose access to your account; their own login is untouched.

**cURL**

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/team/members/uid_sam" \
  -H "Authorization: Bearer FIREBASE_ID_TOKEN"
```

**Response**

```json
{
  "success": true,
  "message": "Team member removed successfully."
}
```

Removal is permanent from your side: a removed member **cannot be reactivated** with the update endpoint — invite them again if you change your mind. Their email is also taken off your account's notification list.

| Status | When |
|---|---|
| `400` | You tried to remove the account owner. |
| `403` | Their access is broader than yours. |
| `404` | No such team member. |

---

## Limiting what a member can see

Three optional fields, accepted on [add](#add-a-team-member-directly), [update](#update-a-team-member) and [invite](#send-an-invitation), decide how much of the account a person sees. They stack: a member limited on more than one is limited by all of them.

**`contact_scope`** — `all` (the default: every contact and conversation) or `assigned` (only the ones assigned to them). With `assigned`, add `"contact_scope_unassigned": true` to also let them see contacts nobody owns yet.

**`contact_scope_axes`** — limits them to named agents, channels or departments:

| Field | Type | Description |
|---|---|---|
| `agents` | string[] | Agent IDs. They only see chats routed to one of these agents. Max 200. |
| `channels` | string[] | Channel names — `whatsapp`, `whatsapp_web`, `sms`, `instagram`, `instagram_private`, `messenger`, `facebook`, `chat_widget`, `telegram`, `line`, `viber`, `tiktok`, `imessage`, `email`, `linkedin`, `skool`, `custom`, `custom_channel`. Max 200. |
| `departments` | string[] | Department IDs (see [Departments](#departments)). They only see leads filed under them. Max 200. |
| `include_unrouted` | boolean | With `agents` set, also show chats that no agent handles. Off by default. Ignored when `agents` is empty. |
| `include_undepartmented` | boolean | With `departments` set, also show chats that are in no department. Off by default. Ignored when `departments` is empty. |

Agent and department IDs are not checked when you save them — an ID that does not exist simply matches nothing, which shows up as an empty inbox rather than an error. Channel names **are** checked: an unrecognised one is rejected with `400`.


None of these three can be set on the account owner — that request is refused with `400`.

---

## List invitations

`GET /team/invites`

The invitations you have sent, newest first, so you can see who has not accepted yet.

**Query parameters**

| Parameter | Required | Description |
|---|---|---|
| `status` | No | Only return invites in this state — `pending`, `accepted`, `declined`, `cancelled` or `expired`. |

**cURL**

```bash
curl "https://api.youraiconnector.com/v1/team/invites?status=pending" \
  -H "Authorization: Bearer FIREBASE_ID_TOKEN"
```

**Response**

```json
{
  "success": true,
  "invites": [
    {
      "id": "inv_abc123",
      "account_owner_uid": "owner_uid_123",
      "account_owner_display_name": "Acme Ltd",
      "invitee_email": "sam@example.com",
      "invitee_uid": null,
      "role": "editor",
      "permission_overrides": [],
      "status": "pending",
      "created_by": "owner_uid_123",
      "created_at": "2026-06-10T12:00:00.000Z",
      "expires_at": "2026-06-17T12:00:00.000Z",
      "responded_at": null
    }
  ]
}
```

The invitation token is never returned — it only ever exists in the email that was sent.

---

## Send an invitation

`POST /team/invites`

Emails someone an invitation to join your team. This is the usual way to add a teammate: they click the link, sign in as themselves, and accept. If they do not have a <span data-t="appName">Your AI Connector</span> account yet, one is created for them and the email walks them through setting a password.

**Request fields**

| Field | Required | Description |
|---|---|---|
| `email` | Yes | Where to send the invitation. |
| `role` | Yes | `admin`, `editor` or `viewer`. |
| `permission_overrides` | No | Per-area exceptions, applied the moment they accept. |
| `contact_scope` | No | Applied when they accept. |
| `contact_scope_unassigned` | No | Applied when they accept. |
| `contact_scope_axes` | No | Applied when they accept. |
| `sub_account_access` | No | Agencies only. Applied when they accept. |

Setting the permissions up front means you do not have to edit the member afterwards — everything is copied onto their membership when they accept.

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/team/invites" \
  -H "Authorization: Bearer FIREBASE_ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "email": "sam@example.com", "role": "editor" }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.youraiconnector.com/v1/team/invites", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${idToken}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ email: "sam@example.com", role: "editor" }),
});
const { invite_id } = await res.json();
```

**Response** — `201 Created`

```json
{
  "success": true,
  "invite_id": "inv_abc123",
  "message": "Team invite sent successfully."
}
```

**Things to plan for**

- **Invitations expire after 7 days.** An expired one can be resent, which starts a fresh 7 days.
- **Pending invitations hold a seat.** Unlike adding a member directly, the seat check here counts active members *plus* pending invitations, so an account with all seats spoken for is refused before the email goes out.
- **20 invitations per day**, counted per account across both sending and resending.

| Status | When |
|---|---|
| `400` | `email` is missing or the role is invalid. |
| `403` | You do not have permission to manage the team, or you tried to grant access above your own. |
| `409` | A pending invitation for that email already exists, or that person is already on your team. |
| `429` | Your plan's team seats are full, or you have hit the 20-invitations-a-day limit. The `error` message says which. |

---

## Cancel an invitation

`DELETE /team/invites/{inviteId}`

Withdraws an invitation before it is accepted. The link in the email stops working.

**cURL**

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/team/invites/inv_abc123" \
  -H "Authorization: Bearer FIREBASE_ID_TOKEN"
```

**Response**

```json
{
  "success": true,
  "message": "Team invite cancelled."
}
```

Both `pending` and `expired` invitations can be cancelled. An invitation that was already accepted, declined or cancelled returns `400`; one that is not yours returns `403`; an unknown ID returns `404`.

---

## Resend an invitation

`POST /team/invites/{inviteId}/resend`

Sends the invitation email again — for when it was missed or went to spam. Works on `pending` and `expired` invitations, and resets the expiry to 7 days from now.

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/team/invites/inv_abc123/resend" \
  -H "Authorization: Bearer FIREBASE_ID_TOKEN"
```

**Response**

```json
{
  "success": true,
  "message": "Team invite resent successfully."
}
```

The new email carries a new link, and **the old link keeps working too**, so a person who finds the first email later is not stuck. Resending counts against the same 20-a-day limit as sending, and reviving an *expired* invitation re-checks your seats — a full plan is refused with `429`.

---

## Accept an invitation

`POST /team/invites/accept`

Accepts an invitation with the token from the invitation email, joining the signed-in person to that account's team.

> **This is an act of your own identity.** Sign in as yourself — it is deliberately refused with `403` while you are working inside somebody else's account.

**Request fields**

| Field | Required | Description |
|---|---|---|
| `invite_token` | Yes | The token from the invitation email link. |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/team/invites/accept" \
  -H "Authorization: Bearer FIREBASE_ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "invite_token": "1f4c…" }'
```

**Response**

```json
{
  "success": true,
  "team_member_id": "owner_uid_123_uid_sam",
  "account_owner_uid": "owner_uid_123",
  "message": "Team invite accepted successfully."
}
```

| Status | When |
|---|---|
| `400` | `invite_token` is missing, or the invitation is for your own account. |
| `403` | The session is working inside another account, or the invitation was sent to a different email address than the one you are signed in with. |
| `404` | The invitation does not exist or has already been used. |
| `429` | The account's seats filled up between the invitation and your acceptance. |
| `504` | The invitation has expired. Ask the sender to resend it. |

---

## Decline an invitation

`POST /team/invites/decline`

Declines an invitation with the token from the email. Like accepting, this is an act of your own identity and is refused while you are working inside another account.

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/team/invites/decline" \
  -H "Authorization: Bearer FIREBASE_ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "invite_token": "1f4c…" }'
```

**Response**

```json
{
  "success": true,
  "message": "Team invite declined."
}
```

---

## Departments

A **department** is a named group of your team — Sales, Customer support, HR. It gives a lead an owner team, can claim new conversations on its own, and can be used to limit what a member sees.

> **These four endpoints take an API key.** Unlike the rest of this page, they authenticate like every other endpoint in the API (see [Authentication](authentication.md)). A signed-in session works too: reading needs `contacts` at `view`, and creating, changing or deleting needs `team_management` at `edit`.

**The department object**

| Field | Type | Description |
|---|---|---|
| `id` | string | The department's ID. Use it in `contact_scope_axes.departments` and in the paths below. |
| `name` | string | What the team is called. Up to 60 characters, unique on the account. |
| `color` | string \| null | Accent colour as `#rrggbb`, or `null`. |
| `member_uids` | string[] | The team members in this department. May include the account owner. |
| `auto_assign_enabled` | boolean | Whether a lead filed under this department is also handed to someone on it. `false` means the department works from a shared queue. |
| `routing_agents` | string[] | New conversations handled by these AI Agents are filed under this department automatically. Empty means no agent rule. |
| `routing_channels` | string[] | New conversations on these channels are filed here automatically. Empty means no channel rule. |
| `created_by` | string \| null | Who created it. |

When both `routing_agents` and `routing_channels` are set, a conversation has to match **both** to be filed here — that is how you give one team "the support agent, but only on WhatsApp".

An account can have up to **50** departments.

### List departments

`GET /team/departments`

```bash
curl "https://api.youraiconnector.com/v1/team/departments?apiKey=YOUR_API_KEY"
```

**Response**

```json
{
  "success": true,
  "departments": [
    {
      "id": "dep_abc123",
      "name": "Sales",
      "color": "#2f6fed",
      "member_uids": ["uid_alice", "uid_bob"],
      "auto_assign_enabled": true,
      "routing_agents": [],
      "routing_channels": ["whatsapp"],
      "created_by": "owner_uid_123"
    }
  ]
}
```

### Create a department

`POST /team/departments`

**Request fields**

| Field | Required | Description |
|---|---|---|
| `name` | Yes | Up to 60 characters. Must not match an existing department. |
| `color` | No | `#rrggbb` hex, or `null`. |
| `member_uids` | No | Who is on it. Every UID must be the account owner or an **active** team member. |
| `auto_assign_enabled` | No | Defaults to `true`. |
| `routing_agents` | No | Agent IDs whose new chats land here. |
| `routing_channels` | No | Channel names whose new chats land here — same vocabulary as `contact_scope_axes.channels`. |

**cURL**

```bash
curl -X POST "https://api.youraiconnector.com/v1/team/departments?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Sales",
    "color": "#2f6fed",
    "member_uids": ["uid_alice", "uid_bob"],
    "routing_channels": ["whatsapp"]
  }'
```

**JavaScript**

```javascript
const res = await fetch("https://api.youraiconnector.com/v1/team/departments", {
  method: "POST",
  headers: {
    "X-API-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Sales",
    color: "#2f6fed",
    member_uids: ["uid_alice", "uid_bob"],
    routing_channels: ["whatsapp"],
  }),
});
const { department } = await res.json();
```

**Python**

```python
import requests

res = requests.post(
    "https://api.youraiconnector.com/v1/team/departments",
    headers={"X-API-Key": "YOUR_API_KEY"},
    json={
        "name": "Sales",
        "color": "#2f6fed",
        "member_uids": ["uid_alice", "uid_bob"],
        "routing_channels": ["whatsapp"],
    },
)
department = res.json()["department"]
```

**Response** — `201 Created`

```json
{
  "success": true,
  "department": {
    "id": "dep_abc123",
    "name": "Sales",
    "color": "#2f6fed",
    "member_uids": ["uid_alice", "uid_bob"],
    "auto_assign_enabled": true,
    "routing_agents": [],
    "routing_channels": ["whatsapp"],
    "created_by": "owner_uid_123"
  }
}
```

| Status | When |
|---|---|
| `400` | `name` is missing or too long, `color` is not `#rrggbb`, a channel name is not recognised, a listed UID is not an active member of this team, or you already have 50 departments. |
| `409` | A department with that name already exists. |

### Update a department

`PATCH /team/departments/{departmentId}`

Changes a department. Only the fields you send are changed.

```bash
curl -X PATCH "https://api.youraiconnector.com/v1/team/departments/dep_abc123?apiKey=YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "member_uids": ["uid_alice"], "auto_assign_enabled": false }'
```

**Response**

```json
{
  "success": true,
  "department": {
    "id": "dep_abc123",
    "name": "Sales",
    "color": "#2f6fed",
    "member_uids": ["uid_alice"],
    "auto_assign_enabled": false,
    "routing_agents": [],
    "routing_channels": ["whatsapp"],
    "created_by": "owner_uid_123"
  }
}
```

Sending no recognised fields returns `400`; an unknown department returns `404`; a name that clashes with another department returns `409`.

### Delete a department

`DELETE /team/departments/{departmentId}`

```bash
curl -X DELETE "https://api.youraiconnector.com/v1/team/departments/dep_abc123?apiKey=YOUR_API_KEY"
```

**Response**

```json
{
  "success": true,
  "deleted": "dep_abc123"
}
```

> **Deleting a department that someone is limited to is refused.** The `400` response names the members whose visibility is narrowed to it, so you can re-scope them first. That is deliberate: silently un-limiting them would hand them your whole customer base with nothing to show it happened.

Contacts filed under a deleted department are not rewritten — they simply stop showing a department, and the next time you file them it sticks.

---

## Check your own permissions

`GET /team/permissions`

Returns what the signed-in person is allowed to do in the account they are currently working in. Use it to hide buttons a member cannot use, instead of letting them discover the limit through an error.

**cURL**

```bash
curl "https://api.youraiconnector.com/v1/team/permissions" \
  -H "Authorization: Bearer FIREBASE_ID_TOKEN"
```

**Response — the account owner**

```json
{
  "success": true,
  "role": "owner",
  "is_team_mode": false,
  "permissions": {
    "campaigns": "full",
    "contacts": "full",
    "messages": "full",
    "appointments": "full",
    "settings": "full",
    "billing": "full",
    "team_management": "full",
    "analytics": "full",
    "phone_numbers": "full",
    "integrations": "full",
    "faqs": "full",
    "daily_summaries": "full"
  }
}
```

**Response — a team member working inside an account**

```json
{
  "success": true,
  "role": "editor",
  "is_team_mode": true,
  "permissions": { "campaigns": "edit", "billing": "none", "…": "…" },
  "member": {
    "uid": "uid_sam",
    "email": "sam@example.com",
    "display_name": "Sam Rivera",
    "account_owner_uid": "owner_uid_123"
  }
}
```

`role` is `owner` when the signed-in person is the account owner; otherwise it is their team role. `member` is only present in team mode, and carries `contact_scope`, `contact_scope_unassigned` and `contact_scope_axes` when their membership has them.

---

## Session tokens

Five endpoints mint a one-time sign-in token for switching between accounts. They all answer the same way:

```json
{
  "success": true,
  "customToken": "eyJhbGciOi…"
}
```

The token is exchanged for a session with the Firebase client SDK. **It is not an API key and cannot be sent as one**, which is why these endpoints are only useful inside a first-party app.

| Endpoint | What it does | Body |
|---|---|---|
| `POST /team/tokens/team-member` | Lets a team member start working inside an account they belong to. | `account_owner_uid` (required) |
| `POST /team/tokens/return-from-team` | Takes them back out, to their own account. | — |
| `POST /team/tokens/assist` | Lets <span data-t="appName">Your AI Connector</span> staff open a customer's account to help. Staff only. | `customerUid` |
| `POST /team/tokens/return-to-admin` | Ends an assist session and returns staff to their own account. | — |
| `POST /team/tokens/agency-assist` | Lets an agency open one of its client sub-accounts — or, called without one, return to the agency account. | `subAccountUid` (optional) |

Each refuses with `403` when the session is not entitled to it: not a member of that account, not staff, that sub-account is not on your agency or has not been granted to you, or the session is not currently in the mode the endpoint ends.

---

## Assign a platform role

`POST /team/users/{targetUid}/role`

Sets a user's **platform** role — `User`, `Dev`, `Support` or `Agency`. This is not team membership: it is what kind of <span data-t="appName">Your AI Connector</span> account somebody has.

This endpoint is restricted to <span data-t="appName">Your AI Connector</span> staff, and the last remaining `Dev` cannot be demoted. Listed for completeness; it is not part of managing your own team.

```json
{
  "success": true,
  "targetUid": "uid_sam",
  "role": "Agency",
  "claimUpdated": true
}
```

| Status | When |
|---|---|
| `400` | `role` is missing or not one of the four, or this would remove the last `Dev`. |
| `403` | You are not staff, or the session is working inside another account. |
| `404` | No such user. |

---

## Team API errors

Team endpoints return the standard error envelope, always with `error_code` alongside the HTTP status:

```json
{
  "success": false,
  "error_code": 403,
  "error": "Cannot grant \"full\" access to \"billing\" — exceeds your own permissions."
}
```

| Status | When it happens on a team endpoint |
|---|---|
| `400` | A required field is missing or invalid, or the action is not allowed in this state (reactivating a removed member, suspending the owner, deleting a department someone is limited to). |
| `401` | You sent an API key to an endpoint that needs a signed-in person — see [Authentication](#authentication-these-endpoints-need-a-signed-in-person). |
| `403` | You do not have `team_management` permission, the change exceeds your own access, or the action is refused while working inside another account. |
| `404` | No such member, invitation, department or user. |
| `409` | Already a team member, a pending invitation already exists, or a department by that name exists. |
| `429` | Team seats are full, the 20-invitations-a-day limit is reached, or you hit the API rate limit. |
| `504` | The invitation you tried to accept has expired. |

The shared codes every endpoint can return — `429` (rate limit) and `500` — are listed with retry guidance in [Errors & Pagination](errors-and-pagination.md).

---

## Related

- [Team Management](../settings/team-management.md) — the same features in the dashboard, with screenshots.
- [Authentication](authentication.md) — how to send a Firebase ID token instead of an API key.
- [Contacts API](contacts.md) — the contacts a member's visibility limits apply to.

