Skip to main content

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:

ParameterTypeDescription
namestringFilter 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.

List tagsGET /api/tags
GET /api/tags is a HAL collection (db.query handler, model Tag) with tag rows embedded under `inf:tag`. The only filter is the `name` query param (partial match). There is no permission pre-block: any authenticated user in the tenant can read it.
Response · 200
{
"_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"
]
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

Tag properties:

FieldTypeDescription
idstring (UUID)Tag id
tenantstringOwning Tenant slug
namestringTag name
colorstringColor, stored verbatim (Material palette name)
sourcestring | nullProvenance of the Tag, when set
sourceIdstring | nullSource identifier, when set
createdAt / updatedAtdateTimestamps

POST /api/tags

Create a Tag.

Authentication: permission.tags.create

Pre-blocks: permission.tags.create

Request Body:

FieldTypeRequiredDescription
namestringYesTag name
colorstringNoColor (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.

Create a tagPOST /api/tags
POST /api/tags requires permission.tags.create (isMemberPlus / role 2; admins qualify). Payload is { name (required), color? } only — unknown fields are rejected (no stripUnknown) and `color` accepts an empty string. `color` is stored verbatim; the product uses Material palette names (red, amber, blue, ...), not hex. Responds 201 with a Location header.
Request body
{
"name": "urgent",
"color": "red"
}
Response · 201
{
"_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
}
Captured from the API examples test suite; ids and timestamps are normalized.

GET /api/tags/{id}

Retrieve a single Tag.

Authentication: Required (no permission pre-block)

Pre-blocks: tag.lookup

Path Parameters:

ParameterTypeDescription
idstring (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.

Get a tagGET /api/tags/00000000-0000-4000-8000-000000000001
GET /api/tags/{id} resolves through the tag.lookup server method (plain Tag.findById, NOT a read_access scope). `{id}` MUST be a UUID — a non-uuid is a 400, a missing uuid is a 404. The response links to `inf:permissions`; it has no `parentId` field (not a column on Tag), and the `inf:parent-tag` link only appears for legacy tags with a parentId set.
Response · 200
{
"_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"
}
Captured from the API examples test suite; ids and timestamps are normalized.

PUT /api/tags/{id}

Update a Tag's name or color.

Authentication: permission.tag.edit

Pre-blocks: tag.lookup, permission.tag.edit

Path Parameters:

ParameterTypeDescription
idstring (UUID)Tag id. Must be a UUID.

Request Body:

FieldTypeDescription
namestringNew Tag name
colorstringNew 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.

Update a tagPUT /api/tags/00000000-0000-4000-8000-000000000001
PUT /api/tags/{id} requires permission.tag.edit (-> tag:write -> isMemberPlus). It is the db.update handler with NO payload validation (only the uuid param is validated), so it accepts { name?, color? } and returns 200 with the re-fetched tag. Reusing an existing tag name returns 200 (no 409).
Request body
{
"name": "critical",
"color": "deepOrange"
}
Response · 200
{
"_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"
}
Captured from the API examples test suite; ids and timestamps are normalized.

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:

ParameterTypeDescription
idstring (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.

Delete a tagDELETE /api/tags/00000000-0000-4000-8000-000000000001
DELETE /api/tags/{id} requires permission.tag.delete (-> tag:write). It is the db.remove handler and returns 200 with an EMPTY body (Informer convention), NOT 204 as the page claims. Deleting cascades to the tag’s entity associations (onDelete: cascade). Deleting a well-formed-but-missing uuid still returns 200.
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.
Cascade deletion

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:

ParameterTypeDescription
idstring (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.

Get a tag’s permissionsGET /api/tags/00000000-0000-4000-8000-000000000001/permissions
GET /api/tags/{id}/permissions resolves the tag via tag.lookup, then the `permissions` handler returns a map of the caller’s semantic permissions on that tag (edit/delete/write/rename). Booleans reflect the requesting user; a superuser sees all true.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/tags/00000000-0000-4000-8000-000000000001/permissions"
}
},
"edit": true,
"delete": true,
"write": true,
"rename": true
}
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/tags-bulk

Add, update, and delete Tags in a single request.

Authentication: permission.tags.create

Pre-blocks: permission.tags.create

Path name

The registered path is /api/tags-bulk, not /api/tags/_bulk-update.

Request Body:

FieldTypeDescription
addarrayTags to create (default [])
removearrayTag UUIDs to delete (default [])
updatearrayTags to update (default [])

The payload is stripUnknown.

add array items:

FieldTypeDescription
namestringTag name
colorstringColor (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:

FieldTypeDescription
idstring (UUID)Tag id
namestringNew Tag name
colorstringNew 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.

Bulk add, update, and delete tagsPOST /api/tags-bulk
POST /api/tags-bulk (the page calls it _bulk-update) requires permission.tags.create. Payload is { add?, remove?, update? }, each defaulting to [] and stripUnknown. `add` items validate ONLY { name?, color? } — a client-supplied `id` on an add item is dropped (the server assigns the UUID). `remove` is tag UUIDs; `update` items are { id (uuid), name?, color? }. Processing order is add, then remove, then update. Returns 200 with an EMPTY body (no success object).
Request body
{
"add": [
{
"name": "archived",
"color": "grey"
}
],
"remove": [
"00000000-0000-4000-8000-000000000001"
],
"update": [
{
"id": "00000000-0000-4000-8000-000000000002",
"name": "critical",
"color": "red"
}
]
}
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/tags-bulk-assign

Add and remove Tags across multiple entities at once.

Authentication: Required (no top-level permission gate)

Path name

The registered path is /api/tags-bulk-assign, not /api/tags/_bulk-assign.

Request Body:

FieldTypeRequiredDescription
entitiesarray of UUIDYesEntity ids to tag (a single value is coerced to an array)
addTagsarray of UUIDNoTag ids to add
removeTagsarray of UUIDNoTag 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.

Bulk assign and unassign tags across entitiesPOST /api/tags-bulk-assign
POST /api/tags-bulk-assign (the page calls it _bulk-assign) has NO top-level permission gate: it prunes the supplied entity ids by each entity’s `{type}:assignTags` permission (entities the caller cannot tag are silently skipped), then removes `removeTags` and adds `addTags`. Payload is { entities (uuid[] required), addTags? (uuid[]), removeTags? (uuid[]) }. Supported types: reports, datasets, datasources, jobs, queries, templates, assistants, libraries, apps, toolkits, integrations. Returns 200 with an EMPTY body regardless of how many entities matched.
Request body
{
"entities": [
"00000000-0000-4000-8000-000000000001"
],
"addTags": [
"00000000-0000-4000-8000-000000000002"
]
}
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.