Skip to main content

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.

Per-entity comment collections live on each entity page

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).

Get a commentGET /api/comments/00000000-0000-4000-8000-000000000001
GET /api/comments/{id} resolves through the comment.lookup server method (404 when missing). It is a plain read with no permission check — any authenticated session in the tenant may read a comment by id. The HAL body embeds inf:user (the author) and exposes an inf:comments link to ./replies.
Response · 200
{
"_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
}
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

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:

ParameterTypeDescription
sortstringSort order (defaults to -createdAt, newest first)
limitnumberMaximum 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).

List a comment’s repliesGET /api/comments/00000000-0000-4000-8000-000000000001/replies
GET /api/comments/{id}/replies is a HAL collection of the child comments (parentId = {id}), sorted -createdAt (newest first). Rows render under _embedded["inf:comment"], each embedding inf:user. Query params are {?sort,limit}. No permission check (readable within the tenant).
Response · 200
{
"_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
}
}
}
]
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

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:

FieldTypeRequiredDescription
messagestringNoThe 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.

Reply to a commentPOST /api/comments/00000000-0000-4000-8000-000000000001/replies
POST /api/comments/{id}/replies creates a child comment: parentId = {id}, userId = the caller, inheriting the parent comment’s entity. The body field is `message` (the only accepted field; stripUnknown drops the rest; not joi-required). Builds an "<entity>AddedComment" activity, then responds 201 with the reply and a Location header. Runs in a DB transaction.
Request body
{
"message": "I can mock that up — what date range should it cover?"
}
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": "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
}
Captured from the API examples test suite; ids and timestamps are normalized.

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.

No payload restriction on edit

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:

FieldTypeDescription
messagestringThe updated comment text

Example Request:

{ "message": "Great template! Could we add a trailing-12-months trend chart?" }

Response:

Responds 200 with the updated Comment.

Edit a commentPUT /api/comments/00000000-0000-4000-8000-000000000001
PUT /api/comments/{id} requires permission.comment.edit (the comment’s own author, or an admin/owner). It is a db.update keyed by the {id} param with no payload validation, so the whole payload is applied to the comment (send `message` to change the text). Returns the updated comment with 200 and re-builds the parent entity’s AddedComment activity.
Request body
{
"message": "Great template! Could we add a trailing-12-months trend chart?"
}
Response · 200
{
"_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
}
}
Captured from the API examples test suite; ids and timestamps are normalized.

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.

Delete a commentDELETE /api/comments/00000000-0000-4000-8000-000000000001
DELETE /api/comments/{id} requires permission.comment.delete (a superuser, or the comment’s own author). The db.remove handler returns h.continue, so the response is 200 with an EMPTY body (Informer convention), NOT 204. Replies cascade via the parentId FK, and the matching AddedComment activity is removed.
Response · 200 · no body
Captured from the API examples test suite; ids and timestamps are normalized.
Replies cascade

Deleting a Comment also removes its replies through the parentId foreign key.