cnvs.app AI Facing API

Endpoints designed for LLM consumption — schematic SVG previews and clean JSON snapshots.

GitHub OpenAPI

Operations 6

GET /api/boards/{boardId}/wait Long-poll: block until the next edit #
GET /json/{boardId} Board snapshot (MCP get_board shape) #
HEAD /json/{boardId} Cheap existence + freshness probe #
GET /json Board snapshot (via query param) #
GET /svg-preview/{boardId} Schematic SVG preview #
GET /svg-preview Schematic SVG preview (via query param) #

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-ai-facing-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-ai-facing-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Cnvs App AI Facing 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 AI-facing 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: AI-facing
  description: Endpoints designed for LLM consumption — schematic SVG previews and clean JSON snapshots.
paths:
  /api/boards/{boardId}/wait:
    parameters:
    - $ref: '#/components/parameters/BoardIdPath'
    get:
      tags:
      - AI-facing
      summary: 'Long-poll: block until the next edit'
      description: 'REST equivalent of the MCP `wait_for_update` tool. Blocks until the next debounced edit burst lands on this board, or `timeout_ms` elapses. Designed for AI clients without MCP push notifications: call it after your turn is done, refresh with `GET /json/{id}` (using the returned ETag) when it resolves with `updated: true`. Resolves ~3 s after the edit burst settles (same debounce as push notifications).'
      operationId: waitForBoardUpdate
      parameters:
      - name: timeout_ms
        in: query
        required: false
        description: Milliseconds to block before giving up. Clamped to [1000, 55000]; default 25000.
        schema:
          type: integer
          minimum: 1000
          maximum: 55000
          default: 25000
      responses:
        '200':
          description: 'Either an edit landed (`updated: true`) or the timeout elapsed (`timedOut: true`). Returned ETag matches what `GET /json/{id}` would produce right now — save it and send back as `If-None-Match` on the next read to skip the 200 body when nothing has changed.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  boardId:
                    type: string
                  updated:
                    type: boolean
                  timedOut:
                    type: boolean
                  etag:
                    type: string
                required:
                - boardId
                - updated
                - timedOut
                - etag
        '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
  /json/{boardId}:
    parameters:
    - $ref: '#/components/parameters/BoardIdPath'
    get:
      tags:
      - AI-facing
      summary: Board snapshot (MCP get_board shape)
      description: 'Full structured JSON snapshot of a board, identical in shape to the MCP `get_board` tool. Includes every text node''s COMPLETE content (Mermaid source stays intact inside the `content` field), every stroke''s point array, and image metadata. Heavy image payloads (>8 kB base64) are elided (`dataUrl: null`); tiny images (small SVGs, icon-sized PNGs) ride along inline so AI clients can render them without a second round-trip.


        The `boardId` can be supplied as a trailing path segment OR as `?board=...` query param — the query form accepts a raw UUID, a hash URL like `https://cnvs.app/#`, or any URL whose path tail is the id.


        **Caching for pollers**: every response carries a weak `ETag` header derived from the board''s last-updated timestamp + per-kind item counts. Send it back as `If-None-Match` and unchanged boards short-circuit to `304 Not Modified` (empty body, no rate-limit burn). `HEAD` is also supported for cheap existence/freshness probes.'
      operationId: getBoardJson
      parameters:
      - name: If-None-Match
        in: header
        required: false
        description: Weak ETag from a previous response. Unchanged boards respond `304 Not Modified`.
        schema:
          type: string
      responses:
        '200':
          description: Board snapshot.
          headers:
            ETag:
              description: Weak ETag. Reuse with `If-None-Match` to poll cheaply.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BoardSnapshot'
        '304':
          description: Unchanged since the supplied `If-None-Match`. Empty body.
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/BoardLocked'
        '429':
          $ref: '#/components/responses/RateLimited'
    head:
      tags:
      - AI-facing
      summary: Cheap existence + freshness probe
      description: Same headers as `GET /json/{boardId}` but zero body. Returns 200 with ETag if the board exists, 404 otherwise.
      operationId: headBoardJson
      responses:
        '200':
          description: Board exists; ETag returned.
          headers:
            ETag:
              schema:
                type: string
        '404':
          $ref: '#/components/responses/NotFound'
    servers:
    - url: https://cnvs.app
      description: Production
  /json:
    get:
      tags:
      - AI-facing
      summary: Board snapshot (via query param)
      description: 'Same as `/json/{boardId}` but with the id in the query string. Convenient when you have a full cnvs URL and don''t want to strip the hash: `GET /json?board=https://cnvs.app/#`.'
      operationId: getBoardJsonQuery
      parameters:
      - name: board
        in: query
        required: true
        description: 'Raw board UUID, hash URL (`https://cnvs.app/#<id>`), or bare id. Alias: `id`.'
        schema:
          type: string
      responses:
        '200':
          description: Board snapshot.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BoardSnapshot'
        '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
  /svg-preview/{boardId}:
    parameters:
    - $ref: '#/components/parameters/BoardIdPath'
    get:
      tags:
      - AI-facing
      summary: Schematic SVG preview
      description: Compact schematic render of the board — a few kB of plain SVG text, directly consumable by multimodal LLMs as an image. Shows texts as labeled rectangles, strokes as polylines with exact world coordinates, and images as placeholder boxes (or tiny thumbnails if provided). Mermaid blocks currently render as a `[mermaid diagram]` placeholder — fetch `/json/{id}` for the raw source. AI-authored items are rendered with a purple border so the viewer can tell apart AI and human contributions.
      operationId: getSvgPreview
      responses:
        '200':
          description: SVG preview.
          content:
            image/svg+xml:
              schema:
                type: string
                format: binary
        '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
  /svg-preview:
    get:
      tags:
      - AI-facing
      summary: Schematic SVG preview (via query param)
      description: Same as `/svg-preview/{boardId}` but with the id in the query string.
      operationId: getSvgPreviewQuery
      parameters:
      - name: board
        in: query
        required: true
        description: 'Raw board UUID, hash URL, or bare id. Alias: `id`.'
        schema:
          type: string
      responses:
        '200':
          description: SVG preview.
          content:
            image/svg+xml:
              schema:
                type: string
                format: binary
        '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
    ImageNode:
      type: object
      description: Image metadata. Full `dataUrl` is NOT included in this shape — fetch `/api/boards/{id}` if you need raw pixels.
      properties:
        id:
          type: string
        x:
          type: number
        y:
          type: number
        width:
          type: number
        height:
          type: number
        thumbDataUrl:
          type:
          - string
          - 'null'
          description: Tiny preview embedded in SVG previews (≤8 kB).
        author:
          type:
          - string
          - 'null'
        last_updated:
          type: string
      required:
      - id
      - x
      - y
      - width
      - height
    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
    TextNode:
      type: object
      description: A draggable, editable text block. Content supports cnvs markup (Markdown-ish) and Mermaid diagrams. When a node contains Mermaid, its entire content is one ```mermaid fenced block (one diagram per node).
      properties:
        id:
          type: string
        x:
          type: number
          description: World x (+x right).
        y:
          type: number
          description: World y (+y DOWN, standard SVG).
        content:
          type: string
          description: Raw text content (up to 100 000 chars). May include Markdown, Mermaid, emoji.
        color:
          type:
          - string
          - 'null'
          description: Resolved ink value stored on the node — the palette hex for a named color (e.g. `red` → `#ff3b30`) or the CSS variable `var(--text-color)` for theme-aware ink. Writes accept only the color NAMES, never raw hex.
        width:
          type:
          - number
          - 'null'
          description: Explicit wrapping width in px, or null for auto.
        postit:
          type: integer
          enum:
          - 0
          - 1
          description: 1 renders as a yellow sticky note.
        kind:
          type: string
          enum:
          - text
          - link
          description: '`text` is a free-form markdown/Mermaid node; `link` is a URL capsule rendered as a clickable pill. Both live in the same table — `kind` lets machine clients distinguish them without guessing from content.'
        author:
          type:
          - string
          - 'null'
          description: 'Author tag: `user:<uuid>` for browser edits, `ai:<label>` for MCP edits. IMMUTABLE after creation — subsequent moves / edits by other collaborators do NOT change this value.'
        last_updated:
          type: string
          description: UTC timestamp.
      required:
      - id
      - x
      - y
      - content
    StrokeLine:
      type: object
      description: A freehand stroke — an ordered list of world-coordinate points.
      properties:
        id:
          type: string
        points:
          type: array
          description: Array of `[x, y]` pairs in board world coordinates.
          items:
            type: array
            items:
              type: number
            minItems: 2
            maxItems: 2
        color:
          type:
          - string
          - 'null'
        author:
          type:
          - string
          - 'null'
        last_updated:
          type: string
      required:
      - id
      - points
    BoardSnapshot:
      type: object
      description: Full board state as returned by `/json/{id}` and the MCP `get_board` tool. Same shape as the `cnvs://board/{id}/state.json` MCP resource.
      properties:
        boardId:
          type: string
        mode:
          type: string
          enum:
          - draw
          - todo
          description: Board mode. `draw` (default) = infinite-canvas whiteboard; `todo` = kanban task board. Switchable whenever the board is empty of real content (drawings + tasks); empty/seeded columns don't count, so a cleared board can switch again.
        texts:
          type: array
          items:
            $ref: '#/components/schemas/TextNode'
        lines:
          type: array
          items:
            $ref: '#/components/schemas/StrokeLine'
        images:
          type: array
          items:
            $ref: '#/components/schemas/ImageNode'
        columns:
          type: array
          description: Kanban columns (present / non-empty on `todo`-mode boards).
          items:
            $ref: '#/components/schemas/Column'
        tasks:
          type: array
          description: Kanban tasks (present / non-empty on `todo`-mode boards).
          items:
            $ref: '#/components/schemas/Task'
      required:
      - boardId
      - texts
      - lines
      - images
  responses:
    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:
    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.