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/accountsroutes (andacc_-prefixed ids) keep working until 2026-10-14 and answer withDeprecation/Sunsetheaders plus ameta.deprecationnotice. Please update to/api/v1/teams(resource typeteams, id prefixtea_) 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
{
"data": {
"type": "teams",
"attributes": {
"name": "Metropolitan Museum — Night Watch"
}
}
}
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.
curl https://platform.tagbase.io/api/v1/teams \
-X POST \
-H "Authorization: Bearer $TAGBASE_API_KEY" \
-H "Content-Type: application/vnd.api+json" \
-d '{ "data": { "type": "teams", "attributes": { "name": "Metropolitan Museum — Night Watch" } } }'
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",
},
body: JSON.stringify({
data: { type: "teams", attributes: { name: "Metropolitan Museum — Night Watch" } },
}),
});
const team = await res.json();
$response = $client->post("https://platform.tagbase.io/api/v1/teams", [
"headers" => [
"Authorization" => "Bearer " . getenv("TAGBASE_API_KEY"),
"Content-Type" => "application/vnd.api+json",
],
"json" => [
"data" => ["type" => "teams", "attributes" => ["name" => "Metropolitan Museum — Night Watch"]],
],
]);
$team = json_decode((string) $response->getBody(), true);
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"}
],
json: %{data: %{type: "teams", attributes: %{name: "Metropolitan Museum — Night Watch"}}}
).body
Response: 201 Created
The new subteam, plus its freshly minted API key as an included
api_keys resource. The key’s secret is shown here and
nowhere else. Capture it now.
{
"data": {
"type": "teams",
"id": "tea_abcdef0123456789",
"attributes": { "name": "Metropolitan Museum — Night Watch" },
"relationships": {
"api_keys": {
"data": [ { "type": "api_keys", "id": "key_abcdef0123456789" } ]
}
}
},
"included": [
{
"type": "api_keys",
"id": "key_abcdef0123456789",
"attributes": { "secret": "key_abcdef0123456789: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 writer app.
The request takes no body.
Response
{
"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.
curl https://platform.tagbase.io/api/v1/teams/tea_abcdef0123456789 \
-H "Authorization: Bearer $TAGBASE_API_KEY" \
-H "Accept: application/vnd.api+json"
const res = await fetch(
"https://platform.tagbase.io/api/v1/teams/tea_abcdef0123456789",
{
headers: {
"Authorization": `Bearer ${process.env.TAGBASE_API_KEY}`,
"Accept": "application/vnd.api+json",
},
},
);
const team = await res.json();
$response = $client->get("https://platform.tagbase.io/api/v1/teams/tea_abcdef0123456789", [
"headers" => [
"Authorization" => "Bearer " . getenv("TAGBASE_API_KEY"),
"Accept" => "application/vnd.api+json",
],
]);
$team = json_decode((string) $response->getBody(), true);
team =
Req.get!("https://platform.tagbase.io/api/v1/teams/tea_abcdef0123456789",
headers: [
{"authorization", "Bearer #{System.fetch_env!("TAGBASE_API_KEY")}"},
{"accept", "application/vnd.api+json"}
]
).body
Response: 200 OK
The team, with its active 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.
{
"data": {
"type": "teams",
"id": "tea_abcdef0123456789",
"attributes": { "name": "Metropolitan Museum — Night Watch" },
"relationships": {
"api_keys": {
"data": [ { "type": "api_keys", "id": "key_abcdef0123456789" } ]
}
}
}
}
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
idand key when you create it. - Subteams nest one level beneath the presenting team; deeper hierarchies aren’t exposed through the API.