Skip to content

Comments and annotations

Comments are threaded notes on an entry, optionally pinned to one exact version. Annotations are marks drawn over one exact preview or thumbnail file. Reading needs read scope; posting, editing, reacting and uploading need write.

Method and path Purpose
GET /entries/{entry_id}/comments List comments
POST /entries/{entry_id}/comments Post a comment or reply
PATCH /comments/{comment_id} Edit (author or admin)
DELETE /comments/{comment_id} Delete (author or admin)
POST /comments/{comment_id}/resolve, /unresolve Resolve or reopen a thread
PUT /comments/{comment_id}/reactions Add a reaction
DELETE /comments/{comment_id}/reactions/{emoji} Remove your reaction
POST /attachments Upload a PNG to attach
PUT /attachments/{attachment_id}/strokes Upload the attachment’s editable markup
GET /attachments/{attachment_id}, /strokes Read an attachment
POST /entries/{entry_id}/annotations Draw an annotation
GET /entries/{entry_id}/annotations List annotations
PATCH /annotations/{annotation_id} Link a discussion to an annotation
DELETE /annotations/{annotation_id} Delete (author or admin)
{
"id": 91,
"entry_id": 412,
"object_id": 1880,
"parent_id": null,
"body": "Pivot looks off on this version, @Lee Lead",
"author": { "id": 7, "name": "<user>", "display_name": "Sam Rigger" },
"mentions": [{ "id": 12, "name": "<user>", "display_name": "Lee Lead" }],
"attachments": [{ "id": 301, "width": 1920, "height": 1080, "size": 402113, "has_strokes": true }],
"reactions": [{ "emoji": "👀", "count": 2, "me": true, "names": ["Sam Rigger", "Lee Lead"] }],
"created_at": "2026-07-25T12:00:00Z",
"updated_at": null,
"updated_by": 7,
"resolved_at": null,
"resolved_by": null
}
Field Description
object_id The version the comment is about, or null for the entry as a whole.
parent_id Set on replies. Threads are one level deep.
updated_at, updated_by updated_at is set after an edit. updated_by is the user id of whoever last wrote the comment (the author until someone edits it).
resolved_at, resolved_by Set when the thread is resolved; resolved_by is a user stub.
reactions One chip per emoji; me is true if you reacted with it.

Returns {"comments": [comment]}, oldest first, replies included. No pagination. The same list is embedded in GET /entries/{entry_id}.

{ "body": "Pivot looks off on this version, @Lee Lead", "object_id": 1880, "attachment_ids": [301] }
Field Type Description
body string, required 1 to 4,000 characters of plain text.
object_id integer Pin to a version of this entry’s lineage. 404 OBJECT_NOT_FOUND if unknown, 409 LINEAGE_CONFLICT if from another lineage.
parent_id integer Reply to a top-level comment on the same entry. A reply to a reply, or to another entry’s comment, is 422; unknown is 404 COMMENT_NOT_FOUND.
attachment_ids array Up to 10 of your own uploads from POST /attachments.

Returns 201 with the comment. People watching the entry, the thread’s author and anyone mentioned get notifications.

Write @ followed by a teammate’s display name (@Lee Lead) or login name. Mentions are resolved when the comment is saved; text that matches nobody, or matches more than one person’s display name, stays plain text. Resolved people appear in mentions. Editing a comment notifies only people newly mentioned by the edit.

{ "body": "Pivot fixed in the next version", "attachment_ids": [] }

body is required. attachment_ids is optional: omit it to keep the attachments, send a list to replace them. Only the author or an admin can edit (403). Returns the comment.

Author or admin. Returns 204. Deleting a top-level comment deletes its replies.

POST /comments/{comment_id}/resolve and POST /comments/{comment_id}/unresolve, no body. Anyone in the workspace can resolve or reopen. Only top-level comments can be resolved (422 for replies). Repeating a call changes nothing. Returns the comment.

PUT /comments/{comment_id}/reactions with {"emoji": "🎉"} returns 204 and is idempotent. DELETE /comments/{comment_id}/reactions/{emoji} (URL-encode the emoji) removes your reaction. Allowed emoji: 👍 👎 ❤️ 😄 😮 😢 🎉 🚀 👀 🔥 ✅ ❌. Others are 422.

Attachments are PNG images (typically marked-up screenshots) bound to a comment or a review note.

  1. POST /attachments with the raw PNG bytes as the body (Content-Type: image/png, up to 8 MiB). Returns 201 {"id", "width", "height", "size"}. Until it is attached, only you (and admins) can see it.
  2. Optionally PUT /attachments/{attachment_id}/strokes with a JSON document (up to 1 MiB) holding the editable markup your tool drew. Only the uploader can do this. Returns {id, width, height, size, has_strokes}.
  3. Pass the id in attachment_ids on a comment, a review request or a review response. Each attachment can belong to one of these only; an attachment that is someone else’s is 403, already attached is 409.

GET /attachments/{attachment_id} and GET /attachments/{attachment_id}/strokes redirect (302) to the PNG or JSON. Attached files are visible to the whole workspace; unattached ones only to their uploader and admins (404 for others).

Attach an upload within a day; unattached uploads are eligible for cleanup after that.

An annotation is drawn over one exact preview or thumbnail file (a preview artifact), so it never drifts when the preview is regenerated. Get the ids from preview_artifact_id and thumbnail_artifact_id on entry detail, and render against GET /previews/artifacts/{artifact_id}.

{
"object_id": 1880,
"preview_artifact_id": 5521,
"shape": "rect",
"geometry": { "x": 0.25, "y": 0.1, "w": 0.2, "h": 0.15 },
"comment_id": 91
}
Field Type Description
object_id integer, required A version of this entry’s lineage.
preview_artifact_id integer, required A preview or thumbnail file of that version (422 otherwise).
shape string, required rect, ellipse, freehand or point3d.
geometry object, required See below. At most 16 KiB.
comment_id integer A top-level comment on the same entry to hold the discussion.
Shape Geometry
rect, ellipse Exactly {x, y, w, h}, each 0 to 1, relative to the image.
freehand Exactly {points: [[x, y], ...]}, 2 to 2,000 points, each coordinate 0 to 1.
point3d Exactly {point: [x, y, z], camera: {...}}: a world-space point on a 3D preview plus your viewer’s camera object, stored as sent.

Returns 201:

{
"id": 44,
"entry_id": 412,
"object_id": 1880,
"preview_artifact_id": 5521,
"comment_id": 91,
"shape": "rect",
"geometry": { "x": 0.25, "y": 0.1, "w": 0.2, "h": 0.15 },
"author": { "id": 7, "name": "<user>", "display_name": "Sam Rigger" },
"created_at": "2026-07-25T12:05:00Z"
}
Endpoint Detail
GET /entries/{entry_id}/annotations?object_id= {"annotations": [annotation]}, oldest first. object_id limits to one version.
PATCH /annotations/{annotation_id} {"comment_id"}: attach a discussion to an annotation that has none. Anyone can do this once; an annotation that already has one is 409 CONFLICT. The comment must be top-level on the same entry (422). Returns the annotation.
DELETE /annotations/{annotation_id} Author or admin. 204.

Unknown ids: 404 ANNOTATION_NOT_FOUND.

Comments, resolutions and annotations appear in the activity feed as comment.create, comment.resolve and annotation.create.