Notes: Comments
Section titled “Notes: Comments”The Notes Comments API manages threaded conversations attached to document nodes inside notebook trees. Use these endpoints to list comment decorations on a document, create a new comment with an anchor (document, block, or text range), edit an existing comment, re-anchor a thread when surrounding text changes, mark threads as resolved, or delete them entirely. List operations are paginated via limit, offset, and cursor; write operations accept an optional expectedVersion to guard against lost-update races.
All endpoints live under the notes service for your container cluster. Every URL in this page is rooted at:
https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com
Substitute {projectId} (24 hex characters), {containerId} (24 hex characters), and {server} (for example node-us) with your cluster’s values. A working concrete hostname is https://67e89abc123def456789abcd-890abcdef12345678901cdef-notes-1.node-us.containers.hoody.com.
List comment anchors
Section titled “List comment anchors”Returns lightweight per-thread anchor metadata for the comment decorations rendered in the editor sidebar. This is the cheapest way to enumerate threads when you only need anchor locations and resolution state, not the full conversation body.
GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comment-anchors
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
limit | query | integer | No | Maximum anchors to return. Default 500. |
offset | query | integer | No | Number of anchors to skip before returning results. Default 0. |
cursor | query | string | No | Opaque pagination cursor returned by a previous call. |
notebookId | path | string | Yes | Identifier of the notebook containing the document node. |
nodeId | path | string | Yes | Identifier of the document node whose anchors are listed. |
Response
Section titled “Response”{ "anchors": [ { "threadId": "th_8c1f7e23a0b14d6f9c2e8a4f1b3d5e72", "anchor": { "anchorType": "text-range", "anchorBlockId": "blk_3f8a90b1c2d4e5f6a7b8c9d0", "startBlockId": "blk_3f8a90b1c2d4e5f6a7b8c9d0", "startOffset": 142, "endBlockId": "blk_3f8a90b1c2d4e5f6a7b8c9d0", "endOffset": 178, "anchorQuote": "We will ship this change behind a feature flag.", "anchorContextBefore": "After review with the platform team,", "anchorContextAfter": "Please confirm before merging.", "anchorStatus": "active", "anchorUpdatedAt": "2025-03-14T11:02:48Z" }, "anchorStatus": "active", "resolvedAt": null, "version": 4 } ], "nextCursor": "eyJ0aWQiOiJ0aF84YzFmN2UyM2EwYiJ9", "hasMore": false}{ "message": "Invalid query parameter: limit", "code": "BAD_REQUEST", "details": [ { "path": "limit", "message": "must be a positive integer" } ]}{ "message": "Insufficient permission to list comments on this document", "code": "FORBIDDEN"}{ "message": "Document node not found", "code": "NOT_FOUND"}{ "message": "Document is locked by another operation", "code": "CONFLICT"}SDK usage
Section titled “SDK usage”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.notes.comments.listAnchors('nb_8f3a1c2e', 'nd_42b1e7', { limit: 200 });List comments
Section titled “List comments”Returns all comments for a document node, including reply threads. Use this when you need the full conversation body, not just the anchor metadata.
GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
limit | query | integer | No | Maximum comments to return. Default 100. |
offset | query | integer | No | Number of comments to skip before returning results. Default 0. |
cursor | query | string | No | Opaque pagination cursor returned by a previous call. |
notebookId | path | string | Yes | Identifier of the notebook containing the document node. |
nodeId | path | string | Yes | Identifier of the document node whose comments are listed. |
Response
Section titled “Response”{ "comments": [ { "id": "cm_7a4f31c2b9e84d6f9a0c1e3b4d5f6a78", "documentId": "nd_42b1e7", "parentId": null, "anchorBlockId": "blk_3f8a90b1c2d4e5f6a7b8c9d0", "anchorType": "text-range", "startBlockId": "blk_3f8a90b1c2d4e5f6a7b8c9d0", "startOffset": 142, "endBlockId": "blk_3f8a90b1c2d4e5f6a7b8c9d0", "endOffset": 178, "anchorQuote": "We will ship this change behind a feature flag.", "anchorContextBefore": "After review with the platform team,", "anchorContextAfter": "Please confirm before merging.", "anchorStatus": "active", "anchorUpdatedAt": "2025-03-14T11:02:48Z", "version": 4, "content": "Can we tighten this scope? \"feature flag\" is doing a lot of work here.", "createdAt": "2025-03-14T11:02:48Z", "createdBy": "usr_a1b2c3d4", "createdByName": "Alex Rivera", "updatedAt": "2025-03-14T11:08:12Z", "resolvedAt": null, "resolvedBy": null } ], "nextCursor": null, "hasMore": false}{ "message": "Invalid query parameter: cursor", "code": "BAD_REQUEST", "details": [ { "path": "cursor", "message": "is not a valid pagination token" } ]}{ "message": "Insufficient permission to list comments on this document", "code": "FORBIDDEN"}{ "message": "Document node not found", "code": "NOT_FOUND"}{ "message": "Document is locked by another operation", "code": "CONFLICT"}SDK usage
Section titled “SDK usage”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.notes.comments.list('nb_8f3a1c2e', 'nd_42b1e7', { limit: 50 });Create a comment
Section titled “Create a comment”Creates a new top-level comment or a reply (when parentId is provided) on a document node. If anchor is supplied, the comment is attached to a document, block, or text range; otherwise the comment is unanchored.
POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
notebookId | path | string | Yes | Identifier of the notebook containing the document node. |
nodeId | path | string | Yes | Identifier of the document node that receives the comment. |
Request body
Section titled “Request body”| Name | Type | Required | Description |
|---|---|---|---|
content | string | Yes | Comment body. 1 to 10000 characters. |
parentId | string | No | Identifier of the parent comment when creating a reply. |
anchorBlockId | string | No | Convenience block id when the comment attaches to a single block. |
anchor | object | No | Structured anchor. One of document, block (with blockId), or text-range (with startBlockId, startOffset, endBlockId, endOffset, and optional quote, contextBefore, contextAfter). |
{ "content": "Can we tighten this scope? \"feature flag\" is doing a lot of work here.", "anchor": { "type": "text-range", "startBlockId": "blk_3f8a90b1c2d4e5f6a7b8c9d0", "startOffset": 142, "endBlockId": "blk_3f8a90b1c2d4e5f6a7b8c9d0", "endOffset": 178, "quote": "We will ship this change behind a feature flag.", "contextBefore": "After review with the platform team,", "contextAfter": "Please confirm before merging." }}Response
Section titled “Response”{ "id": "cm_7a4f31c2b9e84d6f9a0c1e3b4d5f6a78", "documentId": "nd_42b1e7", "parentId": null, "anchorBlockId": "blk_3f8a90b1c2d4e5f6a7b8c9d0", "anchorType": "text-range", "startBlockId": "blk_3f8a90b1c2d4e5f6a7b8c9d0", "startOffset": 142, "endBlockId": "blk_3f8a90b1c2d4e5f6a7b8c9d0", "endOffset": 178, "anchorQuote": "We will ship this change behind a feature flag.", "anchorContextBefore": "After review with the platform team,", "anchorContextAfter": "Please confirm before merging.", "anchorStatus": "active", "anchorUpdatedAt": "2025-03-14T11:02:48Z", "version": 1, "content": "Can we tighten this scope? \"feature flag\" is doing a lot of work here.", "createdAt": "2025-03-14T11:02:48Z", "createdBy": "usr_a1b2c3d4", "createdByName": "Alex Rivera", "updatedAt": null, "resolvedAt": null, "resolvedBy": null}{ "message": "Request body validation failed", "code": "BAD_REQUEST", "details": [ { "path": "content", "message": "must be between 1 and 10000 characters" } ]}{ "message": "Insufficient permission to comment on this document", "code": "FORBIDDEN"}{ "message": "Document node not found", "code": "NOT_FOUND"}{ "message": "Anchor references a block that no longer exists", "code": "ANCHOR_INVALID"}{ "message": "Internal error while persisting comment", "code": "INTERNAL_ERROR"}SDK usage
Section titled “SDK usage”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.notes.comments.create('nb_8f3a1c2e', 'nd_42b1e7', { content: 'Can we tighten this scope? "feature flag" is doing a lot of work here.', anchor: { type: 'text-range', startBlockId: 'blk_3f8a90b1c2d4e5f6a7b8c9d0', startOffset: 142, endBlockId: 'blk_3f8a90b1c2d4e5f6a7b8c9d0', endOffset: 178, quote: 'We will ship this change behind a feature flag.', },});Edit a comment
Section titled “Edit a comment”Updates the body of an existing comment. Pass expectedVersion to make the write conditional; the request fails with a 409 if the version on the server no longer matches.
PATCH /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
notebookId | path | string | Yes | Identifier of the notebook containing the comment. |
nodeId | path | string | Yes | Identifier of the document node containing the comment. |
commentId | path | string | Yes | Identifier of the comment to edit. |
Request body
Section titled “Request body”| Name | Type | Required | Description |
|---|---|---|---|
content | string | Yes | Replacement comment body. 1 to 10000 characters. |
expectedVersion | integer | No | Version the client last observed; minimum 1. The write fails when the server’s current version differs. |
{ "content": "Can we tighten this scope and link to the rollout doc?", "expectedVersion": 4}Response
Section titled “Response”{ "id": "cm_7a4f31c2b9e84d6f9a0c1e3b4d5f6a78", "documentId": "nd_42b1e7", "parentId": null, "anchorBlockId": "blk_3f8a90b1c2d4e5f6a7b8c9d0", "anchorType": "text-range", "startBlockId": "blk_3f8a90b1c2d4e5f6a7b8c9d0", "startOffset": 142, "endBlockId": "blk_3f8a90b1c2d4e5f6a7b8c9d0", "endOffset": 178, "anchorQuote": "We will ship this change behind a feature flag.", "anchorContextBefore": "After review with the platform team,", "anchorContextAfter": "Please confirm before merging.", "anchorStatus": "active", "anchorUpdatedAt": "2025-03-14T11:02:48Z", "version": 5, "content": "Can we tighten this scope and link to the rollout doc?", "createdAt": "2025-03-14T11:02:48Z", "createdBy": "usr_a1b2c3d4", "createdByName": "Alex Rivera", "updatedAt": "2025-03-14T11:21:03Z", "resolvedAt": null, "resolvedBy": null}{ "message": "You do not have permission to edit this comment", "code": "FORBIDDEN"}{ "message": "Comment not found", "code": "NOT_FOUND"}{ "message": "Comment was modified by another client", "code": "VERSION_CONFLICT", "details": [ { "path": "expectedVersion", "message": "server has version 6, expected 4" } ]}SDK usage
Section titled “SDK usage”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.notes.comments.edit('nb_8f3a1c2e', 'nd_42b1e7', 'cm_7a4f31c2', { content: 'Can we tighten this scope and link to the rollout doc?', expectedVersion: 4,});Re-anchor a comment thread
Section titled “Re-anchor a comment thread”Updates the anchor of a comment thread after surrounding text has moved or changed. Use this when an anchor becomes orphaned and the new location is known.
POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}/reanchor
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
notebookId | path | string | Yes | Identifier of the notebook containing the thread. |
nodeId | path | string | Yes | Identifier of the document node containing the thread. |
commentId | path | string | Yes | Identifier of the root comment of the thread. |
Request body
Section titled “Request body”| Name | Type | Required | Description |
|---|---|---|---|
anchor | object | Yes | Replacement anchor. One of document, block (with blockId), or text-range (with startBlockId, startOffset, endBlockId, endOffset, and optional quote, contextBefore, contextAfter). |
expectedVersion | integer | No | Version the client last observed; minimum 1. The write fails when the server’s current version differs. |
{ "anchor": { "type": "block", "blockId": "blk_a09f81e2c3d4b5a69788f9e0" }, "expectedVersion": 5}Response
Section titled “Response”{ "id": "cm_7a4f31c2b9e84d6f9a0c1e3b4d5f6a78", "documentId": "nd_42b1e7", "parentId": null, "anchorBlockId": "blk_a09f81e2c3d4b5a69788f9e0", "anchorType": "block", "startBlockId": null, "startOffset": null, "endBlockId": null, "endOffset": null, "anchorQuote": null, "anchorContextBefore": null, "anchorContextAfter": null, "anchorStatus": "active", "anchorUpdatedAt": "2025-03-14T12:04:55Z", "version": 6, "content": "Can we tighten this scope and link to the rollout doc?", "createdAt": "2025-03-14T11:02:48Z", "createdBy": "usr_a1b2c3d4", "createdByName": "Alex Rivera", "updatedAt": "2025-03-14T12:04:55Z", "resolvedAt": null, "resolvedBy": null}{ "message": "Request body validation failed", "code": "BAD_REQUEST", "details": [ { "path": "anchor.type", "message": "must be one of document, block, text-range" } ]}{ "message": "You do not have permission to modify this thread", "code": "FORBIDDEN"}{ "message": "Comment thread not found", "code": "NOT_FOUND"}{ "message": "Anchor target block does not exist on this revision", "code": "ANCHOR_INVALID"}SDK usage
Section titled “SDK usage”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.notes.comments.reanchor('nb_8f3a1c2e', 'nd_42b1e7', 'cm_7a4f31c2', { anchor: { type: 'block', blockId: 'blk_a09f81e2c3d4b5a69788f9e0' }, expectedVersion: 5,});Resolve a comment
Section titled “Resolve a comment”Marks a comment as resolved. Once resolved, the thread remains visible in the document but is filtered out of the default unresolved view.
POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}/resolve
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
notebookId | path | string | Yes | Identifier of the notebook containing the comment. |
nodeId | path | string | Yes | Identifier of the document node containing the comment. |
commentId | path | string | Yes | Identifier of the comment to resolve. |
Request body
Section titled “Request body”| Name | Type | Required | Description |
|---|---|---|---|
expectedVersion | integer | No | Version the client last observed; minimum 1. The write fails when the server’s current version differs. |
{ "expectedVersion": 5}Response
Section titled “Response”{ "id": "cm_7a4f31c2b9e84d6f9a0c1e3b4d5f6a78", "documentId": "nd_42b1e7", "parentId": null, "anchorBlockId": "blk_a09f81e2c3d4b5a69788f9e0", "anchorType": "block", "startBlockId": null, "startOffset": null, "endBlockId": null, "endOffset": null, "anchorQuote": null, "anchorContextBefore": null, "anchorContextAfter": null, "anchorStatus": "active", "anchorUpdatedAt": "2025-03-14T12:04:55Z", "version": 6, "content": "Can we tighten this scope and link to the rollout doc?", "createdAt": "2025-03-14T11:02:48Z", "createdBy": "usr_a1b2c3d4", "createdByName": "Alex Rivera", "updatedAt": "2025-03-14T12:18:02Z", "resolvedAt": "2025-03-14T12:18:02Z", "resolvedBy": "usr_f0e1d2c3"}{ "message": "You do not have permission to resolve this comment", "code": "FORBIDDEN"}{ "message": "Comment not found", "code": "NOT_FOUND"}{ "message": "Comment was modified by another client", "code": "VERSION_CONFLICT", "details": [ { "path": "expectedVersion", "message": "server has version 7, expected 5" } ]}SDK usage
Section titled “SDK usage”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.notes.comments.resolve('nb_8f3a1c2e', 'nd_42b1e7', 'cm_7a4f31c2', { expectedVersion: 5,});Delete a comment
Section titled “Delete a comment”Deletes a comment and any replies under it. The thread’s anchor is removed from the document and its identifier is released.
DELETE /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
expectedVersion | query | integer | No | Version the client last observed. The delete fails with a 409 if the server’s current version differs. |
notebookId | path | string | Yes | Identifier of the notebook containing the comment. |
nodeId | path | string | Yes | Identifier of the document node containing the comment. |
commentId | path | string | Yes | Identifier of the comment to delete. |
Response
Section titled “Response”{ "success": true}{ "message": "You do not have permission to delete this comment", "code": "FORBIDDEN"}{ "message": "Comment not found", "code": "NOT_FOUND"}{ "message": "Comment was modified by another client", "code": "VERSION_CONFLICT", "details": [ { "path": "expectedVersion", "message": "server has version 6, expected 5" } ]}SDK usage
Section titled “SDK usage”import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.notes.comments.delete('nb_8f3a1c2e', 'nd_42b1e7', 'cm_7a4f31c2', { expectedVersion: 5 });