Conversation comments API
Comment threads anchored to a conversation message (or to the whole conversation), with
@mention of participants and resolve/unresolve. All endpoints require an authenticated
admin session and are nested under a conversation:
GET /admin/v1/conversations/<CID>/commentsPOST /admin/v1/conversations/<CID>/commentsPATCH /admin/v1/conversations/<CID>/comments/<ID>DELETE /admin/v1/conversations/<CID>/comments/<ID>
Access model (trust boundary)
<CID> is the authorization anchor. Every request first resolves the caller's read
access to the conversation — the caller must be the conversation owner or a member
of the project a conversation is shared into (shared_in_project). This is tenant-scoped
for free (a conversation carries no tenant column; tenant derives from its owner), so a user
from another tenant is neither owner nor member and is refused. There is no platform-admin
bypass: an admin who is neither owner nor project member cannot read a conversation's
comments.
Failure is closed and does not leak existence: no read access, no such conversation, or a
comment whose conversation_id does not match <CID> all return 404.
Comment bodies and @mention targets are user-authored conversation content and are stored
in the clear, consistent with the platform's documented at-rest posture for conversation
content (PII masking protects the upstream provider, not at-rest storage of a user's own
conversation). Comments are deleted with their conversation by the tenant retention
Löschfrist and by GDPR user-erase (via database cascade; an erased user's authored comments
are additionally counted in the deletion record).
List comments
GET /admin/v1/conversations/<CID>/comments
Returns the conversation's non-deleted comments (oldest first) as a JSON array ([] when
there are none). A database error returns 500 — never an empty array — so an outage is
never mistaken for an empty thread. Each element:
| Field | Type | Description |
|---|---|---|
id |
string | Comment identifier. |
conversation_id |
string | Parent conversation. |
message_id |
string | null | Anchored message; null = conversation-level (or the anchored message was hard-deleted). |
anchor_start |
integer | null | Inline text-range start (codepoint offset). |
anchor_end |
integer | null | Inline text-range end (codepoint offset). |
author_id |
string | Comment author. |
body |
string | Comment text. |
mention_user_ids |
string[] | Validated mentioned user ids (always an array). |
resolved_at |
unix seconds | null | null = open thread. |
resolved_by |
string | null | Who resolved it. |
created_at |
unix seconds | Creation time. |
updated_at |
unix seconds | Last update time. |
Creating a comment
POST /admin/v1/conversations/<CID>/comments
Required role: authenticated with read access to <CID> (else 404).
Accepted body:
| Field | Type | Required | Rules |
|---|---|---|---|
body |
string | yes | Valid UTF-8, non-empty after trimming, ≤ 4000 codepoints. |
message_id |
string | no | Must be a live (non-deleted) message of <CID>. |
anchor_start, anchor_end |
integer | no | Both-or-neither; require message_id; 0 ≤ start ≤ end; end ≤ the message's codepoint length. |
mention_user_ids |
string[] | no | ≤ 20 entries; each must be a conversation participant (owner or a non-deleted project member). |
On success returns 201 with { "id": "<comment id>" }.
Rejected inputs (all 400 unless noted):
| Condition | Status |
|---|---|
body missing / not a string / empty / whitespace only |
400 |
body is not valid UTF-8 |
400 |
body longer than 4000 codepoints |
400 |
message_id not a string, or not a live message of this conversation |
400 |
anchor offsets present without message_id |
400 |
only one of anchor_start / anchor_end present |
400 |
anchor offsets not numbers, or start < 0, end < start, or end beyond the message length |
400 |
mention_user_ids not an array |
400 |
| more than 20 mentions | 400 |
| a mentioned id is not a participant of this conversation | 400 |
no read access to <CID> |
404 |
@mention targets are validated against the conversation's participant set (owner plus, for
a project-shared conversation, that project's non-deleted members). A non-participant id is
rejected with the same 400 as a non-existent id, so the endpoint is not a cross-tenant /
cross-project user-existence oracle. Mentions are deduplicated and stored as a JSON array;
an empty list is stored as SQL NULL. Delivery of mention notifications is out of scope for
this endpoint (a future capability).
Resolve / unresolve a comment
PATCH /admin/v1/conversations/<CID>/comments/<ID>
Body: { "resolved": true | false } (a non-boolean is 400). Allowed for the comment
author or the conversation owner; anyone else with read access gets 403. Returns the
updated comment row (so a client can reconcile to the server's resolved_by / resolved_at).
A comment id not under <CID>, or an already-deleted comment, returns 404.
Deleting a comment
DELETE /admin/v1/conversations/<CID>/comments/<ID>
Soft-deletes the comment. Allowed for the comment author or the conversation owner (owner
moderation); anyone else with read access gets 403. A missing or already-deleted comment,
or one not under <CID>, returns 404.