Comments API Threading, mentions, reactions, and reference anchoring for workspace and share entities.

Base URL: https://api.fast.io/current/ Auth: Bearer {jwt_token} Content-Type: application/json Format: JSON

Comments use JSON request bodies (Content-Type: application/json), unlike most other Fastio endpoints which use application/x-www-form-urlencoded.


Endpoint Summary

MethodEndpointDescription
GET /current/comments/{entity_type}/{parent_id}/ List all comments in a workspace or share
GET /current/comments/{entity_type}/{parent_id}/{node_id}/ List comments for a specific node
POST /current/comments/{entity_type}/{parent_id}/ Create or update a comment on an entity
POST /current/comments/{entity_type}/{parent_id}/{node_id}/ Create or update a comment on a node
GET /current/comments/fileshare/{fileshare_id}/{node_id}/ List a File Share recipient's comments on a node
POST /current/comments/fileshare/{fileshare_id}/{node_id}/ Create or update a comment as a File Share recipient
POST /current/comments/{comment_id}/update/ Edit a comment by ID (works for every comment surface)
POST /current/comments/{comment_id}/resolve/ Resolve a comment thread
POST /current/comments/{comment_id}/reopen/ Reopen a resolved comment thread
POST /current/comments/{comment_id}/assign/ Set, replace, or clear a thread's assignee
GET /current/comments/{comment_id}/details/ Get a single comment's details
DELETE /current/comments/{comment_id}/delete/ Soft-delete a comment and its replies
POST /current/comments/bulk/delete/ Bulk soft-delete multiple comments
POST /current/comments/{comment_id}/reactions/ Add or change an emoji reaction
DELETE /current/comments/{comment_id}/reactions/ Remove an emoji reaction
GET /current/comments/{comment_id}/attachments/ List a comment's attachments (hydrated, access-gated)
POST /current/comments/{comment_id}/attachments/ Attach one or many objects to a comment
POST /current/comments/{comment_id}/attachments/detach/ Detach an object from a comment
POST /current/comments/{comment_id}/unlink/ Unlink a comment from the entity it was linked to
POST /current/comments/{comment_id}/suggestion/accept/ Accept a suggestion (editor or headless)
POST /current/comments/{comment_id}/suggestion/confirm/ Confirm an editor accept with the saved note version
POST /current/comments/{comment_id}/suggestion/release/ Give back an editor accept
POST /current/comments/{comment_id}/suggestion/reject/ Reject a suggestion
POST /current/comments/{comment_id}/suggestion/withdraw/ Withdraw your suggestion
POST /current/comments/{comment_id}/suggestion/undo/ Return a decided suggestion to pending
GET /current/comments/preferences/ Your global comment email preference
POST /current/comments/preferences/ Set your global comment email preference
GET /current/comments/preferences/{node_id}/ Your comment email preference for one file
POST /current/comments/preferences/{node_id}/ Set or clear the per-file override

Searching comments: To search comments by keyword, use the unified search endpoint — GET /current/workspace/{workspace_id}/search/ (or /current/share/{share_id}/search/) returns a comments bucket alongside files and other types, each with its own pagination. See Unified Search in the Storage Operations reference. Comment search results are permission-filtered to comments the caller can see.


Path Parameters

Comments are scoped to a workspace, share, or File Share, and optionally to a specific node (file or folder) within it. When no node ID is specified, all comments across the entire workspace or share are returned.

ParameterTypeFormatDescription
{entity_type} string "workspace", "share", or "fileshare" Entity type discriminator. For "fileshare" the {node_id} segment is required (see File Share Comments).
{parent_id} string 19-digit numeric (a File Share also accepts its opaque id_alt) Workspace, share, or File Share ID. Note: Do not confuse with parent_id in the POST request body, which is the parent comment ID for threading.
{node_id} string Alphanumeric opaque ID File or folder ID within the entity (optional for workspace/share, required for fileshare)
{comment_id} string Alphanumeric opaque ID Comment identifier

File Share comments. When {entity_type} is "fileshare", {parent_id} is the File Share's 19-digit numeric profile ID. Comments on a File Share are scoped to that File Share — a recipient sees only the comments made under it, never comments on the same underlying file from the owning workspace or from another File Share. Commenting requires the viewer to be signed in; public anyone-with-link visitors cannot comment. Comments are available only when the File Share owner has enabled comments, and they are always disabled on public anyone-with-link shares. A client should read the File Share's effective comments_enabled flag from its details response to decide whether to show the comment UI.


Comment Object

Every comment returned by the API has this structure:

FieldTypeDescription
id string Alphanumeric opaque ID of the comment
entity string Opaque ID of the entity the comment belongs to (workspace, share, or node)
user_id string 19-digit numeric ID of the comment author's account. Always present — File Share recipients comment as signed-in accounts too
actor object Who wrote the comment and whether an agent acted for them: user_id, kind (human, agent, api_key, app, system, unknown), agent_name, name_source, credential_type, verified. verified is true only for Fastio’s own built-in agent; any other agent_name is self-declared — display it (e.g. “Dobby (via API key) for Derek”), never treat it as verified. Comments created before actor attribution existed report kind: "unknown" credited to the comment’s author, unless an earlier record names that same author, and are never verified. Full reference: Actor Attribution in the Storage reference
parent_id string|null Opaque ID of the parent comment (for replies), or null for top-level
body string Comment text content (may include mention markup)
profile_type string|null The container type ("workspace" or "share") that owns the entity
profile_id string|null 19-digit numeric ID of the workspace or share that owns the entity
scope_id string|null When a comment originated under a File Share and is surfaced in a workspace context, the 19-digit numeric ID of that File Share — a present (non-null) value marks the comment as "via File Share". Null/omitted for comments that originated directly in the workspace or share
linked_entity_type string|null Type of the entity the comment is linked to (see Unlink Comment), or null when unlinked. New comments are never linked
linked_entity_id string|null Opaque ID of the linked entity, or null
external_author_email string|null Email of an external author on a legacy externally-authored comment; otherwise omitted/null
external_author_name string|null Display name of the external author, when present; otherwise omitted/null
external_author_token_id string|null Opaque ID of the access token the external author used, when present; otherwise omitted/null
reference object|null Anchoring reference to a position in a file (see Reference Anchoring)
mentions array The server-validated set of mentioned profile IDs that took effect (19-digit numeric strings). Always present; empty when no mention took effect. This is the authoritative took-effect set — mentions in the body markup that fail validation (e.g. a non-member) are excluded here
reactions object Map of emoji character to total reaction count (e.g., {"👍": 3}). With no reactions it serializes as an empty array [], not {}
user_reaction string|null The current authenticated user's emoji reaction, or null
version_hash string|null Hash representing the current comment content version
version integer Monotonically increasing version counter (starts at 1)
attachment_count integer Number of objects attached to the comment (see Comment Attachments). Present on comment list rows and the comment detail response
attachments array Hydrated, access-gated attachment rows (see Comment Attachments). Detail endpoint only
edited_at string|null Timestamp of last edit if the comment has been edited (YYYY-MM-DD HH:MM:SS UTC), or null
can_edit boolean Whether the requesting user can edit this comment
can_delete boolean Whether the requesting user can delete this comment
can_reply boolean Whether the comment can be replied to (top-level only)
created string Creation timestamp (YYYY-MM-DD HH:MM:SS UTC)
updated string Last-updated timestamp (YYYY-MM-DD HH:MM:SS UTC)
deleted string|null Deletion timestamp (YYYY-MM-DD HH:MM:SS UTC), or null if active. Included only on the details and delete responses — list rows never carry it, even with include_deleted=true.

Null fields are omitted on most responses. On the list, create/update, update-by-ID and delete responses, a field whose value is null is left out rather than sent as null — parent_id, profile_type, profile_id, scope_id, linked_entity_*, external_author_*, reference, user_reaction, version_hash and edited_at. The details and unlink responses send them as null. Treat a missing key and null the same.

Note: the server-validated mentions set is surfaced as the top-level mentions array described above. The raw internal properties object (threading, pinning, content-filter flags, etc.) is never returned — only the curated mentions list is exposed.

Service accounts. A workspace service-account key may read comments in its own workspace; an editor may also create, edit, delete, react to and attach to them. Comments on a share or a File Share are refused with 403 and error.params.reason = service_account_not_permitted (even when the share belongs to the workspace), and comments that originated under a File Share (scope_id set) are left out of what it reads. A comment it writes has an actor carrying service_account: true, credential_id, credential_hint, account_type: "service_account" and the account's name (see Service-account keys). A service account is never notified of a mention or reply.

Thread State Fields (root comments only)

A thread is a root comment (one with no parent_id) plus its replies. These fields carry the thread's state and live on the root; a reply returns them as null/omitted, the same as any other null field (see Null fields are omitted above).

FieldTypeDescription
kind string "comment" or "suggestion". Root only; defaults to "comment"
resolved boolean Whether the thread is resolved
resolved_by object|null {user_id, name} of whoever resolved the thread, or null
resolved_at string|null Resolution timestamp (YYYY-MM-DD HH:MM:SS UTC), or null
assignee object|null The thread's single assignee: {user_id, name, agent?: {agent_name, credential_type, display, credential_id?}}, or null when unassigned. agent.credential_id is included only when the caller is the assignee themselves or an admin
assigned_by object|null {user_id, name} of whoever made the current assignment, or null
assigned_at string|null Assignment timestamp (YYYY-MM-DD HH:MM:SS UTC), or null
state_version integer A monotonic counter, incremented on every thread-state change (resolve, reopen, assign). Send it back as if_state_version to guard a write against a change you haven't seen yet (see Concurrency under Thread State below)
for_me boolean Whether this thread is “for” the calling identity. For a person: assigned to them with no agent qualifier, or they are mentioned in the thread (root or any reply). For an agent credential: assigned to them via that same credential
for_my_agents boolean True when the thread is assigned to the calling person via any of their agent connections
can_resolve boolean Whether the caller may resolve or reopen this thread. Always false on File Share comments
can_assign boolean Whether the caller may assign this thread. Always false on File Share comments
can_accept boolean Whether the caller may accept this thread's suggestion (only meaningful when kind is "suggestion")
can_reject boolean Whether the caller may reject this thread's suggestion (only meaningful when kind is "suggestion")

List Comments

GET /current/comments/{entity_type}/{parent_id}/

List all comments across the entire workspace or share.

GET /current/comments/{entity_type}/{parent_id}/{node_id}/

List comments for a specific node within an entity.

Without {node_id}: returns all comments across the entire workspace or share. With {node_id}: returns only comments on that specific file or folder.

Note: Only comments created after 2026-03-15 are included in scope-level results (without {node_id}); older comments are accessible via the node-level endpoint.

Auth: Required (JWT). Rate limited.

Query Parameters

ParameterTypeRequiredDefaultConstraintsDescription
sort string No asc "asc" or "desc" Sort order by creation time
limit integer No — Min: 2, Max: 200 Number of comments to return
offset integer No 0 Min: 0 Number of comments to skip
page integer No — Min: 1 Page number (alternative to offset-based pagination)
include_deleted boolean No false — Include soft-deleted comments in results
reference_type string No — — Filter by reference anchor type: "video", "image", "document", "general", or "audio" (see Reference Anchoring)
include_total boolean No false — Include total count and pagination metadata in response
group string No — "threads" Page by root thread instead of by individual comment — see Thread Grouping below
status string No "all" "open", "resolved", or "all" Filter threads by state: open is an unresolved comment thread, resolved is a resolved one
for_me string No — "true" or "agents" Filter to threads that are “for” the caller — see for_me / for_my_agents under Thread State Fields above
kind string No "comment" "comment", "suggestion", or "all" Filter root threads by kind
include_suggestions boolean No false — Include suggestion threads in the results. Suggestion threads are hidden unless requested; kind=suggestion implies this

Request Example

curl -X GET "https://api.fast.io/current/comments/workspace/1234567890123456789/?limit=50&include_total=true" \
  -H "Authorization: Bearer {jwt_token}"

Response

{
  "result": true,
  "comments": [
    {
      "id": "abc123opaqueid",
      "entity": "xyz789opaqueid",
      "user_id": "1234567890123456789",
      "actor": {
        "user_id": "1234567890123456789",
        "kind": "human",
        "agent_name": null,
        "name_source": null,
        "credential_type": "session",
        "verified": false
      },
      "body": "This looks great!",
      "profile_type": "workspace",
      "profile_id": "1234567890123456789",
      "mentions": [],
      "reactions": {"👍": 2, "❤️": 1},
      "user_reaction": "👍",
      "version": 1,
      "created": "2025-01-15 10:30:00 UTC",
      "updated": "2025-01-15 10:30:00 UTC",
      "can_edit": true,
      "can_delete": true,
      "can_reply": true,
      "attachment_count": 0
    }
  ],
  "count": 1,
  "allowed": true,
  "remaining": 95,
  "total": 5,
  "limit": 50,
  "offset": 0
}

Response Fields

FieldTypeDescription
comments array Array of comment objects
count int Number of comments in this response
allowed bool Whether the current user can post new comments (based on the organization's comment limit)
remaining int Remaining comments allowed under the organization's limit (only present if a limit applies)
total int Total number of comments (only if include_total=true). On a share, the number of top-level comments you can see
limit int Limit used in query (only if include_total=true)
offset int Offset used in query (only if include_total=true)

Thread Grouping (group=threads)

Without group=threads, the response is the flat shape above, unchanged. With group=threads, limit and offset page by root thread instead of by individual comment, and the response is:

{
  "result": true,
  "threads": [
    {
      "id": "abc123opaqueid",
      "kind": "comment",
      "resolved": false,
      "state_version": 2,
      "replies": [ { "id": "def456opaqueid", "parent_id": "abc123opaqueid" } ],
      "replies_total": 1
    }
  ],
  "counts": {"all": 12, "open": 9, "done": 3, "for_me": 2, "for_me_agents": 0, "open_comments": 8, "pending_suggestions": 1},
  "limits": {"root_threads_used": 12, "root_threads_max": 1000},
  "limit": 50,
  "offset": 0,
  "total": 12
}
FieldTypeDescription
threads array Root comments, each carrying the Thread State Fields above plus replies
threads[].replies array The thread's replies, embedded and capped per root (see replies_offset / replies_limit under Get Comment Details for loading the rest)
threads[].replies_total int Total reply count for the thread, which may exceed the number embedded in replies
counts object {all, open, done, for_me, for_me_agents, open_comments, pending_suggestions} — root-thread counts honoring scope and visibility, but ignoring the status and for_me filters, so filter chips stay stable. open combines open comment threads and pending suggestion threads into one header count; done combines resolved comments and decided suggestions. The suggestion-related counts are populated only when suggestion threads are in scope (include_suggestions=true or kind includes suggestion)
counts_partial bool Present (true) only when the surface has more than 5,000 root threads: the thread listing examines the first 5,000 roots in listing order, and counts, total and the filtered pages cover only those
truncated / truncated_limit bool / integer Present with counts_partial (truncated_limit = 5,000): the group=threads or filtered listing is truncated — root threads past the first 5,000 in listing order cannot be reached by its paging, status / kind / for_me filters or counts. Narrow the read (a single file's node) to reach them. The default flat list (no filter, no group) is not bounded this way
replies_truncated bool Present (true) only when the page's threads together have more than 10,000 replies: some replies arrays are then cut short and their replies_total is a lower bound; load the rest per thread with GET /current/comments/{root_id}/details/
limits object {root_threads_used, root_threads_max} for the file. root_threads_max is null when the plan has no cap (unlimited). See Thread Limits under Thread State below
total int Total number of root threads matching the filters (present the same way include_total governs it on the flat response)

Access Levels

RoleAccess
Workspace Owner/MemberFull access — sees all comments
Share Owner/Admin/MemberFull access — sees all comments
Share Guest (Send, Receive)Filtered — sees their own threads with every reply in them, plus threads started by share owners, admins or members with those people's replies and the guest's own replies. Other guests' threads and replies are hidden
Share Guest (Exchange)Full access — sees all comments
Signed-in non-member of a share open to any signed-in userSend and Exchange: sees all comments. Receive: same as a Send/Receive guest
Anyone-with-link shareComments are disabled

Error Responses

Reading the error tables: the four-digit 16xx/17xx values below are HTTP-status classes, not error.code. The error.code a client actually receives is assigned per endpoint, so use the HTTP status as the gate and a documented error.code — five or six digits, plus the 9661-9669 family — only as a refinement. A 16xx value identifies the status class — useful for telling which kind of failure occurred — but comparing one against error.code will never match. Five- and six-digit codes (and the 9661-9669 family) are real error.code values. If you widen a check from a specific code to a status, widen what you assert with it — a status covers failures the narrower code did not, so a message written for that one code becomes a confident falsehood on the rest.

Error CodeHTTP StatusMessageCause
1605 (Invalid Input)406Invalid API format...Missing entity type or parent ID
1605 (Invalid Input)406Entity type must be "workspace", "share", or "fileshare"Invalid entity type
1680 (Access Denied)401Invalid entity or insufficient permissionsUser lacks access
1609 (Not Found)404Invalid entity or insufficient permissionsEntity not found
1610 (Internal Error)500Failed to retrieve commentsInternal error
(generated per call site)503Comment thread state is temporarily unavailable. Please try again.The threads could not be classified (comment vs suggestion) — error.params.reason = comment_thread_state_unavailable, retryable: true. Never answered with suggestions shown as comments

Share reads are filtered and paged by thread. On a share (GET /current/comments/share/{share_id}/ and .../{node_id}/), the access table above decides what you see, and comments on files you cannot view (for example, another guest's upload on a Receive share) are never shown. limit and offset count the top-level comments you can see, and each one is followed by its replies that you can see, so count can be larger than limit. With include_total=true, total is the number of top-level comments you can see. allowed and remaining reflect the share's full comment count, not only the comments visible to you. A comment you cannot see answers 404 "Comment not found" on its details, reactions and attachments routes, and replying to it answers 404 "Parent comment not found". Share search results follow the same rules.

Workspace node reads include File Share comments. A file shared through a File Share is the same node that lives in the owning workspace, so a workspace node read (GET /current/comments/workspace/{workspace_id}/{node_id}/) also returns comments left by File Share recipients on that file. Those File Share comments appear read-only in the workspace view: can_reply, can_edit, and can_delete are false (replies and edits to a File Share comment go through the File Share's own surface). The total count reflects everything visible in the listing (the workspace's own comments plus the File Share comments), while the allowed / remaining fields reflect only the workspace's own writable comments — so remaining is not reduced by File Share comments you cannot edit. This inclusion is one-directional: workspace members see File Share comments, but File Share recipients never see the workspace's internal comments.


Create or Update Comment

POST /current/comments/{entity_type}/{parent_id}/

Create a new comment or update an existing one on an entity.

POST /current/comments/{entity_type}/{parent_id}/{node_id}/

Create a new comment or update an existing one on a specific node.

Auth: Required (JWT). Rate limited.

Content-Type: application/json

Request Body

FieldTypeRequiredConstraintsDescription
body string Required 1–8192 UTF-8 characters (raw, not bytes); display text excluding mentions max 500 UTF-8 characters — a mention inside markdown code counts in full (see Mentions). Over-limit is rejected, never truncated Comment text content
comment_id string No Valid opaque ID Include to update an existing comment; omit to create new
parent_id string No Valid opaque ID Parent comment ID for threaded reply (single-level only). Note: This is the parent comment's opaque ID, not the workspace/share ID from the URL path {parent_id}.
properties object No — Arbitrary key-value metadata to attach
reference object No See Reference Anchoring Anchoring reference to a position in a file
target_id string No Valid object ID Inline attachment on a new comment — a single object to attach (see Comment Attachments). Ignored on update
target_ids array<string> No Non-empty array Inline attachments on a new comment — multiple objects to attach, up to 25 per comment. Ignored on update
reopen boolean No Reply only (parent_id set) When true, reopens the thread as part of posting the reply, atomically. A plain reply (no reopen) never reopens a resolved thread. On a top-level create: 400 comment_thread_action_reply_only
assign object No Reply only (parent_id set) One of the body forms accepted by POST /comments/{comment_id}/assign/ (see Thread State: Resolve, Reopen, Assign). Applied atomically with the reply. On a top-level create: 400 comment_thread_action_reply_only

Request Example — Create a new comment

curl -X POST "https://api.fast.io/current/comments/workspace/1234567890123456789/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "body": "Great work on this document!"
  }'

Request Example — Reply to a comment

curl -X POST "https://api.fast.io/current/comments/workspace/1234567890123456789/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "body": "Thanks for the feedback!",
    "parent_id": "abc123opaqueid"
  }'

Request Example — Comment with mention and page reference

curl -X POST "https://api.fast.io/current/comments/workspace/1234567890123456789/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "body": "Hey @[user:9876543210987654321:Jane Smith], can you review page 3?",
    "reference": {"type": "document", "page": 3}
  }'

Request Example — Update an existing comment

curl -X POST "https://api.fast.io/current/comments/workspace/1234567890123456789/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "comment_id": "def456opaqueid",
    "body": "Updated: This looks great after the revision."
  }'

Response

{
  "result": true,
  "comment": {
    "id": "abc123opaqueid",
    "entity": "xyz789opaqueid",
    "user_id": "1234567890123456789",
    "actor": {
      "user_id": "1234567890123456789",
      "kind": "agent",
      "agent_name": "Dobby",
      "name_source": "api_key_label",
      "credential_type": "api_key",
      "verified": false
    },
    "body": "Great work on this document!",
    "profile_type": "workspace",
    "profile_id": "1234567890123456789",
    "mentions": [],
    "reactions": [],
    "version": 1,
    "created": "2025-01-15 10:30:00 UTC",
    "updated": "2025-01-15 10:30:00 UTC",
    "can_edit": true,
    "can_delete": true,
    "can_reply": true
  },
  "attachments": [],
  "attachment_count": 0
}

Response Fields

FieldTypeDescription
comment object The created or updated comment object (full schema above)
attachments array Create only (omitted on an update). The hydrated attachments written with the new comment, returned at the top level beside comment, not inside it
attachment_count integer Create only (omitted on an update). Number of objects attached to the new comment, at the top level beside comment

Access Levels

RoleAccess
Workspace Owner/MemberCan create and edit own comments
Workspace GuestCan comment on (and edit their own comments on) a note they are allowed to edit; on any other file or folder, commenting needs workspace membership. View-only access is read-only
Share OwnerCan create and edit own comments
Share GuestCan only post if comments are enabled on the share

Error Responses

Error CodeHTTP StatusMessageCause
1680 (Access Denied)401Authentication required to post commentsUser not authenticated
1605 (Invalid Input)406Invalid JSON in request bodyMalformed JSON
1605 (Invalid Input)406Comment body must be between 1 and 8192 charactersBody length out of range
1605 (Invalid Input)406Comment text (excluding mentions) must not exceed 500 charactersDisplay text too long — note that a mention inside markdown code counts in full (see Mentions)
1605 (Invalid Input)406Your comment appears to contain spam content...Spam detected
1605 (Invalid Input)406Invalid parent comment ID formatMalformed parent_id
1609 (Not Found)404Parent comment not foundReferenced parent does not exist
1609 (Not Found)404Parent comment not foundParent is on a different entity or scope (reported the same as a missing parent)
1605 (Invalid Input)406Invalid reference data: ...Reference failed validation; error.params.reason = anchor_too_large when the encoded object exceeds 2048 bytes, else anchor_invalid (bad shape or an unknown key)
1605 (Invalid Input)406The anchor of an existing comment cannot be changedAn edit (comment_id set) sent a reference whose exact, prefix or suffix differs from the stored one; error.params.reason = anchor_invalid
10368 (Rate Limited)429Too many requests, rate limit exceeded.The standard rate-limit response: too many comments created in a short time. Wait the number of seconds in the Retry-After header, then retry. Edits are not counted
(generated per call site)409Thread limit reached for this fileCreating a new thread (a top-level, non-reply comment) would exceed the file's thread limit; error.params.reason = comment_thread_limit_reached. See Thread Limits under Thread State below
(generated per call site)503Thread limit could not be checkedThe thread-limit check could not complete; error.params.reason = comment_thread_limit_unavailable. Retryable
(generated per call site)400reopen must be a boolean / assign must be an objecterror.params.reason = reopen_invalid or assignee_invalid
(generated per call site)400Reply-only thread actionreopen / assign sent on a top-level create; error.params.reason = comment_thread_action_reply_only
(generated per call site)400Assignee refusedassign on a reply: error.params.reason = assignee_invalid or assignee_no_access (as on assign/)
(generated per call site)403Thread action refusedreopen / assign on a reply the caller may not change, or on a File Share comment; error.params.reason = comment_thread_forbidden or comment_thread_read_only
(generated per call site)409Thread changedreopen on a suggestion thread (comment_is_suggestion) or a concurrent thread change that kept conflicting (state_version_conflict); the reply is not posted
(generated per call site)503Thread state unavailableerror.params.reason = comment_thread_state_unavailable. Retryable; the reply is not posted
1609 (Not Found)404Comment not foundComment ID for update not found
1680 (Access Denied)401You do not have permission to edit this commentUser is not the comment author
1610 (Internal Error)500Failed to save commentInternal error

Notes


File Share Comments

GET /current/comments/fileshare/{fileshare_id}/{node_id}/

List a File Share recipient's comments on a node.

POST /current/comments/fileshare/{fileshare_id}/{node_id}/

Create or update a comment as a File Share recipient.

fileshare is a third {entity_type} on the comment surface, used by File Share recipients to read and post comments on a file inside a File Share. It shares the request/response shapes of the workspace/share List and Create endpoints above — the only differences are the fileshare discriminator and that the {node_id} segment is required (a File Share comment always targets a specific node, never the whole container).

Auth: Required. The caller must have access to the File Share (a signed-in recipient); anonymous callers are denied. Comments left here are the File Share recipient's own comments on the node.

Path Parameters

ParameterTypeFormatDescription
{fileshare_id} string Opaque id_alt (preferred) or 19-digit numeric File Share ID: the File Share's id_alt (the id in its share link), or its legacy numeric profile ID
{node_id} string Alphanumeric opaque ID The file or folder node within the File Share. Required

Request Example — list

curl -X GET "https://api.fast.io/current/comments/fileshare/adheih5r326qjiqvk4wfamvt4rqeh/23d4zvoprfmonyhy2wwjpoccqjaiz/" \
  -H "Authorization: Bearer {jwt_token}"

Request Example — create

curl -X POST "https://api.fast.io/current/comments/fileshare/adheih5r326qjiqvk4wfamvt4rqeh/23d4zvoprfmonyhy2wwjpoccqjaiz/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{"body": "Received — thanks for sending this over."}'

The list and create responses use the same envelope and Comment Object schema as the entity-scoped endpoints above. A File Share comment carries the File Share's ID in scope_id; user_id is the recipient's account ID.

Notes

Error Responses

Error CodeHTTP StatusMessageCause
1605 (Invalid Input)406Entity type must be "workspace", "share", or "fileshare"Invalid entity type
1680 (Access Denied)401Invalid entity or insufficient permissionsCaller cannot access the File Share
1609 (Not Found)404Invalid entity or insufficient permissionsFile Share or node not found

Update Comment by ID

POST /current/comments/{comment_id}/update/

Edit an existing comment by its ID. Author-only. Works for every comment surface — workspace, share, node, and File Share.

Auth: Required (JWT). Rate limited.

Content-Type: application/json

Request Body

FieldTypeRequiredConstraintsDescription
body string Required 1–8192 UTF-8 characters (raw, not bytes); display text excluding mentions max 500 UTF-8 characters — a mention inside markdown code counts in full (see Mentions). Over-limit is rejected, never truncated Replacement comment text
properties object No — Arbitrary key-value metadata to merge
reference object No See Reference Anchoring Replacement anchoring reference

Request Example

curl -X POST "https://api.fast.io/current/comments/abc123opaqueid/update/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{
    "body": "Updated: this looks great after the revision."
  }'

Response

The full updated comment object (same shape as Create or Update Comment above), with edited_at populated.

Error Responses

Error CodeHTTP StatusMessageCause
1605 (Invalid Input)406Comment body must be between 1 and 8192 charactersBody length out of range
1605 (Invalid Input)406Comment is deletedThe comment has been deleted
1605 (Invalid Input)406Invalid reference dataReference failed validation; error.params.reason = anchor_too_large or anchor_invalid (see Reference Anchoring)
1605 (Invalid Input)406The anchor of an existing comment cannot be changedThe reference changes the stored exact, prefix or suffix; error.params.reason = anchor_invalid
1609 (Not Found)404Comment not foundUnknown comment ID, or the comment's surface is not visible to you
1680 (Access Denied)401You do not have permission to edit this commentCaller is not the comment author

Notes


Get Comment Details

GET /current/comments/{comment_id}/details/

Get a single comment's full details.

When the comment is a root (no parent_id), the response carries the Thread State Fields described under Comment Object above.

Auth: Required (JWT). Rate limited.

Query Parameters (root comments only)

ParameterTypeRequiredDefaultConstraintsDescription
replies_offset integer No 0 Min: 0 Number of replies to skip, for paging a root thread's replies beyond what a group=threads list page embeds
replies_limit integer No 50 Max: 200 Number of replies to return

Request Example

curl -X GET "https://api.fast.io/current/comments/abc123opaqueid/details/" \
  -H "Authorization: Bearer {jwt_token}"

Response

{
  "result": true,
  "comment": {
    "id": "abc123opaqueid",
    "entity": "xyz789opaqueid",
    "user_id": "1234567890123456789",
    "actor": {
      "user_id": "1234567890123456789",
      "kind": "human",
      "agent_name": null,
      "name_source": null,
      "credential_type": "session",
      "verified": false
    },
    "parent_id": null,
    "body": "This looks great!",
    "profile_type": "workspace",
    "profile_id": "1234567890123456789",
    "scope_id": null,
    "linked_entity_type": null,
    "linked_entity_id": null,
    "external_author_email": null,
    "external_author_name": null,
    "external_author_token_id": null,
    "reference": null,
    "mentions": [],
    "reactions": [],
    "user_reaction": null,
    "version_hash": null,
    "version": 1,
    "edited_at": null,
    "created": "2025-01-15 10:30:00 UTC",
    "updated": "2025-01-15 10:30:00 UTC",
    "deleted": null,
    "can_edit": true,
    "can_delete": true,
    "can_reply": true,
    "attachment_count": 0,
    "attachments": []
  }
}

On a root comment, comment additionally carries the Thread State Fields above, plus replies (array, paged by replies_offset / replies_limit) and replies_total (int, the thread's total reply count). These three are omitted on a reply.

Error Responses

Error CodeHTTP StatusMessageCause
1605 (Invalid Input)406Comment ID is requiredMissing comment ID
1605 (Invalid Input)406Invalid comment ID formatMalformed opaque ID
1609 (Not Found)404Comment not foundComment does not exist
1680 (Access Denied)401You do not have permission to view this commentUser lacks access to the entity
(generated per call site)503Comment thread state is temporarily unavailable. Please try again.The comment's thread (or, for a reply, its root) could not be read — error.params.reason = comment_thread_state_unavailable, retryable: true

Delete Comment

DELETE /current/comments/{comment_id}/delete/

Soft-delete a comment. Recursive — also deletes the replies to this comment that you can access. A reply you cannot access (for example, one on a surface you have no access to) is left in place and is never named in the response.

Who can delete: the comment's author, and workspace/share admins or owners (moderation — admins can remove any comment on their workspace or share). File Share comments can only be deleted by their author; they are never moderator-deletable, including from the workspace view. The can_delete flag on every returned comment reflects this, so clients should drive the delete affordance from can_delete.

Auth: Required (JWT). Rate limited.

Request Example

curl -X DELETE "https://api.fast.io/current/comments/abc123opaqueid/delete/" \
  -H "Authorization: Bearer {jwt_token}"

Response

{
  "result": true,
  "message": "Comment deleted successfully",
  "comment": {
    "id": "abc123opaqueid",
    "entity": "xyz789opaqueid",
    "user_id": "1234567890123456789",
    "actor": {
      "user_id": "1234567890123456789",
      "kind": "human",
      "agent_name": null,
      "name_source": null,
      "credential_type": "session",
      "verified": false
    },
    "body": "This looks great!",
    "profile_type": "workspace",
    "profile_id": "1234567890123456789",
    "mentions": [],
    "reactions": [],
    "version": 1,
    "created": "2025-01-15 10:30:00 UTC",
    "updated": "2025-01-16 08:00:00 UTC",
    "deleted": "2025-01-16 08:00:00 UTC",
    "can_edit": false,
    "can_delete": false,
    "can_reply": true
  },
  "skipped": []
}

skipped lists the ids of replies you can access that could not be deleted (normally empty). Each can be deleted by its own author or by a moderator; deleting the parent again does not retry them.

Access Levels

RoleAccess
Comment AuthorCan delete own comments
Workspace/Share Owner or AdminCan delete any comment on their workspace or share (moderation), except File Share comments
Other UsersDenied

Error Responses

Error CodeHTTP StatusMessageCause
1605 (Invalid Input)406Comment ID is requiredMissing comment ID
1605 (Invalid Input)406Invalid comment ID formatMalformed opaque ID
1609 (Not Found)404Comment not foundComment does not exist, or you cannot access it
1605 (Invalid Input)406Comment is already deletedComment was previously deleted (only reported to a caller who can access it)
1680 (Access Denied)401You do not have permission to delete this commentNot author, entity owner, or admin
1610 (Internal Error)500Failed to delete commentInternal error

Bulk Delete Comments

POST /current/comments/bulk/delete/

Bulk soft-delete multiple comments. Each comment is processed independently — partial success is possible. Each comment is deleted exactly as the single delete does it, including its replies that you can access.

⚠ Per-item isolation holds only when every element of comment_ids is a STRING. A non-string element (a JSON number, boolean, or null) is not isolated to its own result entry — it aborts the entire batch. The abort is not a rollback: ids are processed in array order and each delete is committed as it is made, so every id BEFORE the offending element is already deleted and stays deleted. Processing stops at the offending element, and you get a generic 500 instead of the per-item results report — so you cannot tell from the response which ids went through. Nothing is deleted only when the offending element is the first one. Serialise every id as a JSON string.

Service accounts. Every comment in the batch is checked before anything is deleted. If any of them is one a service account is refused on (a comment on a share or a File Share), the whole request fails with 403 and error.params.reason = service_account_not_permitted, and nothing in the batch is deleted. Otherwise the batch is processed per item as above.

Auth: Required (JWT). Rate limited.

Content-Type: application/json

Request Body

FieldTypeRequiredConstraintsDescription
comment_ids array<string> Required Max 100 items Array of comment opaque IDs to delete

Request Example

curl -X POST "https://api.fast.io/current/comments/bulk/delete/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{"comment_ids": ["abc123opaqueid", "def456opaqueid", "ghi789opaqueid"]}'

Response

{
  "result": true,
  "success": true,
  "deleted_count": 3,
  "failed_count": 0,
  "total_count": 3,
  "results": {
    "abc123opaqueid": {"id": "abc123opaqueid", "success": true, "error": null},
    "def456opaqueid": {"id": "def456opaqueid", "success": true, "error": null},
    "ghi789opaqueid": {"id": "ghi789opaqueid", "success": true, "error": null}
  },
  "skipped": []
}

Response Fields

FieldTypeDescription
success bool true only if all comments were deleted (failed_count === 0)
deleted_count int Number of successfully deleted comments
failed_count int Number that failed to delete
total_count int Total number of IDs provided
results object Per-comment results keyed by comment ID
results.{id}.success bool Whether this comment was deleted
results.{id}.error string|null Error message if failed, null on success
skipped array<string> Ids of replies you can access that a cascade could not delete (normally empty); each can be deleted by its own author or by a moderator. Replies you cannot access are left in place and never listed

Per-Comment Error Messages

ErrorCause
Invalid comment ID formatNot a valid opaque ID
Comment not foundNo comment with this ID
Comment already deletedPreviously soft-deleted (only reported when you can access the comment)
Permission deniedUser is not author, entity owner, or admin
Failed to delete commentInternal error
Processing errorUnhandled error while processing this comment

Request-Level Error Responses

Error CodeHTTP StatusMessageCause
1605 (Invalid Input)406Invalid JSON request bodyMalformed JSON
1605 (Invalid Input)406comment_ids array is requiredMissing or non-array field
1605 (Invalid Input)406comment_ids array cannot be emptyEmpty array
1605 (Invalid Input)406Maximum 100 comments can be deleted at onceExceeds bulk limit

Replies: like single delete, bulk delete also deletes the replies you can access. An id already deleted earlier in the same batch (as a reply, or listed twice) is reported as a success.


Add or Change Reaction

POST /current/comments/{comment_id}/reactions/

Add an emoji reaction to a comment. One reaction per user per comment — sending a new reaction replaces any previous one.

Auth: Required (JWT). Rate limited.

Content-Type: application/json

Request Body

FieldTypeRequiredConstraintsDescription
emoji string Required Single emoji character; max 2 UTF-8 characters; must match Unicode emoji ranges Emoji to react with

Request Example

curl -X POST "https://api.fast.io/current/comments/abc123opaqueid/reactions/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{"emoji": "👍"}'

Response

{
  "result": true,
  "reactions": {"👍": 3, "❤️": 1},
  "user_reaction": "👍"
}

Response Fields

FieldTypeDescription
reactions object Map of emoji to total reaction count across all users ([] when there are none)
user_reaction string|null The current user's active reaction emoji

Error Responses

Error CodeHTTP StatusMessageCause
1605 (Invalid Input)406Comment ID is requiredMissing comment ID
1605 (Invalid Input)406Invalid comment ID formatMalformed opaque ID
1609 (Not Found)404Comment not foundComment does not exist or is deleted
1680 (Access Denied)401You do not have permission to react to this commentUser lacks entity access
1605 (Invalid Input)406emoji parameter is requiredMissing or non-string emoji
1605 (Invalid Input)406Invalid emoji characterDoes not match Unicode emoji pattern
1605 (Invalid Input)406Only single emoji allowedString exceeds 2 UTF-8 characters
1610 (Internal Error)500Failed to save reactionInternal error

Remove Reaction

DELETE /current/comments/{comment_id}/reactions/

Remove an emoji reaction from a comment.

Auth: Required (JWT). Rate limited.

Content-Type: application/json

Request Body

FieldTypeRequiredConstraintsDescription
emoji string No Must match Unicode emoji ranges if provided Specific emoji to remove. Omit to remove any reaction by the current user

Request Example — Remove specific emoji

curl -X DELETE "https://api.fast.io/current/comments/abc123opaqueid/reactions/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{"emoji": "👍"}'

Request Example — Remove any reaction

curl -X DELETE "https://api.fast.io/current/comments/abc123opaqueid/reactions/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{}'

Response

{
  "result": true,
  "reactions": {"❤️": 1},
  "user_reaction": null
}

Response Fields

FieldTypeDescription
reactions object Updated map of emoji to total reaction count ([] when none remain)
user_reaction string|null Current user's reaction after removal (null if removed)

Error Responses

Error CodeHTTP StatusMessageCause
1605 (Invalid Input)406Comment ID is requiredMissing comment ID
1605 (Invalid Input)406Invalid comment ID formatMalformed opaque ID
1609 (Not Found)404Comment not foundComment does not exist or is deleted
1680 (Access Denied)401You do not have permission to react to this commentUser lacks entity access
1605 (Invalid Input)406emoji must be a stringNon-string emoji parameter
1605 (Invalid Input)406Invalid emoji characterDoes not match Unicode emoji pattern
1610 (Internal Error)500Failed to remove reactionInternal error

Notes: Removing a reaction that does not exist returns success (idempotent). A "removed" event is triggered only when a reaction was actually removed.


POST /current/comments/{comment_id}/unlink/

Clear a comment's link to the entity it was linked to. The comment itself is neither deleted nor moved — only its link is cleared.

Auth: Required (JWT). Rate limited.

Authorization re-runs against the comment's own scope — the workspace, share, or File Share that owns it — not against whatever context you are calling from. On top of that, only the comment's author, or a workspace/share admin or owner (moderation; never on a File Share comment), may unlink it.

Request Body (JSON, optional)

FieldTypeRequiredConstraintsDescription
entity_id string No — The entity the comment was linked to. Supply it to make a repeat call idempotent: if the comment is already unlinked, the call returns success instead of the "not linked" error below. Without it, unlinking an already-unlinked comment is an error.

Response

The updated comment, in the standard shape (see Comment Object), with its link cleared.

Error Responses

Error CodeHTTP StatusMessageCause
152022406Comment ID is required{comment_id} missing
176691406Invalid comment ID format{comment_id} is not a valid opaque ID
123525, 100562 or 148282404Comment not foundUnknown comment ID — distinct load checks, each with its own code
148282406Comment is not linked to any entityNothing to clear, and no entity_id was sent
varies per call site — read error.code from the response401, or 404 when the scope hides the commentYou do not have permission to unlink this comment, or Comment not foundThe access check on the comment’s own scope refused. The code is assigned by whichever check failed, and the status follows that check’s category
123757401You do not have permission to unlink this commentYou may write comments on the scope but are neither the comment's author nor a moderator
148282 or 151137500Failed to unlink commentInternal error — two distinct failure paths

Notes: A comment-unlinked event fires only when the comment actually had a link to clear.


Thread State: Resolve, Reopen, Assign

A thread is a root comment (one with no parent_id) plus its replies. Thread state lives on the root — open or resolved, a single assignee, and state_version, a counter that increments on every change (see Thread State Fields under Comment Object above). Replies carry no thread state: sending a reply's ID to any of these three routes returns 400 comment_not_root.

All three routes:

Comments that belong to a File Share are read- and post-only. All three routes refuse them with 403 comment_thread_read_only.

Resolve

POST /current/comments/{comment_id}/resolve/

Marks the thread resolved.

Auth: Required (JWT). Rate limited.

Content-Type: application/json

Request Body (optional)

FieldTypeRequiredDescription
if_state_versionintegerNoGuards the write against a change you haven't seen yet — see Concurrency below

Who may resolve: the thread's author, the current assignee, anyone who can edit the item, or a workspace or share admin.

Request Example

curl -X POST "https://api.fast.io/current/comments/abc123opaqueid/resolve/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{}'

Response

{"result": true, "comment": {...}}, the updated thread.

Idempotency: resolving an already-resolved thread returns it unchanged, with 200. resolved_by and resolved_at are not restamped, and no event fires.

Reopen

POST /current/comments/{comment_id}/reopen/

Reopens a resolved thread. Same body, permissions and idempotency as Resolve.

A “Resolve” undo is a reopen call with if_state_version set to the state_version that the resolve returned.

Request Example

curl -X POST "https://api.fast.io/current/comments/abc123opaqueid/reopen/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{"if_state_version": 3}'

Response

{"result": true, "comment": {...}}.

Assign

POST /current/comments/{comment_id}/assign/

Sets, replaces, or clears the thread's single assignee. Anyone who can comment on the item may assign.

Auth: Required (JWT). Rate limited.

Content-Type: application/json

Request Body (exactly one form, plus an optional if_state_version)

BodyMeaning
{"assignee_user_id": "<user id>"}Assign a person (or a service account)
{"assignee_user_id": "<user id>", "assignee_agent": {"credential_type": "oauth"|"api_key", "credential_id": "<id>"}}Assign a person, to be handled by one of their own connected agents. The credential must be a live connection that belongs to that user, and its current scopes must reach the thread's surface: the share, for a thread started on a share (even when you reach it through the share's workspace), otherwise the workspace (an API key scoped elsewhere, read-only, or an OAuth connection restricted to other places is refused). Its display name comes from the connection, not from the request
{"assignee": "self"}Assign yourself. When you call with an agent connection, the assignment is to you, via that connection
{"assignee": "ripley"}Assign Ripley, on your behalf
{"clear": true}Unassign

Request Example

curl -X POST "https://api.fast.io/current/comments/abc123opaqueid/assign/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{"assignee_user_id": "1234567890123456789"}'

Response

{"result": true, "comment": {...}}.

Notes

Concurrency (if_state_version)

if_state_version is optional on resolve, reopen and assign. When it is present and the thread has changed since you read it, the route returns 409 with error.params.reason = state_version_conflict and error.params.state_version set to the current value. Reload and retry.

If the thread is already in the state you asked for, the call succeeds unchanged even with a stale if_state_version — that makes a retry after a lost response safe.

Errors

Gate on HTTP status, then error.params.reason.

Error CodeHTTP StatusreasonWhen
(generated per call site)400comment_not_rootThe ID is a reply
(generated per call site)400assignee_invalidBad assign body or agent connection
(generated per call site)400assignee_no_accessThe assignee cannot see the item
(generated per call site)403comment_thread_read_onlyFile Share comment
(generated per call site)403comment_thread_forbiddenYou may not resolve or reopen this thread
(generated per call site)409state_version_conflictif_state_version mismatch
(generated per call site)503comment_thread_state_unavailableTemporary; retryable

Thread Limits

Each file has a limit on its threads (top-level comments, and suggestion threads too). Replies are never counted.

When a new thread would exceed the limit, creating it (see Create or Update Comment above) fails with 409 comment_thread_limit_reached. If the limit cannot be checked, it fails with 503 comment_thread_limit_unavailable (retryable). See Thread Grouping under List Comments above for the limits block that reports usage against the cap.


Suggestions

A suggestion is a proposed edit to a workspace note: a comment thread with kind: "suggestion" whose root carries suggestion{}. The anchor (the root's reference exact / prefix / suffix) is the text it changes. Suggestions exist on workspace notes only.

Create a suggestion

POST /current/comments/workspace/{workspace_id}/{note_id}/

The comment create body, plus:

FieldTypeRequiredDescription
kindstringYes"suggestion" ("comment" or omitted creates a plain comment)
suggestionobjectYes{op, replacement}: op is "replace" or "delete"; replacement is the new text ("" for a delete). The anchor's exact needs at least one non-whitespace character; a replace's replacement must be non-empty, stay within one block (no line break), differ from exact, and carry no control character (U+0000-U+001F except tab, DEL), zero-width space (U+200B) or line / paragraph separator (U+2028, U+2029)
referenceobjectYesThe anchor; exact is the text the suggestion changes
bodystringNoThe rationale. May be empty

A suggestion is never a reply (parent_id), never an edit (comment_id) and takes no attachments (target_ids / target_id): those are 400 with error.params.reason = suggestion_invalid, as is an invalid suggestion object, or a suggestion object sent without kind: "suggestion". A surface other than a workspace note is 400 suggestion_unsupported_surface. A suggestion counts against the file's thread limit like any root comment. The response comment is the root row with its thread fields and suggestion{}.

POST /current/comments/workspace/{workspace_id}/{note_id}/suggestions/bulk/ creates up to 50 in one request: {items: [{reference, suggestion, body?}]} returns {created: [...], failed: [{index, reason}]}.

Reading suggestions

Suggestion threads are hidden from comment lists, details, counts and search unless you opt in with include_suggestions=true (or kind=suggestion on a list). Opted in, each suggestion root row carries:

FieldTypeDescription
suggestion.opstring"replace" or "delete"
suggestion.replacementstringThe proposed text
suggestion.statestringpending, accepting (an accept holds it), accepted, rejected or withdrawn
suggestion.state_versionintegerThe suggestion's own revision (separate from the thread's state_version); send it as if_state_version
suggestion.decided_by / decided_atobject|null / string|nullWho decided it, and when
suggestion.applied_version_idstring|nullThe note version an accept produced
can_accept / can_rejectbooleanWhether the caller may accept (edit rights on the note) or reject it now: false once decided, while another accept holds it, and on a deleted root (group=threads and details/ rows)
resolved / resolved_by / resolved_atA suggestion root reads as resolved once decided (accepted, rejected or withdrawn), by its decider
can_editbooleanFalse once the suggestion may no longer be edited (accepting, decided, or a headless accept was sent)

In counts, an open suggestion (pending or accepting) counts in open and pending_suggestions; a decided one counts in done. status=open / status=resolved judge a suggestion by its own state. Workspace search (GET /current/workspace/{workspace_id}/search/) returns suggestion comments only with include_suggestions=true.

Edit, withdraw, reject, undo

withdraw, reject and undo accept if_state_version; reject and withdraw also accept confirm_unsettled (boolean, default false; see below — any other type is 400 suggestion_invalid). Replies, reactions and mentions on a suggestion thread work like on any comment.

An unsettled headless accept blocks reject and withdraw. Once a headless accept has sent the edit to the note and no applied answer has been recorded, the suggestion is unsettled: the edit may already be in the note. This lasts after the accept's hold expires and the suggestion reads pending again — expiry alone never settles it. While it is unsettled, reject and withdraw answer 409 with error.params.reason = suggestion_apply_unknown, retryable: true, and retry_after while a hold remains. Two ways forward: accept again in headless mode (the same idempotency_key while the hold lasts, any key once it has expired) — only an applied answer settles the suggestion, as accepted; or check the note yourself and, if you still want to decide, send the reject or withdraw again with "confirm_unsettled": true. That decides as usual even though the edit may be in the note (the response carries "unsettled_acknowledged": true); remove the edit from the note yourself if it landed. confirm_unsettled never overrides a live hold: while one remains the reject or withdraw still answers 409 suggestion_not_pending with retry_after, so wait for it to end. An editor-mode accept that confirms also settles the suggestion (as accepted). No other accept answer (suggestion_stale, suggestion_apply_unknown, ...) proves the earlier send was not applied, so none settles it.

Delete

Deleting the root ends the suggestion: it can no longer be decided or edited, and it leaves lists and search like any deleted comment. A read that includes deleted comments (include_deleted, details/) can still show it, with can_accept / can_reject false. Deleting the workspace removes its suggestions.


Accept a Suggestion

POST /current/comments/{comment_id}/suggestion/accept/

Accept a suggested edit on a workspace note. {comment_id} is the suggestion's thread (root comment). Auth required. You need edit rights on the note; agent platform tokens cannot accept.

Request Body (JSON)

FieldTypeRequiredDescription
modestringNoeditor (default): you hold the note open and apply the edit yourself, then call confirm. headless: Fastio applies the edit for you.
idempotency_keystringNoHeadless only. 1-128 characters of A-Z a-z 0-9 . _ : -, chosen by you per accept attempt. Send the SAME key when you retry that attempt.
accept_tokenstringNoEditor only. To resume your own accept, send the accept_token it returned.
if_state_versionintegerNoThe suggestion's state_version you last saw; a mismatch is 409 state_version_conflict.

One accept at a time. An accept holds the suggestion for a 90-second lease (renewed while a headless accept runs) plus a 25-second grace, so up to 116 seconds after its last renewal; retry_after always says when the hold ends. While it is held, any OTHER accept attempt — including your own with a different idempotency_key, with none, or in the other mode — gets 409 with error.params.reason = suggestion_not_pending, retryable: true and retry_after. Only a retry of the same attempt (same idempotency_key, or the editor's own accept_token) picks the held accept back up.

Headless Outcomes

HTTPreasonMeaningWhat to do
200—Applied. Body {comment, state: "accepted", applied_version_id}Done. Repeating the accept returns the same result.
409suggestion_staleThe anchored text no longer matches, so this request applied nothing. An earlier send of the same suggestion may still have changed the note, so the hold is kept until it expires (retry_after) and the suggestion stays unsettledCheck the note. Once the hold ends (retry_after), reject it with confirm_unsettled: true, or create a new suggestion
409suggestion_needs_editorThe note needs an editor to apply this suggestion (for example, it is open in an editor that cannot apply it for you), and nothing was applied: this was the first send of the attempt. editor_reason says why; retryable says whether trying again may work. The hold is released and the suggestion is pending again, not unsettledIf retryable is true, accept again after a short wait (about 10 seconds). Otherwise open the note and accept it in the editor
409suggestion_not_pendingAnother accept holds it, or it was already decidedRe-read the suggestion; retry after retry_after
409suggestion_lease_invalidThe accept was taken over before anything was sentAccept again
503suggestion_apply_unknownThe edit was sent (by this request or an earlier one of the same attempt) and MAY have been applied. Every outcome other than applied or stale after sending is reported this way (timeouts, outages, storage failures, refusals, and the note being busy or needing an editor when an earlier send of the same attempt may have applied; editor_reason is included when known), except a first send that applied nothing (suggestion_needs_editor, above) or that the service refused before applying anything (suggestion_apply_unavailable or suggestion_accept_forbidden, below)Re-read the note. Retry with the same idempotency_key — the retry is answered from the first attempt's record and never applies twice — or wait retry_after seconds for the hold to expire
503suggestion_apply_unavailableNothing was applied. Either this request sent nothing and no earlier send of this attempt was known to it (the service is unavailable, or the suggestion could not be read or written; an overlapping retry of the same attempt may still be in flight), or the service refused the edit before applying any of it (retryable: false, a server-side problem): the hold is released, the suggestion is pending again and not unsettled, so reject and withdraw work as usualRetry later with the same idempotency_key if retryable is true. When it is false, retrying will not help until the problem is fixed: accept in the editor, or try again later
403suggestion_accept_forbiddenYou cannot edit this note, or your edit rights could not be confirmed when the edit was sent. Nothing was applied; the suggestion is pending again—

Every successful accept response carries comment — the suggestion's root row with its suggestion{} (the leased replacement included) — in editor and headless mode, on a first accept, on a resume of the same attempt and on a repeat of an accept that already applied.

Editor mode returns {comment, state: "accepting", accept_token, lease_expires, lease_seconds, apply_generation}. Apply the edit in your editor, save, then call POST /current/comments/{comment_id}/suggestion/confirm/ with {accept_token, applied_version_id} (the saved version of this note), or POST /current/comments/{comment_id}/suggestion/release/ with {accept_token} to give it back. A headless accept whose edit was sent cannot be released by anyone: release answers 503 suggestion_apply_unknown with retry_after, and the hold ends when it is confirmed or expires.


Suggestion Decisions: Withdraw, Reject, Undo, Confirm, Release

These routes act on a suggestion thread on a workspace note. {comment_id} is the suggestion's root comment. Every route takes a JSON object body (an empty body is {}) and answers {"result": true, "comment": {...}, "state": "..."}: comment is the root row with its thread fields and suggestion{}, and state is the suggestion's state after the call. Each route first checks that you can comment on the note, so the common errors at the end of each table apply to every one.

Withdraw a Suggestion

POST /current/comments/{comment_id}/suggestion/withdraw/

The suggestion's author takes it back: pending → withdrawn.

Auth: Required (JWT). Rate limited.

Content-Type: application/json

Who may call it: The suggestion's author only (the author of its root comment). You must be able to comment on the note.

Request Body

FieldTypeRequiredDescription
if_state_versionintegerNoThe suggestion's state_version you last saw (not the thread's); a mismatch is 409 state_version_conflict. Must be a non-negative integer (otherwise 400)
confirm_unsettledbooleanNoDefault false. true decides even while a headless accept that was sent is unsettled (its edit may already be in the note — check the note first); the response then carries unsettled_acknowledged: true. Any other type is 400 suggestion_invalid

Request Example

curl -X POST "https://api.fast.io/current/comments/abc123opaqueid/suggestion/withdraw/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{}'

Response

{"result": true, "comment": {...}, "state": "withdrawn"} — comment is the root row with its thread fields and suggestion{}. Adds "unsettled_acknowledged": true when confirm_unsettled decided over an unsettled headless accept.

Errors

Gate on HTTP status, then error.params.reason.

Error CodeHTTP StatusreasonWhen
(generated per call site)400— (“if_state_version must be a non-negative integer”)if_state_version is not a non-negative integer
(generated per call site)400suggestion_invalidconfirm_unsettled is not a boolean
(generated per call site)403suggestion_forbiddenYou are not the suggestion's author
(generated per call site)409suggestion_apply_unknownA headless accept was sent and never confirmed, so its edit may already be in the note (also after its hold expired). retryable: true, state, and retry_after while a hold remains. Check the note, then send confirm_unsettled: true, or accept again
(generated per call site)409suggestion_not_pendingAn accept holds the suggestion: retryable: true, retry_after. Or it was already decided (by someone else, or to another state): state
(generated per call site)409state_version_conflictif_state_version mismatch, or a concurrent change; state_version and state give the current values
(generated per call site)400— (“Invalid comment ID format”)The {comment_id} path segment is not a comment id
(generated per call site)401 / 404 / 406— (“Invalid entity or insufficient permissions”)You cannot comment where the suggestion lives; the status follows the access check (401 access denied, 404 not found, 406 otherwise)
(generated per call site)404— (“Comment not found”)Unknown or deleted comment
(generated per call site)403comment_thread_read_onlyA File Share comment
(generated per call site)400comment_not_rootThe id is a reply, not the suggestion root
(generated per call site)400— (“The request body must be a JSON object”)The body is not a JSON object
(generated per call site)400comment_not_suggestionThe thread is not a suggestion
(generated per call site)400suggestion_unsupported_surfaceThe suggestion is not on a workspace note
(generated per call site)404— (“The note this suggestion belongs to no longer exists”)The note was purged
(generated per call site)503comment_thread_state_unavailableThe thread state could not be read; retryable: true
(generated per call site)503suggestion_state_unavailableThe suggestion could not be read or written; retryable: true

Reject a Suggestion

POST /current/comments/{comment_id}/suggestion/reject/

Reject a pending suggestion: pending → rejected.

Auth: Required (JWT). Rate limited.

Content-Type: application/json

Who may call it: Anyone who may resolve the thread: its author, the current assignee, anyone who can edit the note, or a workspace admin. You must be able to comment on the note.

Request Body

FieldTypeRequiredDescription
if_state_versionintegerNoThe suggestion's state_version you last saw (not the thread's); a mismatch is 409 state_version_conflict. Must be a non-negative integer (otherwise 400)
confirm_unsettledbooleanNoDefault false. true decides even while a headless accept that was sent is unsettled (its edit may already be in the note — check the note first); the response then carries unsettled_acknowledged: true. Any other type is 400 suggestion_invalid

Request Example

curl -X POST "https://api.fast.io/current/comments/abc123opaqueid/suggestion/reject/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{"if_state_version": 2}'

Response

{"result": true, "comment": {...}, "state": "rejected"} — comment is the root row with its thread fields and suggestion{}. Adds "unsettled_acknowledged": true when confirm_unsettled decided over an unsettled headless accept.

Errors

Gate on HTTP status, then error.params.reason.

Error CodeHTTP StatusreasonWhen
(generated per call site)400— (“if_state_version must be a non-negative integer”)if_state_version is not a non-negative integer
(generated per call site)400suggestion_invalidconfirm_unsettled is not a boolean
(generated per call site)403suggestion_forbiddenYou may not resolve this thread
(generated per call site)409suggestion_apply_unknownA headless accept was sent and never confirmed, so its edit may already be in the note (also after its hold expired). retryable: true, state, and retry_after while a hold remains. Check the note, then send confirm_unsettled: true, or accept again
(generated per call site)409suggestion_not_pendingAn accept holds the suggestion: retryable: true, retry_after. Or it was already decided (by someone else, or to another state): state
(generated per call site)409state_version_conflictif_state_version mismatch, or a concurrent change; state_version and state give the current values
(generated per call site)400— (“Invalid comment ID format”)The {comment_id} path segment is not a comment id
(generated per call site)401 / 404 / 406— (“Invalid entity or insufficient permissions”)You cannot comment where the suggestion lives; the status follows the access check (401 access denied, 404 not found, 406 otherwise)
(generated per call site)404— (“Comment not found”)Unknown or deleted comment
(generated per call site)403comment_thread_read_onlyA File Share comment
(generated per call site)400comment_not_rootThe id is a reply, not the suggestion root
(generated per call site)400— (“The request body must be a JSON object”)The body is not a JSON object
(generated per call site)400comment_not_suggestionThe thread is not a suggestion
(generated per call site)400suggestion_unsupported_surfaceThe suggestion is not on a workspace note
(generated per call site)404— (“The note this suggestion belongs to no longer exists”)The note was purged
(generated per call site)503comment_thread_state_unavailableThe thread state could not be read; retryable: true
(generated per call site)503suggestion_state_unavailableThe suggestion could not be read or written; retryable: true

Undo a Suggestion Decision

POST /current/comments/{comment_id}/suggestion/undo/

Return a decided suggestion to pending. There is no server-side time window.

Auth: Required (JWT). Rate limited.

Content-Type: application/json

Who may call it: The user who decided it only. Undoing an accepted suggestion also needs edit rights on the note.

Request Body

FieldTypeRequiredDescription
if_state_versionintegerNoThe suggestion's state_version you last saw. Without it the undo is still pinned to the state and state_version the request read, so a suggestion that changed underneath is 409 state_version_conflict, never undone. Must be a non-negative integer (otherwise 400)
modestringNoeditor (default) or headless. Undoing an accepted suggestion needs editor
reverted_version_idstringTo undo an accepted suggestionThe note version your editor saved after applying the inverse edit. It must be a version of this note (at most 128 characters); it is stored with the suggestion. Not needed for a rejected or withdrawn suggestion

Request Example

curl -X POST "https://api.fast.io/current/comments/abc123opaqueid/suggestion/undo/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{"mode": "editor", "reverted_version_id": "{version_id}"}'

Response

{"result": true, "comment": {...}, "state": "pending"}.

Errors

Gate on HTTP status, then error.params.reason.

Error CodeHTTP StatusreasonWhen
(generated per call site)400— (“if_state_version must be a non-negative integer”)if_state_version is not a non-negative integer
(generated per call site)400suggestion_invalidmode is not editor or headless; or reverted_version_id is too long or is not a version of this note
(generated per call site)409suggestion_needs_editorUndoing an accepted suggestion with mode: headless or without reverted_version_id. editor_reason = undo_requires_editor, retryable: false
(generated per call site)403suggestion_undo_forbiddenYou did not decide this suggestion, or it is accepted and you cannot edit the note
(generated per call site)409suggestion_not_pendingAn accept holds the suggestion: retryable: true, retry_after
(generated per call site)409state_version_conflictThe suggestion changed since the request read it, or if_state_version mismatch; state_version and state give the current values
(generated per call site)503suggestion_state_unavailableThe reverted_version_id lookup failed; retryable: true
(generated per call site)400— (“Invalid comment ID format”)The {comment_id} path segment is not a comment id
(generated per call site)401 / 404 / 406— (“Invalid entity or insufficient permissions”)You cannot comment where the suggestion lives; the status follows the access check (401 access denied, 404 not found, 406 otherwise)
(generated per call site)404— (“Comment not found”)Unknown or deleted comment
(generated per call site)403comment_thread_read_onlyA File Share comment
(generated per call site)400comment_not_rootThe id is a reply, not the suggestion root
(generated per call site)400— (“The request body must be a JSON object”)The body is not a JSON object
(generated per call site)400comment_not_suggestionThe thread is not a suggestion
(generated per call site)400suggestion_unsupported_surfaceThe suggestion is not on a workspace note
(generated per call site)404— (“The note this suggestion belongs to no longer exists”)The note was purged
(generated per call site)503comment_thread_state_unavailableThe thread state could not be read; retryable: true
(generated per call site)503suggestion_state_unavailableThe suggestion could not be read or written; retryable: true

Confirm an Editor Accept

POST /current/comments/{comment_id}/suggestion/confirm/

Finish an editor-mode accept: record the note version your editor saved with the edit applied. accepting → accepted.

Auth: Required (JWT). Rate limited.

Content-Type: application/json

Who may call it: The holder of the editor accept (its accept_token), with edit rights on the note. Fastio's built-in agent cannot confirm.

Request Body

FieldTypeRequiredDescription
accept_tokenstringYesThe accept_token the editor-mode accept returned (1-128 characters)
applied_version_idstringYesThe note version your editor saved with the edit applied. It must be a version of this note

Request Example

curl -X POST "https://api.fast.io/current/comments/abc123opaqueid/suggestion/confirm/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{"accept_token": "{accept_token}", "applied_version_id": "{version_id}"}'

Response

{"result": true, "comment": {...}, "state": "accepted", "applied_version_id": "{version_id}"}.

Errors

Gate on HTTP status, then error.params.reason.

Error CodeHTTP StatusreasonWhen
(generated per call site)403suggestion_accept_forbiddenFastio's built-in agent, or you cannot edit the note
(generated per call site)400suggestion_invalidaccept_token or applied_version_id is missing or too long, or applied_version_id is not a version of this note
(generated per call site)409suggestion_lease_invalidThe token is not the suggestion's current accept (taken over, or held by someone else), or the suggestion was accepted with a different version. state gives the current state
(generated per call site)409suggestion_not_pendingThe suggestion was rejected or withdrawn
(generated per call site)409state_version_conflictA concurrent change kept the write from landing; state_version and state give the current values
(generated per call site)503suggestion_state_unavailableThe applied_version_id lookup failed; retryable: true
(generated per call site)400— (“Invalid comment ID format”)The {comment_id} path segment is not a comment id
(generated per call site)401 / 404 / 406— (“Invalid entity or insufficient permissions”)You cannot comment where the suggestion lives; the status follows the access check (401 access denied, 404 not found, 406 otherwise)
(generated per call site)404— (“Comment not found”)Unknown or deleted comment
(generated per call site)403comment_thread_read_onlyA File Share comment
(generated per call site)400comment_not_rootThe id is a reply, not the suggestion root
(generated per call site)400— (“The request body must be a JSON object”)The body is not a JSON object
(generated per call site)400comment_not_suggestionThe thread is not a suggestion
(generated per call site)400suggestion_unsupported_surfaceThe suggestion is not on a workspace note
(generated per call site)404— (“The note this suggestion belongs to no longer exists”)The note was purged
(generated per call site)503comment_thread_state_unavailableThe thread state could not be read; retryable: true
(generated per call site)503suggestion_state_unavailableThe suggestion could not be read or written; retryable: true

Release an Editor Accept

POST /current/comments/{comment_id}/suggestion/release/

Give back an editor-mode accept without applying it: accepting → pending.

Auth: Required (JWT). Rate limited.

Content-Type: application/json

Who may call it: The holder of the accept (its accept_token). You must be able to comment on the note.

Request Body

FieldTypeRequiredDescription
accept_tokenstringYesThe accept_token the accept returned (1-128 characters)

Request Example

curl -X POST "https://api.fast.io/current/comments/abc123opaqueid/suggestion/release/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{"accept_token": "{accept_token}"}'

Response

{"result": true, "comment": {...}, "state": "pending"}.

Errors

Gate on HTTP status, then error.params.reason.

Error CodeHTTP StatusreasonWhen
(generated per call site)400suggestion_invalidaccept_token is missing or too long
(generated per call site)409suggestion_not_pendingThe suggestion was already decided; state
(generated per call site)409suggestion_lease_invalidThe token is not the suggestion's current accept, or someone else holds it
(generated per call site)503suggestion_apply_unknownA headless accept sent the edit, which may have been applied: retryable: true, retry_after (the hold left; also a Retry-After header)
(generated per call site)409state_version_conflictA concurrent change kept the write from landing; state_version and state give the current values
(generated per call site)400— (“Invalid comment ID format”)The {comment_id} path segment is not a comment id
(generated per call site)401 / 404 / 406— (“Invalid entity or insufficient permissions”)You cannot comment where the suggestion lives; the status follows the access check (401 access denied, 404 not found, 406 otherwise)
(generated per call site)404— (“Comment not found”)Unknown or deleted comment
(generated per call site)403comment_thread_read_onlyA File Share comment
(generated per call site)400comment_not_rootThe id is a reply, not the suggestion root
(generated per call site)400— (“The request body must be a JSON object”)The body is not a JSON object
(generated per call site)400comment_not_suggestionThe thread is not a suggestion
(generated per call site)400suggestion_unsupported_surfaceThe suggestion is not on a workspace note
(generated per call site)404— (“The note this suggestion belongs to no longer exists”)The note was purged
(generated per call site)503comment_thread_state_unavailableThe thread state could not be read; retryable: true
(generated per call site)503suggestion_state_unavailableThe suggestion could not be read or written; retryable: true

Comment Email Notifications

Fastio emails people about comment activity that concerns them. Each kind of email is separate:

Resolving or reopening a thread never sends email. Agents and service accounts are never emailed; work assigned to your agent is emailed to you. You are not emailed about your own actions, except a reply the Ripley agent posts on your behalf. Before an email is sent, Fastio checks again that the comment still exists, that you can still see it, and that your preference allows it. Email links open the file with the comment selected (?comment={comment_id}).

Preference Modes

ModeEmails
mine_and_mentions (default)mentions, assignments, replies on threads you started or are assigned to, and your suggestions being decided
alleverything above, plus replies on threads you replied in
noneno comment email

A preference is global, with an optional override per file. The override wins; without one the file uses your global mode; without a global mode the default applies.

GET /current/comments/preferences/

Your global comment email mode.

{ "result": true, "mode": "mine_and_mentions" }

POST /current/comments/preferences/

Set your global mode. JSON body:

{ "mode": "all" }

mode is one of all, mine_and_mentions, none, or inherit (clears your global setting so the default applies). The response is the same as GET.

GET /current/comments/preferences/{node_id}/

Your mode for one file. You must be able to view the file.

{ "result": true, "mode": "none", "inherited": false }

inherited is true when the file has no override and mode comes from your global setting (or the default).

POST /current/comments/preferences/{node_id}/

Set or clear the override for one file. JSON body { "mode": "all" | "mine_and_mentions" | "none" | "inherit" }; inherit removes the override. The response is the same as GET.

Errors (error.params.reason):

OAuth connections restricted per place need the comment tool on the file's workspace or share to POST a per-file preference; reading it needs read.


Comment Attachments

Attach arbitrary platform objects to a comment to give it context — a file or folder, a sign envelope, a share, a File Share, or a workspace. An attachment is a lightweight reference: it records what is attached, not a copy of it.

Attachment Object (hydrated)

Every attachment returned by the API is hydrated and access-gated to the caller: an object the caller cannot see (or that no longer exists) is returned with available: false and display_name: null rather than leaking its name. Render defensively: when available is false, never show a name — show a generic placeholder, and never assume display_name is non-null.

FieldTypeDescription
target_idstringThe attached object's ID (19-digit numeric for a workspace/share/envelope/File Share, or an opaque ID for a file/folder node)
target_typestringCoarse object type: node, envelope, share, workspace, or fileshare
kindstringAlias of target_type (same value)
display_namestring|nullThe object's resolved display name, or null when the caller cannot see it or it has no name
availablebooltrue when the caller can access the object (and display_name is authoritative); false when it is inaccessible, missing, or could not be resolved

List Comment Attachments

GET /current/comments/{comment_id}/attachments/

List all objects attached to a comment, hydrated and access-gated to the caller.

Auth: Required (JWT). Rate limited.

Response

{
  "result": true,
  "comment_id": "abc123opaqueid",
  "attachments": [
    {
      "target_id": "23d4zvoprfmonyhy2wwjpoccqjaiz",
      "target_type": "node",
      "kind": "node",
      "display_name": "spec.pdf",
      "available": true
    },
    {
      "target_id": "9876543210987654321",
      "target_type": "share",
      "kind": "share",
      "display_name": null,
      "available": false
    }
  ],
  "count": 2
}

Attach Objects to a Comment

POST /current/comments/{comment_id}/attachments/

Attach one object (target_id) or many (target_ids) to a comment. Idempotent (already-attached objects are skipped); the new objects are committed atomically (a partial-batch failure attaches nothing). Returns the full, updated hydrated attachment list.

Auth: Required (JWT). Rate limited. Content-Type: application/json.

Request Body

FieldTypeRequiredConstraintsDescription
target_idstringOne of target_id / target_idsValid object IDA single object to attach
target_idsarray<string>One of target_id / target_idsNon-empty arrayMultiple objects to attach

A request whose new attachments would push the comment over the 25-attachment cap is rejected before anything is written.

Request Example — single

curl -X POST "https://api.fast.io/current/comments/abc123opaqueid/attachments/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{"target_id": "23d4zvoprfmonyhy2wwjpoccqjaiz"}'

Request Example — bulk

curl -X POST "https://api.fast.io/current/comments/abc123opaqueid/attachments/" \
  -H "Authorization: Bearer {jwt_token}" \
  -H "Content-Type: application/json" \
  -d '{"target_ids": ["23d4zvoprfmonyhy2wwjpoccqjaiz", "9876543210987654321"]}'

Response

{
  "result": true,
  "comment_id": "abc123opaqueid",
  "attached": 1,
  "attachments": [
    {
      "target_id": "23d4zvoprfmonyhy2wwjpoccqjaiz",
      "target_type": "node",
      "kind": "node",
      "display_name": "spec.pdf",
      "available": true
    }
  ],
  "count": 1
}

Response Fields

FieldTypeDescription
comment_idstringThe comment the attachments belong to
attachedintNumber of objects newly attached by this request (0 when every target was already attached)
attachmentsarrayThe full hydrated attachment list after the operation
countintNumber of attachments in the list

Error Responses

Error CodeHTTP StatusMessageCause
1605 (Invalid Input)406A target_id or non-empty target_ids array is requiredNo target supplied
143686406target_ids exceeds the maximum allowed input size of 200More than 200 raw entries supplied. This is a raw-input bound, separate from — and looser than — the 25-attachments-per-comment cap, which is applied afterwards to the deduplicated set
1605 (Invalid Input)406Invalid attachment targetA target ID could not be decoded
1605 (Invalid Input)406Attachment limit reached (maximum 25 per comment)The new attachments would exceed the cap
1680 (Access Denied)401You do not have permission to modify attachments on this commentCaller is not the comment author
1609 (Not Found)404Comment not foundComment does not exist, is not visible to you, or is trashed

Detach an Object from a Comment

POST /current/comments/{comment_id}/attachments/detach/

Remove an attachment by its target_id. Idempotent — detaching an object that is not attached returns success with removed: false. Returns the updated hydrated attachment list.

Auth: Required (JWT). Rate limited. Content-Type: application/json.

Request Body

FieldTypeRequiredConstraintsDescription
target_idstringYesValid object IDThe object to detach

Response

{
  "result": true,
  "comment_id": "abc123opaqueid",
  "removed": true,
  "attachments": [],
  "count": 0
}

Response Fields

FieldTypeDescription
comment_idstringThe comment the attachment belonged to
removedbooltrue if an attachment was actually removed; false if the object was not attached
attachmentsarrayThe full hydrated attachment list after the operation
countintNumber of attachments remaining

Attaching at comment-create time (inline)

You can attach objects when creating a comment by including target_id or target_ids in the comment-create body (see Create or Update Comment). Inline attachments apply only to a new comment — they are ignored on an update, and the comment plus its attachments are written together so the comment never appears without them. An invalid target or one that would exceed the cap rejects the whole create.

attachment_count is returned on every comment list row and on the comment detail response. The hydrated attachments array is returned only on the comment detail response (GET /current/comments/{comment_id}/details/). All counts are computed live on each read.


Threading

Comments support single-level threading only.

Comment A (top-level)
├── Comment B (reply to A)
├── Comment C (reply to B → auto-flattened to reply to A)
└── Comment D (reply to A)

Mentions

Use mention markup in the body field to tag other users:

@[user:USER_ID:Display Name]

To summon Ripley into a workspace file's thread, use the token @[agent:ripley] (or assign the thread with {"assignee": "ripley"}); see the AI reference.

ComponentDescription
userLiteral prefix (always user)
USER_IDThe user's 19-digit numeric profile ID
Display NameDisplay name to show in the UI

Character limit details

Example body with mentions

Hey @[user:1234567890123456789:Jane Smith] and @[user:9876543210987654321:Bob Jones], please review this file.

Mentions inside code

This is a live behaviour change. A mention that sits inside markdown code — a fenced block, or a single-line inline backtick span — is treated as text the author is showing, not as a hail:

Two consequences to plan for: someone who is notified today by a fenced mention stops being notified, and a long body that hid mention markup inside a fence may now be rejected by the display cap that previously discounted it.

What counts as code — deliberately narrow, and matched per line:

What is NOT recognised as code. A mention in any of these still notifies, and is still discounted from the display cap, exactly as before:

The bias is deliberate. Failing to recognise code is safe — it is the behaviour that was already live. Inventing code where there is none would silently drop a real person's notification. When in doubt, it is not code.


Reference Anchoring

Comments can be anchored to a specific position within a file using the reference object. The type field is required and selects which additional fields are valid.

type values

typeApplies toValid anchor fields
"video"Video filestimestamp (or timestamp_start + timestamp_end); optional region
"audio"Audio filestimestamp (or timestamp_start + timestamp_end)
"document"Documents / textpage, text_snippet, section, and the text-anchor fields exact / prefix / suffix / start_offset / end_offset
"image"Imagesregion
"general"Any filenone (a bare {"type": "general"} anchor)

Anchor fields

FieldTypeUsed withDescription
timestamp number video, audio Position in seconds (non-negative). Mutually exclusive with the range fields
timestamp_start / timestamp_end number video, audio A time range in seconds (both non-negative, end > start)
page integer document Page number (positive integer)
text_snippet string document The referenced text passage (max 500 characters)
section object document A line/character range: {"start": {"line", "char"}, "end": {"line", "char"}}; line is a positive integer
exact / prefix / suffix string document Text-selection anchor: the selected text (exact, 1-500 characters) plus the text just before and after it (prefix / suffix, max 100 characters each). exact requires both prefix and suffix, and they require exact
start_offset / end_offset integer document Character offsets of the selection (non-negative; both or neither; end_offset > start_offset). A hint only: the quote (exact + prefix + suffix) is the anchor
offset_space string document The text space the offsets are measured in. The only accepted value is "plaintext-utf16-nl" (rendered plain text, UTF-16 code units, one newline between blocks)
v integer document Text-anchor schema version. The only accepted value is 2
source_version_id string document The file version the anchor was taken on (non-empty, max 128 characters)
region object video, image Rectangular area with keys x1, y1, x2, y2, each a number 0–100 (percent of width/height), with x2 > x1 and y2 > y1

Any unrecognized type is rejected with "Invalid reference type: {value}", and any key not listed above with "Unknown reference key: {key}" (error.params.reason = anchor_invalid). The whole reference object, JSON-encoded, may not exceed 2048 bytes (non-ASCII characters count as their 6-byte escaped form); a larger one is rejected with error.params.reason = anchor_too_large.

The text anchor is immutable. Once a comment is created, its exact, prefix and suffix cannot be changed, added or removed by an edit — either edit route rejects that with "The anchor of an existing comment cannot be changed" (error.params.reason = anchor_invalid). Resending the same values, or omitting reference, is fine; the offsets may change.

Example — Video timestamp

{
  "body": "The transition at this point needs work.",
  "reference": {"type": "video", "timestamp": 42.5}
}

Example — Document page

{
  "body": "Typo in the second paragraph.",
  "reference": {"type": "document", "page": 7}
}

Example — Image region

{
  "body": "This area needs higher contrast.",
  "reference": {"type": "image", "region": {"x1": 20, "y1": 15, "x2": 55, "y2": 40}}
}

Example — Document text snippet

{
  "body": "This sentence should be rephrased.",
  "reference": {"type": "document", "text_snippet": "The quick brown fox jumps over the lazy dog."}
}

Content Filtering

Comment bodies are stored as sent — the filtering stages below are currently inactive:

↑ Back to top