Comments
Endpoints for the operations shared across every commentable entity: read a single Comment by id, edit it, delete it, list its replies, and post a reply.
A Comment is polymorphic. It attaches to exactly one parent entity (a Report,
Dataset, Datasource, Query, Team, Job, Template, Assistant, or Library) through
a single <type>Id column, and the model resolves which one is set to report
its target type. Replies are modeled with a self-referencing parentId: a
top-level Comment has parentId = null, while a reply carries the parent
Comment's id and inherits the same parent entity. Every Comment serialization
embeds its author (inf:user), and each saved message is scanned for
@mentions.
The collection routes that list and create comments for a specific entity
(GET / POST /api/<entity>/{id}/comments) are documented on that entity's
page (for example the Template Comments page). This page covers only the
comment-by-id operations plus the replies collection.
GET /api/comments/{id}
Read a single Comment by id.
Authentication: Any authenticated session in the tenant
This route resolves through the comment.lookup server method and has no
permission pre-block, so comments are open-read within the tenant. It answers
404 when the id does not exist and 400 on a malformed id.
Response:
The HAL body embeds inf:user (the author) and exposes an inf:comments link
to ./replies (the reply collection).
{
"_links": {
"self": {
"href": "https://informer.example.com/api/comments/00000000-0000-4000-8000-000000000001"
},
"inf:comments": {
"href": "https://informer.example.com/api/comments/00000000-0000-4000-8000-000000000001/replies"
}
},
"permissions": {
"edit": true,
"delete": true
},
"id": "00000000-0000-4000-8000-000000000001",
"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-000000000002",
"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",
"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,
"aiMessage": null,
"aiConsent": false,
"hasSharedDatasources": false,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"initials": "EA",
"colorIndex": 4,
"avatarColor": {
"background": "#81C784",
"text": "#1B5E20"
},
"managesPassword": true
}
}
}
GET /api/comments/{id}/replies
List a Comment's replies.
Authentication: Any authenticated session in the tenant
This route has no permission pre-block (readable within the tenant). It returns
the child comments (those whose parentId equals the path id), sorted
-createdAt so the newest reply comes first.
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
sort | string | Sort order (defaults to -createdAt, newest first) |
limit | number | Maximum number of replies to return |
Response:
A HAL collection whose rows render under _embedded["inf:comment"] (not a
top-level items array). Each reply embeds its author (inf:user).
{
"_links": {
"self": {
"href": "https://informer.example.com/api/comments/00000000-0000-4000-8000-000000000001/replies?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"
}
},
"permissions": {
"edit": true,
"delete": true
},
"id": "00000000-0000-4000-8000-000000000002",
"message": "Agreed — a trailing-12-months line chart would be ideal.",
"public": false,
"userId": "admin",
"datasetId": null,
"datasourceId": null,
"reportId": null,
"queryId": null,
"teamId": null,
"jobId": null,
"templateId": null,
"assistantId": null,
"libraryId": null,
"mentions": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"parentId": "00000000-0000-4000-8000-000000000001",
"_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",
"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,
"aiMessage": null,
"aiConsent": false,
"hasSharedDatasources": false,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"initials": "EA",
"colorIndex": 4,
"avatarColor": {
"background": "#81C784",
"text": "#1B5E20"
},
"managesPassword": true
}
}
}
]
}
}
POST /api/comments/{id}/replies
Reply to a Comment.
Authentication: Any authenticated session in the tenant
The handler resolves the parent Comment and its parent entity (via that entity's
lookup server method), then creates a child Comment with parentId set to the
path id, userId set to the caller, and the same parent entity inherited from
the Comment being replied to. It builds an <entity>AddedComment activity and
runs in a database transaction. If the parent entity's type has no lookup
server method, the route answers 400 with Unknown entity type for comment.
Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
message | string | No | The reply text. The only accepted field (the payload is stripUnknown); not required by the schema |
Example Request:
{ "message": "I can mock that up — what date range should it cover?" }
Response:
Responds 201 Created with the new reply and a Location header.
{
"message": "I can mock that up — what date range should it cover?"
}
{
"_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": "I can mock that up — what date range should it cover?",
"parentId": "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,
"templateId": null,
"assistantId": null,
"libraryId": null
}
PUT /api/comments/{id}
Edit a Comment.
Authentication: permission.comment.edit (the Comment's own author, or an admin/owner)
This route is a db.update keyed by the {id} path param. It has no
validate.payload block, so the whole payload is applied to the Comment
instance. Send message to change the text. A postUpdate side-effect rebuilds
the parent entity's AddedComment activity.
Unlike the create routes, this route does not stripUnknown or restrict to a
message-only field. Whatever you send in the payload is set onto the Comment,
so send only the fields you intend to change.
Request Body:
| Field | Type | Description |
|---|---|---|
message | string | The updated comment text |
Example Request:
{ "message": "Great template! Could we add a trailing-12-months trend chart?" }
Response:
Responds 200 with the updated Comment.
{
"message": "Great template! Could we add a trailing-12-months trend chart?"
}
{
"_links": {
"self": {
"href": "https://informer.example.com/api/comments/00000000-0000-4000-8000-000000000001"
}
},
"permissions": {
"edit": true,
"delete": true
},
"id": "00000000-0000-4000-8000-000000000001",
"message": "Great template! Could we add a trailing-12-months trend chart?",
"public": false,
"userId": "admin",
"datasetId": null,
"datasourceId": null,
"reportId": null,
"queryId": null,
"teamId": null,
"jobId": null,
"templateId": "00000000-0000-4000-8000-000000000002",
"assistantId": null,
"libraryId": null,
"mentions": null,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"tenant": "acme",
"parentId": null,
"user": {
"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",
"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,
"aiMessage": null,
"aiConsent": false,
"hasSharedDatasources": false,
"createdAt": "2026-01-15T16:20:00.000Z",
"updatedAt": "2026-01-15T16:20:00.000Z",
"avatar": null
}
}
DELETE /api/comments/{id}
Delete a Comment.
Authentication: permission.comment.delete (a superuser, or the Comment's own author)
This route is a db.remove whose handler returns h.continue, so the response
is 200 with an empty body (Informer convention), not 204. A postRemove
side-effect deletes the matching AddedComment activity.
Response:
Responds 200 with an empty body.
Deleting a Comment also removes its replies through the parentId foreign key.