cnvs.app Kanban API

Todo-mode kanban board: mode switch, columns, tasks, row (lane) titles, and shared column width. Mirrors the MCP kanban tools one-to-one (`set_board_mode`, `create_column`, `create_task`, `move_task`, `set_lane`, `set_column_width`, …).

GitHub OpenAPI

Operations 8

POST /api/boards/{boardId}/mode Set board mode (draw / todo) #
POST /api/boards/{boardId}/column-width Set the shared kanban column width #
POST /api/boards/{boardId}/lanes Set or clear a kanban row (lane) title #
POST /api/boards/{boardId}/columns Create or update a kanban column #
DELETE /api/boards/{boardId}/columns/{itemId} Delete a kanban column #
POST /api/boards/{boardId}/tasks Create or update a kanban task #
POST /api/boards/{boardId}/tasks/{itemId}/move Move a kanban task #
DELETE /api/boards/{boardId}/tasks/{itemId} Delete a kanban task #

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-kanban-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-kanban-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Cnvs App Kanban 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 Kanban 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: Kanban
  description: 'Todo-mode kanban board: mode switch, columns, tasks, row (lane) titles, and shared column width. Mirrors the MCP kanban tools one-to-one (`set_board_mode`, `create_column`, `create_task`, `move_task`, `set_lane`, `set_column_width`, …).'
paths:
  /api/boards/{boardId}/mode:
    parameters:
    - $ref: '#/components/parameters/BoardIdPath'
    post:
      tags:
      - Kanban
      summary: Set board mode (draw / todo)
      description: 'Mirrors the MCP `set_board_mode` tool. A board is either `draw` (the default infinite-canvas whiteboard) or `todo` (a kanban task board with columns and cards). **The mode is switchable WHENEVER the board is empty of real content** — drawings (text/strokes/images) and tasks. Empty or seeded columns DON''T count (switching to `draw` clears them), so a board that once held content but is now cleared becomes switchable again, and you can flip `draw` ↔ `todo` freely until the first stroke/text/image or task lands. Calling this on a board that still holds real content returns `400 { code: "board_not_empty" }`. Switching an empty board to `todo` seeds the starter columns; pass an optional `template` (`kanban` — the default — / `sprint` / `bugs`) to choose the column set, returned in the `columns` array.'
      operationId: setBoardMode
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - mode
              properties:
                mode:
                  type: string
                  enum:
                  - draw
                  - todo
                  description: '`draw` = infinite-canvas whiteboard (default). `todo` = kanban task board.'
                template:
                  type: string
                  enum:
                  - kanban
                  - sprint
                  - bugs
                  description: Starter column set when switching to `todo`. Defaults to `kanban`. Ignored for `draw`.
            example:
              mode: todo
              template: sprint
      responses:
        '200':
          description: Mode set.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  mode:
                    type: string
                    enum:
                    - draw
                    - todo
                  columns:
                    type: array
                    items:
                      $ref: '#/components/schemas/Column'
                required:
                - ok
                - mode
        '400':
          description: 'Invalid mode, or the board still holds real content (`code: board_not_empty`) so the mode can''t change until it''s cleared.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Board mode can only be changed while the board is empty of real content.
                code: board_not_empty
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/BoardLocked'
        '429':
          $ref: '#/components/responses/RateLimited'
    servers:
    - url: https://cnvs.app
      description: Production
  /api/boards/{boardId}/column-width:
    parameters:
    - $ref: '#/components/parameters/BoardIdPath'
    post:
      tags:
      - Kanban
      summary: Set the shared kanban column width
      description: 'Board-level setting: the pixel width every kanban column shares. Only valid on a `todo`-mode board. Mirrors the WebSocket `set_column_width` frame and the MCP `set_column_width` tool; exists mainly so a snapshot restore can recreate the saved width. The value is clamped server-side to the [200, 480]px range. Read the current value as `colWidth` from `GET /json/{id}`.'
      operationId: setColumnWidth
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - width
              properties:
                width:
                  type: number
                  description: Column width in px. Clamped to [200, 480].
            example:
              width: 280
      responses:
        '200':
          description: Width set (after clamping).
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  width:
                    type: number
                required:
                - ok
                - width
        '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
  /api/boards/{boardId}/lanes:
    parameters:
    - $ref: '#/components/parameters/BoardIdPath'
    post:
      tags:
      - Kanban
      summary: Set or clear a kanban row (lane) title
      description: 'Board-level setting: the optional title of a kanban row, keyed on the integer `lane` index (a lane is not its own entity, just the index columns carry). Only valid on a `todo`-mode board. Mirrors the WebSocket `lane_upsert` frame and the MCP `set_lane` tool; exists mainly so a snapshot restore can recreate saved row names. An empty `title` clears the row name. Read current lane titles from the `lanes[]` array of `GET /json/{id}`.'
      operationId: upsertLane
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - lane
              properties:
                lane:
                  type: integer
                  minimum: 0
                  description: Row index the title applies to.
                title:
                  type: string
                  description: Row name. Empty / omitted clears it.
                author:
                  type: string
                  description: Optional author tag; defaults to `ai:rest`. Immutable after the row is first named.
            example:
              lane: 0
              title: Sprint 1
      responses:
        '200':
          description: Row title set or cleared.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  lane:
                    type: integer
                  title:
                    type: string
                required:
                - ok
                - lane
                - title
        '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}/columns:
    parameters:
    - $ref: '#/components/parameters/BoardIdPath'
    post:
      tags:
      - Kanban
      summary: Create or update a kanban column
      description: Mirrors the MCP `create_column` / `update_column` tools. Only valid on a `todo`-mode board. Creates a NEW column with a fresh UUID, OR updates an existing column in place if you pass its `id`. Max 200 columns per board (≤ 20 per row, ≤ 10 rows).
      operationId: addColumn
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddColumnRequest'
            examples:
              create:
                value:
                  title: Backlog
                  sort: 0
              update:
                value:
                  id: c0a1...
                  title: In review
                  sort: 2
      responses:
        '200':
          description: Column created or updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Column'
        '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}/columns/{itemId}:
    parameters:
    - $ref: '#/components/parameters/BoardIdPath'
    - $ref: '#/components/parameters/ItemIdPath'
    delete:
      tags:
      - Kanban
      summary: Delete a kanban column
      description: Mirrors the MCP `delete_column` tool. Deletes the column AND every task inside it. Discover column ids via `GET /json/{id}` (the `columns[]` array).
      operationId: deleteColumn
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  id:
                    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
  /api/boards/{boardId}/tasks:
    parameters:
    - $ref: '#/components/parameters/BoardIdPath'
    post:
      tags:
      - Kanban
      summary: Create or update a kanban task
      description: 'Mirrors the MCP `create_task` / `update_task` tools. Only valid on a `todo`-mode board. Creates a NEW task with a fresh UUID, OR updates an existing task in place if you pass its `id`. Max 1000 tasks per board, 20 000 chars per task content. In the snapshot a task''s `content` is an opaque JSON string (e.g. `{"description":...}`) — treat it as a blob. It may also carry an optional `boards` array of attached cnvs board ids (`{"boards":["",...]}`), set when a user drags boards from their Recent rail onto the task.


        Over MCP there are extra task tools without a dedicated REST path: `create_tasks` (bulk-create an array in one call), `query_tasks` (server-side filter by assignee / priority / done / overdue / due-window), and `export_tasks` (the same output as `GET tasks.csv` / `tasks.md`). `due_date` must be an ISO 8601 date (`YYYY-MM-DD` or full date-time); non-ISO values are rejected `400 { field: "due_date" }`.'
      operationId: addTask
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddTaskRequest'
            examples:
              minimal:
                value:
                  column_id: c0a1...
                  name: Ship kanban docs
              full:
                value:
                  column_id: c0a1...
                  name: Ship kanban docs
                  description: Update llms.txt + openapi
                  due_date: '2026-06-30'
                  priority: H
                  assignee: alex
                  sort: 0
      responses:
        '200':
          description: Task created or updated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        '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}/tasks/{itemId}/move:
    parameters:
    - $ref: '#/components/parameters/BoardIdPath'
    - $ref: '#/components/parameters/ItemIdPath'
    post:
      tags:
      - Kanban
      summary: Move a kanban task
      description: Mirrors the MCP `move_task` tool. Moves a task into a (possibly different) column and sets its sort position within that column.
      operationId: moveTask
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - column_id
              - sort
              properties:
                column_id:
                  type: string
                  description: Destination column id.
                sort:
                  type: number
                  description: Sort position within the destination column.
            example:
              column_id: c0a2...
              sort: 1
      responses:
        '200':
          description: Moved.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  column_id:
                    type: string
                  sort:
                    type: number
        '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
  /api/boards/{boardId}/tasks/{itemId}:
    parameters:
    - $ref: '#/components/parameters/BoardIdPath'
    - $ref: '#/components/parameters/ItemIdPath'
    delete:
      tags:
      - Kanban
      summary: Delete a kanban task
      description: Mirrors the MCP `delete_task` tool. Deletes a single card. Discover task ids via `GET /json/{id}` (the `tasks[]` array).
      operationId: deleteTask
      responses:
        '200':
          description: Deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                  id:
                    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:
  schemas:
    Task:
      type: object
      description: A kanban task (card) on a `todo`-mode board.
      properties:
        id:
          type: string
        column_id:
          type: string
          description: Column the card currently lives in.
        name:
          type: string
          description: Card title.
        content:
          type:
          - string
          - 'null'
          description: Opaque JSON string blob, e.g. `{"description":...}`. Treat as a blob — do not parse application meaning out of it. Max 20 000 chars.
        due_date:
          type:
          - string
          - 'null'
          description: ISO 8601 due date, or null.
        priority:
          type:
          - string
          - 'null'
          enum:
          - H
          - M
          - L
          - null
          description: High / Medium / Low, or null.
        assignee:
          type:
          - string
          - 'null'
        done:
          type: boolean
          description: Whether the task is completed (checked off). Defaults to false.
        sort:
          type: number
          description: Order within the column.
        author:
          type:
          - string
          - 'null'
        last_updated:
          type: string
          description: UTC timestamp.
      required:
      - id
      - column_id
      - name
    Error:
      type: object
      properties:
        error:
          type: string
          description: Human-readable error message.
      required:
      - error
    AddColumnRequest:
      type: object
      required:
      - title
      properties:
        id:
          type: string
          description: Pass an existing id to UPDATE that column; omit to CREATE a new one.
        title:
          type: string
        sort:
          type: number
          description: Left-to-right position. Defaults to the end if omitted on create.
        lane:
          type: integer
          default: 0
          description: Optional row index for multi-lane layout. Defaults to 0 (top row).
        color:
          type: string
          enum:
          - red
          - blue
          - green
          description: Optional column title color — one of `red` / `blue` / `green`. Omit for the default.
    Column:
      type: object
      description: A kanban column on a `todo`-mode board.
      properties:
        id:
          type: string
        title:
          type: string
        sort:
          type: number
          description: Left-to-right order of the column.
        lane:
          type: integer
          default: 0
          description: Row index for multi-lane layout. Defaults to 0 (top row).
        color:
          type:
          - string
          - 'null'
          enum:
          - red
          - blue
          - green
          - null
          description: Column title color — one of `red` / `blue` / `green`, or null for the default.
        author:
          type:
          - string
          - 'null'
        last_updated:
          type: string
          description: UTC timestamp.
      required:
      - id
      - title
    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}$
    AddTaskRequest:
      type: object
      required:
      - column_id
      - name
      properties:
        id:
          type: string
          description: Pass an existing id to UPDATE that task in place; omit to CREATE a new one.
        column_id:
          type: string
          description: Column the card belongs to.
        name:
          type: string
          description: Card title.
        description:
          type: string
          description: Optional free-text body. Stored inside the task's opaque `content` JSON blob (≤ 20 000 chars).
        due_date:
          type: string
          description: Optional ISO 8601 due date.
        priority:
          type: string
          enum:
          - H
          - M
          - L
          description: Optional High / Medium / Low priority.
        assignee:
          type: string
          description: Optional assignee label.
        done:
          type: boolean
          description: Mark the task completed (checked off). Defaults to false on create; omit on update to leave unchanged.
        sort:
          type: number
          description: Optional order within the column.
        author:
          $ref: '#/components/schemas/AuthorTag'
  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'
  parameters:
    ItemIdPath:
      name: itemId
      in: path
      required: true
      description: Item id (text / stroke / image). Discover via `GET /json/{id}`.
      schema:
        type: string
    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
  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.