Tags API
List and manage tags, tag and untag creators, campaigns, accounts and videos one at a time or in bulk, and filter lists by tag.
Tags are free-form labels on creators, campaigns, accounts and videos. The organization keeps one list of tags (the vocabulary) for each entity type, so a creator tag can only go on creators. These endpoints are the same ones the MCP tag tools use (list_tags, add_tags, apply_tags and the rest). See the Tags guide for how people use them in the app.
Tags are not an account's tracking_hashtags (hashtag tracking mode), the hashtags in a video's caption, or the tags on Knowledge entries.
Entity types: creator, campaign, account, video. Colors: gray, blue, green, orange, purple, pink, red, yellow. A color of null shows as gray.
Permissions#
| Action | Needs |
|---|---|
| Read tags, and filter lists by tag | The read scope |
| Create tags, and tag or untag anything | The write scope |
| Rename, recolor, merge and delete tags | The write scope, and the key must act as an org admin (403 insufficient_role otherwise) |
Every write accepts an Idempotency-Key header and is recorded in the API audit log.
Names#
A tag name is trimmed, and every run of spaces becomes one space. It must be 1 to 40 characters, with no commas, and it can't look like an id (a UUID). Names match case-insensitively: VIP, vip and Vip are one tag, and the tag keeps the casing it was created with.
Wherever an endpoint takes tags, each entry can be a tag's name or its id. An entry that is a UUID is treated as an id; anything else is a name. Names that have no tag yet are created on the fly when you add or set tags.
Limits#
| Limit | Value |
|---|---|
| Tags on one creator, campaign, account or video | 20 |
| Tags per entity type in one organization | 500 |
| Tags named in one request | 20 |
Items in one POST /api/v2/tags/apply | 500 |
| Tags in one list filter | 20 |
| Source tags in one merge | 20 |
Tag fields#
| Field | Type | Description |
|---|---|---|
id | uuid | Tag ID |
entity_type | creator | campaign | account | video | What the tag is for |
name | string | The name, in the casing it was created with |
color | string | null | One of the colors above; null is gray |
usage_count | number | How many items carry the tag. Removed creators and accounts and excluded videos keep their tags but aren't counted |
created_by | uuid | null | The user who created it |
created_via | ui | api | mcp | Where it was created: the app, the REST API or MCP |
created_at, updated_at | ISO 8601 |
Creators, campaigns, accounts and videos carry a shorter form, { id, name, color }, in their tags array.
GET/api/v2/tags#
List the organization's tags with their usage counts. Needs the read scope.
curl -H "Authorization: Bearer tga_<key>" \
"https://www.trackagoat.com/api/v2/tags?entity_type=creator"| Parameter | Type | Description |
|---|---|---|
entity_type | creator | campaign | account | video | Only tags for this entity type. Leave it out for all four (grouped by type, each sorted by name). |
q | string | Case-insensitive part of the name (up to 100 characters). |
{
"data": [
{
"id": "uuid",
"entity_type": "creator",
"name": "Paid",
"color": "green",
"usage_count": 8,
"created_by": "uuid",
"created_via": "ui",
"created_at": "2026-10-14T15:02:11Z",
"updated_at": "2026-10-14T15:02:11Z"
},
{
"id": "uuid",
"entity_type": "creator",
"name": "Q4 launch",
"color": null,
"usage_count": 12,
"created_by": "uuid",
"created_via": "api",
"created_at": "2026-10-14T15:04:40Z",
"updated_at": "2026-10-14T15:04:40Z"
}
],
"error": null,
"meta": { "total": 2 }
}The list isn't paginated.
POST/api/v2/tags#
Add a tag to the vocabulary without putting it on anything. Needs the write scope. You rarely need this: adding tags by name creates them.
curl -X POST \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"entity_type": "video", "name": "Sponsored", "color": "orange"}' \
https://www.trackagoat.com/api/v2/tags| Field | Type | Description |
|---|---|---|
entity_type | string | Required. |
name | string | Required. See Names. |
color | string | null | Optional. Defaults to gray. |
Returns the tag. A new tag is 201 with meta.created: true. If a tag of that type already has the name (in any casing), it is returned with 200 and meta.created: false, and its color is left unchanged (recoloring needs an org admin, with PATCH).
| HTTP | Cause |
|---|---|
| 400 | invalid_body: a bad name (empty, over 40 characters, a comma, or an id) or an unknown entity_type or color. |
| 403 | Key lacks the write scope. |
| 422 | tag_vocabulary_full: the organization already has 500 tags of this type. |
PATCH/api/v2/tags/{id}#
Rename a tag or change its color. The new name shows everywhere the tag is used. Needs the write scope and an org admin.
curl -X PATCH \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-d '{"name": "Paid creators", "color": "blue"}' \
https://www.trackagoat.com/api/v2/tags/<uuid>| Field | Type | Description |
|---|---|---|
name | string | New name. See Names. |
color | string | null | New color; null resets it to gray. |
Send at least one. Returns the updated tag.
| HTTP | Cause |
|---|---|
| 400 | invalid_body. |
| 403 | insufficient_role: the key acts as a member who isn't an org admin. |
| 404 | not_found: no tag with that id in your organization. |
| 409 | tag_name_taken: another tag of the same type already has that name. meta.existing_tag_id is its id. Merge the two instead. |
DELETE/api/v2/tags/{id}#
Delete a tag and remove it from every item that carries it. This can't be undone. Needs the write scope and an org admin.
curl -X DELETE \
-H "Authorization: Bearer tga_<key>" \
https://www.trackagoat.com/api/v2/tags/<uuid>{ "data": { "id": "uuid", "deleted": true, "removed_from": 14 }, "error": null, "meta": null }removed_from is how many items had the tag. 403 insufficient_role for a key that doesn't act as an org admin, 404 not_found when the tag isn't in your organization.
POST/api/v2/tags/merge#
Fold duplicate tags into one. Everything that carries a source tag gets the target tag (an item that already has the target keeps one), then the source tags are deleted. Needs the write scope and an org admin.
curl -X POST \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"target_tag_id": "<uuid of VIP>", "source_tag_ids": ["<uuid of vips>", "<uuid of VIP creators>"]}' \
https://www.trackagoat.com/api/v2/tags/merge| Field | Type | Description |
|---|---|---|
target_tag_id | uuid | The tag to keep. |
source_tag_ids | uuid[] | 1 to 20 tags to fold into the target and delete. |
{
"data": {
"tag": { "id": "uuid", "entity_type": "creator", "name": "VIP", "color": "purple", "usage_count": 23, "created_by": "uuid", "created_via": "ui", "created_at": "2026-10-14T15:02:11Z", "updated_at": "2026-10-14T16:30:00Z" },
"moved": 9,
"deleted_tags": 2
},
"error": null,
"meta": null
}tag is the target as listed, moved is how many items gained the target tag, and deleted_tags is how many source tags were deleted.
| HTTP | Cause |
|---|---|
| 400 | invalid_body (including a merge whose only source is the target). tag_type_mismatch: the tags are for different entity types. |
| 403 | insufficient_role. |
| 404 | not_found: the target or a source tag isn't in your organization. |
POST/api/v2/tags/apply#
Add tags to, or remove them from, up to 500 items of one entity type. Needs the write scope.
curl -X POST \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"entity_type": "creator", "entity_ids": ["<uuid1>", "<uuid2>", "<uuid3>"], "tags": ["Q4 launch", "Paid"]}' \
https://www.trackagoat.com/api/v2/tags/apply| Field | Type | Description |
|---|---|---|
entity_type | string | Required. |
entity_ids | uuid[] | 1 to 500 items of that type. |
tags | string[] | 1 to 20 tag names or ids. |
mode | add | remove | add (default) keeps each item's other tags and creates names that have no tag yet. remove takes these tags off. |
{
"data": {
"entity_type": "creator",
"entity_ids": ["uuid", "uuid"],
"not_found_ids": ["uuid"],
"tags": [
{ "id": "uuid", "name": "Q4 launch", "color": null },
{ "id": "uuid", "name": "Paid", "color": "green" }
],
"added": 3,
"removed": 0,
"created_tags": ["Q4 launch"],
"not_found_tags": []
},
"error": null,
"meta": null
}| Field | Description |
|---|---|
entity_ids | The items found in your organization. Removed creators and accounts aren't. |
not_found_ids | Requested ids that aren't items of this type in your organization. They are skipped. |
tags | The tags the request named. |
added, removed | How many item and tag pairs were added or removed. An item that already had a tag isn't counted again. |
created_tags | Names this call created. |
not_found_tags | In remove mode, names or ids that matched no tag. |
| HTTP | Cause |
|---|---|
| 400 | invalid_body. invalid_tag_name: a name breaks the rules (meta.invalid lists each input and its problem). unknown_tag_ids (add mode): an id isn't a tag of this type in your organization (meta.unknown_tag_ids). |
| 404 | not_found: none of the entity_ids are in your organization (meta.not_found_ids). |
| 422 | too_many_tags: an item would carry more than 20 tags. Nothing is written. tag_vocabulary_full: creating the new names would take the organization over 500 tags of this type. |
One item's tags#
Each creator, campaign, account and video has a tags endpoint:
/api/v2/creators/{id}/tags/api/v2/campaigns/{id}/tags/api/v2/accounts/{id}/tags/api/v2/videos/{id}/tags
All four return 404 not_found when the item isn't in your organization. Removed creators and accounts are 404, as they are for GET /api/v2/creators/{id} and GET /api/v2/accounts/{id}.
GET#
The item's tags, sorted by name. Needs the read scope.
curl -H "Authorization: Bearer tga_<key>" \
https://www.trackagoat.com/api/v2/creators/<uuid>/tags{
"data": [
{ "id": "uuid", "name": "Paid", "color": "green" },
{ "id": "uuid", "name": "Q4 launch", "color": null }
],
"error": null,
"meta": null
}POST: add tags#
Adds tags and keeps the ones the item already has. Names with no tag yet are created. Needs the write scope.
curl -X POST \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"tags": ["Sponsored"]}' \
https://www.trackagoat.com/api/v2/videos/<uuid>/tagsBody: tags, 1 to 20 names or ids.
PUT: set tags#
Replaces the item's tags with exactly this list. Tags not in the list are taken off, and an empty list clears them. Names with no tag yet are created. Needs the write scope.
curl -X PUT \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-d '{"tags": ["Q4 launch", "Paid"]}' \
https://www.trackagoat.com/api/v2/creators/<uuid>/tagsBody: tags, 0 to 20 names or ids.
DELETE: remove tags#
Takes tags off the item. The tags stay in the vocabulary. The tags go in a JSON body. Needs the write scope.
curl -X DELETE \
-H "Authorization: Bearer tga_<key>" \
-H "Content-Type: application/json" \
-d '{"tags": ["Paid"]}' \
https://www.trackagoat.com/api/v2/creators/<uuid>/tagsBody: tags, 1 to 20 names or ids.
Response#
POST, PUT and DELETE return the item's tags after the write, sorted by name, with counts in meta:
{
"data": [
{ "id": "uuid", "name": "Q4 launch", "color": null }
],
"error": null,
"meta": { "added": 0, "removed": 1, "created_tags": [] }
}meta field | Description |
|---|---|
added, removed | How many tags were put on or taken off. |
created_tags | Names this call created. |
not_found | DELETE only, when present: names or ids that matched no tag. |
| HTTP | Cause |
|---|---|
| 400 | invalid_body. invalid_tag_name (meta.invalid). unknown_tag_ids (POST and PUT): an id isn't a tag of this type in your organization (meta.unknown_tag_ids). |
| 404 | not_found. |
| 422 | too_many_tags: the item would carry more than 20 tags. tag_vocabulary_full. |
Filtering lists by tag#
These list endpoints take a tag filter and return tags ([{ id, name, color }]) on every item:
GET /api/v2/creators/{id}, GET /api/v2/accounts/{id} and GET /api/v2/videos/{id} also return tags.
| Parameter | Description |
|---|---|
tags | Comma-separated tag names (any casing) or ids, up to 20. Tag names can't contain commas, so a comma always separates two tags. |
tags_match | any (default): items with at least one of the tags. all: items with every one of them. |
curl -H "Authorization: Bearer tga_<key>" \
"https://www.trackagoat.com/api/v2/creators?tags=q4%20launch,paid&tags_match=all"A name that matches no tag is ignored with any; if none of them match, the list is empty. With all, any unknown name makes the list empty. The filter runs in the database, so limit, cursor and hasMore work as they do without it. More than 20 tags is 400 invalid_query.