Skip to main content

Comments & Favorites

Endpoints for commenting on Templates, toggling a Template as a favorite, and managing the caller's per-user Template settings.

These are all per-user routes: they need only an authenticated user, and the favorite and settings records are scoped to the caller (the favorite by principalId, the settings by userId). Each route resolves its Template through template.lookup(params.id) first, so an id the caller cannot see returns 404.

Comments

Users can comment on a Template for collaboration and discussion. Comments are threaded: each comment links to its replies.

GET /api/templates/{id}/comments

List the Template's top-level comments (those whose parentId is null), newest first.

Authentication: Required

Pre-blocks: template.lookup(params.id)

Query Parameters:

ParameterTypeDefaultDescription
sortstring-createdAtSort order
limitinteger-Maximum results to return

Response:

A HAL collection ({ start, count, total } with _embedded["inf:comment"]). Each comment embeds inf:user (the author) and exposes an inf:comments link to its replies (./replies). Replies are reached through that link, not this list.

List commentsGET /api/templates/00000000-0000-4000-8000-000000000001/comments
A HAL collection of top-level comments (parentId null, newest first via -createdAt), each embedding inf:user (the author) and exposing an inf:comments (./replies) link. Query params are {?sort,limit}.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/comments?sort=-createdAt&limit=0"
}
},
"start": 0,
"count": 1,
"total": 1,
"_embedded": {
"inf:comment": [
{
"_links": {
"self": {
"href": "https://informer.example.com/api/comments/00000000-0000-4000-8000-000000000002"
},
"inf:comments": {
"href": "https://informer.example.com/api/comments/00000000-0000-4000-8000-000000000002/replies"
}
},
"permissions": {
"edit": true,
"delete": true
},
"id": "00000000-0000-4000-8000-000000000002",
"message": "Great template! Could we add a chart showing trends?",
"public": false,
"userId": "admin",
"datasetId": null,
"datasourceId": null,
"reportId": null,
"queryId": null,
"teamId": null,
"jobId": null,
"templateId": "00000000-0000-4000-8000-000000000001",
"assistantId": null,
"libraryId": null,
"mentions": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"parentId": null,
"_embedded": {
"inf:user": {
"_links": {
"self": {
"href": "https://informer.example.com/api/users/admin"
},
"inf:memberships": {
"href": "https://informer.example.com/api/users/admin/memberships"
},
"inf:avatar-upload": {
"href": "https://informer.example.com/api/users/admin/_upload/{uploadId}/avatar-upload",
"templated": true
},
"inf:password": {
"href": "https://informer.example.com/api/users/admin/password"
},
"inf:feed": {
"href": "https://informer.example.com/api/users/admin/feed"
},
"inf:viewed-feed": {
"href": "https://informer.example.com/api/users/admin/_markFeedViewed"
},
"inf:superuser": {
"href": "https://informer.example.com/api/users/admin/superuser"
}
},
"permissions": {
"changeDomain": false,
"changeProfile": true,
"changePassword": true,
"delete": false,
"edit": true,
"reassign": true,
"superuser": true,
"enable": true,
"changeTheme": true,
"lock": true,
"setGlobalPermissions": true,
"manageLogin": true
},
"domain": "local",
"username": "admin",
"displayName": "acme Administrator",
"familyName": "Administrator",
"givenName": "acme",
"middleName": null,
"email": null,
"data": null,
"superuser": true,
"timezone": "America/New_York",
"settings": {
"log": {
"log": false,
"info": false,
"warn": false,
"debug": false,
"error": true,
"remote": false
}
},
"enabled": true,
"source": null,
"sourceId": null,
"lastViewedFeed": null,
"tenant": "acme",
"globalPermissions": {
"ai": false,
"jobCreation": true,
"viewAllTeams": true,
"viewAllUsers": true,
"painlessScriptCreation": true
},
"userFields": {},
"locked": false,
"lockedAt": null,
"loginAttempts": 0,
"passwordSetAt": "2026-01-15T16:20:00.000Z",
"passwordExpiresAt": "2026-01-15T16:20:00.000Z",
"forgotPasswordRequestTime": null,
"mfa": null,
"mfaConfig": {},
"aiMessage": null,
"aiConsent": false,
"hasSharedDatasources": false,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"initials": "NA",
"colorIndex": 4,
"avatarColor": {
"background": "#81C784",
"text": "#1B5E20"
},
"managesPassword": true
}
}
}
]
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

POST /api/templates/{id}/comments

Add a comment to a Template.

Authentication: Required

Pre-blocks: template.lookup(params.id)

Request Body:

The payload is stripUnknown, so message is the only field that is kept; any other field is dropped.

FieldTypeRequiredDescription
messagestringNoComment text

message is not required by the schema (a comment with no message is allowed).

Example Request:

{
"message": "Great template! Could we add a chart showing trends?"
}

Response:

Responds 201 Created with the new comment and a Location header. Creating a comment also records a templateAddedComment activity. Runs in a database transaction.

Add a commentPOST /api/templates/00000000-0000-4000-8000-000000000001/comments
The body field is `message` (the only accepted field; stripUnknown drops the rest). Creates the comment plus a templateAddedComment activity, then responds 201 with the comment and a Location header. Runs in a DB transaction.
Request body
{
"message": "Great template! Could we add a chart showing trends?"
}
Response · 201
{
"_links": {
"self": {
"href": "https://informer.example.com/api/comments/00000000-0000-4000-8000-000000000002"
}
},
"permissions": {
"edit": true,
"delete": true
},
"id": "00000000-0000-4000-8000-000000000002",
"public": false,
"tenant": "acme",
"message": "Great template! Could we add a chart showing trends?",
"templateId": "00000000-0000-4000-8000-000000000001",
"userId": "admin",
"updatedAt": "2026-01-15T16:20:00.000Z",
"createdAt": "2026-01-15T16:20:00.000Z",
"mentions": null,
"teamId": null,
"_altid": null,
"datasetId": null,
"datasourceId": null,
"reportId": null,
"queryId": null,
"jobId": null,
"parentId": null,
"assistantId": null,
"libraryId": null
}
Captured from the API examples test suite; ids and timestamps are normalized.

Favorites

Users can mark a Template as a favorite for quick access. The favorite is a presence toggle scoped to the caller (principalId is the caller's username).

GET /api/templates/{id}/favorite

Check whether the caller has favorited this Template.

Authentication: Required

Pre-blocks: template.lookup(params.id)

Response:

Responds 200 with the favorite record when the caller has favorited the Template. When the caller has not, the route answers 404. There is no { favorited: false } body.

Check whether a template is favoritedGET /api/templates/00000000-0000-4000-8000-000000000001/favorite
Returns the favorite record (200) when favorited by the caller, or 404 when it is not. There is no { favorited: false } body.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/favorite"
}
},
"permissions": {},
"id": "00000000-0000-4000-8000-000000000002",
"principalId": "admin",
"reportId": null,
"datasetId": null,
"queryId": null,
"datasourceId": null,
"jobId": null,
"assistantId": null,
"templateId": "00000000-0000-4000-8000-000000000001",
"libraryId": null,
"appId": null,
"integrationId": null,
"toolkitId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme"
}
Captured from the API examples test suite; ids and timestamps are normalized.

PUT /api/templates/{id}/favorite

Add the Template to the caller's favorites.

Authentication: Required

Pre-blocks: template.lookup(params.id)

Response:

Responds 200 with the favorite record (the route upserts and returns the row; it does not use .created(), so the status is 200, not 201). The toggle is idempotent: favoriting an already-favorited Template returns the same record.

Favorite a templatePUT /api/templates/00000000-0000-4000-8000-000000000001/favorite
pgUpserts the favorite for the caller (principalId = username) and returns the favorite record with 200 (not 201 — no .created() is used).
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/favorite"
}
},
"permissions": {},
"id": "00000000-0000-4000-8000-000000000002",
"tenant": "acme",
"templateId": "00000000-0000-4000-8000-000000000001",
"principalId": "admin",
"reportId": null,
"datasetId": null,
"queryId": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"datasourceId": null,
"jobId": null,
"assistantId": null,
"libraryId": null,
"appId": null,
"integrationId": null,
"toolkitId": null
}
Captured from the API examples test suite; ids and timestamps are normalized.

DELETE /api/templates/{id}/favorite

Remove the Template from the caller's favorites.

Authentication: Required

Pre-blocks: template.lookup(params.id)

Response:

Responds 200 with an empty body (Informer convention), not 204.

Unfavorite a templateDELETE /api/templates/00000000-0000-4000-8000-000000000001/favorite
Removes the favorite for the caller. Responds 200 with an EMPTY body (Informer convention), not 204.
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.

User Settings

Templates carry per-user settings for customizing the editing experience. The settings record is scoped to the caller (userId), so each user has an independent settings row for the same Template.

GET /api/templates/{id}/settings

Get the caller's settings for this Template.

Authentication: Required

Pre-blocks: template.lookup(params.id)

Response:

Responds 200 with the caller's settings row, whose stored values live under settings. When the caller has no settings row yet, the response is an empty object {} (not 404). The record exposes an inf:restore link (./settings/_restore).

Get per-user template settingsGET /api/templates/00000000-0000-4000-8000-000000000001/settings
Returns the caller’s UserSettings row, or an empty object {} when none exist (not 404). Exposes an inf:restore (./_restore) link.
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/settings"
},
"inf:restore": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/settings/_restore"
}
},
"id": "00000000-0000-4000-8000-000000000002",
"datasetId": null,
"reportId": null,
"queryId": null,
"templateId": "00000000-0000-4000-8000-000000000001",
"userId": "admin",
"settings": {
"autoSave": false,
"fontSize": 14,
"editorTheme": "light"
},
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"snapshots": null
}
Captured from the API examples test suite; ids and timestamps are normalized.

PUT /api/templates/{id}/settings

Update the caller's settings for this Template.

Authentication: Required

Pre-blocks: template.lookup(params.id)

Request Body:

An object of user-specific settings. The entire payload is stored wholesale as the row's settings object (creating the row if it does not exist, otherwise replacing the stored object). It is not merged with the existing settings.

Example Request:

{
"editorTheme": "light",
"autoSave": false,
"fontSize": 14
}

Response:

Responds 200, echoing the payload back.

Update per-user template settingsPUT /api/templates/00000000-0000-4000-8000-000000000001/settings
Stores the WHOLE payload as the caller’s UserSettings.settings object (creates the row if absent, otherwise updates it) and echoes the payload back with 200. The payload is stored wholesale, not merged.
Request body
{
"editorTheme": "light",
"autoSave": false,
"fontSize": 14
}
Response · 200
{
"_links": {
"self": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/settings"
},
"inf:restore": {
"href": "https://informer.example.com/api/templates/00000000-0000-4000-8000-000000000001/settings/_restore"
}
},
"editorTheme": "light",
"autoSave": false,
"fontSize": 14
}
Captured from the API examples test suite; ids and timestamps are normalized.
Settings are replaced, not merged

The payload becomes the stored settings object as a whole. To change one field, send the complete settings object you want to keep, not just the field that changed.


POST /api/templates/{id}/settings/_restore

Restore the caller's Template settings to defaults by deleting their settings row.

Authentication: Required

Pre-blocks: template.lookup(params.id)

Response:

Responds 200 with an empty body, not 204. After this, GET .../settings returns {} again.

Restore per-user template settingsPOST /api/templates/00000000-0000-4000-8000-000000000001/settings/_restore
Deletes the caller’s UserSettings row (db.remove). Responds 200 with an EMPTY body, not 204.
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.