API Reference
Comments API

Comments API

Inline comment threads on posts — create, list, reply, resolve/unresolve, and delete. All routes live under /api/posts/{id}/comments..., require a Cognito JWT, and use snake_case request bodies (per the API-wide convention).

For the feature-level model (fields, lifecycle, real-time sync, notifications), see Post Comments & Threads. This page is the endpoint-by-endpoint wire contract; the authoritative spec is specs/api/comments.md in the repo.

Route summary

MethodPathDescription
POST/api/posts/{id}/commentsAdd a comment
GET/api/posts/{id}/comments?workspaceId=List comments
DELETE/api/posts/{id}/comments/{comment_id}?workspaceId=Delete a comment
POST/api/posts/{id}/comments/{comment_id}/resolveResolve / unresolve
POST/api/posts/{id}/comments/{comment_id}/repliesAdd a reply
DELETE/api/posts/{id}/comments/{comment_id}/replies/{reply_id}?workspaceId=Delete a reply

Authorization

Every route checks, in order:

  1. Workspace membership — the caller must belong to workspace_id (body field on POSTs, workspaceId query param on GET/DELETE), or every route returns 403 Not a workspace member.
  2. Post existencepost_id must exist inside that workspace, or 404 Post not found.

Delete routes add a third check:

  1. Author-or-privileged — you must be the comment's/reply's author (created_by), or "privileged": your platform role (JWT custom:role) or your workspace membership role is admin/owner. Otherwise 403 Not authorized to delete this comment (or ... this reply).
⚠️

Resolve, add-reply, and list do not apply the author-or-privileged check — any workspace member can resolve someone else's comment or reply to any thread. Only delete is restricted.

Add Comment

POST /api/posts/{id}/comments

Body

{
  "workspace_id": "ws-1",
  "body": "Can we punch up this opening line?",
  "mark_id": "c1a2b3c4-...",
  "text_excerpt": "the hook needs work"
}

mark_id and text_excerpt are optional — omit both for a page-level comment with no anchored highlight.

Response 201

{
  "comment_id": "3f9e...",
  "post_id": "post-abc",
  "workspace_id": "ws-1",
  "mark_id": "c1a2b3c4-...",
  "text_excerpt": "the hook needs work",
  "body": "Can we punch up this opening line?",
  "created_by": "user-111",
  "created_by_name": "Alice",
  "created_at": "2026-07-01T09:00:00+00:00",
  "updated_at": "2026-07-01T09:00:00+00:00",
  "resolved_at": null,
  "resolved_by": null,
  "replies": []
}
StatusMeaning
201Created
400workspace_id or body missing/blank
403Not a workspace member
404Post not found

Triggers a WebSocket broadcast (action: "created") and, if the post has an author with a known email, a post_comment_added Slack DM to them.

List Comments

GET /api/posts/{id}/comments?workspaceId=ws-1

Returns all comments (resolved and unresolved) in insertion order — no server-side filtering or pagination. The frontend's "Show resolved" toggle filters client-side.

Response 200

{ "items": [ /* comment objects, same shape as Add Comment's response */ ], "count": 2 }
StatusMeaning
200OK, count: 0 / items: [] if none exist
400workspaceId query param missing/blank
403Not a workspace member
404Post not found

Delete Comment

DELETE /api/posts/{id}/comments/{comment_id}?workspaceId=ws-1

Deletes the comment and all of its replies in one operation.

Response 200

{ "deleted": true }
StatusMeaning
200Deleted
400workspaceId query param missing/blank
403Not a workspace member
403Not the comment's author and not privileged
404Post not found
404Comment not found

Triggers a WebSocket broadcast with action: "deleted".

Resolve Comment

POST /api/posts/{id}/comments/{comment_id}/resolve

This route toggles state — it resolves an open comment or reopens a resolved one, depending on the comment's current resolved_at. There is no separate unresolve route and no body field to pick a direction.

Body

{ "workspace_id": "ws-1" }

Response 200 — the updated comment. When resolving: resolved_at becomes the current timestamp, resolved_by becomes your user id. When unresolving, both become null.

StatusMeaning
200Toggled, returns updated comment
400workspace_id missing/blank
403Not a workspace member
404Post or comment not found

Triggers a WebSocket broadcast with action: "resolved" or "unresolved". No Slack notification is sent for this action.

Add Reply

POST /api/posts/{id}/comments/{comment_id}/replies

Body

{ "workspace_id": "ws-1", "body": "Agreed, tightening now." }

Response 200 — the full comment object with the new reply appended to replies:

{
  "...": "comment fields",
  "replies": [
    {
      "reply_id": "8b1c...",
      "body": "Agreed, tightening now.",
      "created_by": "user-222",
      "created_by_name": "Bob",
      "created_at": "2026-07-01T09:05:00+00:00"
    }
  ]
}
StatusMeaning
200Reply added, returns updated comment
400workspace_id or body missing/blank
403Not a workspace member
404Post or comment not found

Triggers a WebSocket broadcast with action: "reply_added". If the replier isn't the comment's original author, sends that author a post_comment_reply Slack DM (no self-notification when replying to your own comment).

Delete Reply

DELETE /api/posts/{id}/comments/{comment_id}/replies/{reply_id}?workspaceId=ws-1

Removes one reply, leaving the parent comment and any other replies intact.

Response 200

{ "deleted": true }
StatusMeaning
200Deleted
400workspaceId query param missing/blank
403Not a workspace member
403Not the reply's author and not privileged
404Post not found
404Comment not found
404Reply not found

Triggers a WebSocket broadcast with action: "reply_deleted".

Real-time delivery

Every mutating route above broadcasts a lightweight event over the same connections table used by the collaboration WebSocket API:

{ "type": "comment", "action": "created", "postId": "post-abc", "commentId": "3f9e..." }

The payload carries no comment content — clients re-fetch GET /api/posts/{id}/comments on receipt. Delivery is best-effort: if the Lambda's WS_API_ENDPOINT / CONNECTIONS_TABLE_NAME env vars aren't set, or a connection is stale, the broadcast is silently skipped for that connection without affecting the HTTP response.