Tags API Overview
Endpoints for creating and managing Tags, the labels used to categorize and
filter Informer content. Tags apply across many entity types (Reports,
Datasets, Datasources, Jobs, Queries, Templates, Assistants, Libraries, Apps,
Toolkits, and Integrations). All routes are prefixed with /api.
A Tag stores id, tenant, name, color, source, and sourceId. The
color field is stored verbatim, but the product UI emits Material palette
names (red, amber, blue, orange, grey, deepOrange, ...) rather than
hex codes. Hex strings are accepted, but the examples below use the palette
names the product actually writes.
Reading Tags requires only an authenticated session within the Tenant; there is
no permission pre-block on the list, the single-Tag lookup, or the per-Tag
permissions endpoint. Creating Tags and the bulk-update endpoint require
permission.tags.create (member-plus, role 2; Superusers qualify). Editing and
deleting an individual Tag require permission.tag.edit and
permission.tag.delete respectively, both of which resolve to tag:write.
GET /api/tags
List the Tenant's Tags, with optional name search.
Authentication: Required (no permission pre-block)
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
name | string | Filter Tags by name (partial match) |
Response:
A HAL collection (the db.query handler over the Tag model) with Tag rows
embedded under _embedded["inf:tag"]. Each embedded Tag carries a permissions
map reflecting the caller's semantic permissions on that Tag. The collection
has no permission gate, so any authenticated user in the Tenant can read it.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/tags"
}
},
"start": 0,
"count": 4,
"total": 4,
"_embedded": {
"inf:tag": [
{
"_links": {
"self": {
"href": "https://informer.example.com/api/tags/00000000-0000-4000-8000-000000000001"
}
},
"permissions": {
"edit": true,
"delete": true,
"write": true,
"rename": true
},
"id": "00000000-0000-4000-8000-000000000001",
"tenant": "acme",
"name": "draft",
"color": "amber",
"source": null,
"sourceId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/tags/00000000-0000-4000-8000-000000000002"
}
},
"permissions": {
"edit": true,
"delete": true,
"write": true,
"rename": true
},
"id": "00000000-0000-4000-8000-000000000002",
"tenant": "acme",
"name": "high-priority",
"color": "orange",
"source": null,
"sourceId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
},
{
"_links": {
"self": {
"href": "https://informer.example.com/api/tags/00000000-0000-4000-8000-000000000003"
}
},
"permissions": {
"edit": true,
"delete": true,
"write": true,
"rename": true
},
"id": "00000000-0000-4000-8000-000000000003",
"tenant": "acme",
"name": "obsolete",
"color": "grey",
"source": null,
"sourceId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
},
"… 1 more items (4 total) — omitted from docs"
]
}
}
Tag properties:
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Tag id |
tenant | string | Owning Tenant slug |
name | string | Tag name |
color | string | Color, stored verbatim (Material palette name) |
source | string | null | Provenance of the Tag, when set |
sourceId | string | null | Source identifier, when set |
createdAt / updatedAt | date | Timestamps |
POST /api/tags
Create a Tag.
Authentication: permission.tags.create
Pre-blocks: permission.tags.create
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Tag name |
color | string | No | Color (Material palette name). An empty string is accepted. |
The payload accepts only name and color. It is not stripUnknown, so any
other field causes a 400.
Example Request:
{
"name": "urgent",
"color": "red"
}
Response:
Responds 201 Created with the new Tag and a Location header.
{
"name": "urgent",
"color": "red"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/tags/00000000-0000-4000-8000-000000000001"
}
},
"permissions": {
"edit": true,
"delete": true,
"write": true,
"rename": true
},
"id": "00000000-0000-4000-8000-000000000001",
"tenant": "acme",
"name": "urgent",
"color": "red",
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z",
"_altid": null,
"source": null,
"sourceId": null
}
GET /api/tags/{id}
Retrieve a single Tag.
Authentication: Required (no permission pre-block)
Pre-blocks: tag.lookup
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string (UUID) | Tag id. Must be a UUID. |
The route resolves the Tag through the tag.lookup server method, which does a
plain Tag.findById (not a read_access scope). The id must be a UUID: a
non-UUID is a 400, and a well-formed-but-missing UUID is a 404.
Response:
The Tag, with an inf:permissions link. There is no parentId field (it is
not a column on the Tag model). An inf:parent-tag link appears only for legacy
Tags that have a parentId set, which never happens on a freshly created Tag.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/tags/00000000-0000-4000-8000-000000000001"
},
"inf:permissions": {
"href": "https://informer.example.com/api/tags/00000000-0000-4000-8000-000000000001/permissions"
}
},
"permissions": {
"edit": true,
"delete": true,
"write": true,
"rename": true
},
"id": "00000000-0000-4000-8000-000000000001",
"tenant": "acme",
"name": "draft",
"color": "amber",
"source": null,
"sourceId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
PUT /api/tags/{id}
Update a Tag's name or color.
Authentication: permission.tag.edit
Pre-blocks: tag.lookup, permission.tag.edit
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string (UUID) | Tag id. Must be a UUID. |
Request Body:
| Field | Type | Description |
|---|---|---|
name | string | New Tag name |
color | string | New color (Material palette name) |
This is the db.update handler. Only the UUID path parameter is validated; the
body itself is not schema-validated, so it accepts name and color and
applies them. Reusing an existing Tag name returns 200 (it does not 409).
Example Request:
{
"name": "critical",
"color": "deepOrange"
}
Response:
Responds 200 with the re-fetched Tag.
{
"name": "critical",
"color": "deepOrange"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/tags/00000000-0000-4000-8000-000000000001"
}
},
"permissions": {
"edit": true,
"delete": true,
"write": true,
"rename": true
},
"id": "00000000-0000-4000-8000-000000000001",
"tenant": "acme",
"name": "critical",
"color": "deepOrange",
"source": null,
"sourceId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z"
}
DELETE /api/tags/{id}
Delete a Tag and remove it from every entity it was applied to.
Authentication: permission.tag.delete
Pre-blocks: tag.lookup, permission.tag.delete
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string (UUID) | Tag id. Must be a UUID. |
Response:
Responds 200 with an empty body (Informer's db.remove convention), not
204. Deleting a well-formed-but-missing UUID still returns 200.
Deleting a Tag cascades to its entity associations (onDelete: cascade),
removing the Tag from every entity it was applied to.
GET /api/tags/{id}/permissions
Get the caller's semantic permissions on a Tag.
Authentication: Required (no permission pre-block)
Pre-blocks: tag.lookup
Path Parameters:
| Parameter | Type | Description |
|---|---|---|
id | string (UUID) | Tag id. Must be a UUID. |
Response:
A map of the requesting user's semantic permissions on the Tag
(edit, delete, write, rename). The booleans reflect the requesting user;
a Superuser sees all of them as true.
{
"_links": {
"self": {
"href": "https://informer.example.com/api/tags/00000000-0000-4000-8000-000000000001/permissions"
}
},
"edit": true,
"delete": true,
"write": true,
"rename": true
}
POST /api/tags-bulk
Add, update, and delete Tags in a single request.
Authentication: permission.tags.create
Pre-blocks: permission.tags.create
The registered path is /api/tags-bulk, not /api/tags/_bulk-update.
Request Body:
| Field | Type | Description |
|---|---|---|
add | array | Tags to create (default []) |
remove | array | Tag UUIDs to delete (default []) |
update | array | Tags to update (default []) |
The payload is stripUnknown.
add array items:
| Field | Type | Description |
|---|---|---|
name | string | Tag name |
color | string | Color (Material palette name) |
An add item validates only name and color. A client-supplied id on an
add item is dropped, because the server assigns the UUID.
update array items:
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Tag id |
name | string | New Tag name |
color | string | New color (Material palette name) |
Example Request:
{
"add": [
{ "name": "archived", "color": "grey" }
],
"remove": [
"00000000-0000-4000-8000-000000000001"
],
"update": [
{ "id": "00000000-0000-4000-8000-000000000002", "name": "critical", "color": "red" }
]
}
Response:
Responds 200 with an empty body (there is no success object). Processing order
is add, then remove, then update.
{
"add": [
{
"name": "archived",
"color": "grey"
}
],
"remove": [
"00000000-0000-4000-8000-000000000001"
],
"update": [
{
"id": "00000000-0000-4000-8000-000000000002",
"name": "critical",
"color": "red"
}
]
}
POST /api/tags-bulk-assign
Add and remove Tags across multiple entities at once.
Authentication: Required (no top-level permission gate)
The registered path is /api/tags-bulk-assign, not /api/tags/_bulk-assign.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
entities | array of UUID | Yes | Entity ids to tag (a single value is coerced to an array) |
addTags | array of UUID | No | Tag ids to add |
removeTags | array of UUID | No | Tag ids to remove |
This route has no top-level permission gate. Instead it prunes the supplied
entities by each entity's {type}:assignTags permission: entities the caller
cannot tag are silently skipped. It then removes removeTags and adds addTags
on the remaining entities. Supported entity types are Reports, Datasets,
Datasources, Jobs, Queries, Templates, Assistants, Libraries, Apps, Toolkits,
and Integrations.
Example Request:
{
"entities": [
"00000000-0000-4000-8000-000000000001"
],
"addTags": [
"00000000-0000-4000-8000-000000000002"
]
}
Response:
Responds 200 with an empty body, regardless of how many entities matched.
{
"entities": [
"00000000-0000-4000-8000-000000000001"
],
"addTags": [
"00000000-0000-4000-8000-000000000002"
]
}