Notes: Comments
Section titled “Notes: Comments”Use these endpoints to manage threaded comments anchored to notebook document nodes. Each comment is attached to a document through an anchor (the whole document, a single block, or a text range), and every comment payload carries a version that increments on each update. Pass that version back as expectedVersion on edit, re-anchor, resolve, and delete so concurrent edits are detected. The list endpoints support limit/offset and an opaque cursor for pagination.
List comment anchors
Section titled “List comment anchors”Returns lightweight thread anchor metadata for comment decorations. Use this endpoint to render sidebar markers, gutter dots, and orphaned-anchor indicators without loading every comment’s full body and author data.
GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comment-anchors
Section titled “GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comment-anchors”Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
notebookId | path | string | Yes | Identifier of the notebook that owns the document |
nodeId | path | string | Yes | Identifier of the document node whose anchors are returned |
limit | query | integer | No | Maximum number of anchors to return. Defaults to 500 |
offset | query | integer | No | Number of anchors to skip from the start of the result set. Defaults to 0 |
cursor | query | string | No | Opaque pagination cursor returned by a previous call |
curl -X GET "https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comment-anchors?limit=200" \ -H "Authorization: Bearer $HOODY_TOKEN"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
for await (const item of client.notes.comments.listAnchorsIterator( 'nb8f3a1c2e3b4d5f6a7b8c9de', 'nd42b1e7c8d9e0f1a2b3c4d5e', { limit: 200 },)) { console.log(item);}{ "anchors": [ { "threadId": "cm7a4f31c2d8e9f1a2b3c4d5e", "anchor": { "anchorType": "text-range", "anchorBlockId": "blk4e9a72b3c8d1e4f6a7b8c9d2", "startBlockId": "blk4e9a72b3c8d1e4f6a7b8c9d2", "startOffset": 12, "endBlockId": "blk4e9a72b3c8d1e4f6a7b8c9d2", "endOffset": 48, "anchorQuote": "this needs a citation", "anchorContextBefore": "Note that ", "anchorContextAfter": " for the overview.", "anchorStatus": "active", "anchorUpdatedAt": "2024-11-05T14:22:31.000Z" }, "anchorStatus": "active", "resolvedAt": null, "version": 1 } ], "nextCursor": null, "hasMore": false}{ "message": "Invalid query parameter", "code": "VALIDATION_ERROR", "details": [ { "path": "limit", "message": "limit must be a positive integer" } ]}{ "message": "Forbidden", "code": "FORBIDDEN", "details": []}| Error Code | Title | Description | Resolution |
|---|---|---|---|
notebook_no_access | No access to notebook | The notebook exists but the caller is not a member, or it is not open | Verify user identity or request access from the owner |
{ "message": "Notebook or node not found", "code": "NOT_FOUND", "details": []}| Error Code | Title | Description | Resolution |
|---|---|---|---|
notebook_not_found | Notebook not found | No notebook exists with the provided ID | Verify the notebook ID in the URL |
{ "message": "Concurrent update conflict", "code": "VERSION_CONFLICT", "details": []}List comments
Section titled “List comments”Returns all comments for a document node, including replies. Each entry carries the comment, its author, its anchor, and its resolution state.
GET /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments
Section titled “GET /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 that owns the document |
nodeId | path | string | Yes | Identifier of the document node whose comments are returned |
limit | query | integer | No | Maximum number of comments to return. Defaults to 100 |
offset | query | integer | No | Number of comments to skip. Defaults to 0 |
cursor | query | string | No | Opaque pagination cursor returned by a previous call |
curl -X GET "https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments?limit=50" \ -H "Authorization: Bearer $HOODY_TOKEN"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
for await (const item of client.notes.comments.listIterator( 'nb8f3a1c2e3b4d5f6a7b8c9de', 'nd42b1e7c8d9e0f1a2b3c4d5e', { limit: 50 },)) { console.log(item);}{ "comments": [ { "id": "cm7a4f31c2d8e9f1a2b3c4d5e", "documentId": "nd42b1e7c8d9e0f1a2b3c4d5e", "parentId": null, "anchorBlockId": "blk4e9a72b3c8d1e4f6a7b8c9d2", "anchorType": "text-range", "startBlockId": "blk4e9a72b3c8d1e4f6a7b8c9d2", "startOffset": 12, "endBlockId": "blk4e9a72b3c8d1e4f6a7b8c9d2", "endOffset": 48, "anchorQuote": "this needs a citation", "anchorContextBefore": "Note that ", "anchorContextAfter": " for the overview.", "anchorStatus": "active", "anchorUpdatedAt": "2024-11-05T14:22:31.000Z", "version": 1, "content": "Please add a citation for this claim.", "createdAt": "2024-11-05T14:22:31.000Z", "createdBy": "usera1b2c3d4e5f6a7b8c9d0e1f", "createdByName": "Maya Lin", "updatedAt": null, "resolvedAt": null, "resolvedBy": null } ], "nextCursor": null, "hasMore": false}{ "message": "Invalid query parameter", "code": "VALIDATION_ERROR", "details": [ { "path": "limit", "message": "limit must be a positive integer" } ]}{ "message": "Forbidden", "code": "FORBIDDEN", "details": []}| Error Code | Title | Description | Resolution |
|---|---|---|---|
notebook_no_access | No access to notebook | The notebook exists but the caller is not a member, or it is not open | Verify user identity or request access from the owner |
{ "message": "Notebook or node not found", "code": "NOT_FOUND", "details": []}| Error Code | Title | Description | Resolution |
|---|---|---|---|
notebook_not_found | Notebook not found | No notebook exists with the provided ID | Verify the notebook ID in the URL |
{ "message": "Concurrent update conflict", "code": "VERSION_CONFLICT", "details": []}Create a comment
Section titled “Create a comment”Creates a new comment on a document node. Pass an anchor to attach the comment to a specific location (document, block, or text range), or pass parentId to reply to an existing comment.
POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments
Section titled “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 that owns the document |
nodeId | path | string | Yes | Identifier of the document node to attach the comment to |
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 this is a reply |
anchorBlockId | string | No | Identifier of the block the comment is attached to |
anchor | object | No | Anchor describing where in the document the comment is positioned |
The anchor field is a discriminated union on type. Use one of the following forms:
{ "type": "document" }to anchor the comment to the whole document{ "type": "block", "blockId": "<block id>" }to anchor the comment to a single block{ "type": "text-range", "startBlockId": "<block id>", "startOffset": <integer>, "endBlockId": "<block id>", "endOffset": <integer>, "quote": "<text>", "contextBefore": "<text>", "contextAfter": "<text>" }to anchor the comment to a text range.startBlockId,startOffset,endBlockId, andendOffsetare required;quote,contextBefore, andcontextAfterare optional.
curl -X POST "https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "content": "Please add a citation for this claim.", "anchor": { "type": "text-range", "startBlockId": "blk4e9a72b3c8d1e4f6a7b8c9d2", "startOffset": 12, "endBlockId": "blk4e9a72b3c8d1e4f6a7b8c9d2", "endOffset": 48, "quote": "this needs a citation" } }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const comment = await client.notes.comments.create( 'nb8f3a1c2e3b4d5f6a7b8c9de', 'nd42b1e7c8d9e0f1a2b3c4d5e', { content: 'Please add a citation for this claim.', anchor: { type: 'text-range', startBlockId: 'blk4e9a72b3c8d1e4f6a7b8c9d2', startOffset: 12, endBlockId: 'blk4e9a72b3c8d1e4f6a7b8c9d2', endOffset: 48, quote: 'this needs a citation', }, },);{ "id": "cm7a4f31c2d8e9f1a2b3c4d5e", "documentId": "nd42b1e7c8d9e0f1a2b3c4d5e", "parentId": null, "anchorBlockId": "blk4e9a72b3c8d1e4f6a7b8c9d2", "anchorType": "text-range", "startBlockId": "blk4e9a72b3c8d1e4f6a7b8c9d2", "startOffset": 12, "endBlockId": "blk4e9a72b3c8d1e4f6a7b8c9d2", "endOffset": 48, "anchorQuote": "this needs a citation", "anchorContextBefore": null, "anchorContextAfter": null, "anchorStatus": "active", "anchorUpdatedAt": "2024-11-05T14:22:31.000Z", "version": 1, "content": "Please add a citation for this claim.", "createdAt": "2024-11-05T14:22:31.000Z", "createdBy": "usera1b2c3d4e5f6a7b8c9d0e1f", "createdByName": "Maya Lin", "updatedAt": null, "resolvedAt": null, "resolvedBy": null}{ "message": "Invalid request body", "code": "VALIDATION_ERROR", "details": [ { "path": "content", "message": "content is required" } ]}{ "message": "Forbidden", "code": "FORBIDDEN", "details": []}| Error Code | Title | Description | Resolution |
|---|---|---|---|
notebook_no_access | No access to notebook | The notebook exists but the caller is not a member, or it is not open | Verify user identity or request access from the owner |
{ "message": "Notebook, node, or parent comment not found", "code": "NOT_FOUND", "details": []}| Error Code | Title | Description | Resolution |
|---|---|---|---|
notebook_not_found | Notebook not found | No notebook exists with the provided ID | Verify the notebook ID in the URL |
{ "message": "Concurrent update conflict", "code": "VERSION_CONFLICT", "details": []}{ "message": "Internal server error", "code": "INTERNAL_ERROR", "details": []}Edit a comment
Section titled “Edit a comment”Edits the content of an existing comment. Pass expectedVersion to require the server’s current version to match; the call returns 409 when it does not.
PATCH /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}
Section titled “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 that owns the document |
nodeId | path | string | Yes | Identifier of the document node the comment is attached 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, 1 to 10000 characters |
expectedVersion | integer | No | Version returned by the previous read; the call returns 409 if the comment has changed since |
curl -X PATCH "https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "content": "Please add a citation here.", "expectedVersion": 1 }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const updated = await client.notes.comments.update( 'nb8f3a1c2e3b4d5f6a7b8c9de', 'nd42b1e7c8d9e0f1a2b3c4d5e', 'cm7a4f31c2d8e9f1a2b3c4d5e', { content: 'Please add a citation here.', expectedVersion: 1 },);{ "id": "cm7a4f31c2d8e9f1a2b3c4d5e", "documentId": "nd42b1e7c8d9e0f1a2b3c4d5e", "parentId": null, "anchorBlockId": "blk4e9a72b3c8d1e4f6a7b8c9d2", "anchorType": "text-range", "startBlockId": "blk4e9a72b3c8d1e4f6a7b8c9d2", "startOffset": 12, "endBlockId": "blk4e9a72b3c8d1e4f6a7b8c9d2", "endOffset": 48, "anchorQuote": "this needs a citation", "anchorContextBefore": null, "anchorContextAfter": null, "anchorStatus": "active", "anchorUpdatedAt": "2024-11-05T14:22:31.000Z", "version": 2, "content": "Please add a citation here.", "createdAt": "2024-11-05T14:22:31.000Z", "createdBy": "usera1b2c3d4e5f6a7b8c9d0e1f", "createdByName": "Maya Lin", "updatedAt": "2024-11-05T15:10:02.000Z", "resolvedAt": null, "resolvedBy": null}{ "message": "Forbidden", "code": "FORBIDDEN", "details": []}| Error Code | Title | Description | Resolution |
|---|---|---|---|
notebook_no_access | No access to notebook | The notebook exists but the caller is not a member, or it is not open | Verify user identity or request access from the owner |
{ "message": "Notebook, node, or comment not found", "code": "NOT_FOUND", "details": []}| Error Code | Title | Description | Resolution |
|---|---|---|---|
notebook_not_found | Notebook not found | No notebook exists with the provided ID | Verify the notebook ID in the URL |
{ "message": "Comment version does not match expectedVersion", "code": "VERSION_CONFLICT", "details": [ { "path": "expectedVersion", "message": "supplied expectedVersion does not match current version" } ]}Re-anchor a comment
Section titled “Re-anchor a comment”Updates the root comment anchor for a comment thread. Use this when the original anchor has been orphaned (the underlying text or block has moved or been deleted) and the comment should be reattached elsewhere.
POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}/reanchor
Section titled “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 that owns the document |
nodeId | path | string | Yes | Identifier of the document node the comment is attached to |
commentId | path | string | Yes | Identifier of the comment thread root to re-anchor |
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
anchor | object | Yes | New anchor for the comment thread. Uses the same three variants as create: { "type": "document" }, { "type": "block", "blockId": "<block id>" }, or a text-range with startBlockId, startOffset, endBlockId, endOffset (and optional quote, contextBefore, contextAfter) |
expectedVersion | integer | No | Version returned by the previous read; the call returns 409 if the comment has changed since |
curl -X POST "https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}/reanchor" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "anchor": { "type": "block", "blockId": "blk5a8b92d4e7c1f3a6b9d2e5c8" }, "expectedVersion": 2 }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const reanchored = await client.notes.comments.setAnchor( 'nb8f3a1c2e3b4d5f6a7b8c9de', 'nd42b1e7c8d9e0f1a2b3c4d5e', 'cm7a4f31c2d8e9f1a2b3c4d5e', { anchor: { type: 'block', blockId: 'blk5a8b92d4e7c1f3a6b9d2e5c8' }, expectedVersion: 2, },);{ "id": "cm7a4f31c2d8e9f1a2b3c4d5e", "documentId": "nd42b1e7c8d9e0f1a2b3c4d5e", "parentId": null, "anchorBlockId": "blk5a8b92d4e7c1f3a6b9d2e5c8", "anchorType": "block", "startBlockId": null, "startOffset": null, "endBlockId": null, "endOffset": null, "anchorQuote": null, "anchorContextBefore": null, "anchorContextAfter": null, "anchorStatus": "active", "anchorUpdatedAt": "2024-11-05T15:34:18.000Z", "version": 3, "content": "Please add a citation here.", "createdAt": "2024-11-05T14:22:31.000Z", "createdBy": "usera1b2c3d4e5f6a7b8c9d0e1f", "createdByName": "Maya Lin", "updatedAt": "2024-11-05T15:34:18.000Z", "resolvedAt": null, "resolvedBy": null}{ "message": "Invalid request body", "code": "VALIDATION_ERROR", "details": [ { "path": "anchor", "message": "anchor is required" } ]}{ "message": "Forbidden", "code": "FORBIDDEN", "details": []}| Error Code | Title | Description | Resolution |
|---|---|---|---|
notebook_no_access | No access to notebook | The notebook exists but the caller is not a member, or it is not open | Verify user identity or request access from the owner |
{ "message": "Notebook, node, or comment not found", "code": "NOT_FOUND", "details": []}| Error Code | Title | Description | Resolution |
|---|---|---|---|
notebook_not_found | Notebook not found | No notebook exists with the provided ID | Verify the notebook ID in the URL |
{ "message": "Comment version does not match expectedVersion", "code": "VERSION_CONFLICT", "details": [ { "path": "expectedVersion", "message": "supplied expectedVersion does not match current version" } ]}Resolve a comment
Section titled “Resolve a comment”Marks a comment as resolved. The response carries the updated resolvedAt and resolvedBy fields.
POST /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}/resolve
Section titled “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 that owns the document |
nodeId | path | string | Yes | Identifier of the document node the comment is attached to |
commentId | path | string | Yes | Identifier of the comment to resolve |
Request Body
Section titled “Request Body”| Name | Type | Required | Description |
|---|---|---|---|
expectedVersion | integer | No | Version returned by the previous read; the call returns 409 if the comment has changed since |
curl -X POST "https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}/resolve" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "expectedVersion": 3 }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const resolved = await client.notes.comments.resolve( 'nb8f3a1c2e3b4d5f6a7b8c9de', 'nd42b1e7c8d9e0f1a2b3c4d5e', 'cm7a4f31c2d8e9f1a2b3c4d5e', { expectedVersion: 3 },);{ "id": "cm7a4f31c2d8e9f1a2b3c4d5e", "documentId": "nd42b1e7c8d9e0f1a2b3c4d5e", "parentId": null, "anchorBlockId": "blk5a8b92d4e7c1f3a6b9d2e5c8", "anchorType": "block", "startBlockId": null, "startOffset": null, "endBlockId": null, "endOffset": null, "anchorQuote": null, "anchorContextBefore": null, "anchorContextAfter": null, "anchorStatus": "active", "anchorUpdatedAt": "2024-11-05T15:34:18.000Z", "version": 4, "content": "Please add a citation here.", "createdAt": "2024-11-05T14:22:31.000Z", "createdBy": "usera1b2c3d4e5f6a7b8c9d0e1f", "createdByName": "Maya Lin", "updatedAt": "2024-11-05T15:50:44.000Z", "resolvedAt": "2024-11-05T15:50:44.000Z", "resolvedBy": "usera1b2c3d4e5f6a7b8c9d0e1f"}{ "message": "Forbidden", "code": "FORBIDDEN", "details": []}| Error Code | Title | Description | Resolution |
|---|---|---|---|
notebook_no_access | No access to notebook | The notebook exists but the caller is not a member, or it is not open | Verify user identity or request access from the owner |
{ "message": "Notebook, node, or comment not found", "code": "NOT_FOUND", "details": []}| Error Code | Title | Description | Resolution |
|---|---|---|---|
notebook_not_found | Notebook not found | No notebook exists with the provided ID | Verify the notebook ID in the URL |
{ "message": "Comment version does not match expectedVersion", "code": "VERSION_CONFLICT", "details": [ { "path": "expectedVersion", "message": "supplied expectedVersion does not match current version" } ]}Delete a comment
Section titled “Delete a comment”Deletes a comment and all of its replies. Pass expectedVersion as a query parameter to require an exact version match; if the comment has been updated since you read it, the call returns 409 and you should refetch before retrying.
DELETE /api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}
Section titled “DELETE /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 that owns the document |
nodeId | path | string | Yes | Identifier of the document node the comment is attached to |
commentId | path | string | Yes | Identifier of the comment to delete |
expectedVersion | query | integer | No | Version returned by the previous read; the call returns 409 if the comment has changed since |
curl -X DELETE "https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com/api/v1/notes/notebooks/{notebookId}/nodes/{nodeId}/comments/{commentId}?expectedVersion=4" \ -H "Authorization: Bearer $HOODY_TOKEN"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-notes-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
const result = await client.notes.comments.delete( 'nb8f3a1c2e3b4d5f6a7b8c9de', 'nd42b1e7c8d9e0f1a2b3c4d5e', 'cm7a4f31c2d8e9f1a2b3c4d5e', { expectedVersion: 4 },);{ "success": true}{ "message": "Forbidden", "code": "FORBIDDEN", "details": []}| Error Code | Title | Description | Resolution |
|---|---|---|---|
notebook_no_access | No access to notebook | The notebook exists but the caller is not a member, or it is not open | Verify user identity or request access from the owner |
{ "message": "Notebook, node, or comment not found", "code": "NOT_FOUND", "details": []}| Error Code | Title | Description | Resolution |
|---|---|---|---|
notebook_not_found | Notebook not found | No notebook exists with the provided ID | Verify the notebook ID in the URL |
{ "message": "Comment version does not match expectedVersion", "code": "VERSION_CONFLICT", "details": [ { "path": "expectedVersion", "message": "supplied expectedVersion does not match current version" } ]}