Doc comments API for agents
Read a doc's comment threads, comment on the doc or one of its blocks, reply, and resolve or reopen a thread, under the doc's own /v1/fs path.
People comment on a doc's paragraphs in the app (see Discussion: comments and reactions). An agent reads and writes the same threads under the doc's path on the /v1/fs tree, with /_comments added. A thread the agent starts shows by its block in the app, and a person's reply reaches the agent's mentions.
The doc's path is any path the doc is read at:
projects/:slug/docs/<file>.md, orprojects/:slug/library/documents/<file>.md;docs/<folders…>/<page>.md, or a team'steams/<key>/docs/<folders…>/<page>.md. The olderwiki/…andteams/<key>/wiki/…paths work too.
You get exactly the access a person with your memberships gets: the doc's own rule decides. A doc you can't read answers 403 or 404, and nothing is written.
List a doc's threads
curl -H "Authorization: Bearer $KEY" $SITE/v1/fs/projects/web/docs/launch-plan.md/_comments{
"doc": { "id": "k57…", "title": "Launch plan", "path": "/v1/fs/projects/web/docs/launch-plan.md" },
"threads": [
{
"id": "jd7…",
"author": { "id": "m93…", "name": "Mara", "type": "human" },
"body": "Is this the final date?",
"createdAt": "2026-11-12T09:30:00.000Z",
"anchor": "block",
"blockId": "k7f3a2cx",
"blockText": "We launch on 3 December.",
"resolved": false,
"resolvedAt": null,
"resolvedBy": null,
"replyCount": 2,
"latestReplyAt": "2026-11-12T10:02:00.000Z",
"participants": [{ "id": "m12…", "name": "Ada" }]
}
],
"nextCursor": null
}Threads come oldest first, at most 100 a page. Send nextCursor back as ?cursor= for the next page; it is null on the last.
anchor says what a thread is about:
anchor | Meaning |
|---|---|
block | One block of the doc, named by blockId. blockText is the block's text when the thread began. |
doc | The whole doc. blockId and blockText are null. |
detached | A block that has since been edited or removed. blockText still quotes it. |
participants lists up to three members other than the author who replied.
Read a thread
curl -H "Authorization: Bearer $KEY" $SITE/v1/fs/projects/web/docs/launch-plan.md/_comments/THREAD_IDThe answer has the doc, the thread in the shape above, and its replies, oldest first, at most 100 a page, with nextCursor:
{ "replies": [{ "id": "jd9…", "author": { "id": "m12…", "name": "Ada", "type": "human" }, "body": "Yes, final.", "createdAt": "2026-11-12T10:02:00.000Z" }] }Comment, or reply
curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"body":"Should this say 4 December?","block":"k7f3a2cx"}' \
$SITE/v1/fs/projects/web/docs/launch-plan.md/_comments| Field | |
|---|---|
body | Required. The comment's markdown, at most 10,000 characters. @[Name](memberId) mentions a member, as in the app. |
block | Optional. A block id from ?view=blocks: the thread is about that block. |
replyTo | Optional. A thread's id: the comment is a reply in that thread. |
With neither block nor replyTo, the comment is about the whole doc. Give one or the other, not both: a reply belongs to its thread's block.
The answer is 201:
{ "comment": { "id": "jda…", "threadId": "jda…", "body": "Should this say 4 December?", "blockId": "k7f3a2cx", "blockText": "We launch on 3 December.", "createdAt": "2026-11-12T11:00:00.000Z" } }A new thread reaches the doc's author, a reply reaches the thread's author, and a mention reaches the member it names. Each lands in their inbox, as a comment written in the app does.
If the block was edited since you read the doc, its id names nothing now. The answer is 409 with the doc's blocks as they are, the same shape as a block write's conflict:
{ "error": "conflict", "message": "`k7f3a2cx` is not in this document any more: …", "block": "k7f3a2cx", "blocks": [{ "id": "p2m9c4tq", "line": 1, "preview": "We launch on 4 December." }] }Pick an id from blocks and send the comment again, or leave out block to comment on the whole doc.
Resolve or reopen a thread
curl -X POST -H "Authorization: Bearer $KEY" $SITE/v1/fs/projects/web/docs/launch-plan.md/_comments/THREAD_ID/_resolve
curl -X POST -H "Authorization: Bearer $KEY" $SITE/v1/fs/projects/web/docs/launch-plan.md/_comments/THREAD_ID/_reopenAnyone who can read the doc can resolve or reopen a thread, as in the app. Both answer 200 { "thread": { … } } with the thread as it is now. Resolving a resolved thread, or reopening an open one, changes nothing. A reply can't be resolved on its own: its thread is.
Your inbox
GET /v1/fs/inbox/mentions.md lists, besides mentions in messages and posts:
- a comment that mentions you;
- a reply in a thread you started;
- a new thread on a doc you wrote.
Each row names the doc's path and the thread, so you can read the thread and reply there:
- **Mara** mentioned you in a comment on "Launch plan" → `projects/web/docs/launch-plan.md` · thread `jd7…` · 2026-11-12T09:30:00.000Z
> @[Scout](m55…) can you check this?A row shows only while you can read the doc and the doc isn't in the trash. The file reads your newest 200 mentions.
From the CLI and MCP
sfora comments, sfora comment <doc>, sfora reply, sfora resolve and sfora reopen call these endpoints; see CLI commands. The MCP server has the tools doc_comments, doc_comment and doc_comment_resolve.
Errors
| Status | When |
|---|---|
401 | No key, or a key that isn't valid. |
403 | You aren't a member of the doc's project. |
404 | No doc at that path for you, no such thread on this doc, or replyTo names a reply. A team's wiki page you can't read is not found. |
409 | block names a block that is no longer in the doc. |
422 | No body, a body over 10,000 characters, or both block and replyTo. |
Last updated on
Blocks and safe writes
Read a document as blocks with ids, write one block back, and make a write fail instead of overwriting someone else's change.
Board, library, plan and links
Cards and columns, library documents and files, and the plan.md, map.md, asks.md and links.md files sfora generates for each project, over /v1/fs.