# Get a team

> One team and its full membership, pending invitations included.

Canonical page: https://www.rundiffusion.com/docs/api/teams/get
Endpoint: GET /api/v2/teams/{team_id}
Authorization: OAuth device flow or Company API Access Token

---

One team in the shape [List teams](/docs/api/teams/list) returns, plus its
membership. Reading a team is how you turn a `user_id` from elsewhere in the API,
on a generation for instance, into a person you can name.

Requires the `MANAGE_TEAMS` permission on the company the team belongs to. The
path `team_id` also names that company, so there is no separate selector.

That permission comes from the caller's **company role**, which is a separate
grant from their team role. [Get identity](/docs/api/me) returns it under
`company_permissions`, not in the `permissions` map beside it. See
[Two roles, two maps](/docs/api/me#two-roles-two-maps).

`GET /api/v2/teams/{team_id}`

Authorization: OAuth device flow or Company API Access Token

## Request

**Headers**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Authorization` | string | Yes | Bearer token. This endpoint accepts an OAuth device flow token or a Company API Access Token. A Personal API Access Token is refused. See [Authentication](/docs/api/authentication). |

**Path parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `team_id` | string | Yes | The team to read, from [List teams](/docs/api/teams/list). It also names the company, so no separate selector is needed. A team in another company returns 404. |

cURL:

```bash
curl "https://api2.rundiffusion.com/api/v2/teams/2D8NEx…" \
  -H "Authorization: Bearer $RUNDIFFUSION_TOKEN"
```

JavaScript:

```javascript
const team = await fetch(
  'https://api2.rundiffusion.com/api/v2/teams/2D8NEx…',
  { headers: { Authorization: `Bearer ${process.env.RUNDIFFUSION_TOKEN}` } },
).then(r => r.json());
```

Python:

```python
team = requests.get(
    f"{BASE}/teams/{team_id}",
    headers=HEADERS,
).json()
```

## Response

`200 OK` with the team and its members.

```json
{
  "id": "2D8NEx…",
  "created": "2026-03-14T09:02:11+00:00",
  "name": "Design Studio",
  "is_active": true,
  "members": [
    {
      "user_id": "k3PqV9…",
      "email": "sam@example.com",
      "created": "2026-03-14T09:03:40+00:00",
      "is_pending": false,
      "invitation_url": null,
      "joined": "2026-03-14T09:04:52+00:00"
    },
    {
      "user_id": null,
      "email": "new_hire@example.com",
      "created": "2026-05-02T11:20:07+00:00",
      "is_pending": true,
      "invitation_url": "https://app.rundiffusion.com/team-signup?teamName=Design%20Studio&teamId=2D8NEx…&inviteCode=8f14e45f…",
      "joined": null
    }
  ]
}
```

The team's own fields are documented on
[List teams](/docs/api/teams/list#team-object), so a client that parses the
listing parses this for free.

### Team member

| Name | Type | Description |
| --- | --- | --- |
| `user_id` | string \| null | The member's user ID. Always null while is_pending is true, since there is no user until someone accepts. |
| `email` | string | The email address the membership belongs to. |
| `created` | string (ISO 8601) | When the invitation was created, in UTC. Present whether or not it has been accepted, so it is the timestamp a pending member has, and what you sort or chase stale invitations by. |
| `is_pending` | boolean | Whether the invitation is still outstanding (not accepted yet). |
| `invitation_url` | string \| null | The link to send an invitee, or null once they have joined. It is the same link the dashboard copy button produces, so a link taken from either place is the same link. It carries a secret that grants membership of the team, so treat it as one. |
| `joined` | string (ISO 8601) \| null | When the member accepted, in UTC, or null while the invitation is still pending. |

Members are ordered by email, and the list includes people who have been invited
but have not accepted. Branch on `is_pending`, which says exactly that.

`created` is there either way, so a pending member is not a row with no
timestamp: it is how you find invitations that have been sitting unanswered, and
paired with `joined` it tells you how long one took to accept.

> **An invitation link is a credential**
>
> `invitation_url` carries a secret that lets whoever opens it join the team as
> that invitee. Send it to the person it was made for and nowhere else: not into
> a shared channel, a ticket, or a log.
>
> It is returned only while `is_pending` is true. Once someone joins it is
> `null`, so this response never carries a live invite for a member who no longer
> needs one.

## Errors

**Status codes**

| Name | Type | Description |
| --- | --- | --- |
| `401` | UNAUTHENTICATED | Missing, malformed, or expired token. Refresh it and retry. |
| `403` | PERMISSION_DENIED | The caller lacks MANAGE_TEAMS on the company, or presented a Personal API Access Token. |
| `404` | TEAM_NOT_FOUND | No such team, or it belongs to another company. The two cases are deliberately indistinguishable. |
| `429` | RATE_LIMITED | Too many requests. See [Rate limits](/docs/api/rate-limits#teams). |

A team in another company returns `404`, not `403`. Holding `MANAGE_TEAMS` on
your own company says nothing about whether some other company's team exists, and
a `403` would answer that, so both cases return the same code and message.
