Comments let users leave inline feedback anchored to a document. Use these endpoints to list anchors, create new threads, edit or delete messages, re-anchor threads when content shifts, and resolve conversations.
All endpoints operate against the container-scoped notes service:
https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.icu
List comment anchors
Section titled “List comment anchors”GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comment-anchors
Returns lightweight thread anchor metadata for comment decorations. Use this endpoint to render comment markers in a document without fetching every full comment body.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
notebookId | path | string | Yes | Identifier of the notebook that owns the document node. |
nodeId | path | string | Yes | Identifier of the document node to list anchors for. |
limit | query | integer | No | Maximum number of anchors to return. Default: 500. |
offset | query | integer | No | Number of anchors to skip before returning results. Default: 0. |
cursor | query | string | No | Opaque cursor returned by a previous call for keyset pagination. |
Response
Section titled “Response”{ "anchors": [ { "threadId": "cm_7a4f31c2", "anchor": { "anchorType": "text-range", "anchorBlockId": null, "startBlockId": "blk_8c91a2", "startOffset": 14, "endBlockId": "blk_8c91a2", "endOffset": 42, "anchorQuote": "experimental design", "anchorContextBefore": "Review the ", "anchorContextAfter": " before merging.", "anchorStatus": "active", "anchorUpdatedAt": "2026-03-12T18:22:05.117Z" }, "anchorStatus": "active", "resolvedAt": null, "version": 3 } ], "nextCursor": null, "hasMore": false}{ "message": "Invalid cursor", "code": "BAD_REQUEST", "details": [ { "path": "cursor", "message": "Cursor must be a base64-encoded string." } ]}{ "message": "Caller does not have access to this notebook.", "code": "FORBIDDEN", "details": []}{ "message": "Document node not found.", "code": "NOT_FOUND", "details": []}{ "message": "Conflicting concurrent update.", "code": "CONFLICT", "details": []}await client.notes.comments.listAnchors("nb_8f3a1c2e", "nd_42b1e7", { limit: 200 });List comments
Section titled “List comments”GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments
Returns all comments for a document node, including reply threads.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
notebookId | path | string | Yes | Identifier of the notebook that owns the document node. |
nodeId | path | string | Yes | Identifier of the document node to list comments for. |
limit | query | integer | No | Maximum number of comments to return. Default: 100. |
offset | query | integer | No | Number of comments to skip before returning results. Default: 0. |
cursor | query | string | No | Opaque cursor returned by a previous call for keyset pagination. |
Response
Section titled “Response”{ "comments": [ { "id": "cm_7a4f31c2", "documentId": "nd_42b1e7", "parentId": null, "anchorBlockId": "blk_8c91a2", "anchorType": "text-range", "startBlockId": "blk_8c91a2", "startOffset": 14, "endBlockId": "blk_8c91a2", "endOffset": 42, "anchorQuote": "experimental design", "anchorContextBefore": "Review the ", "anchorContextAfter": " before merging.", "anchorStatus": "active", "anchorUpdatedAt": "2026-03-12T18:22:05.117Z", "version": 3, "content": "Can we tighten this sentence? It feels vague.", "createdAt": "2026-03-10T14:01:22.991Z", "createdBy": "u_91b3d4", "createdByName": "Maya Patel", "updatedAt": null, "resolvedAt": null, "resolvedBy": null } ], "nextCursor": null, "hasMore": false}{ "message": "Invalid limit", "code": "BAD_REQUEST", "details": [ { "path": "limit", "message": "Limit must be a positive integer." } ]}{ "message": "Caller does not have access to this notebook.", "code": "FORBIDDEN", "details": []}{ "message": "Document node not found.", "code": "NOT_FOUND", "details": []}{ "message": "Conflicting concurrent update.", "code": "CONFLICT", "details": []}await client.notes.comments.list("nb_8f3a1c2e", "nd_42b1e7", { limit: 50 });Create a comment
Section titled “Create a comment”POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments
Creates a new comment on a document node. The anchor field can target the whole document, a specific block, or a text range.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
notebookId | path | string | Yes | Identifier of the notebook that owns the document node. |
nodeId | path | string | Yes | Identifier of the document node the comment is attached to. |
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
content | string | Yes | Comment body. Between 1 and 10000 characters. |
parentId | string | No | Identifier of the parent comment when posting a reply. |
anchorBlockId | string | No | Identifier of the block the comment targets when anchor is not supplied. |
anchor | object | No | Anchor descriptor. One of: document, block, or text-range. |
The anchor field accepts one of the following shapes:
{ "type": "document" }{ "type": "block", "blockId": "blk_8c91a2" }{ "type": "text-range", "startBlockId": "blk_8c91a2", "startOffset": 14, "endBlockId": "blk_8c91a2", "endOffset": 42, "quote": "experimental design", "contextBefore": "Review the ", "contextAfter": " before merging."}Response
Section titled “Response”{ "id": "cm_7a4f31c2", "documentId": "nd_42b1e7", "parentId": null, "anchorBlockId": "blk_8c91a2", "anchorType": "text-range", "startBlockId": "blk_8c91a2", "startOffset": 14, "endBlockId": "blk_8c91a2", "endOffset": 42, "anchorQuote": "experimental design", "anchorContextBefore": "Review the ", "anchorContextAfter": " before merging.", "anchorStatus": "active", "anchorUpdatedAt": "2026-03-12T18:22:05.117Z", "version": 1, "content": "Can we tighten this sentence? It feels vague.", "createdAt": "2026-03-12T18:22:05.117Z", "createdBy": "u_91b3d4", "createdByName": "Maya Patel", "updatedAt": null, "resolvedAt": null, "resolvedBy": null}{ "message": "Invalid request body", "code": "BAD_REQUEST", "details": [ { "path": "content", "message": "Content is required and must be 1-10000 characters." } ]}{ "message": "Caller does not have access to this notebook.", "code": "FORBIDDEN", "details": []}{ "message": "Document node not found.", "code": "NOT_FOUND", "details": []}{ "message": "Conflicting concurrent update.", "code": "CONFLICT", "details": []}{ "message": "Unexpected server error while creating comment.", "code": "INTERNAL_SERVER_ERROR", "details": []}await client.notes.comments.create("nb_8f3a1c2e", "nd_42b1e7", { content: "Can we tighten this sentence? It feels vague.", anchor: { type: "text-range", startBlockId: "blk_8c91a2", startOffset: 14, endBlockId: "blk_8c91a2", endOffset: 42, quote: "experimental design", contextBefore: "Review the ", contextAfter: " before merging." }});Edit a comment
Section titled “Edit a comment”PATCH /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}
Edits the content of an existing comment. Pass expectedVersion to enforce optimistic concurrency.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
notebookId | path | string | Yes | Identifier of the notebook that owns the document node. |
nodeId | path | string | Yes | Identifier of the document node the comment belongs to. |
commentId | path | string | Yes | Identifier of the comment to edit. |
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
content | string | Yes | New comment body. Between 1 and 10000 characters. |
expectedVersion | integer | No | Current comment version. Update fails with 409 if it does not match. |
Response
Section titled “Response”{ "id": "cm_7a4f31c2", "documentId": "nd_42b1e7", "parentId": null, "anchorBlockId": "blk_8c91a2", "anchorType": "text-range", "startBlockId": "blk_8c91a2", "startOffset": 14, "endBlockId": "blk_8c91a2", "endOffset": 42, "anchorQuote": "experimental design", "anchorContextBefore": "Review the ", "anchorContextAfter": " before merging.", "anchorStatus": "active", "anchorUpdatedAt": "2026-03-12T18:22:05.117Z", "version": 4, "content": "Can we tighten this sentence? It feels vague in the intro.", "createdAt": "2026-03-10T14:01:22.991Z", "createdBy": "u_91b3d4", "createdByName": "Maya Patel", "updatedAt": "2026-03-13T09:14:51.502Z", "resolvedAt": null, "resolvedBy": null}{ "message": "Caller does not have access to this notebook.", "code": "FORBIDDEN", "details": []}{ "message": "Comment not found.", "code": "NOT_FOUND", "details": []}{ "message": "Version conflict.", "code": "CONFLICT", "details": [ { "path": "expectedVersion", "message": "Expected version 3 but current version is 4." } ]}await client.notes.comments.edit("nb_8f3a1c2e", "nd_42b1e7", "cm_7a4f31c2", { content: "Can we tighten this sentence? It feels vague in the intro.", expectedVersion: 3});Re-anchor a comment thread
Section titled “Re-anchor a comment thread”POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}/reanchor
Updates the root comment anchor for a comment thread. Use this when surrounding content has moved and the original block IDs no longer resolve.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
notebookId | path | string | Yes | Identifier of the notebook that owns the document node. |
nodeId | path | string | Yes | Identifier of the document node the comment belongs to. |
commentId | path | string | Yes | Identifier of the root comment of the thread to re-anchor. |
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
anchor | object | Yes | New anchor descriptor. One of: document, block, or text-range. |
expectedVersion | integer | No | Current thread version. Update fails with 409 if it does not match. |
The anchor field accepts one of the following shapes:
{ "type": "document" }{ "type": "block", "blockId": "blk_8c91a2" }{ "type": "text-range", "startBlockId": "blk_8c91a2", "startOffset": 0, "endBlockId": "blk_8c91a2", "endOffset": 24, "quote": "experimental design", "contextBefore": "Review the ", "contextAfter": " before merging."}Response
Section titled “Response”{ "id": "cm_7a4f31c2", "documentId": "nd_42b1e7", "parentId": null, "anchorBlockId": "blk_8c91a2", "anchorType": "text-range", "startBlockId": "blk_8c91a2", "startOffset": 0, "endBlockId": "blk_8c91a2", "endOffset": 24, "anchorQuote": "experimental design", "anchorContextBefore": "Review the ", "anchorContextAfter": " before merging.", "anchorStatus": "active", "anchorUpdatedAt": "2026-03-14T11:02:48.661Z", "version": 5, "content": "Can we tighten this sentence? It feels vague.", "createdAt": "2026-03-10T14:01:22.991Z", "createdBy": "u_91b3d4", "createdByName": "Maya Patel", "updatedAt": "2026-03-14T11:02:48.661Z", "resolvedAt": null, "resolvedBy": null}{ "message": "Invalid anchor", "code": "BAD_REQUEST", "details": [ { "path": "anchor", "message": "Anchor.type must be one of document, block, text-range." } ]}{ "message": "Caller does not have access to this notebook.", "code": "FORBIDDEN", "details": []}{ "message": "Comment not found.", "code": "NOT_FOUND", "details": []}{ "message": "Version conflict.", "code": "CONFLICT", "details": [ { "path": "expectedVersion", "message": "Expected version 4 but current version is 5." } ]}await client.notes.comments.reanchor("nb_8f3a1c2e", "nd_42b1e7", "cm_7a4f31c2", { anchor: { type: "text-range", startBlockId: "blk_8c91a2", startOffset: 0, endBlockId: "blk_8c91a2", endOffset: 24, quote: "experimental design", contextBefore: "Review the ", contextAfter: " before merging." }, expectedVersion: 4});Resolve a comment
Section titled “Resolve a comment”POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}/resolve
Marks a comment as resolved. The comment stays in the thread history but no longer surfaces as an open item.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
notebookId | path | string | Yes | Identifier of the notebook that owns the document node. |
nodeId | path | string | Yes | Identifier of the document node the comment belongs to. |
commentId | path | string | Yes | Identifier of the comment to resolve. |
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
expectedVersion | integer | No | Current comment version. Update fails with 409 if it does not match. |
Response
Section titled “Response”{ "id": "cm_7a4f31c2", "documentId": "nd_42b1e7", "parentId": null, "anchorBlockId": "blk_8c91a2", "anchorType": "text-range", "startBlockId": "blk_8c91a2", "startOffset": 14, "endBlockId": "blk_8c91a2", "endOffset": 42, "anchorQuote": "experimental design", "anchorContextBefore": "Review the ", "anchorContextAfter": " before merging.", "anchorStatus": "active", "anchorUpdatedAt": "2026-03-12T18:22:05.117Z", "version": 6, "content": "Can we tighten this sentence? It feels vague.", "createdAt": "2026-03-10T14:01:22.991Z", "createdBy": "u_91b3d4", "createdByName": "Maya Patel", "updatedAt": null, "resolvedAt": "2026-03-15T08:30:12.044Z", "resolvedBy": "u_91b3d4"}{ "message": "Caller does not have access to this notebook.", "code": "FORBIDDEN", "details": []}{ "message": "Comment not found.", "code": "NOT_FOUND", "details": []}{ "message": "Version conflict.", "code": "CONFLICT", "details": []}await client.notes.comments.resolve("nb_8f3a1c2e", "nd_42b1e7", "cm_7a4f31c2", { expectedVersion: 5});Delete a comment
Section titled “Delete a comment”DELETE /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}
Deletes a comment and its replies.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
notebookId | path | string | Yes | Identifier of the notebook that owns the document node. |
nodeId | path | string | Yes | Identifier of the document node the comment belongs to. |
commentId | path | string | Yes | Identifier of the comment to delete. |
expectedVersion | query | integer | No | Current comment version. Delete fails with 409 if it does not match. |
Response
Section titled “Response”{ "success": true}{ "message": "Caller does not have access to this notebook.", "code": "FORBIDDEN", "details": []}{ "message": "Comment not found.", "code": "NOT_FOUND", "details": []}{ "message": "Version conflict.", "code": "CONFLICT", "details": []}await client.notes.comments.delete("nb_8f3a1c2e", "nd_42b1e7", "cm_7a4f31c2", { expectedVersion: 6});