cnvs.app Reads API

The Reads API from cnvs.app — 2 operation(s) for reads.

GitHub OpenAPI

Operations 2

GET /api/boards/{boardId}/tasks.csv Export tasks as CSV #
GET /api/boards/{boardId}/tasks.md Export tasks as Markdown #

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-reads-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-reads-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Cnvs App Reads 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 Reads 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: Reads
paths:
  /api/boards/{boardId}/tasks.csv:
    parameters:
    - $ref: '#/components/parameters/BoardIdPath'
    - name: download
      in: query
      required: false
      description: 'Set to `1` to receive `Content-Disposition: attachment` instead of the default `inline`. Only the literal `1` opts in — the default is deliberately unchanged so existing readers (curl, API explorers, agents) keep displaying the document rather than downloading it.'
      schema:
        type: string
        enum:
        - '1'
    get:
      tags:
      - Reads
      summary: Export tasks as CSV
      description: 'Export a `todo`-mode board''s columns + tasks as CSV — one row per task, RFC-4180 quoted. Header: `lane,column,name,priority,assignee,due_date,done,description`. `lane` is the title of the swimlane (row) the task''s column sits in, falling back to the lane''s 1-based row number (`1`, `2`, …) when that lane has no title. Mirrors the MCP `export_tasks` tool with `format: "csv"`. A `draw` board (no columns) still returns 200 with just the header row, so callers don''t have to branch on mode first.'
      operationId: exportTasksCsv
      responses:
        '200':
          description: CSV document.
          content:
            text/csv:
              schema:
                type: string
              example: 'lane,column,name,priority,assignee,due_date,done,description

                Backend,To do,Ship docs,H,alex,2026-06-30,false,Update llms.txt'
        '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.md:
    parameters:
    - $ref: '#/components/parameters/BoardIdPath'
    - name: download
      in: query
      required: false
      description: 'Set to `1` to receive `Content-Disposition: attachment` instead of the default `inline`. Only the literal `1` opts in — the default is deliberately unchanged so existing readers (curl, API explorers, agents) keep displaying the document rather than downloading it.'
      schema:
        type: string
        enum:
        - '1'
    get:
      tags:
      - Reads
      summary: Export tasks as Markdown
      description: 'Export a `todo`-mode board''s columns + tasks as a Markdown checklist grouped by column. Done cards render as `[x]`, open as `[ ]`; priority / assignee / due_date ride inline as trailing tags. Mirrors the MCP `export_tasks` tool (default `format: "markdown"`). A `draw` board (no columns) still returns 200 with just the board heading.'
      operationId: exportTasksMarkdown
      responses:
        '200':
          description: Markdown document.
          content:
            text/markdown:
              schema:
                type: string
              example: '# Task board <id>


                ## To do

                - [ ] Ship docs  _(priority: H, @alex, due 2026-06-30)_

                '
        '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:
    Error:
      type: object
      properties:
        error:
          type: string
          description: Human-readable error message.
      required:
      - error
  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.