# Notes: Comments

**Page:** api/notes/comments

[Download Raw Markdown](./api/notes/comments.md)

---

{/* AUTO-GENERATED — Do not edit manually. Regenerate with: npm run docs:api:generate */}



The Notes Comments API lets you create, list, edit, resolve, re-anchor, and delete comment threads attached to document nodes inside a notebook. Each comment is tied to an anchor (the whole document, a specific block, or a text range within a block). The edit, resolve, re-anchor, and delete operations support an optional `expectedVersion` for optimistic concurrency.

## List comment anchors

Returns lightweight thread anchor metadata for comment decorations. This endpoint is intended for clients that need to render comment markers without fetching full thread bodies.




```ts
const result = await client.notes.comments.listAnchors({
  notebookId: "nb_8f3a1c2e",
  nodeId: "nd_42b1e7",
  limit: 500,
});
```




```bash
curl "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2e/nodes/nd_42b1e7/comment-anchors?limit=500" \
  -H "Authorization: Bearer <token>"
```




### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Identifier of the notebook containing the document |
| `nodeId` | path | string | Yes | Identifier of the document node |
| `limit` | query | integer | No | Maximum number of anchors to return. Default: `500` |
| `offset` | query | integer | No | Number of anchors to skip. Default: `0` |
| `cursor` | query | string | No | Opaque pagination cursor returned by a previous response |

### Response




```json
{
  "anchors": [
    {
      "threadId": "thr_91c2a4",
      "anchor": {
        "anchorType": "text-range",
        "anchorBlockId": "blk_3f2c01",
        "startBlockId": "blk_3f2c01",
        "startOffset": 12,
        "endBlockId": "blk_3f2c01",
        "endOffset": 48,
        "anchorQuote": "the implementation should be idempotent",
        "anchorContextBefore": "we agreed that ",
        "anchorContextAfter": " in all write paths.",
        "anchorStatus": "active",
        "anchorUpdatedAt": "2026-01-14T09:12:33.000Z"
      },
      "anchorStatus": "active",
      "resolvedAt": null,
      "version": 1
    }
  ],
  "nextCursor": null,
  "hasMore": false
}
```




```json
{
  "message": "Invalid cursor",
  "code": "invalid_cursor",
  "details": [
    { "path": "cursor", "message": "Cursor is malformed or expired" }
  ]
}
```




```json
{
  "message": "Forbidden",
  "code": "forbidden",
  "details": []
}
```




```json
{
  "message": "Not found",
  "code": "not_found",
  "details": [
    { "path": "nodeId", "message": "Node does not exist in this notebook" }
  ]
}
```




```json
{
  "message": "Conflict",
  "code": "conflict",
  "details": [
    { "path": "nodeId", "message": "Node has been moved or archived" }
  ]
}
```




## List comments

Returns all comments attached to a document node, paginated by cursor.




```ts
const result = await client.notes.comments.list({
  notebookId: "nb_8f3a1c2e",
  nodeId: "nd_42b1e7",
  limit: 100,
});
```




```bash
curl "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2e/nodes/nd_42b1e7/comments?limit=100" \
  -H "Authorization: Bearer <token>"
```




### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Identifier of the notebook containing the document |
| `nodeId` | path | string | Yes | Identifier of the document node |
| `limit` | query | integer | No | Maximum number of comments to return. Default: `100` |
| `offset` | query | integer | No | Number of comments to skip. Default: `0` |
| `cursor` | query | string | No | Opaque pagination cursor returned by a previous response |

### Response




```json
{
  "comments": [
    {
      "id": "cm_7a4f31c2",
      "documentId": "nd_42b1e7",
      "parentId": null,
      "anchorBlockId": "blk_3f2c01",
      "anchorType": "text-range",
      "startBlockId": "blk_3f2c01",
      "startOffset": 12,
      "endBlockId": "blk_3f2c01",
      "endOffset": 48,
      "anchorQuote": "the implementation should be idempotent",
      "anchorContextBefore": "we agreed that ",
      "anchorContextAfter": " in all write paths.",
      "anchorStatus": "active",
      "anchorUpdatedAt": "2026-01-14T09:12:33.000Z",
      "version": 3,
      "content": "Should we add a note about the retry budget?",
      "createdAt": "2026-01-14T09:12:33.000Z",
      "createdBy": "usr_a1b2c3",
      "createdByName": "Jamie Park",
      "updatedAt": "2026-01-14T10:04:11.000Z",
      "resolvedAt": null,
      "resolvedBy": null
    }
  ],
  "nextCursor": null,
  "hasMore": false
}
```




```json
{
  "message": "Invalid cursor",
  "code": "invalid_cursor",
  "details": [
    { "path": "cursor", "message": "Cursor is malformed or expired" }
  ]
}
```




```json
{
  "message": "Forbidden",
  "code": "forbidden",
  "details": []
}
```




```json
{
  "message": "Not found",
  "code": "not_found",
  "details": [
    { "path": "nodeId", "message": "Node does not exist in this notebook" }
  ]
}
```




```json
{
  "message": "Conflict",
  "code": "conflict",
  "details": [
    { "path": "nodeId", "message": "Node has been moved or archived" }
  ]
}
```




## Create a comment

Creates a new comment on a document node. The `anchor` field describes where the comment is attached. Pass `parentId` to create a reply to an existing comment.




```ts
const comment = await client.notes.comments.create({
  notebookId: "nb_8f3a1c2e",
  nodeId: "nd_42b1e7",
  data: {
    content: "Should we add a note about the retry budget?",
    anchor: {
      type: "text-range",
      startBlockId: "blk_3f2c01",
      startOffset: 12,
      endBlockId: "blk_3f2c01",
      endOffset: 48,
      quote: "the implementation should be idempotent",
      contextBefore: "we agreed that ",
      contextAfter: " in all write paths.",
    },
  },
});
```




```bash
curl -X POST "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2e/nodes/nd_42b1e7/comments" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Should we add a note about the retry budget?",
    "anchor": {
      "type": "text-range",
      "startBlockId": "blk_3f2c01",
      "startOffset": 12,
      "endBlockId": "blk_3f2c01",
      "endOffset": 48,
      "quote": "the implementation should be idempotent",
      "contextBefore": "we agreed that ",
      "contextAfter": " in all write paths."
    }
  }'
```




### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Identifier of the notebook containing the document |
| `nodeId` | path | string | Yes | Identifier of the document node |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `content` | string | Yes | Comment text. Length: 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. Use the structured `anchor` field for richer anchors |
| `anchor` | object | No | Structured anchor describing where the comment is placed. One of `document`, `block`, or `text-range` shapes |

### Response




```json
{
  "id": "cm_7a4f31c2",
  "documentId": "nd_42b1e7",
  "parentId": null,
  "anchorBlockId": "blk_3f2c01",
  "anchorType": "text-range",
  "startBlockId": "blk_3f2c01",
  "startOffset": 12,
  "endBlockId": "blk_3f2c01",
  "endOffset": 48,
  "anchorQuote": "the implementation should be idempotent",
  "anchorContextBefore": "we agreed that ",
  "anchorContextAfter": " in all write paths.",
  "anchorStatus": "active",
  "anchorUpdatedAt": "2026-01-14T09:12:33.000Z",
  "version": 1,
  "content": "Should we add a note about the retry budget?",
  "createdAt": "2026-01-14T09:12:33.000Z",
  "createdBy": "usr_a1b2c3",
  "createdByName": "Jamie Park",
  "updatedAt": null,
  "resolvedAt": null,
  "resolvedBy": null
}
```




```json
{
  "message": "Invalid request",
  "code": "invalid_request",
  "details": [
    { "path": "content", "message": "content must be between 1 and 10000 characters" }
  ]
}
```




```json
{
  "message": "Forbidden",
  "code": "forbidden",
  "details": []
}
```




```json
{
  "message": "Not found",
  "code": "not_found",
  "details": [
    { "path": "nodeId", "message": "Node does not exist in this notebook" }
  ]
}
```




```json
{
  "message": "Conflict",
  "code": "conflict",
  "details": [
    { "path": "anchor.startBlockId", "message": "Anchor block has been deleted" }
  ]
}
```




```json
{
  "message": "Internal server error",
  "code": "internal_error",
  "details": []
}
```




## Re-anchor a comment thread

Updates the root comment anchor for an existing thread. Use this when the original anchor target has moved or been edited and the comment needs to track a new location.




```ts
const updated = await client.notes.comments.reanchor({
  notebookId: "nb_8f3a1c2e",
  nodeId: "nd_42b1e7",
  commentId: "cm_7a4f31c2",
  data: {
    anchor: {
      type: "block",
      blockId: "blk_3f2c01",
    },
    expectedVersion: 1,
  },
});
```




```bash
curl -X POST "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2e/nodes/nd_42b1e7/comments/cm_7a4f31c2/reanchor" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "anchor": { "type": "block", "blockId": "blk_3f2c01" },
    "expectedVersion": 1
  }'
```




### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Identifier of the notebook containing the document |
| `nodeId` | path | string | Yes | Identifier of the document node |
| `commentId` | path | string | Yes | Identifier of the root comment in the thread |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `anchor` | object | Yes | New anchor for the thread. One of `document`, `block`, or `text-range` shapes |
| `expectedVersion` | integer | No | Current version of the comment. If it does not match, the request is rejected with `409` |

### Response




```json
{
  "id": "cm_7a4f31c2",
  "documentId": "nd_42b1e7",
  "parentId": null,
  "anchorBlockId": "blk_3f2c01",
  "anchorType": "block",
  "startBlockId": null,
  "startOffset": null,
  "endBlockId": null,
  "endOffset": null,
  "anchorQuote": null,
  "anchorContextBefore": null,
  "anchorContextAfter": null,
  "anchorStatus": "active",
  "anchorUpdatedAt": "2026-01-14T11:02:18.000Z",
  "version": 2,
  "content": "Should we add a note about the retry budget?",
  "createdAt": "2026-01-14T09:12:33.000Z",
  "createdBy": "usr_a1b2c3",
  "createdByName": "Jamie Park",
  "updatedAt": "2026-01-14T11:02:18.000Z",
  "resolvedAt": null,
  "resolvedBy": null
}
```




```json
{
  "message": "Invalid request",
  "code": "invalid_request",
  "details": [
    { "path": "anchor", "message": "anchor must be one of document, block, or text-range" }
  ]
}
```




```json
{
  "message": "Forbidden",
  "code": "forbidden",
  "details": []
}
```




```json
{
  "message": "Not found",
  "code": "not_found",
  "details": [
    { "path": "commentId", "message": "Comment does not exist on this node" }
  ]
}
```




```json
{
  "message": "Version conflict",
  "code": "version_conflict",
  "details": [
    { "path": "expectedVersion", "message": "Expected version 1, found 2" }
  ]
}
```




## Resolve a comment

Marks a comment as resolved and records the resolving user and timestamp.




```ts
const resolved = await client.notes.comments.resolve({
  notebookId: "nb_8f3a1c2e",
  nodeId: "nd_42b1e7",
  commentId: "cm_7a4f31c2",
  data: {
    expectedVersion: 2,
  },
});
```




```bash
curl -X POST "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2e/nodes/nd_42b1e7/comments/cm_7a4f31c2/resolve" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{ "expectedVersion": 2 }'
```




### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Identifier of the notebook containing the document |
| `nodeId` | path | string | Yes | Identifier of the document node |
| `commentId` | path | string | Yes | Identifier of the comment to resolve |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `expectedVersion` | integer | No | Current version of the comment. If it does not match, the request is rejected with `409` |

### Response




```json
{
  "id": "cm_7a4f31c2",
  "documentId": "nd_42b1e7",
  "parentId": null,
  "anchorBlockId": "blk_3f2c01",
  "anchorType": "text-range",
  "startBlockId": "blk_3f2c01",
  "startOffset": 12,
  "endBlockId": "blk_3f2c01",
  "endOffset": 48,
  "anchorQuote": "the implementation should be idempotent",
  "anchorContextBefore": "we agreed that ",
  "anchorContextAfter": " in all write paths.",
  "anchorStatus": "active",
  "anchorUpdatedAt": "2026-01-14T09:12:33.000Z",
  "version": 3,
  "content": "Should we add a note about the retry budget?",
  "createdAt": "2026-01-14T09:12:33.000Z",
  "createdBy": "usr_a1b2c3",
  "createdByName": "Jamie Park",
  "updatedAt": "2026-01-14T11:08:01.000Z",
  "resolvedAt": "2026-01-14T11:08:01.000Z",
  "resolvedBy": "usr_a1b2c3"
}
```




```json
{
  "message": "Forbidden",
  "code": "forbidden",
  "details": []
}
```




```json
{
  "message": "Not found",
  "code": "not_found",
  "details": [
    { "path": "commentId", "message": "Comment does not exist on this node" }
  ]
}
```




```json
{
  "message": "Version conflict",
  "code": "version_conflict",
  "details": [
    { "path": "expectedVersion", "message": "Expected version 2, found 3" }
  ]
}
```




## Edit a comment

Edits the content of an existing comment. Only the author of the comment is permitted to edit it.




```ts
const edited = await client.notes.comments.edit({
  notebookId: "nb_8f3a1c2e",
  nodeId: "nd_42b1e7",
  commentId: "cm_7a4f31c2",
  data: {
    content: "Should we add a note about the retry budget and the circuit breaker?",
    expectedVersion: 1,
  },
});
```




```bash
curl -X PATCH "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2e/nodes/nd_42b1e7/comments/cm_7a4f31c2" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Should we add a note about the retry budget and the circuit breaker?",
    "expectedVersion": 1
  }'
```




### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Identifier of the notebook containing the document |
| `nodeId` | path | string | Yes | Identifier of the document node |
| `commentId` | path | string | Yes | Identifier of the comment to edit |

### Request Body

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `content` | string | Yes | New comment text. Length: 1 to 10000 characters |
| `expectedVersion` | integer | No | Current version of the comment. If it does not match, the request is rejected with `409` |

### Response




```json
{
  "id": "cm_7a4f31c2",
  "documentId": "nd_42b1e7",
  "parentId": null,
  "anchorBlockId": "blk_3f2c01",
  "anchorType": "text-range",
  "startBlockId": "blk_3f2c01",
  "startOffset": 12,
  "endBlockId": "blk_3f2c01",
  "endOffset": 48,
  "anchorQuote": "the implementation should be idempotent",
  "anchorContextBefore": "we agreed that ",
  "anchorContextAfter": " in all write paths.",
  "anchorStatus": "active",
  "anchorUpdatedAt": "2026-01-14T09:12:33.000Z",
  "version": 2,
  "content": "Should we add a note about the retry budget and the circuit breaker?",
  "createdAt": "2026-01-14T09:12:33.000Z",
  "createdBy": "usr_a1b2c3",
  "createdByName": "Jamie Park",
  "updatedAt": "2026-01-14T10:04:11.000Z",
  "resolvedAt": null,
  "resolvedBy": null
}
```




```json
{
  "message": "Forbidden",
  "code": "forbidden",
  "details": [
    { "path": "commentId", "message": "Only the comment author can edit this comment" }
  ]
}
```




```json
{
  "message": "Not found",
  "code": "not_found",
  "details": [
    { "path": "commentId", "message": "Comment does not exist on this node" }
  ]
}
```




```json
{
  "message": "Version conflict",
  "code": "version_conflict",
  "details": [
    { "path": "expectedVersion", "message": "Expected version 1, found 2" }
  ]
}
```




## Delete a comment

Deletes a comment and all of its replies. This action is irreversible.




```ts
const result = await client.notes.comments.delete({
  notebookId: "nb_8f3a1c2e",
  nodeId: "nd_42b1e7",
  commentId: "cm_7a4f31c2",
  expectedVersion: 3,
});
```




```bash
curl -X DELETE "https://api.hoody.com/api/v1/notes/notebooks/nb_8f3a1c2e/nodes/nd_42b1e7/comments/cm_7a4f31c2?expectedVersion=3" \
  -H "Authorization: Bearer <token>"
```




### Parameters

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `notebookId` | path | string | Yes | Identifier of the notebook containing the document |
| `nodeId` | path | string | Yes | Identifier of the document node |
| `commentId` | path | string | Yes | Identifier of the comment to delete |
| `expectedVersion` | query | integer | No | Current version of the comment. If it does not match, the request is rejected with `409` |

### Response




```json
{
  "success": true
}
```




```json
{
  "message": "Forbidden",
  "code": "forbidden",
  "details": []
}
```




```json
{
  "message": "Not found",
  "code": "not_found",
  "details": [
    { "path": "commentId", "message": "Comment does not exist on this node" }
  ]
}
```




```json
{
  "message": "Version conflict",
  "code": "version_conflict",
  "details": [
    { "path": "expectedVersion", "message": "Expected version 3, found 4" }
  ]
}
```