# Teams

Create subteams and mint their API keys.


A **team** owns tags and API keys. The team that owns your key is your
**root team**; teams you create through the API are **subteams** of it,
the unit of multi-tenancy. Creating a subteam also mints its first API key.

> **Migrating from the accounts API?** Teams were previously called accounts.
> The old `/api/v1/accounts` routes (and `acc_`-prefixed ids) keep working
> until **2026-10-14** and answer with `Deprecation`/`Sunset` headers plus a
> `meta.deprecation` notice. Please update to `/api/v1/teams` (resource type
> `teams`, id prefix `tea_`) as soon as possible.

## When to use subteams

Subteams are **optional**, and for most integrations the answer is *no*: you
can do everything with your root team's key. Reach for one only when you have
a concrete separation requirement.

What a subteam isolates is **tags and the keys that read them**: a key sees only
its own team's tags, and a tag lookup or verification across teams returns
`404`. It's also the billing and quota boundary: a subteam per customer gives
each tenant its own bill.

Common reasons to create one:

- **Per-customer isolation (multi-tenant / reseller).** You serve multiple
  independent customers whose tags must stay walled off from each other: one
  subteam per customer, each with its own key.
- **Business-unit or regional separation.** One organization that needs hard
  separation between legal entities or regions.
- **Environment separation.** A dedicated subteam for test/staging tags so they
  never mix with production (useful even for a single-customer integration).

Skip them if you can't point to two parties (or environments) that must *not*
see each other's tags. A single application with one set of tags should just use
the root team directly; a subteam would only add key management for no
benefit.

**Decide before you provision at scale.** A tag is owned permanently by the team
that created it, and there's no endpoint to move it to another team later. So if
a tenant or environment will ever need its own isolated tags, create its subteam
*first* and provision under that subteam's key: you can't retrofit the split
onto tags that already exist.

## Fields

| Field  | Type   | Notes                                        |
|--------|--------|----------------------------------------------|
| `id`   | string | `tea_`-prefixed, assigned by the platform.           |
| `name` | string | Required. 1 to 100 characters.                  |

A subteam is always created beneath the team whose key you present; you
don't pass a parent: it's inferred from the key.

## Create a subteam

```
POST /api/v1/teams
```

### Request

```json
{
  "data": {
    "type": "teams",
    "attributes": {
      "name": "Zanna Therapeutics"
    }
  }
}
```

`name` is the only required attribute. The request also accepts optional profile
attributes if you want to store them on the team: `company`, `vat`,
`country`, `address`, `city`, `state`, `zip_code`.

<!-- tabs -->
```bash cURL
curl https://platform.tagbase.io/api/v1/teams \
  -X POST \
  -H "Authorization: Bearer $TAGBASE_API_KEY" \
  -H "Content-Type: application/vnd.api+json" \
  -H "Accept: application/vnd.api+json" \
  -d '{ "data": { "type": "teams", "attributes": { "name": "Zanna Therapeutics" } } }'
```
```js
const res = await fetch("https://platform.tagbase.io/api/v1/teams", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.TAGBASE_API_KEY}`,
    "Content-Type": "application/vnd.api+json",
    "Accept": "application/vnd.api+json",
  },
  body: JSON.stringify({
    data: { type: "teams", attributes: { name: "Zanna Therapeutics" } },
  }),
});
const team = await res.json();
```
```php
$response = $client->post("https://platform.tagbase.io/api/v1/teams", [
    "headers" => [
        "Authorization" => "Bearer " . getenv("TAGBASE_API_KEY"),
        "Content-Type"  => "application/vnd.api+json",
        "Accept"        => "application/vnd.api+json",
    ],
    "json" => [
        "data" => ["type" => "teams", "attributes" => ["name" => "Zanna Therapeutics"]],
    ],
]);
$team = json_decode((string) $response->getBody(), true);
```
```elixir
team =
  Req.post!("https://platform.tagbase.io/api/v1/teams",
    headers: [
      {"authorization", "Bearer #{System.fetch_env!("TAGBASE_API_KEY")}"},
      {"content-type", "application/vnd.api+json"},
      {"accept", "application/vnd.api+json"}
    ],
    json: %{data: %{type: "teams", attributes: %{name: "Zanna Therapeutics"}}}
  ).body
```
<!-- /tabs -->

### Response: `201 Created`

The new subteam, plus its freshly minted API key as an `included`
[api_keys](/docs/api/api-keys) resource. **The key's `secret` is shown here and
nowhere else.** Capture it now.

```json
{
  "data": {
    "type": "teams",
    "id": "tea_UUkZTHiaPbRKiiJ4VLfCgF",
    "attributes": { "name": "Zanna Therapeutics" },
    "relationships": {
      "api_keys": {
        "data": [ { "type": "api_keys", "id": "key_VfUttEQbh1ceW7jtwV66rF" } ]
      }
    }
  },
  "included": [
    {
      "type": "api_keys",
      "id": "key_VfUttEQbh1ceW7jtwV66rF",
      "attributes": { "secret": "key_VfUttEQbh1ceW7jtwV66rF:superstrongrandomsecret" }
    }
  ]
}
```

Store `included[].attributes.secret` immediately: it's the credential you'll use
for every request made on behalf of this subteam.

### Errors

| Status | When                                                          |
|--------|---------------------------------------------------------------|
| `400`  | The body has no `data.attributes` object.                     |
| `401`  | Missing, invalid, or revoked key.                             |
| `422`  | Validation failed (e.g. `name` is missing or out of length).  |

## Create a join link

```
POST /api/v1/teams/join-token
```

Mints a short-lived link that lets whoever opens it join the team that owns
your API key, after signing in to (or signing up for) the TAGBASE platform.
Surface it in your own app when your users need direct platform access to
the team holding their tags, for example to write tags with the mobile
[TAGBASE Writer app](/docs/guides/writer-app).

The request takes no body.

### Response

```json
{
  "meta": {
    "token": "…",
    "expires_in": 600,
    "url": "https://platform.tagbase.io/dashboard/teams/join/…"
  }
}
```

The link expires after ten minutes. It is not single-use: several people can
join through the same link while it lasts, so share it only through channels
you trust. Anyone who opens it becomes a member of the team.

## Retrieve a team

```
GET /api/v1/teams/:id
```

Fetch your own team, or a subteam you created. The key you present must own
the team (be its root team, or be the team itself). Otherwise the
platform responds `404`.

<!-- tabs -->
```bash cURL
curl https://platform.tagbase.io/api/v1/teams/tea_UUkZTHiaPbRKiiJ4VLfCgF \
  -H "Authorization: Bearer $TAGBASE_API_KEY" \
  -H "Accept: application/vnd.api+json"
```
```js
const res = await fetch(
  "https://platform.tagbase.io/api/v1/teams/tea_UUkZTHiaPbRKiiJ4VLfCgF",
  {
    headers: {
      "Authorization": `Bearer ${process.env.TAGBASE_API_KEY}`,
      "Accept": "application/vnd.api+json",
    },
  },
);
const team = await res.json();
```
```php
$response = $client->get("https://platform.tagbase.io/api/v1/teams/tea_UUkZTHiaPbRKiiJ4VLfCgF", [
    "headers" => [
        "Authorization" => "Bearer " . getenv("TAGBASE_API_KEY"),
        "Accept"        => "application/vnd.api+json",
    ],
]);
$team = json_decode((string) $response->getBody(), true);
```
```elixir
team =
  Req.get!("https://platform.tagbase.io/api/v1/teams/tea_UUkZTHiaPbRKiiJ4VLfCgF",
    headers: [
      {"authorization", "Bearer #{System.fetch_env!("TAGBASE_API_KEY")}"},
      {"accept", "application/vnd.api+json"}
    ]
  ).body
```
<!-- /tabs -->

### Response: `200 OK`

The team, with its active [api_keys](/docs/api/api-keys) as relationship
references. **Key secrets are never returned here**: a secret is shown only once,
when the key is minted. Read a key's metadata with
[`GET /api/v1/api_keys/:id`](/docs/api/api-keys).

```json
{
  "data": {
    "type": "teams",
    "id": "tea_UUkZTHiaPbRKiiJ4VLfCgF",
    "attributes": { "name": "Zanna Therapeutics" },
    "relationships": {
      "api_keys": {
        "data": [ { "type": "api_keys", "id": "key_VfUttEQbh1ceW7jtwV66rF" } ]
      }
    }
  }
}
```

### Errors

| Status | When                                                           |
|--------|----------------------------------------------------------------|
| `401`  | Missing, invalid, or revoked key.                              |
| `404`  | No such team, or it isn't owned by the team you present. |

## Notes

- A created team can be fetched by id (above); there is no endpoint to list
  teams or to update one. Record the returned `id` and key when you create it.
- Subteams nest one level beneath the presenting team; deeper hierarchies
  aren't exposed through the API.

