---
title: Comment
kind: resource
slug: comment
summary: "Reviewer feedback attached to a Droplet — page-level or pinned to a specific element. Powers the in-app review loop and the AI-edit loop (`drop comments create/list/resolve/delete`)."
---

# Comment

Reviewer feedback attached to a Droplet — page-level or pinned to a specific element. Powers the in-app review loop and the AI-edit loop (`drop comments create/list/resolve/delete`).

A **Comment** is a piece of reviewer feedback attached to a [Droplet](/docs/resources/droplet). Comments can be page-level or **pinned** to a specific element via a CSS selector; they can have **replies** (threads); and they have a **resolved** flag for closing the loop.

Comments are first-class on the API as of feat/043: `create` / `list` / `get` / `resolve` / `unresolve` / `delete`, plus `thumbs-up` / `unthumbs-up` for reactions. `create` inherits **read** permission — anyone who can see the Droplet can comment on it, matching the web UI. **The AI-loop primary close verb is `resolve`, not `delete`** — resolve preserves the conversation history; delete is for actual removal (spam, mistakes).

Reactions today are a single type (thumbs-up). The `reactionType` column on `CommentReaction` is a Postgres enum shaped for future expansion (👏 ❤️ 😄 etc.) without a destructive migration.

## The Comment object

### Attributes

- **`id`** `string` — CUID primary key. Stable identifier — appears in `/api/comments/{id}` paths and in MCP tool inputs.
- **`appSlug`** `string` — The slug of the parent Droplet. Returned by `get` but not by `list` (list is already scoped to a slug).
- **`parentId`** `string · nullable` — Non-null when this comment is a reply to another. The contract returns replies nested under their parent (`comment.replies: Comment[]`), so most callers never need to walk `parentId` themselves.
- **`authorName`** `string` — Display name at the time the comment was authored.
- **`authorEmail`** `string · nullable` — Author email at the time the comment was authored. Nullable for legacy comments.
- **`body`** `string` — Comment text. May include `@mentions` rendered as `@Jane Doe` in markdown export; raw `@[name](email)` form is the storage convention. (Tracks the UI convention from #41 mentions.)
- **`targetSelector`** `string · nullable` — CSS selector identifying the pinned element. `null` for page-level comments. The selector is captured at pin time; if the element later moves or is deleted, the stored selector may not resolve against the current HTML — that's the caller's problem to handle.
- **`resolved`** `boolean` — True when the comment has been marked resolved. Flip via [`POST /api/comments/{id}/resolve`](#resolve) or [`POST /api/comments/{id}/unresolve`](#unresolve). **Resolve is the AI-loop primary close verb**, not delete — preserves the audit trail.
- **`thumbsUpCount`** `number` — Computed at read time. Number of distinct users who have thumbs-up'd this comment. Always ≥ 0.
- **`viewerThumbedUp`** `boolean` — Computed at read time. True when the caller has reacted to this comment with a thumbs-up. False for unauthenticated callers and external share-link viewers.
- **`createdAt`** `timestamp` — ISO 8601 timestamp of creation.
- **`updatedAt`** `timestamp` — ISO 8601 timestamp of last metadata update (resolved-state flips bump this).
- **`targetText`** `string · nullable · less-common` — Snippet of the pinned element's text content at pin time. Used by the UI for pin-target rendering when the selector still matches.
- **`targetVersion`** `number · nullable · less-common` — The Droplet version the pin was created against. Used by the UI to flag stale pins when the live version differs.

### JSON example

```json
{
  "id": "cmt_a1b2c3",
  "appSlug": "my-droplet",
  "parentId": null,
  "authorId": "usr_dan",
  "authorName": "Dan Perrera",
  "authorEmail": "dan.perrera@himarley.com",
  "body": "Can we add a third initiative around onboarding? See last week's retro.",
  "targetSelector": null,
  "targetText": null,
  "targetVersion": null,
  "resolved": false,
  "thumbsUpCount": 2,
  "viewerThumbedUp": true,
  "createdAt": "2026-06-08T22:31:54.000Z",
  "updatedAt": "2026-06-08T22:31:54.000Z"
}
```

## Methods

### Create a Comment

`POST /api/apps/{slug}/comments`

Post a comment on a Droplet. Allowed for anyone who can read it — comment creation inherits READ permission, matching the web UI. Pass `parentId` to reply into an existing thread, or `targetSelector` + `targetText` to pin the comment to an element.

**Pinning durably takes both fields.** The viewer resolves a pin by trying `targetSelector` first, then falling back to finding the element whose text matches `targetText`. A pin with a selector but no `targetText` has no fallback: once the selector stops resolving — which any re-deploy that shifts the DOM will do — the pin is dead. Get both from the live HTML (`GET /api/apps/{slug}/versions/{v}/download`, or `drop download <slug>`); `targetText` must be the element's textContent, trimmed, first 100 characters.

The Droplet version is stamped server-side. @mentions in `body` notify the mentioned users; any who lack access come back in `inaccessibleMentions`, and any auto-granted viewer access come back in `grantedAccess`. Returns 403 when comments are disabled on the Droplet.

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.
- **`body`** _(string · body · required)_ — The comment text (1-8000 characters).
- **`parentId`** _(string · body)_ — Reply into an existing thread. Omit for a top-level comment.
- **`targetSelector`** _(string · body)_ — CSS selector to pin the comment to. Omit for page-level. Pair with `targetText` or the pin breaks on re-deploy.
- **`targetText`** _(string · body)_ — The pin target's textContent, trimmed, first 100 chars. The fallback that makes a pin survive a re-deploy.
- **`targetRect`** _(string · body)_ — Pin position within the target element. Set by the UI / SDK.
- **`targetVersion`** _(number · body)_ — Droplet version the pin was created against.

**CLI**

```bash
drop comments create customer-forum-2026-product-deck --body "This slide needs a source citation for the 40% figure." --target-selector #d2-07-inquiry-assist --target-text "Inquiry Assist"
```

**MCP**

```json
{
  "tool": "drop_comments_create",
  "arguments": {
    "slug": "customer-forum-2026-product-deck",
    "body": "This slide needs a source citation for the 40% figure.",
    "targetSelector": "#d2-07-inquiry-assist",
    "targetText": "Inquiry Assist"
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/apps/customer-forum-2026-product-deck/comments' \
  -H "Authorization: Bearer $DROP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"body":"This slide needs a source citation for the 40% figure.","targetSelector":"#d2-07-inquiry-assist","targetText":"Inquiry Assist"}'
```

**Response** (201)

```json
{
  "comment": {
    "id": "clx9k2p0000008l3f5g7h9k2m",
    "parentId": null,
    "authorId": "usr_jackie",
    "authorName": "Jackie Booth",
    "authorEmail": "jackie.booth@himarley.com",
    "body": "This slide needs a source citation for the 40% figure.",
    "targetSelector": "#d2-07-inquiry-assist",
    "targetText": "Inquiry Assist",
    "targetVersion": 12,
    "resolved": false,
    "thumbsUpCount": 0,
    "viewerThumbedUp": false,
    "createdAt": "2026-08-24T14:02:11.000Z",
    "updatedAt": "2026-08-24T14:02:11.000Z"
  }
}
```

### List Comments on a Droplet

`GET /api/apps/{slug}/comments`

Return all comments on a Droplet the caller can read, in chronological order (oldest first). Each comment includes `thumbsUpCount` and `viewerThumbedUp` (computed on read). Threads are returned nested under `replies`. Filter by `resolved=true|false` to scope to open or resolved comments only; omit to include both.

**Parameters**

- **`slug`** _(string · path · required)_ — The Droplet slug.
- **`resolved`** _(boolean · query)_ — Filter by resolved state. Omit to include both.

**CLI**

```bash
drop comments list my-droplet
```

**MCP**

```json
{
  "tool": "drop_comments_list",
  "arguments": {
    "slug": "my-droplet"
  }
}
```

**HTTP**

```bash
curl -X GET 'https://drop.himarley.ai/api/apps/my-droplet/comments' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
[
  {
    "id": "clx9k2p0000008l3f5g7h9k2m",
    "parentId": null,
    "authorId": "usr_dan",
    "authorName": "Dan Perrera",
    "authorEmail": "dan.perrera@himarley.com",
    "body": "Can we add a third initiative around onboarding?",
    "targetSelector": null,
    "resolved": false,
    "thumbsUpCount": 2,
    "viewerThumbedUp": true,
    "createdAt": "2026-06-08T22:31:54.000Z",
    "updatedAt": "2026-06-08T22:31:54.000Z",
    "replies": []
  }
]
```

### Get a Comment

`GET /api/comments/{id}`

Fetch a single comment by id. Returns 404 if the caller cannot read the parent Droplet (no enumeration leak of comment ids).

**Parameters**

- **`id`** _(string · path · required)_ — The comment id.

**CLI**

```bash
drop comments get clx9k2p0000008l3f5g7h9k2m
```

**MCP**

```json
{
  "tool": "drop_comments_get",
  "arguments": {
    "id": "clx9k2p0000008l3f5g7h9k2m"
  }
}
```

**HTTP**

```bash
curl -X GET 'https://drop.himarley.ai/api/comments/clx9k2p0000008l3f5g7h9k2m' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "id": "clx9k2p0000008l3f5g7h9k2m",
  "parentId": null,
  "authorId": "usr_dan",
  "authorName": "Dan Perrera",
  "authorEmail": "dan.perrera@himarley.com",
  "body": "Can we add a third initiative around onboarding?",
  "targetSelector": null,
  "resolved": false,
  "thumbsUpCount": 2,
  "viewerThumbedUp": true,
  "createdAt": "2026-06-08T22:31:54.000Z",
  "updatedAt": "2026-06-08T22:31:54.000Z"
}
```

### Resolve a Comment

`POST /api/comments/{id}/resolve`

Mark a comment as resolved. **This is the AI-loop primary close verb** — preserves the conversation history (unlike `delete`). Allowed for anyone who can read the parent Droplet (non-destructive). Idempotent: resolving an already-resolved comment returns 204 with no body.

**Parameters**

- **`id`** _(string · path · required)_ — The comment id.

**CLI**

```bash
drop comments resolve clx9k2p0000008l3f5g7h9k2m
```

**MCP**

```json
{
  "tool": "drop_comments_resolve",
  "arguments": {
    "id": "clx9k2p0000008l3f5g7h9k2m"
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/comments/clx9k2p0000008l3f5g7h9k2m/resolve' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "id": "clx9k2p0000008l3f5g7h9k2m",
  "parentId": null,
  "authorId": "usr_dan",
  "authorName": "Dan Perrera",
  "authorEmail": "dan.perrera@himarley.com",
  "body": "Can we add a third initiative around onboarding?",
  "targetSelector": null,
  "resolved": true,
  "thumbsUpCount": 2,
  "viewerThumbedUp": false,
  "createdAt": "2026-06-08T22:31:54.000Z",
  "updatedAt": "2026-06-09T15:02:11.000Z"
}
```

### Unresolve a Comment

`POST /api/comments/{id}/unresolve`

Reopen a previously-resolved comment. Allowed for anyone who can read the parent Droplet. Idempotent: unresolving an already-open comment returns 204.

**Parameters**

- **`id`** _(string · path · required)_ — The comment id.

**CLI**

```bash
drop comments unresolve clx9k2p0000008l3f5g7h9k2m
```

**MCP**

```json
{
  "tool": "drop_comments_unresolve",
  "arguments": {
    "id": "clx9k2p0000008l3f5g7h9k2m"
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/comments/clx9k2p0000008l3f5g7h9k2m/unresolve' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "id": "clx9k2p0000008l3f5g7h9k2m",
  "parentId": null,
  "authorId": "usr_dan",
  "authorName": "Dan Perrera",
  "authorEmail": "dan.perrera@himarley.com",
  "body": "Can we add a third initiative around onboarding?",
  "targetSelector": null,
  "resolved": false,
  "thumbsUpCount": 2,
  "viewerThumbedUp": false,
  "createdAt": "2026-06-08T22:31:54.000Z",
  "updatedAt": "2026-06-09T15:30:00.000Z"
}
```

### Delete a Comment

`DELETE /api/comments/{id}`

Permanently delete a comment. Cascades to its replies AND their reactions. Allowed when the caller is the comment author OR has edit permission on the parent Droplet. **For actual removal only (spam, mistakes) — NOT the AI-loop close verb. Use `resolve` for "this has been addressed".**

**Parameters**

- **`id`** _(string · path · required)_ — The comment id.

**CLI**

```bash
drop comments delete clx9k2p0000008l3f5g7h9k2m
```

**MCP**

```json
{
  "tool": "drop_comments_delete",
  "arguments": {
    "id": "clx9k2p0000008l3f5g7h9k2m"
  }
}
```

**HTTP**

```bash
curl -X DELETE 'https://drop.himarley.ai/api/comments/clx9k2p0000008l3f5g7h9k2m' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "deleted": true,
  "id": "clx9k2p0000008l3f5g7h9k2m",
  "repliesDeleted": 2,
  "reactionsDeleted": 3
}
```

### Add a Thumbs-Up Reaction

`POST /api/comments/{id}/thumbs-up`

Add the caller's thumbs-up to a comment. Allowed for anyone who can read the parent Droplet (inherits READ permission, not comment-create). Idempotent: calling when already reacted returns 204. External share-link viewers receive 403.

**Parameters**

- **`id`** _(string · path · required)_ — The comment id.

**CLI**

```bash
drop comments thumbs-up clx9k2p0000008l3f5g7h9k2m
```

**MCP**

```json
{
  "tool": "drop_comments_thumbs_up",
  "arguments": {
    "id": "clx9k2p0000008l3f5g7h9k2m"
  }
}
```

**HTTP**

```bash
curl -X POST 'https://drop.himarley.ai/api/comments/clx9k2p0000008l3f5g7h9k2m/thumbs-up' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "id": "clx9k2p0000008l3f5g7h9k2m",
  "thumbsUpCount": 3,
  "viewerThumbedUp": true
}
```

### Remove a Thumbs-Up Reaction

`DELETE /api/comments/{id}/thumbs-up`

Remove the caller's thumbs-up from a comment. Idempotent: calling when no reaction exists returns 204.

**Parameters**

- **`id`** _(string · path · required)_ — The comment id.

**CLI**

```bash
drop comments unthumbs-up clx9k2p0000008l3f5g7h9k2m
```

**MCP**

```json
{
  "tool": "drop_comments_unthumbs_up",
  "arguments": {
    "id": "clx9k2p0000008l3f5g7h9k2m"
  }
}
```

**HTTP**

```bash
curl -X DELETE 'https://drop.himarley.ai/api/comments/clx9k2p0000008l3f5g7h9k2m/thumbs-up' \
  -H "Authorization: Bearer $DROP_TOKEN"
```

**Response** (200)

```json
{
  "id": "clx9k2p0000008l3f5g7h9k2m",
  "thumbsUpCount": 2,
  "viewerThumbedUp": false
}
```
