# Charta board API

Charta is a shared whiteboard. A person has it open in a browser tab; you
(an agent) read and change the same board over HTTP. Every change you make
appears in their tab immediately, and everything they do is visible to you.
You can also show up as a participant: a named cursor, messages, pointing
at things, and steering what the person is looking at.

Base URL: `https://1charta.com/api/v1`. All bodies and responses are JSON unless
noted. Every write returns `{"version": n, "applied": [...], "inverse": [...]}`:
`applied` is exactly what happened (full shapes for adds, cascaded
deletes made explicit) and `inverse` is a batch that undoes it.

## Authentication

The person gives you a key that looks like `ck_...`. It is bound to their
open tab and stops working about a minute after they close it. Send it on
every request:

    Authorization: Bearer ck_...

(or `?key=ck_...` in the query string). The key names one board; there is
no board id in the URLs. A missing or dead key gets `401`.

## Suggested first moves

1. `GET /board` to see what is there (or `GET /board/graph` for the structure).
2. `POST /board/presence {"name": "Claude"}` so the person sees who is working.
3. Make changes with `POST /board/ops`, or build a whole structure with a template.
4. `POST /board/say` to tell the person what you did; `POST /board/look` to show them.
5. If the person may be editing at the same time, use `base_version` and
   follow `GET /board/events`.

## Read the board

    GET /board                 -> {"id", "title", "version", "shapes": [ ... ]}
    GET /board/shapes/:id      -> {"version", "shape"}
    GET /board/shapes?kind=sticky&q=launch&tag=todo&in=<frame id | x,y,w,h>&ids=a,b
    GET /board/graph           -> {"nodes", "edges", "frames"} (arrows as edges, frames with their children)
    GET /board/log?since=12    -> the recent change log, with who did what and the inverse of each batch
    GET /board/peers           -> who is present (tabs and agents)
    GET /board/export.svg      -> the board as an SVG image (image/svg+xml)
    GET /board/export.md       -> the board as a Markdown outline (frames as sections, arrows as links)

Shapes are listed back to front. `version` increases by one per change
batch; use it to detect changes or to make conditional writes. Filters on
`GET /board/shapes` combine; `q` matches text, arrow labels and tags,
case-insensitively.

## Shapes

Boxes (`sticky`, `rect`, `ellipse`, `diamond`, `text`, `markdown`, `frame`, `image`, `page`):

| field | type | notes |
|---|---|---|
| `id` | string | assigned by the server unless you provide one (max 64 chars) |
| `kind` | string | one of the kinds above |
| `x`, `y` | number | top-left corner, in board units (about pixels at 100% zoom) |
| `w`, `h` | number | size; defaults: sticky 180x180, rect 180x100, ellipse 160x110, diamond 160x110, frame 800x500, image 240x180. A `text` shape sizes itself: its height always follows the wrapped lines, and its width follows the longest line (80..1200) until you set `w`, which fixes the width and rewraps. |
| `text` | string | shown inside the shape, wrapped and auto-shrunk to fit; `\n` breaks lines. For frames and pages it is the title. A `markdown` shape draws it as Markdown (headings, lists, task lists, quotes, code, bold, italic, strikethrough, links) at `font_size` and fits its own `h` to it; give it a `w`. |
| `color` | `#rrggbb` | fill (for `text`, the ink color) |
| `stroke` | `#rrggbb` or null | border color (default: a subtle one) |
| `font_size` | number | 6..400, the size the text starts from before shrinking to fit (down to 8, or 12 with `autosize`, past which the box grows instead) |
| `autosize` | boolean | stickies default to `true`: the text takes the largest size that fits (short notes big, long ones small) and `font_size` is ignored; set `false` to fix the size. On `text` it means the box hugs its own text; setting `w` turns it off. |
| `grow` | number | read it, do not set it: how much of `h` is height an autosized box added because its text would not fit at the 12 px readability floor. Sending `h` starts the growth again from there. |
| `align` | string | `left`, `center` (default for most) or `right` |
| `url` | string | images: `http(s)://...`, `data:image/...` or a `/files/...` path from an upload. Pages: the `/files/....html` path of an uploaded page, filled in for you when you send `html` (see Pages) |
| `tags` | [string] | up to 20 short labels; searchable with `tag=` and `q=` |
| `locked` | boolean | locked shapes cannot be moved, edited or deleted until unlocked (send `locked: false` in the same update to unlock) |

Arrows (`kind: "arrow"`) connect shapes or points:

| field | type | notes |
|---|---|---|
| `from`, `to` | shape id or null | the shapes the ends attach to (boxes only, not both ends on one shape). Without a port an end is flexible: it sits on the middle of whichever side gives the shortest line and moves round the shape as things move |
| `from_port`, `to_port` | `top`, `right`, `bottom`, `left` or null | dock an attached end to the middle of a side |
| `from_anchor`, `to_anchor` | `[x, y]` or null | pin an attached end to a point of the shape, as fractions of its box (`[0, 0]` top left, `[1, 1]` bottom right); it keeps that spot when the shape moves or is resized |
| `x1`, `y1`, `x2`, `y2` | number | used for an endpoint without a shape |
| `head` | string | `end` (default), `start`, `both` or `none` (a plain line) |
| `label` | string | text drawn at the middle of the arrow |
| `style` | string | `curved` (default: a Bézier leaving each docked end square to its side), `elbow` (right angles, rounded corners) or `straight` |
| `points` | array | waypoints the line passes through, `[[x, y], ...]` in board units (up to 50): curves run smoothly through them, straight lines and elbows turn at them |
| `dash` | string | `solid` (default), `dashed` or `dotted`; the pattern scales with the width |
| `stroke_width` | number | line width in board units, 0.5 to 24 (default 2); the arrowhead grows with it |
| `color`, `tags`, `locked` | | as above |

Frames are titled containers: any shape whose center lies inside a frame
moves with it and counts as its child (`GET /board/graph`,
`GET /board/shapes?in=<frame id>`). Add frames first (or send `back`) so
they sit behind their content.

Sticky colors people expect: yellow `#fff3a0`, pink `#fecaca`, orange
`#fed7aa`, lime `#d9f99d`, green `#bbf7d0`, cyan `#a5f3fc`, blue `#bfdbfe`,
violet `#ddd6fe`, rose `#fbcfe8`, gray `#e5e7eb`.

A comfortable grid step for stickies is 200. The person's viewport
usually shows roughly 1200x800 units around the existing content, so put
new material near what is there, or below it (templates do that by
default), and call `POST /board/look` to bring them there.

## Change the board

The general endpoint applies a batch of operations atomically, in order:

    POST /board/ops
    {"ops": [ ... ], "base_version": 12}

`base_version` is optional; if present and the board has moved on you get
`409 {"error": ..., "version": current}` and nothing is applied. An
invalid op fails the whole batch with `422 {"error": "op 2: ..."}`.

Operations:

```json
{"op": "add", "kind": "sticky", "x": 40, "y": 40, "text": "Hello", "tags": ["idea"]}
{"op": "add", "shape": {"kind": "arrow", "from": "id1", "to": "id2", "label": "then"}}
{"op": "update", "id": "abc", "changes": {"text": "Renamed", "color": "#bfdbfe"}}
{"op": "update", "id": "abc", "text": "Renamed"}
{"op": "move", "id": "abc", "x": 300, "y": 120}
{"op": "duplicate", "id": "abc", "dx": 220, "dy": 0}
{"op": "lock", "id": "abc"}         {"op": "unlock", "id": "abc"}
{"op": "front", "id": "abc"}        {"op": "back", "id": "abc"}
{"op": "index", "id": "abc", "index": 0}
{"op": "delete", "id": "abc"}
{"op": "title", "title": "Q3 roadmap"}
{"op": "clear"}
```

Deleting a shape also deletes arrows attached to it. `index` puts a shape
at a z position (0 = back).

Images: upload the file as the raw request body, then add an image shape
that points at the returned path (PNG, JPEG, GIF or WebP, up to 10 MB):

    POST /board/files   (Content-Type: image/png, body: the bytes)
    -> 201 {"url": "/files/3b1f...9c.png"}
    {"op": "add", "kind": "image", "x": 0, "y": 0, "w": 320, "h": 200, "url": "/files/3b1f...9c.png"}

## Pages: HTML prototypes

A `page` is a small web page you write - one self-contained HTML file
with its CSS and JavaScript inline - shown live on the board as a card and
opened full screen with a double-click. Send the HTML inline with the
shape; the server stores it and fills in `url`:

    {"op": "add", "kind": "page", "x": 0, "y": 0, "w": 400, "h": 300, "text": "Login",
     "html": "<!doctype html><html><body><h1>Sign in</h1>...</body></html>"}

`text` is the page's title, shown over the card and in the full screen
bar. To change a page, send new `html` in an update: every version is
kept and undo brings the old one back. `GET /board/shapes/:id` returns
the `url`; fetch it to read the HTML back. Uploading with
`POST /board/files` (`Content-Type: text/html`) and then setting `url`
works too.

The page runs in a sandbox, and the same sandbox applies wherever it is
opened, so write for it:

- No network at all: no CDN scripts, stylesheets, fonts, images or fetch
  calls. Inline everything; images go in as `data:` URIs.
- No storage: no cookies, `localStorage` or IndexedDB. Keep state in
  memory; it is gone on reload.
- No way out: links to other sites, popups, downloads, `alert` and form
  submission do nothing. A page that navigates or reloads its own frame
  is stopped and shown as a card from then on.
- One file, up to 10 MB. On the canvas the page is live but takes no
  input; in full screen it is interactive and gets the whole viewport,
  so lay it out responsively.
- The look is fixed. Every page is drawn as a black-and-white sketch in
  one hand-written font: colours are removed, shadows and gradients
  dropped, buttons and inputs get a sketched outline (a stylesheet is
  appended to your file as served; Source shows it). Your CSS still
  decides layout, sizes, spacing and what is shown, so spend the effort
  on structure and behaviour, not on looks.

A prototype with several screens is several pages joined by arrows: one
page per screen, an arrow per transition with its label the trigger
("Sign in"), usually inside a frame named after the prototype. In full
screen a page moves to the next one by asking for it:

    <button onclick="parent.postMessage({go: 'Sign in'}, '*')">Sign in</button>

The board follows an arrow leaving the current page whose label is that
name, or whose target page has that title; `{go: ''}` follows the only
arrow out. Nothing happens without an arrow, so the diagram on the board
is exactly what the prototype can do, and a miss shows the person a
toast naming the missing arrow. Backspace (or the Back button) returns to
the previous page, Escape to the board. Nothing is ever sent back to a
page. When a page is ready, say so with `POST /board/say` and point at it
with `POST /board/highlight` or `POST /board/look`; the person opens it.

REST shortcuts:

    PATCH  /board                   {"title": "Q3 roadmap"}
    POST   /board/shapes            {"kind": "rect", "x": 0, "y": 0, "text": "Box"}
    PATCH  /board/shapes/:id        {"x": 10, "text": "Moved"}
    DELETE /board/shapes/:id

## Undo

    POST /board/undo                -> undoes your most recent batch
    POST /board/undo {"version": 13} -> undoes that batch (from anyone, if still in the log)

Or apply the `inverse` you got back from any write with `POST /board/ops`.

## Arrange

These compute positions on the server and apply them as `move`s. `ids`
defaults to every box on the board; arrows always follow. `layout` takes
the shapes in reading order (rows first, then left to right) rather than
in stacking order, so laying out an already-tidy set changes nothing.

    POST /board/layout      {"mode": "grid" | "row" | "column", "ids": [...], "cols": 3, "gap": 40, "x": 0, "y": 0}
    POST /board/align       {"edge": "left" | "right" | "top" | "bottom" | "center" | "middle", "ids": [...]}
    POST /board/distribute  {"axis": "horizontal" | "vertical", "ids": [...]}

## Templates

Build a whole structure in one call. Each returns the usual write result;
`applied` has every created shape with its id. All accept optional `x`,
`y` (default: below the existing content).

    POST /board/templates/mindmap
    {"root": {"text": "Launch", "children": [{"text": "Marketing", "children": ["Blog", "Ads"]}, "Engineering"]},
     "direction": "right" | "down"}

    POST /board/templates/kanban
    {"columns": [{"title": "To do", "items": ["Write spec", {"text": "Review", "color": "#fecaca"}]},
                 {"title": "Doing", "items": []}, {"title": "Done", "items": ["Kickoff"]}]}

    POST /board/templates/flow
    {"steps": ["Start", {"text": "Valid?", "kind": "diamond"}, {"text": "Ship", "label": "yes"}],
     "direction": "right" | "down"}

    POST /board/templates/stickies
    {"items": ["Idea 1", {"text": "Idea 2", "color": "#bbf7d0", "tags": ["top"]}], "cols": 4, "color": "#fff3a0"}

    POST /board/templates/sections
    {"sections": [{"title": "Strengths", "items": ["..."]}, {"title": "Weaknesses", "items": ["..."]},
                  {"title": "Opportunities", "items": []}, {"title": "Threats", "items": []}], "cols": 2}

## Be present

    POST /board/presence   {"name": "Claude", "color": "#7c3aed"}   -> you appear in the people list
    POST /board/cursor     {"x": 120, "y": 80}                       -> your cursor moves on everyone's canvas
    POST /board/say        {"text": "Grouped the ideas by theme"}    -> a message in every tab
    POST /board/highlight  {"ids": ["abc", "def"], "ms": 2500}       -> those shapes pulse
    POST /board/look       {"ids": ["abc"]} or {"x", "y", "w", "h"} or {} (everything) -> every tab eases over there (fitted, never zoomed past 100%)

Presence lasts while you keep calling the API (any request counts) and
fades 45 seconds after your last one. Your changes are labelled with the
name you gave.

## Watch for changes

    GET /board/events?since=12

A Server-Sent Events stream. It first replays batches newer than `since`
(omit it for live changes only), then streams:

    event: ops
    data: {"version": 13, "ops": [...], "inverse": [...], "by": {"type": "tab", "id": "...", "name": "calm otter"}}

`by.type` is `"tab"` for a person and `"agent"` for an API key. The
stream ends with `event: bye` when the key expires.

## Examples

```sh
curl -H "Authorization: Bearer $KEY" https://1charta.com/api/v1/board

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  https://1charta.com/api/v1/board/ops \
  -d '{"ops": [
        {"op": "add", "id": "plan", "kind": "sticky", "x": 40, "y": 40, "text": "Plan"},
        {"op": "add", "id": "build", "kind": "sticky", "x": 300, "y": 40, "text": "Build"},
        {"op": "add", "kind": "arrow", "from": "plan", "to": "build", "label": "then"}
      ]}'

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  https://1charta.com/api/v1/board/templates/mindmap \
  -d '{"root": {"text": "Launch", "children": ["Marketing", "Engineering", "Support"]}}'

curl -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  https://1charta.com/api/v1/board/look -d '{}'
```
