Docs API

A doc card's pages are read and written as Markdown. GET /boards/:id/cards/:cardId/doc reads every page with a hash of each; POST /boards/:id/docs makes a doc holding up to 50 pages; POST …/doc/pages adds pages; PATCH …/doc/pages/:pageId rewrites, edits or renames one page. A rewrite names the hash the page was read at, so it never erases a change made since, and it keeps every block it didn't change, comments included.

A doc card holds pages of writing. Over the API you read them and write them as Markdown, page by page: the same Markdown the export gives you.

GET /boards/:id/cards/:cardId/doc

A doc card's text: every page, in the order its page list shows them, as Markdown.

{
  "board_id": "9f1c…", "card_id": "c-3", "title": "Treatment", "url": "…",
  "page_count": 2,
  "pages": [
    { "id": "p1", "title": "Opening", "parent_id": null, "depth": 0,
      "markdown": "A diner at night…\n", "chars": 1840, "hash": "5d1f0c2a", "truncated": false },
    { "id": "p2", "title": "Close", "parent_id": null, "depth": 0,
      "markdown": null, "chars": 960, "hash": "0be4a9d1", "truncated": false }
  ],
  "complete": false
}
Parameter
pageOne page, by its id
max_charsMarkdown handed back across the pages: 20,000 characters by default, at most 200,000

Pages fill in order until max_chars runs out. The page that crosses it is cut and marked truncated, and any after it come back with markdown: null, so every page is listed with its length in chars either way. complete says whether you have all of it; read the rest one page at a time. A page nested under another carries that page's id in parent_id, and a depth one more.

Links in the text carry their real address, and comments are left out: GET /boards/:id/comments lists them, with the text each is on. An uploaded image's source is r2: followed by its storage key. A screenplay reads one line to a paragraph, scene headings in bold. hash changes whenever the page's Markdown does, and a read of one page carries it as the ETag.

A card that is not a doc answers 400, naming the route that reads it.

POST /boards/:id/docs

A new doc card holding the pages you send, placed in free space on the board, at x and y, in a section, or at the top.

{
  "title": "Tokyo noir",
  "pages": [
    { "title": "Logline", "markdown": "A night-shift detective follows a dead phone through Shinjuku." },
    { "markdown": "# Places\n\n- Shinjuku\n- [Golden Gai](https://example.com/gg)" }
  ]
}
Field
pagesRequired. Up to 50, each { title?, markdown }, with up to 200,000 characters of Markdown between them
titleThe card's title. Left out, its one page's title, else "Untitled doc"
x, yWhere the card goes. Left out, it is placed where it covers nothing
groupThe section it goes in, by name; the board's group of that name, made if it has none
position"top" puts it at the top-left, ahead of the sections. Not with group, x or y: sending it with any of them is refused with 400 bad_request
w, hThe card's width and height. Left out, 320 by 240

A page with no title takes the words of its first heading, and a first page with neither takes the card's title, as a new doc's first page does in the app. Titles are cut at 200 characters.

{
  "board_id": "9f1c…",
  "card": { "id": "api-m2x9k1-a81f3c", "kind": "doc", "title": "Tokyo noir", … },
  "url": "https://clusters.soleilpictures.com/?to=c:9f1c…:api-m2x9k1-a81f3c",
  "pages": [
    { "id": "p_k3v8x0q2m1c4", "title": "Logline", "parent_id": null, "depth": 0, "chars": 64, "hash": "5d1f0c2a" },
    { "id": "p_7hd2w9zr8b0e", "title": "Places", "parent_id": null, "depth": 0, "chars": 57, "hash": "0be4a9d1" }
  ],
  "live": true
}

When group named a section the answer also carries groups, the section it names with its id, and "created": true if the call made it, as cards do.

That is a 201. The doc counts as one card toward the card cap.

POST /boards/:id/cards/:cardId/doc/pages

More pages for a doc that exists: after the pages it has, or, with parent_page_id, under that page as its sub-pages. pages is as above. The answer is a 201 listing the new pages.

PATCH /boards/:id/cards/:cardId/doc/pages/:pageId

One page changed, in one of three ways:

SendDoes
markdown and page_hashRewrites the page. markdown is the whole page as it should now read; page_hash is the hash you read it at, or send that as If-Match
editsUp to 50 { "old_text", "new_text" }, applied in order. Each old_text must appear in the page's Markdown exactly once
titleRenames the page, alone or with either of the others
{
  "markdown": "# Places\n\n- Shinjuku at 4am\n- [Golden Gai](https://example.com/gg)\n",
  "page_hash": "0be4a9d1"
}
{
  "board_id": "9f1c…", "card_id": "api-m2x9k1-a81f3c", "url": "…",
  "page": { "id": "p_7hd2w9zr8b0e", "title": "Places", "parent_id": null, "depth": 0, "chars": 66, "hash": "77a0e3b4" },
  "blocks": { "kept": 1, "removed": 1, "added": 1 },
  "changed": true
}

Only what you changed is rewritten. The page you send is matched against the page as it stands, block by block, by each block's Markdown. A heading, paragraph, list, table or quote whose Markdown you left alone stays the same block, so a comment on it, its text colour, an embedded board and anything else Markdown has no words for stay with it. blocks counts what was kept, removed and added. Send a page back as it was and nothing is written: changed is false.

A rewrite can't erase anyone's change. When the page's hash is no longer the one you name, the write is refused with 409 page_changed and nothing is written. Read the page again and make your change to that. edits need no hash, because the exact text each one names is its own check.

Before a page is rewritten or edited, the board is saved to its Version history as it was, marked API.

What the Markdown carries

MarkdownIn the doc
# to ######Headings
**bold**, *italic*, ~~strike~~, ` code , ==highlight==, <u>underline</u>, <sub>, <sup>`The same marks
- and 1. lists, nestedLists, numbered from where yours starts
- [ ] and - [x]A checklist, when every item in the list is one
>A quote
Fenced codeA code block, with its language
TablesTables, each column aligned as you aligned it
---A divider
[words](https://…), a bare https:// address, mailto:Links
![caption](r2:<image_key>)A picture, when the file is already on the board
A single line breakA line break; a blank line starts a new paragraph

Links go only to http, https and mailto addresses; any other keeps its words without the link. A picture is placed only from a file already on the board: an image card's image_key from GET /boards/:id/cards, written r2:<image_key> as a read gives it back (the bare key works too). Any other picture becomes a link to its address, so writing a doc never fetches anything into it. Other HTML stays as text.

A screenplay is written in the app. The API writes prose docs, and answers 400 for a screenplay.

Live, or not at all

Doc writes go through the cluster's live room, as an edit in the app does, so anyone with the doc open sees the change land, on the board or in a doc shared on its own. When the room can't be reached, or doesn't confirm the change, the answer is 503 room_unavailable and nothing was written: try again.

Search inside documents finds what you wrote straight away.

Frequently asked questions

Will rewriting a page lose its comments or colours?

Not on the parts you leave alone. A rewrite keeps every block whose Markdown is unchanged as the same block, so a comment on it, its text colour and anything else Markdown can't express stay with it. Only the blocks you change are written fresh.

What if someone edits the page while my script is working on it?

A whole-page rewrite names the hash of the page as you read it. If the page has changed since, the write is refused with 409 page_changed and nothing is written. Read it again and make your change to that.

Can a doc written through the API hold pictures?

Pictures already in the cluster, yes. Write ![caption](r2:<image_key>) with the image_key of an image card on the board. Any other picture becomes a link to it, so writing a doc never fetches anything into it.

Why did I get 503 room_unavailable?

A doc is written through the cluster's live room or not at all, and the room couldn't be reached or didn't confirm the change. Nothing was written, so it is safe to try again.

Machine-readable: /docs/api/docs.md · /llms.txt
What changed and when: /changelog