cnvs.app Items API
Add / update / move / delete draw-board items (texts, strokes, images, links). Mirrors MCP tools one-to-one.
Add / update / move / delete draw-board items (texts, strokes, images, links). Mirrors MCP tools one-to-one.
Every API here is available over the APIs.io API and to AI agents over MCP.
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
find_apisBrowse and filter every API in the catalog.get_api_artifactsOne API's artifacts, grouped by type.get_openapiThe primary OpenAPI for this API.find_similar_apisAPIs that look like this one.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.curl "https://apis.io/api/v1/apis/cnvs-app:cnvs-app-items-api"
curl "https://apis.io/api/v1/apis?limit=25"
Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: Cnvs App Items API
version: 1.0.0
contact:
name: cnvs.app
url: https://cnvs.app/about/
license:
name: Public service — no account required
url: https://cnvs.app/about/
x-accessLock:
modes:
- write
- all
keyFormat: 8 chars [a-z0-9] (6-char legacy keys also accepted)
channel:
rest: X-Board-Key header
mcp: X-Board-Key header OR `access_key` tool argument
ws: Sec-WebSocket-Protocol subprotocol `cnvs-key.<code>` (server echoes it back in the 101 response). `?key=<code>` query parameter is accepted as a deprecated fallback but leaks the key into access logs.
endpoints:
lock: POST /api/boards/{id}/lock
unlock: POST /api/boards/{id}/unlock
verify: POST /api/boards/{id}/verify-key
rejection:
rest:
status: 401
body:
code: board_locked
lockMode: write|all
mcp:
jsonRpcCode: -32001
data:
code: board_locked
lockMode: write|all
ws:
status: 401
note: Upgrade rejected for read-locked boards; write-locked allows the upgrade but server drops mutation frames with type=error code=board_locked.
note: By default boards are open (anyone with the ID can read + write). When locked, mode `write` only requires the key for mutations; `all` requires it for both reads and writes. There is no recovery — losing the key loses the board.
x-boardImportLimits:
maxItems: 2550
maxBatchStatements: 320
maxBoundParamsPerStatement: 100
countedCollections:
- texts
- lines
- images
- columns
- tasks
- lanes
maxLaneTitles: 10
laneTitleRule: Each lane title must reference a lane used by a column created in the same request.
infrastructureToken: X-Import-Token is server-to-server. A valid token selects the infrastructure REST/MCP and board-create rate tiers; it never bypasses hard caps, validation, storage, board access locks, or Cloudflare platform limits.
x-cors:
allowOrigin: '*'
allowMethods:
- GET
- POST
- DELETE
- OPTIONS
allowHeaders:
- Content-Type
- Mcp-Session-Id
- If-None-Match
- X-Board-Key
note: 'Every public endpoint (/api/boards/..., /json/..., /svg-preview/..., /mcp) returns permissive CORS headers and handles OPTIONS preflight. The board ID is the access credential when the board is unlocked; locked boards additionally require the X-Board-Key header. X-Import-Token is intentionally absent: it is a server-to-server credential, so browser clients cannot send it and belong on the standard per-IP rate tier.'
x-fieldLimits:
maxAuthorChars: 80
maxThumbnailBytes: 8000
textWidth:
min: 160
max: 4096
columnWidth:
min: 200
max: 480
default: 280
maxLaneTitleChars: 200
maxColumnTitleChars: 200
maxTaskNameChars: 500
maxAssigneeChars: 200
x-operationLimits:
maxRecolorItems: 500
maxBatchOps: 500
wsContinuation:
maxFrames: 500
idleMs: 500
x-quotasManifest:
url: /quotas.json
note: Machine-readable single source of truth for limits. Cache ≤ 5 min.
x-rateLimits:
standard:
restMcpPerBoard:
requests: 60
windowSeconds: 10
per: boardId
boardCreate:
requests: 5
windowSeconds: 60
per: clientIp
enforcement: Cloudflare Rate Limiting binding; permissive and eventually consistent per edge location
infrastructure:
credential: valid X-Import-Token (server-to-server only)
restMcpPerBoard:
requests: 600
windowSeconds: 10
per: boardId
boardCreate:
requests: 50
windowSeconds: 60
per: infrastructureToken
perNote: Keyed by a one-way digest of the token VALUE (the credential never enters limiter keys or telemetry). There is a single import-token secret, so all trusted callers share this one bucket — it is not a per-integration allowance.
enforcement: Cloudflare Rate Limiting binding; permissive and eventually consistent per edge location
webSocket:
requests: 60
windowSeconds: 10
per: boardId
infrastructureTokenChangesTier: false
security:
lockUnlockVerify:
requests: 5
windowSeconds: 60
per: clientIp
infrastructureTokenChangesTier: false
scope: Browser WebSocket frames are rate-limited inside the per-board BoardServer Durable Object — single-writer and strong. REST and MCP are rate-limited in whichever Worker isolate serves the request; because Cloudflare routes traffic across many isolates (regions, colos, warm/cold starts), a caller distributing load effectively gets N × the advertised cap. Treat REST/MCP as a soft per-isolate ceiling, not a strong global guarantee. X-Import-Token raises only the REST/MCP and board-create tiers; it does not bypass hard caps or board access locks.
response:
rest:
status: 429
headers:
Retry-After: <seconds>
body:
code: rate_limited
retryAfterSeconds: 10
mcp:
jsonRpcCode: -32000
data:
code: rate_limited
retryAfterSeconds: 10
ws:
body:
type: error
code: rate_limited
retryAfterSeconds: 10
note: Strong for browser WS (DO-enforced), soft for REST/MCP (isolate-local; bypassable via distribution). Cloudflare's platform-level limits remain the hard wall. Live values in /quotas.json.
x-requestBodyLimit:
bytes: 5242880
note: Single-request body cap (5 MiB) enforced via Content-Length AND during the streamed body read. Chunked / header-less clients cannot bypass the cap. HTTP 413 (REST) / JSON-RPC -32000 code:payload_too_large (MCP).
x-webSocketErrors:
note: 'Every WebSocket error frame is `{type: "error", code, message, originType, originId}` and is sent ONLY to the socket whose mutation failed — never broadcast. `originType`/`originId` echo the frame that caused it.'
codes:
rate_limited: Rejection — per-board WS budget exceeded (see x-rateLimits). Nothing was written.
board_locked: Rejection — board is write-locked and this socket has no valid key (see x-accessLock). Nothing was written.
quota_exceeded: Rejection — a per-board quota would be exceeded; carries `kind`. Nothing was written.
invalid_payload: Rejection — the frame failed validation; carries `field`. Nothing was written.
server_error: 'NOT a rejection — the mutation threw server-side (e.g. a D1 storage reset mid-write). The throw may occur before, during or after the write commits, so the outcome is unknown: the change may be fully stored, partly stored, or absent. Re-read the board (GET /json/{id}) and reconcile; do not assume local state is correct, and do not blindly undo the change.'
description: 'Operations tagged Items across 2 of this provider''s published API definitions: cnvs-app-openapi.json, cnvs-app-openapi.yml. Each path carries the servers of the definition it was published in.'
servers:
- url: https://cnvs.app
description: Production
tags:
- name: Items
description: Add / update / move / delete draw-board items (texts, strokes, images, links). Mirrors MCP tools one-to-one.
paths:
/api/boards/{boardId}/texts:
parameters:
- $ref: '#/components/parameters/BoardIdPath'
post:
tags:
- Items
summary: Create or update a text node
description: 'Mirrors the MCP `add_text` tool. Creates a NEW text node with a fresh UUID, OR updates an existing node if you pass its `id` (preferred over creating duplicates). Content supports cnvs markup (Markdown-ish, including `[]` / `[ ]` / `[x]` task-list checkboxes that render as clickable circles and toggle in source on click) and Mermaid diagrams — when using Mermaid, the ENTIRE content must be a single ```mermaid fenced block (one diagram per node). Set `postit: true` for a yellow sticky-note style.'
operationId: addText
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AddTextRequest'
examples:
simpleSticky:
value:
x: 100
y: 200
content: '# Hello from REST!'
postit: true
mermaid:
value:
x: 400
y: 200
content: "```mermaid\nflowchart LR\n A --> B\n B --> C\n```"
width: 320
responses:
'200':
description: Text created or updated.
content:
application/json:
schema:
$ref: '#/components/schemas/AddTextResponse'
'400':
$ref: '#/components/responses/BadRequest'
'404':
$ref: '#/components/responses/NotFound'
'413':
$ref: '#/components/responses/QuotaExceeded'
'401':
$ref: '#/components/responses/BoardLocked'
'429':
$ref: '#/components/responses/RateLimited'
servers:
- url: https://cnvs.app
description: Production
/api/boards/{boardId}/{kind}/{itemId}/move:
parameters:
- $ref: '#/components/parameters/BoardIdPath'
- name: kind
in: path
required: true
description: 'Item kind. Use the same plural form as the creation endpoint: `texts`, `links`, `strokes` (or the alias `lines` — matches the JSON snapshot key), or `images`.'
schema:
type: string
enum:
- texts
- links
- strokes
- lines
- images
- $ref: '#/components/parameters/ItemIdPath'
post:
tags:
- Items
summary: Move any item
description: Unified move endpoint — works for every item kind. For `texts` / `links` / `images` the top-left lands at (x, y); for `strokes` the whole point array is translated so the bbox top-left sits at (x, y) (no need to re-send the point list). The creator's `author` tag is preserved (immutable after creation) so a move by a collaborator does NOT relabel who originally authored the item.
operationId: moveItem
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MoveTextRequest'
example:
x: 320
y: 480
responses:
'200':
description: Moved.
content:
application/json:
schema:
type: object
properties:
id:
type: string
x:
type: number
y:
type: number
'400':
$ref: '#/components/responses/BadRequest'
'404':
$ref: '#/components/responses/NotFound'
'413':
$ref: '#/components/responses/QuotaExceeded'
'401':
$ref: '#/components/responses/BoardLocked'
'429':
$ref: '#/components/responses/RateLimited'
servers:
- url: https://cnvs.app
description: Production
/api/boards/{boardId}/strokes:
parameters:
- $ref: '#/components/parameters/BoardIdPath'
post:
tags:
- Items
summary: Draw a freehand stroke
description: 'Mirrors the MCP `draw_stroke` tool. Stroke width is fixed at 3 px; `color` is a NAME, not hex (case-insensitive): `auto`/`black`/omitted for theme-aware ink, or `red`, `blue`, `green`, `orange`, `yellow`, `pink`, `purple`, `maroon`, `brown`, `gray`, `lightgray`, `teal`, `sage`, `sky`, `lavender` — anything else (including a literal hex) silently clamps to `auto`. Accepts points as nested `[[x,y],...]`, flat `[x1,y1,x2,y2,...]`, or a JSON string of either — the server normalises.'
operationId: addStroke
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AddStrokeRequest'
example:
points:
- - 100
- 100
- - 200
- 150
- - 300
- 180
color: red
responses:
'200':
description: Stroke drawn.
content:
application/json:
schema:
type: object
properties:
id:
type: string
pointCount:
type: integer
color:
type: string
bbox:
type: object
properties:
x:
type: integer
y:
type: integer
width:
type: integer
height:
type: integer
author:
type: string
'400':
$ref: '#/components/responses/BadRequest'
'404':
$ref: '#/components/responses/NotFound'
'413':
$ref: '#/components/responses/QuotaExceeded'
'401':
$ref: '#/components/responses/BoardLocked'
'429':
$ref: '#/components/responses/RateLimited'
servers:
- url: https://cnvs.app
description: Production
/api/boards/{boardId}/images:
parameters:
- $ref: '#/components/parameters/BoardIdPath'
post:
tags:
- Items
summary: Paste an image onto the board
description: 'Mirrors the MCP `add_image` tool. `dataUrl` must be a `data:image/(png|jpeg|gif|webp|svg+xml);base64,...` string, ≤ ~900 kB. Only image payloads are accepted: the decoded header bytes are sniffed (PNG/JPEG/GIF/WebP magic bytes or an `` root), so ordinary PDFs, archives and other files relabeled as images are rejected with 400 (a header check, not a full image decode). SVG content must be declared `svg+xml` and raster content must not. Hosted URLs are NOT accepted — if you have a URL, fetch the bytes yourself and re-encode as a data URL. Strongly recommended: also pass `thumbDataUrl` (≤8 kB, ~64 px) — it gets embedded into SVG previews so other AI viewers see an actual image instead of a placeholder box.
Example (upload a 1×1 PNG):
```
curl -X POST https://cnvs.app/api/boards//images \
-H ''Content-Type: application/json'' \
-d ''{"x":200,"y":300,"width":80,"height":80,"dataUrl":"data:image/png;base64,iVBORw0KGgo..."}''
```
For a hosted image, convert first:
```
BASE64=$(curl -sL https://example.com/pic.png | base64)
curl -X POST https://cnvs.app/api/boards//images \
-H ''Content-Type: application/json'' \
-d "{\"x\":200,\"y\":300,\"width\":400,\"height\":300,\"dataUrl\":\"data:image/png;base64,$BASE64\"}"
```'
operationId: addImage
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AddImageRequest'
responses:
'200':
description: Image placed.
content:
application/json:
schema:
type: object
properties:
id:
type: string
x:
type: number
y:
type: number
width:
type: number
height:
type: number
hasThumbDataUrl:
type: boolean
description: Whether a thumbnail was stored alongside the full image. Governs whether `/svg-preview` inlines the image or shows a placeholder.
author:
type: string
'400':
description: Bad data URL, too large, or invalid dimensions.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
$ref: '#/components/responses/NotFound'
'413':
$ref: '#/components/responses/QuotaExceeded'
'401':
$ref: '#/components/responses/BoardLocked'
'429':
$ref: '#/components/responses/RateLimited'
servers:
- url: https://cnvs.app
description: Production
/api/boards/{boardId}/links:
parameters:
- $ref: '#/components/parameters/BoardIdPath'
post:
tags:
- Items
summary: Drop a URL capsule
description: Mirrors the MCP `add_link` tool. Renders as a clickable pill showing the hostname. Use this instead of POST /texts when the node is just a link — the capsule styling signals clickability to humans.
operationId: addLink
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AddLinkRequest'
example:
x: 100
y: 400
url: https://cnvs.app/about/
responses:
'200':
description: Link placed.
content:
application/json:
schema:
type: object
properties:
id:
type: string
x:
type: number
y:
type: number
url:
type: string
author:
type: string
kind:
type: string
enum:
- link
'400':
$ref: '#/components/responses/BadRequest'
'404':
$ref: '#/components/responses/NotFound'
'413':
$ref: '#/components/responses/QuotaExceeded'
'401':
$ref: '#/components/responses/BoardLocked'
'429':
$ref: '#/components/responses/RateLimited'
servers:
- url: https://cnvs.app
description: Production
/api/boards/{boardId}/{kind}/{itemId}:
parameters:
- $ref: '#/components/parameters/BoardIdPath'
- name: kind
in: path
required: true
description: Item kind. `links` is an alias for `texts` (links live in the same table — delete via either path). `lines` is an alias for `strokes` (matches the `/json` snapshot key — use either spelling).
schema:
type: string
enum:
- texts
- links
- strokes
- lines
- images
- $ref: '#/components/parameters/ItemIdPath'
delete:
tags:
- Items
summary: Delete an item by id
description: Mirrors the MCP `erase` tool. `kind` MUST match the item type; unknown kinds are rejected with HTTP 400 (no silent targeting of the wrong table). `links` is an alias for `texts` since they live in the same table — you may delete a link via either path.
operationId: deleteItem
responses:
'200':
description: Deleted.
content:
application/json:
schema:
type: object
properties:
ok:
type: boolean
id:
type: string
kind:
type: string
'400':
$ref: '#/components/responses/BadRequest'
'404':
$ref: '#/components/responses/NotFound'
'401':
$ref: '#/components/responses/BoardLocked'
'429':
$ref: '#/components/responses/RateLimited'
servers:
- url: https://cnvs.app
description: Production
components:
responses:
QuotaExceeded:
description: Per-board quota exceeded (too many texts, images, strokes, or total image bytes). Response body names the specific limit that was hit.
content:
application/json:
schema:
$ref: '#/components/schemas/QuotaError'
examples:
tooManyImages:
value:
error: This board already has 50 images; the cap is 50. Delete one first.
code: quota_exceeded
kind: images_per_board
tooManyBytes:
value:
error: Total image bytes on this board would be 10.3 MB; the cap is 10 MB. Upload a smaller image or erase existing ones.
code: quota_exceeded
kind: image_bytes_per_board
BadRequest:
description: Invalid input (missing or malformed board id).
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
RateLimited:
description: 'Rate limit exceeded. Standard REST/MCP: 60 requests/10s/board; valid X-Import-Token infrastructure tier: 600/10s/board; browser WebSocket always 60/10s/board. REST/MCP enforcement is soft per-isolate; WebSocket enforcement is strong per Durable Object.'
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before retrying.
content:
application/json:
schema:
type: object
properties:
error:
type: string
code:
type: string
enum:
- rate_limited
retryAfterSeconds:
type: integer
required:
- error
- code
BoardLocked:
description: Board is access-locked and the request did not carry a valid key. `lockMode` tells the client whether to prompt for a code or just disable write UI.
headers:
WWW-Authenticate:
schema:
type: string
description: '`Bearer realm="cnvs board", error="board_locked"`.'
content:
application/json:
schema:
type: object
properties:
error:
type: string
code:
type: string
enum:
- board_locked
locked:
type: string
enum:
- read
- write
description: '`read` when lockMode=all, `write` when lockMode=write.'
lockMode:
type: string
enum:
- write
- all
required:
- error
- code
- lockMode
NotFound:
description: Board does not exist or was deleted within the 30-day tombstone window.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
schemas:
Error:
type: object
properties:
error:
type: string
description: Human-readable error message.
required:
- error
MoveTextRequest:
type: object
required:
- x
- y
properties:
x:
type: number
y:
type: number
author:
$ref: '#/components/schemas/AuthorTag'
AddTextResponse:
type: object
properties:
id:
type: string
x:
type: number
y:
type: number
content:
type: string
postit:
type: boolean
author:
type: string
required:
- id
- x
- y
- content
AddImageRequest:
type: object
required:
- x
- y
- width
- height
- dataUrl
properties:
id:
type: string
x:
type: number
y:
type: number
width:
type: number
description: Displayed width in board px.
height:
type: number
description: Displayed height in board px.
dataUrl:
type: string
description: 'Data URL: `data:image/(png|jpeg|gif|webp|svg+xml);base64,<payload>`. Max ~900 kB total. Hosted URLs NOT accepted — fetch + base64-encode first. Only image payloads are accepted: the decoded header bytes are sniffed (PNG/JPEG/GIF/WebP magic bytes or an `<svg>` root), so ordinary PDFs, archives and other files relabeled as images are rejected with 400 (a header check, not a full image decode).'
pattern: ^data:image/(png|jpeg|gif|webp|svg\+xml);base64,
thumbDataUrl:
type:
- string
- 'null'
description: Optional tiny thumbnail (≤8 kB JPEG/PNG/WebP, ~64 px on long edge). Embedded into SVG previews so other AI viewers see the image instead of a placeholder.
author:
$ref: '#/components/schemas/AuthorTag'
AddStrokeRequest:
type: object
required:
- points
properties:
id:
type: string
points:
description: Ordered points. Accepts `[[x,y],[x,y],...]`, flat `[x1,y1,x2,y2,...]`, or a JSON string of either.
oneOf:
- type: array
items:
type: array
items:
type: number
minItems: 2
maxItems: 2
- type: array
items:
type: number
- type: string
color:
type: string
enum:
- auto
- black
- red
- blue
- green
- orange
- yellow
- pink
- purple
- maroon
- brown
- gray
- lightgray
- teal
- sage
- sky
- lavender
description: Ink color NAME — same options as for text. Case-insensitive. Custom hex is rejected-with-clamp, not accepted literally.
author:
$ref: '#/components/schemas/AuthorTag'
AddLinkRequest:
type: object
required:
- x
- y
- url
properties:
id:
type: string
x:
type: number
y:
type: number
url:
type: string
format: uri
author:
$ref: '#/components/schemas/AuthorTag'
QuotaError:
allOf:
- $ref: '#/components/schemas/Error'
- type: object
properties:
code:
type: string
enum:
- quota_exceeded
kind:
type: string
enum:
- texts_per_board
- text_content_chars
- images_per_board
- image_bytes_per_board
- strokes_per_board
- columns_per_board
- tasks_per_board
- task_content_chars
- import_items
- lanes
required:
- code
- kind
AuthorTag:
type: string
description: Author tag, charset `[A-Za-z0-9:_\-.]`, max 80 chars. Defaults to `ai:rest` for REST mutations. Use `ai:<your-label>` (e.g. `ai:claude`, `ai:mybot`) so humans can tell AI edits apart.
pattern: ^[A-Za-z0-9:_\-.]{1,80}$
AddTextRequest:
type: object
required:
- x
- y
- content
properties:
id:
type: string
description: Optional stable id. Pass an existing id to UPDATE that node in place; omit to CREATE a new one with a fresh UUID.
x:
type: number
description: World x (+x right).
y:
type: number
description: World y (+y DOWN, standard SVG).
content:
type: string
description: Raw content (up to 100 000 chars). Supports cnvs markup (Markdown-ish) and Mermaid (one diagram per node — entire content must be a single ```mermaid fenced block).
color:
type: string
enum:
- auto
- black
- red
- blue
- green
- orange
- yellow
- pink
- purple
- maroon
- brown
- gray
- lightgray
- teal
- sage
- sky
- lavender
description: Ink color NAME — same mental model as clicking the ink picker. Case-insensitive. `auto` / `black` / omitted → theme-aware. `red` / `blue` / `green` and the extended palette (`orange`, `yellow`, `pink`, `purple`, `maroon`, `brown`, `gray`, `lightgray`, `teal`, `sage`, `sky`, `lavender`) → emphasis colors. Custom hex codes silently clamp to `auto` so AI writes never end up invisible on dark mode.
width:
type:
- number
- 'null'
description: Explicit width in px (160–4096), or null for auto-fit.
postit:
type: boolean
description: Renders as a yellow sticky note when true.
author:
$ref: '#/components/schemas/AuthorTag'
parameters:
BoardIdPath:
name: boardId
in: path
required: true
description: Board identifier. Charset `[A-Za-z0-9-]`, max 64 characters. Usually a UUID but `open_board` accepts any conforming string.
schema:
type: string
pattern: ^[A-Za-z0-9-]{1,64}$
example: 0a01ba29-bc15-4880-b135-ec47e33d95b2
ItemIdPath:
name: itemId
in: path
required: true
description: Item id (text / stroke / image). Discover via `GET /json/{id}`.
schema:
type: string
securitySchemes:
BoardKey:
type: apiKey
in: header
name: X-Board-Key
description: 8-char [a-z0-9] access key (6-char legacy keys also accepted). Required for write operations on any locked board, and for ALL operations on a board locked with mode=all. Obtain via POST /api/boards/{id}/lock. Browsers carry the key in localStorage under `cnvs:board_key:<id>`.
externalDocs:
description: cnvs.app / MCP reference
url: https://cnvs.app/llms.txt
x-refined-from:
- cnvs-app-openapi.json
- cnvs-app-openapi.yml
x-mcp:
endpoint: https://cnvs.app/mcp
wellKnown: https://cnvs.app/.well-known/mcp.json
transport: streamable-http
note: This REST API covers reads and mutations one-to-one with MCP. MCP additionally provides live `notifications/resources/updated` subscriptions and the `wait_for_update` long-poll tool.