Cheqq Docs
Developers

Documents and Apps

Edit documents and App code from outside Cheqq without overwriting anyone, undo any edit, and publish safely.

Your pages, documents, slide decks and designs, and the source code of your Apps, can be read and edited over the API and MCP. Both keep a full history, so every edit can be undone, and neither lets an outside edit quietly overwrite someone else's.

Documents

Pages, printable documents, slide decks and designs are HTML documents: an index.html plus any styles, scripts and images it links to. Spreadsheets and stored files are listed too, with their details and downloads, but can't be edited over the API.

Read, then edit with the version you read

Every save makes a new version. To edit:

  1. Read it. GET /documents/{documentId} returns its files and its currentVersionId.
  2. Change it. Send the files you changed, whole, to PATCH /documents/{documentId} with baseVersionId set to the currentVersionId you read, and a one-line summary for the history.
  3. Keep going. The answer has the new versionId; use it as baseVersionId for your next edit.
curl -X PATCH https://cheqq.ai/api/platform/v1/documents/DOCUMENT_ID \
  -H "Authorization: Bearer $CHEQQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "baseVersionId": "VERSION_YOU_READ",
    "summary": "Added a pricing section",
    "files": [{ "path": "index.html", "content": "<!doctype html>…" }]
  }'

If someone else edited it since you read it, your edit is refused with 409 DOCUMENT_VERSION_CONFLICT, and nothing is lost: read it again and redo your change on top of theirs. Files you don't send stay as they are, and deletePaths removes files (never index.html). A document people write in Cheqq's document editor is changed block by block instead; see Documents written in blocks.

Documents written in blocks

Printable documents in Cheqq are written in blocks: people type in them together, and Cheqq writes in them too. You can work in one while people are typing, and never write over their words. Instead of sending files, you read it and change it one block at a time.

How to tell. GET /documents/{documentId} answers with "editor": "blocks". Its files are a copy of what people see, so a file write or patch to it is refused with 409 DOCUMENT_USES_BLOCKS, and the error's details names the two endpoints to use instead. Renaming it still works the usual way: a PATCH with only title (and baseVersionId).

1. Read the outline.

curl "https://cheqq.ai/api/platform/v1/documents/DOCUMENT_ID/blocks?mode=outline" \
  -H "Authorization: Bearer $CHEQQ_API_KEY"

Every block comes with its id, a short hash and its first words, and the answer has the document's revision. Then read only what you need as markdown: mode=blocks&blockIds=cq-4k2,cq-4k3, a heading and everything under it with mode=section&headingId=…, or the blocks holding some words with mode=search&query=….

2. Change blocks by their id, with the hash you read.

curl -X POST https://cheqq.ai/api/platform/v1/documents/DOCUMENT_ID/blocks \
  -H "Authorization: Bearer $CHEQQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "summary": "Moved the sign-off to Monday",
    "operations": [
      {
        "op": "replaceWords",
        "blockId": "cq-4k2",
        "hash": "9f1c2a7b0d3e4f56",
        "find": "Friday",
        "replace": "Monday"
      }
    ]
  }'
OperationWhat it does
replaceWordsChanges exact words inside a block and leaves the rest of it as it is. find must appear once in the block. Best for small fixes.
replaceGives a block new markdown.
insertAdds markdown at a place: after or before a block, at the end of a heading's section, or at the end of a tab.
delete, moveRemove a block, or move it to a place (withSection takes a heading and everything under it).
insertPending, fillPlant a section to write later, then write it.
setTitleRename the document.
addTab, renameTab, setTabIcon, moveTab, duplicateTab, deleteTabAdd, rename, move, copy and delete tabs, and give a tab an icon. See Tabs.
setPageChange the page's look, width, paper, orientation, margins or page numbers. See Page.
colorWordsColours exact words in a block: a textColor, a highlight, or both. See Colour.
insertTableRow, insertTableColumn, deleteTableRow, deleteTableColumn, moveTableRow, moveTableColumn, colorTableCells, setColumnWidthsChange a table's rows, columns, cell colours and column widths without rewriting it. See Tables.

Up to 25 operations go in one call. They apply in order, and either all of them apply or none do. The answer has the new revision and each written block's new hash, so you can make your next edit without reading again.

3. People's words win. If someone changed a block since you read it, nothing is written, and the answer is 409 DOCUMENT_BLOCK_STALE with what each refused block says now:

{
  "error": {
    "code": "DOCUMENT_BLOCK_STALE",
    "message": "Someone changed these words since they were read, so nothing was changed. Keep their words and make the edit on top of them.",
    "details": {
      "revision": "58",
      "refused": [
        {
          "index": 0,
          "blockId": "cq-4k2",
          "reason": "changed-since",
          "current": {
            "hash": "2b7e0c91d4a85f36",
            "markdown": "Owners sign off by Tuesday."
          }
        }
      ]
    }
  }
}

Keep their words, and redo your change on top of them with the new hash. A reason like words-not-found means the operation itself didn't fit the block, not that anyone typed. An operation on an existing block with no guard at all is 400 GUARD_REQUIRED.

Changing many blocks at once. Instead of a hash on every operation, pass the revision you read as baseRevision: every block you change must still read as it did then. Recent revisions work; an old one is 409 REVISION_UNKNOWN, so read again. To see what people changed since you last read, ask for mode=changes&sinceRevision=58.

New documents. POST /documents with "format": "document" and markdown, an outline of sections to write later, or both, makes a document written in blocks. Add a page (the same settings as setPage) to start it with a look or paper of its own. The answer lists every block's id and hash, so your first edit needs no read.

People and dates. A mention is written as <span data-type="mention" data-id="USER_ID" data-label="Ada">@Ada</span> and a date as <time data-type="date" datetime="2026-11-03">Nov 3, 2026</time>. A mention notifies the person it names, from your key's owner, so mention someone only when they need to see it. You can only mention people who can open the document.

Your edits are saved in the document's history like anyone's, under your key's name (or your connected app's), and POST /documents/{documentId}/restore still brings back any version. A document nobody has opened in the document editor yet still reads as "editor": "html": reading its blocks answers 409 DOCUMENT_NOT_LIVE, so read and change its files instead. Reading never moves a document into the editor.

Tabs

A long document can be split into tabs, and the outline lists each one with its id, title, icon and hash. Read one tab's outline with mode=outline&tabId=…, or all of it as markdown with mode=tab&tabId=…. To insert at the end of a tab, use "at": "tabEnd" and the tabId as its place.

curl -X POST https://cheqq.ai/api/platform/v1/documents/DOCUMENT_ID/blocks \
  -H "Authorization: Bearer $CHEQQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "summary": "Tidied the tabs",
    "operations": [
      { "op": "addTab", "title": "Appendix", "markdown": "# Raw numbers" },
      { "op": "renameTab", "tabId": "t4k2m9x1a", "title": "Pricing" },
      { "op": "setTabIcon", "tabId": "t4k2m9x1a", "icon": "money" },
      { "op": "moveTab", "tabId": "t4k2m9x1a", "place": { "first": true } },
      { "op": "deleteTab", "tabId": "t8p3q0z7c", "hash": "41f8b2dd8304e883" }
    ]
  }'
  • addTab puts the new tab at the end, or where place says: { "after": "<tabId>" } or { "first": true }. The answer's tabId is the new tab's id; a tab added in the same call can't be written into until the next one.
  • renameTab takes a name of up to 120 characters on one line. Add expectedTitle to rename only if nobody renamed it first.
  • The first tab goes by the document's title: the outline lists the title as its name, and renameTab on whichever tab is first at that point in the call renames the document (its expectedTitle is checked against the title). The tab keeps its own name, which it goes by again if it moves down.
  • setTabIcon takes one of document, chart, money, users, calendar, checklist, folder, star, flag, rocket, idea, target, briefcase, book, chat, mail, map, cart, settings, heart, clock, home, bolt or archive, or null for none.
  • moveTab takes a place the same way as addTab.
  • duplicateTab puts a copy right after the tab, named "Pricing (copy)" unless you send a title. Comments stay on the original.
  • deleteTab needs the tab's hash from the outline, or the call's baseRevision. If anyone wrote in the tab since you read it, nothing is deleted: the refusal is changed-since, with the tab's current hash and name under currentTab. People can bring a deleted tab back from the document's history.

A document keeps at least one tab and holds up to 50. A tab refusal names its tabId, and its reason is one of tab-gone (the tab was deleted), last-tab, tab-limit, or too-large (a copy would make the document too big).

Page

The outline's page holds the document's page settings, and setPage changes any of them. It needs no hash: a setting isn't anyone's words, so the last change wins.

curl -X POST https://cheqq.ai/api/platform/v1/documents/DOCUMENT_ID/blocks \
  -H "Authorization: Bearer $CHEQQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "summary": "Made it landscape with page numbers",
    "operations": [
      { "op": "setPage", "orientation": "landscape", "pageNumbers": true }
    ]
  }'
SettingValues
fontdefault, sans, serif, monoThe look. default is a serif title over sans text. PDF and Word follow it.
widthstandard, wide, fullHow wide the page reads on screen. Never printed.
pageSizeA4, Letter, LegalThe paper for PDF, Word and printing.
orientationportrait, landscape
marginsnarrow, normal, wide
pageNumberstrue, false"2 / 5" at the foot of each printed page. On for new documents.

Send only the settings you change; at least one. A value it doesn't know is 400 VALIDATION_ERROR, and details.issues names the field, such as operations.0.font. The change shows in the document's history and reaches everyone who has it open.

Tables

A table's words read as markdown, which carries no cell colours or widths, so a replace of a table drops them. The table operations change one table in place instead: every other cell keeps its words and colour. Each names the table's blockId and its hash (or the call's baseRevision), and counts rows and columns from 0, where row 0 is the header row.

The outline and a blocks read give each table a table: its rows, columns, headerRow, widths (px, or null) and colours, the coloured cells as { row, column, color }.

curl -X POST https://cheqq.ai/api/platform/v1/documents/DOCUMENT_ID/blocks \
  -H "Authorization: Bearer $CHEQQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "summary": "Added annual totals and marked the cheapest plan",
    "baseRevision": "42",
    "operations": [
      { "op": "insertTableColumn", "blockId": "cq-7tb", "index": 2, "cells": ["Annual", "120", "300"] },
      { "op": "colorTableCells", "blockId": "cq-7tb", "row": 1, "color": "green" },
      { "op": "setColumnWidths", "blockId": "cq-7tb", "widths": [320, 260, 260] }
    ]
  }'
OperationTakes
insertTableRow, insertTableColumnindex, and cells: each new cell's words, a column's from the header down. A column may take a width.
deleteTableRow, deleteTableColumnindex. A table keeps at least one row and one column.
moveTableRow, moveTableColumnfrom and to. The header row stays first.
colorTableCellsA row, a column, or both for one cell, and a color: gray, yellow, orange, red, purple, blue, green, or null to clear it.
setColumnWidthswidths: one per column, 60 to 2000 px, or null. A table wider than the text reaches out across the page, then scrolls.

A refusal's reason is not-a-table, table-merged (a table with merged cells), or out-of-range (an index or a width list that doesn't fit).

Colour

colorWords colours exact words in a block, as replaceWords changes them: find must appear once. It takes a textColor, a highlight, or both, and null clears one. The colours are gray, brown, orange, yellow, green, blue, purple, pink and red.

In markdown, a text colour is <span data-color="red">words</span> and a highlight <mark data-color="yellow">words</mark> (a plain ==highlight== still works). Reads write colours this way, so a replace keeps them. Any other key is 400 VALIDATION_ERROR.

Embeds

A video, map, design or post is an embed, on a line of its own:

:::embed {url="https://www.youtube.com/watch?v=dQw4w9WgXcQ"} :::

An optional title="…" may follow the url. Use the link as the app shares it; the page works out the app's player from it. The apps are the ones in Embed videos, maps and more. A link from any other site is 422 EMBED_NOT_SUPPORTED, and an :::embed line that isn't written this way is 422 EMBED_NOT_READ. Reads write embeds the same way; the Markdown download writes each as a link.

Images and fonts

Text files travel as text. An image or a font travels as base64: reading one with GET /documents/{documentId}/files?path= answers with "encoding": "base64", and to add or replace one, send it the same way:

{
  "baseVersionId": "VERSION_YOU_READ",
  "summary": "Added the logo",
  "files": [
    { "path": "img/logo.png", "encoding": "base64", "content": "iVBORw0KGgo…" }
  ]
}

Size limits count the file itself, not its base64.

For a small change, such as retyping a heading or changing a colour, POST /documents/{documentId}/patch edits elements in place without resending files. Elements are named by their data-cq-id attribute, which GET /documents/{documentId} shows:

{
  "baseVersionId": "VERSION_YOU_READ",
  "summary": "New headline",
  "operations": [
    { "op": "set-text", "nodeId": "cq-1a", "text": "Spring menu" }
  ]
}

When you rewrite a file whole, keep its data-cq-id attributes: comments people left on the document are anchored to them.

Download a file

POST /documents/{documentId}/exports makes a file from a document, the same one Export gives people in Cheqq: pdf or docx for a document, pptx for a deck, png for a design. Add a tabId from the outline to get just one tab, as a PDF or a Word file.

curl -X POST https://cheqq.ai/api/platform/v1/documents/DOCUMENT_ID/exports \
  -H "Authorization: Bearer $CHEQQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "format": "pdf", "tabId": "t4k2m9x1a" }'

A file made before for the same version is "status": "ready" at once. Otherwise the answer is "pending": ask again with the exportId it gave you every few seconds until it's ready. Then download it from downloadUrl with the same key:

curl -o pricing.pdf "DOWNLOAD_URL" -H "Authorization: Bearer $CHEQQ_API_KEY"

With documents:write, a document people are typing in is saved as a new version first, so the file holds what they see. If the last few seconds of typing couldn't be saved in time, the answer says "latestChangesIncluded": false and the file is of the version just before them.

A key with only documents:read never saves a version. It gets a file of the last saved one, and "latestChangesIncluded": false tells you people have typed since. Cheqq saves a version after about half a minute without typing, so asking again a little later gets their newest words.

Versions and restore

GET /documents/{documentId}/versions lists every version, by anyone, with its summary. Read an old one with ?versionId=. POST /documents/{documentId}/restore with baseVersionId (the current version) and restoreVersionId (the one you want back) brings it back as a new version, so nothing in between is lost.

Sharing stays in Cheqq

A document you create over the API is private until you publish it in Cheqq. Deleting a document needs documents:delete, and only its owner can do it; if it was published, its public link stops working at once. A deleted document goes to its owner's Trash in Cheqq, where they can restore it for 30 days.

Apps

An App's source is a React and TypeScript project. You can read every file, and write the ones under src/, public/ and index.html; the rest is set up and managed by Cheqq, and packages are changed with POST /apps/{appId}/dependencies, not by editing package.json.

Every batch of edits saves a checkpoint

PUT /apps/{appId}/files writes up to 50 files at once. Before it writes, Cheqq takes a checkpoint of the whole source and returns it in the answer. Deleting a file does the same.

curl -X PUT https://cheqq.ai/api/platform/v1/apps/APP_ID/files \
  -H "Authorization: Bearer $CHEQQ_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "New opening hours",
    "files": [{ "path": "src/pages/Home.tsx", "content": "…" }]
  }'

Images, fonts and other binary files, say under public/, go as base64: give the file "encoding": "base64" and its base64 as content. Reading one with GET /apps/{appId}/files/content?path= answers the same way, while a text file reads as itself with "encoding": "utf8". The 2 MB per write and 1 MB per read count the files themselves, not their base64.

To undo, send that checkpoint's hash to POST /apps/{appId}/checkpoints/restore. Restoring takes a checkpoint of its own first, so a restore can be undone too. GET /apps/{appId}/checkpoints lists them, including the ones the App Builder takes before each message.

Edits change the App's draft only. Visitors see nothing until you publish.

Check before you publish

  • POST /apps/{appId}/check typechecks and builds the draft and returns any errors to fix. Passing means it compiles, not that it looks right.
  • POST /apps/{appId}/verify opens the draft in a real browser, as a visitor would, and reports errors, failed data calls, whether the page came up blank, and screenshots. It can also click through a short flow. Anything the App would save while it's being checked is tried, never saved. If verifying isn't available, it answers 501 NOT_AVAILABLE; check the App instead, or preview it in the App Builder.

Publish

POST /apps/{appId}/publish builds the draft and puts it live, keeping the previous version for rollback. It answers straight away with 202 and a deploymentId:

{ "deploymentId": "d71c…", "versionNumber": 12, "status": "building" }

Poll GET /apps/{appId}/deployments/{deploymentId} every few seconds until it says deployed (with the addresses it went live at) or failed (with the build log). A build usually takes under a minute.

POST /apps/{appId}/rollback puts an earlier version back live at once, without a rebuild. POST /apps/{appId}/unpublish takes the App offline and keeps everything, so publishing again brings it back. All three need apps:publish.

Publishing never widens who can see an App

If the draft would let visitors reach more than the live App does (a wider audience, more fields or actions on your records, or a filter removed), publishing over the API is refused with 409 APP_ACCESS_WIDENING_UNCONFIRMED. The same goes for rolling back to a version that reached more. Only a person can approve that: open the App in the App Builder (the error's details.builderUrl links to it), review what changes, and publish from there. See Cheqq asks before an App reaches more.

Changing who an App is for, its custom domain and its branding is done in the App Builder too.

Was this page helpful?