Skip to main content
Documentation menu

Developer platform

REST API

The hosted REST API lets people and agents create, read, render, and edit diagrams over plain HTTP, without the browser. It is the same surface the CLI and MCP server speak. Everything lives under the base path /api/v1.

Authentication

Every request carries a Personal Access Token in the standard bearer header. Mint a token in your account settings and send it with each call.

Request header
Authorization: Bearer <your-token>

See Authentication for scopes, rate limits, and the auth error codes.

Conventions

  • Base path is /api/v1. All request and response bodies are JSON.
  • Every endpoint declares a required scope. A read-write token satisfies a read requirement.
  • Access checks return 404 not_found when a target is missing OR is not yours. The two cases are indistinguishable, so the API never leaks whether an id exists.

Every failure shares one envelope shape:

Error envelope
{ "error": "<machine_code>", "message"?: "<human-readable detail>" }

The error value is a stable machine code. The optional message adds human detail and is omitted entirely when there is none.

Endpoints

GET /me

Scope: read. Returns the identity behind the calling token. Useful as a quick check that a token is valid.

200 OK
{
  "id": "<userId>",
  "email": "[email protected]",
  "scope": "read" | "read-write"
}

GET /diagrams

Scope: read. Lists the diagrams you own, newest first, with cursor pagination. List items do not include the scene; fetch a single diagram to read it.

Query paramTypeDescription
limitnumber?How many diagrams to return. Default 50, clamped to the range 1 to 100.
cursorstring?Opaque pagination cursor from a previous page's nextCursor. Treat it as a black box and round-trip it unchanged. Omit for the first page.
filter"active" | "trash"Which set to list. Default "active" (not trashed); "trash" returns trashed diagrams.
200 OK
{
  "items": [
    {
      "id": "<id>",
      "title": "Architecture",
      "folderId": "<id>" | null,
      "starred": false,
      "updatedAt": "2026-06-02T12:00:00.000Z",
      "createdAt": "2026-06-01T09:00:00.000Z"
    }
  ],
  "nextCursor": "<cursor>" | null
}

When nextCursor is non-null, pass it back as the cursor query param to fetch the next page. A null cursor means there are no more pages.

POST /diagrams

Scope: read-write. Creates a diagram from exactly one of spec or scene. Sending both, or neither, is rejected.

FieldTypeDescription
titlestring?Diagram title. The stored, sanitized value is echoed back in the response.
folderIdstring | null?UUID of a folder you own, or null for the root. A non-UUID string is rejected.
specDiagramSpec?A high-level node and edge spec that the server auto-lays-out. Provide this OR scene, never both. See the diagram DSL for the spec shape.
sceneScene?A raw Excalidraw scene, preserved as-is. Provide this OR spec, never both. Any appState you include is dropped.
POST /api/v1/diagrams
POST /api/v1/diagrams
Authorization: Bearer <read-write token>
Content-Type: application/json

{
  "title": "Architecture",
  "spec": {
    "version": "1",
    "nodes": [
      { "id": "client", "label": "Client",     "shape": "rectangle" },
      { "id": "api",    "label": "API Server",  "shape": "rectangle" },
      { "id": "db",     "label": "Database",    "shape": "diamond" },
      { "id": "cache",  "label": "Cache",       "shape": "ellipse" }
    ],
    "edges": [
      { "from": "client", "to": "api", "label": "request" },
      { "from": "api",    "to": "db" },
      { "from": "api",    "to": "cache" }
    ],
    "layout": { "direction": "TB" }
  }
}
201 Created
{
  "id": "<id>",
  "title": "Architecture",
  "folderId": "<id>" | null,
  "ownerId": "<userId>",
  "createdAt": "2026-06-02T12:00:00.000Z",
  "updatedAt": "2026-06-02T12:00:00.000Z",
  "elementCount": 11
}

See the diagram DSL for the full spec grammar, including shorthand edge strings.

GET /diagrams/:id

Scope: read. Returns the full scene. A trashed diagram is still returned, with deletedAt set, so it can be inspected or restored.

200 OK
{
  "id": "<id>",
  "title": "Architecture",
  "folderId": "<id>" | null,
  "ownerId": "<userId>",
  "createdAt": "2026-06-02T12:00:00.000Z",
  "updatedAt": "2026-06-02T12:00:00.000Z",
  "deletedAt": "2026-06-02T13:00:00.000Z" | null,
  "scene": { "elements": [ /* ... */ ], "appState": {} }
}

PATCH /diagrams/:id

Scope: read-write. Renames and/or moves a diagram. At least one field is required. The response does not include the scene.

FieldTypeDescription
titlestring?New title. Renames the diagram.
folderIdstring | null?Including this key MOVES the diagram: a UUID you own moves it into that folder, null moves it to the root. Omitting the key entirely leaves the location unchanged.
PATCH /api/v1/diagrams/:id
PATCH /api/v1/diagrams/<id>
Authorization: Bearer <read-write token>
Content-Type: application/json

{
  "title": "Architecture v2",
  "folderId": "<folder-id>"
}
200 OK
{
  "id": "<id>",
  "title": "Architecture v2",
  "folderId": "<folder-id>" | null,
  "ownerId": "<userId>",
  "createdAt": "2026-06-02T12:00:00.000Z",
  "updatedAt": "2026-06-02T14:00:00.000Z",
  "deletedAt": null
}

DELETE /diagrams/:id

Scope: read-write. Soft-deletes a diagram to the trash. The call is idempotent: deleting an already-trashed diagram still succeeds. Returns no body.

204 No Content
204 No Content

GET /diagrams/:id/render

Scope: read. Renders the diagram's saved scene to a preview. The output format is chosen with ?format=.

Query paramTypeDescription
format"json" | "png" | "svg"Output format. Default "json" returns an envelope with the SVG and base64 PNG; "png" returns image/png bytes; "svg" returns image/svg+xml.

The default json format returns an envelope:

200 OK (format=json)
{
  "isEmpty": false,
  "elementCount": 11,
  "svg": "<svg ...>",
  "png": "<base64, no data: prefix>" | null,
  "note"?: "large_diagram_placeholder" | "render_failed"
}

// empty scene
{ "isEmpty": true, "elementCount": 0, "svg": null, "png": null }

The png format returns image/png bytes (the rendered PNG is 1024 pixels wide, roughly 1024 by 640), and svg returns image/svg+xml. For either image format an empty scene returns 204. Each call renders fresh; there is no PNG cache.

PUT /diagrams/:id/public-link

Scope: read-write. Turns the anonymous public link on or off and optionally sets or clears its password.

FieldTypeDescription
enabled*booleanTurn the public link on or off.
passwordstring | null?A string sets a password, null clears it, and omitting the key leaves the existing password unchanged.
200 OK
{
  "slug": "<slug>",
  "enabled": true,
  "hasPassword": false,
  "version": 1,
  "url": "<absolute public link url>" | null
}

url is an absolute URL when the link is enabled, and null when it is disabled.

POST /diagrams/:id/edit

Scope: read-write. Additively merges an edit fragment into a diagram. Existing elements are never moved, resized, or deleted; the fragment only adds nodes and edges and patches a small allowlist of node properties. The change is reflected immediately in a subsequent read or render.

EditFragment shape
{
  "addNodes":    [ /* new nodes, explicit x/y honored, else auto-placed */ ],
  "addEdges":    [ /* from/to resolve against existing or added node ids */ ],
  "updateNodes": [ /* { id } plus the allowed properties below */ ]
}

Only non-reflow properties can be patched on existing nodes: strokeColor, backgroundColor, fillStyle, and label (plus the required id). Geometry fields like x, y, width, height, and shape are rejected, so an already-placed node never starts overlapping a neighbor.

200 OK
{ "added": ["<id>"], "updated": ["<id>"], "merged": 2 }

See the diagram DSL for the full edit-fragment shape and field rules.

GET /folders

Scope: read. Returns a flat list of every folder you own.

200 OK
{
  "items": [
    { "id": "<id>", "name": "Architecture", "parentId": "<id>" | null }
  ]
}

POST /folders

Scope: read-write. Creates a folder.

FieldTypeDescription
name*stringFolder name. Must be non-empty.
parentIdstring | null?UUID of a parent folder you own to nest under, or null for a top-level folder.
201 Created
{ "id": "<id>", "name": "Architecture", "parentId": "<id>" | null }

PATCH and DELETE /folders/:id

Scope: read-write. PATCH renames a folder with a { "name": "<new name>" } body and returns the updated folder. DELETE removes the folder and returns 204. Member diagrams are not deleted; they become unfiled.

Error codes

Transport and authentication codes, returned by any endpoint:

CodeStatusDescription
unauthorized401Missing, malformed, unknown, expired, or revoked token.
insufficient_scope403A read token was used on a write endpoint.
email_unverified403The token is valid but the account's email is not verified.
not_found404The target does not exist, or is not yours. The two are indistinguishable on purpose.
bad_request400A field is the wrong type, or required fields are missing or conflicting.
invalid_json400The request body is not parseable JSON.
payload_too_large413The request body exceeds the roughly 1 MiB cap.
rate_limited429Over the rate limit. The response carries a Retry-After header.
method_not_allowed405An unsupported HTTP verb for the route.
api_disabled503The API is temporarily disabled.
render_failed503A PNG render failed or timed out.
upstream_unavailable502An edit merge could not be applied because the backend was unreachable.
quota_exceeded403Creating the diagram would exceed your diagram quota.
folder_forbidden403On create, folderId names a folder you do not own.

Content-model codes, returned when a spec, scene, or edit fragment fails validation (422) or exceeds a size cap (413). The accompanying message names the offending element.

CodeStatusDescription
INVALID_SPEC422The spec, or an edit-fragment node patch, is malformed or uses a disallowed field.
UNSUPPORTED_VERSION422A version was given and it is not "1".
MISSING_NODE_ID422A node is not an object, or its id is missing or empty.
DUPLICATE_NODE_ID422Two nodes share the same id.
UNKNOWN_SHAPE422A node's shape is not one of rectangle, ellipse, diamond, or text.
DANGLING_EDGE422An edge's from or to names a node id that does not exist.
MALFORMED_SHORTHAND422A shorthand edge string is missing an endpoint or its arrow.
MALFORMED_EDGE422An edge object is missing from or to, or has a bad arrowhead or label.
SPEC_TOO_LARGE413The spec exceeds the node, edge, or string-length caps.
ID_COLLISION422A node id collides with a generated label or arrow id.
PROTO_KEY422The payload contains a prototype-polluting key (__proto__, constructor, prototype).
NOT_A_SCENE422The scene is not an object, or its elements are not a valid array.
BAD_LINK_SCHEME422A link value uses a scheme that is not allowed.
SCENE_TOO_LARGE413The resulting scene exceeds the 5000-element cap.

Limits and fidelity

What to expect from a rendered diagram

  • appState is always empty. Only elements round-trip through the API. A supplied appState is dropped on create, and reads always return {}.
  • Render reads the saved snapshot of the diagram. An edit merged through the API is reflected immediately, so you can edit and then re-read or re-render and see your own change.
  • PNG output is an approximate preview: no sketchy strokes, system fonts instead of the editor font, and image elements drawn as placeholder rectangles. Good enough for previews, not pixel-parity with the editor.
  • Very large diagrams (over 5000 elements) render a placeholder image instead of the full scene.
  • Hard caps apply: 1000 nodes, 2000 edges, and 5000 total scene elements. Exceeding a cap returns a size error.

Prefer a higher-level client? The CLI and MCP server wrap this same surface.