cirrux

Documents API

Find the Cirrux Docs documents a user owns or has been shared, read one as markdown with its comments and suggestions, edit it, suggest changes to it, comment on it, create one, rename it, and move it in and out of Trash. It lives under https://api.cirrux.co/public_api/v1/documents.

Every endpoint acts on one person’s documents, so it needs a user-scoped OAuth token. A workspace API key is refused with 403 forbidden.

Cirrux Docs itself is in preview: the docs.cirrux.co app is only open to accounts that have been given early access.

The document object

FieldTypeRequiredDescription
objectstringnoAlways document.
uuidstringnoUnique identifier of the document.
workspace_uuidstringnoThe workspace the document belongs to.
titlestring | nullnonull when the document is untitled.
urlstringnoWhere to open the document in Cirrux Docs.
my_rolestringnoYour role on the document: viewer, commenter, editor, manager or owner.
sharedbooleannoWhether the document has been shared with anyone.
ownerobjectnoWho owns it: self (true when that is you), first_name, last_name and display_name.
trashed_atstring | nullnoWhen the document was moved to Trash. It is deleted for good 30 days later.
created_atstringnoWhen the document was created.
updated_atstringnoWhen the title or trash state last changed. Editing the content does not change it.

Endpoints

List documents

GET/v1/documents
GET/v1/documents/trash

The documents you own or have been shared, newest first. The first leaves out the ones in Trash, the second lists only those. Scope docs.read.

FieldTypeRequiredDescription
qstringnoOnly documents whose title contains this, ignoring case.
limitintegernoDocuments per page, 1 to 100. Default 25.
cursorstringnoThe next_cursor of the previous page, while has_more is true.
curl "https://api.cirrux.co/public_api/v1/documents?q=roadmap" \
  -H "Authorization: Bearer $CIRRUX_TOKEN"
{
  "object": "list",
  "url": "/v1/documents",
  "has_more": false,
  "data": [
    {
      "object": "document",
      "uuid": "0b6f0d3e-8c1a-4f6e-9a4b-2d1e5c7a9f10",
      "workspace_uuid": "b3f1c2a4-3333-4a2b-8c3d-000000000002",
      "title": "Q3 roadmap",
      "url": "https://docs.cirrux.co/d/0b6f0d3e-8c1a-4f6e-9a4b-2d1e5c7a9f10",
      "trashed_at": null,
      "created_at": "2026-09-01T10:00:00.000Z",
      "updated_at": "2026-09-01T10:00:00.000Z",
      "my_role": "editor",
      "shared": true,
      "owner": { "self": false, "first_name": "Ada", "last_name": "Lovelace", "display_name": "Ada Lovelace" }
    }
  ]
}

Get a document

GET/v1/documents/{uuid}

One document. A document you cannot see answers 404, the same as one that does not exist. Scope docs.read.

Read a document's content

GET/v1/documents/{uuid}/content

The document as it stands right now, as markdown (CommonMark with GFM strikethrough), with its open comments and suggestions. Scope docs.read; any role can read.

Markdown has no underline or color, so those are inline HTML: <u>underlined</u>, text color as <span data-color="red">red</span> and a highlight as <mark data-color="yellow">marked</mark>. The colors are gray, red, orange, yellow, green, teal, blue, purple and pink. Write them the same way in an edit; any other color is dropped.

Suggestions are marked in the text with CriticMarkup, each followed by a reference to its thread. Comments are not marked in the text: a comment’s quote is the text it is on. Attachments link to attachment:<uuid>.

  • {++added++}{>>s1<<} is text a suggestion adds.
  • {--removed--}{>>s1<<} is text a suggestion takes out.
  • {~~old~>new~~}{>>s1<<} is text a suggestion replaces.
  • {==text==}{>>s1<<} is text a suggestion formats differently.
  • {++¶++}, {--¶--} and {==¶==} at the start of a block are a suggested new paragraph or line break, a removed one, and a changed block type (a paragraph becoming a heading, say).
FieldTypeRequiredDescription
revisionintegernoThe revision the document was read at.
markdownstringnoThe document.
threadsarraynoThe open comments and suggestions, oldest first. Each has uuid, kind (comment or suggestion), ref (the reference a suggestion has in the markdown), status, placement, quote, suggested_text, suggested_format, author, created_at and messages, whose body is markdown too.
threads[].placementstringnoattached while its text is in the document, document for a comment on the document as a whole, and detached for a comment whose text was deleted.
curl "https://api.cirrux.co/public_api/v1/documents/$DOC_UUID/content" \
  -H "Authorization: Bearer $CIRRUX_TOKEN"
{
  "object": "document_content",
  "uuid": "0b6f0d3e-8c1a-4f6e-9a4b-2d1e5c7a9f10",
  "revision": 42,
  "markdown": "# Launch plan\n\nWe ship on {~~Monday~>Friday~~}{>>s1<<}.\n",
  "threads": [
    {
      "uuid": "5d0c3f8e-1b2a-4c3d-8e9f-0a1b2c3d4e5f",
      "kind": "suggestion",
      "ref": "s1",
      "status": "open",
      "placement": "attached",
      "quote": "Monday",
      "suggested_text": "Friday",
      "suggested_format": null,
      "author": { "self": false, "first_name": "Ada", "last_name": "Lovelace", "display_name": "Ada Lovelace" },
      "created_at": "2026-09-01T10:00:00.000Z",
      "messages": [
        {
          "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
          "author": { "self": false, "first_name": "Ada", "last_name": "Lovelace", "display_name": "Ada Lovelace" },
          "body": "Monday is a holiday.\n",
          "created_at": "2026-09-01T10:00:00.000Z",
          "edited_at": null
        }
      ]
    }
  ]
}

Edit a document's content

POST/v1/documents/{uuid}/content

Edits the document with operations aimed at text you quote from the markdown the read returns. They apply in order, each to the document the ones before it left, and land as one revision, or nothing changes. People with the document open see the edit at once. Scope docs.write and the editor role. Answers with the document after the edit, exactly as the read does.

FieldTypeRequiredDescription
operationsarrayyesUp to 100 operations, described below.
base_revisionintegernoThe revision you read. If the document has moved on since, the edit is refused with 409 conflict, and nothing changes.
batch_idstringnoYour own id for the edit, so a retried request is not applied twice.
  • {"type": "replace", "find", "with", "occurrence"?, "before"?, "after"?} replaces text inside one paragraph, heading or list item. with is inline markdown, and an empty one removes the text. The replacement keeps the formatting of what it replaces, plus what it writes.
  • {"type": "insert", "markdown", "after" | "before"} inserts blocks beside an anchor: a quote (the top-level block it is in), heading:Title, section:Title (the heading and what follows it, up to the next heading as high or higher), start or end. A list inserted next to a list item joins that list.
  • {"type": "delete", "find", "occurrence"?, "before"?, "after"?} removes text, across blocks if it spans them. A block whose whole text is found goes with it.
  • {"type": "set_document", "markdown"} sets the whole document. Only the blocks that differ change, so the rest keep their place for people working in it.

A quote is matched against the markdown, and failing that against the text as it reads without markup. One that is not there is target_not_found. One that is there more than once is target_ambiguous, with each match in candidates, unless occurrence (a number from 1, first or last) picks one or before and after (the text right before or after it) narrow it down. Both are 422 with retryable: true, name the refused operation by its index, and change nothing. A 409 conflict carries the document as it is now in current, so the edit can be redone without reading again.

curl -X POST "https://api.cirrux.co/public_api/v1/documents/$DOC_UUID/content" \
  -H "Authorization: Bearer $CIRRUX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "base_revision": 42,
    "operations": [
      { "type": "replace", "find": "Monday", "with": "Friday" },
      { "type": "insert", "after": "Write the post", "markdown": "- Check the numbers" }
    ]
  }'
{
  "error": "target_ambiguous",
  "error_description": "\"Monday\" is in the document 2 times; quote more of it, or say which with occurrence.",
  "operation": 0,
  "candidates": [
    { "occurrence": 1, "snippet": "We ship on Monday." },
    { "occurrence": 2, "snippet": "…the Monday after, we…" }
  ]
}

Attachments

POST/v1/documents/{uuid}/attachments
GET/v1/documents/{uuid}/attachments/{attachment}
GET/v1/documents/{uuid}/attachments/{attachment}/download

Uploads a file to the document. The request body is the file’s raw bytes, and filename in the query string is the name it shows with. Its type is read from the bytes, never from Content-Type: a PNG, JPEG, GIF or WebP image shows in the page (inline: true), and anything else is a file people download. Scope docs.write and the editor role.

Uploading does not place it. The answer carries markdown, which you insert with an edit, alone on its own line: ![chart.png](attachment:<uuid>) for an image, [minutes.pdf](attachment:<uuid>) for a file. Reading the document writes it back the same way. Uploading the same bytes again answers 200 with the same attachment instead of 201 with a new one, so a retry stores nothing twice. A missing filename is 422 invalid_filename, an empty body is 422 empty_file, and a file over 100 MB is 413 file_too_large (with the limit in max_bytes). Attachments count towards the workspace’s storage, so one that would take it past its limit is 422 storage_limit_exceeded.

The attachment:<uuid> links in a document’s markdown name its attachments. GET on one returns it as below, and /download returns its file as raw bytes, with its type in Content-Type and its name in Content-Disposition. Scope docs.read and the viewer role. An attachment of another document is 404, and one whose file has not arrived (upload_status other than completed) is 409 not_uploaded.

FieldTypeRequiredDescription
objectstringnoAlways document_attachment.
uuidstringnoThe attachment.
document_uuidstringnoThe document it belongs to.
filenamestringnoThe name it shows with, without any directories.
content_typestringnoAn image type, or application/octet-stream for anything else.
byte_sizeintegernoIts size in bytes.
upload_statusstringnocompleted, awaiting_upload or failed.
width, heightintegernoAn image's size in pixels, or null.
inlinebooleannoWhether it shows in the page or as a file to download.
markdownstringnoWhat places it in the document.
created_atstringnoWhen it was uploaded.
curl -X POST "https://api.cirrux.co/public_api/v1/documents/$DOC_UUID/attachments?filename=chart.png" \
  -H "Authorization: Bearer $CIRRUX_TOKEN" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @chart.png
curl -X POST "https://api.cirrux.co/public_api/v1/documents/$DOC_UUID/content" \
  -H "Authorization: Bearer $CIRRUX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "operations": [{ "type": "insert", "after": "section:Results", "markdown": "![chart.png](attachment:<uuid>)" }] }'
curl "https://api.cirrux.co/public_api/v1/documents/$DOC_UUID/attachments/$ATTACHMENT_UUID/download" \
  -H "Authorization: Bearer $CIRRUX_TOKEN" \
  -o chart.png

Suggest changes

POST/v1/documents/{uuid}/suggestions
POST/v1/documents/{uuid}/suggestions/{thread}/accept
POST/v1/documents/{uuid}/suggestions/{thread}/reject
DELETE/v1/documents/{uuid}/suggestions/{thread}

Suggests changes instead of making them, so the people working in the document review them first. It takes the replace, insert and delete operations of an edit, aimed at quotes the same way, along with base_revision and batch_id. Each operation becomes a suggestion of its own, which people accept or reject in the editor: text it would take out stays, marked, and what it brings in is added, marked. Give an operation a comment (markdown) to say why. It becomes the first message on that suggestion. Scope docs.comment and the commenter role.

Answers 201 with the document after it, as the read returns it, plus suggestions: the new suggestions’ uuids, one per operation, in order. Quotes are refused as an edit’s are. set_document cannot be suggested (not_suggestible), and text already suggested for removal has nothing_to_suggest.

  • /accept makes the change and /reject takes it back, closing the suggestion in the same revision. Scope docs.write and the editor role. Both answer with the document. A suggestion that was already decided is 422 suggestion_closed.
  • DELETE withdraws a suggestion of yours (or anyone’s if you manage or own the document): its marks go as rejecting it takes them, and it is deleted with its messages.
curl -X POST "https://api.cirrux.co/public_api/v1/documents/$DOC_UUID/suggestions" \
  -H "Authorization: Bearer $CIRRUX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "operations": [
      { "type": "replace", "find": "Monday", "with": "Friday", "comment": "The invite says Friday." },
      { "type": "delete", "find": "We may slip." }
    ]
  }'

Comments

POST/v1/documents/{uuid}/comments
POST/v1/documents/{uuid}/comments/{thread}/replies
POST/v1/documents/{uuid}/comments/{thread}
POST/v1/documents/{uuid}/comments/{thread}/messages/{message}
DELETE/v1/documents/{uuid}/comments/{thread}
DELETE/v1/documents/{uuid}/comments/{thread}/messages/{message}

Comments as people make them in Cirrux Docs: a comment on quoted text is marked on those words, so it shows beside them for everyone with the document open. Scope docs.comment and the commenter role. Every body is markdown. Each write answers with the thread, in the shape the read lists threads in; a delete answers 204.

  • {"body", "on"?, "occurrence"?, "before"?, "after"?} adds a comment on the text on quotes, matched as an edit’s quote is, or on the whole document without it.
  • /replies {"body"} replies; {"status": "resolved" | "open"} on the thread resolves or reopens it. A suggestion is accepted or rejected instead, and is 422 not_a_comment.
  • /messages/{message} {"body"} edits a message you wrote.
  • Deleting a comment takes its replies and its marks with it. Deleting its opening message does the same; deleting a reply deletes just that. Your own, or anyone’s if you manage or own the document. Deleting a suggestion withdraws it.
curl -X POST "https://api.cirrux.co/public_api/v1/documents/$DOC_UUID/comments" \
  -H "Authorization: Bearer $CIRRUX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "on": "Monday", "body": "Is **Monday** firm?" }'

Create a document

POST/v1/documents

Creates an empty document in the token’s workspace, owned by you, and returns it with 201. The body is optional. Scope docs.write.

FieldTypeRequiredDescription
titlestringnoAt most 255 characters. Leave it out for an untitled document.

Update a document

POST/v1/documents/{uuid}

Renames a document and/or moves it in or out of Trash. Fields you leave out are not changed, and a request is checked in full before anything is written, so a refusal changes nothing. Scope docs.write.

FieldTypeRequiredDescription
titlestring | nullnoThe new title. An empty string or null makes it untitled. Needs the editor role.
trashedbooleannotrue moves it to Trash, false restores it. Needs the owner or manager role.
curl https://api.cirrux.co/public_api/v1/documents/$DOC_UUID \
  -H "Authorization: Bearer $CIRRUX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title": "Q3 roadmap (final)"}'

Move a document to Trash

POST/v1/documents/{uuid}/trash

The same as updating with {"trashed": true}, and it needs the same owner or manager role. Trashing a trashed document changes nothing. Scope docs.write.

Errors

Error codes

FieldTypeRequiredDescription
forbidden403noYour role on the document does not allow this change. The description names the role it needs.
not_found404noNo such document or attachment, or you cannot see it.
comment_not_found404noNo such comment or message on this document.
suggestion_not_found404noNo such suggestion on this document, when accepting or rejecting.
conflict409noThe document moved past base_revision. The body carries current.
document_trashed409noThe document is in Trash, so its content, comments and suggestions cannot change.
not_uploaded409noThe attachment’s file has not finished uploading.
file_too_large413noThe attachment is over 100 MB.
invalid_uuid422noA uuid in the path is not a UUID.
invalid_title422noThe title is not a string of at most 255 characters.
invalid_trashed422notrashed is not a boolean.
target_not_found, target_ambiguous422noA quote is not in the document, or is there more than once. Carries retryable: true.
invalid_operation, invalid_operations, invalid_markdown, target_spans_blocks422noAn operation is malformed, the edit is too large, its markdown cannot be read, or a replace reaches across blocks.
invalid_base_revision, invalid_batch_id422nobase_revision is not a revision number, or batch_id is not a string of 1 to 64 characters.
not_suggestible, nothing_to_suggest422noThe change cannot be suggested (set_document), or the text is already suggested for removal.
suggestion_closed, suggestion_not_in_document, not_a_suggestion422noThe suggestion was already decided, none of it is left in the document, or the thread is a comment.
invalid_body, invalid_status, not_a_comment422noA comment body is blank, status is not open or resolved, or the thread is a suggestion.
invalid_filename, empty_file, storage_limit_exceeded422noAn attachment has no filename, holds no bytes, or would take the workspace past its storage.