cnvs.app Items API

Add / update / move / delete draw-board items (texts, strokes, images, links). Mirrors MCP tools one-to-one.

GitHub OpenAPI

Operations 6

POST /api/boards/{boardId}/texts Create or update a text node #
POST /api/boards/{boardId}/{kind}/{itemId}/move Move any item #
POST /api/boards/{boardId}/strokes Draw a freehand stroke #
POST /api/boards/{boardId}/images Paste an image onto the board #
DELETE /api/boards/{boardId}/{kind}/{itemId} Delete an item by id #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • 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.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/cnvs-app:cnvs-app-items-api"
All apis
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.

Get an API 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 Specification

cnvs-app-items-api-openapi.yml Raw ↑
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.