How a Board Works
This page explains the board data model and the mechanics behind every board operation: how tables and rows relate, how typed columns and cells work, how rich-text and file attachments behave, how row comments and visibility are scoped, how row authentication works, and how filtering, sorting, and exports are evaluated. For a quick orientation and copy-paste examples, start with the Boards introduction.
The data model
Boards form a three-level hierarchy:
- Board — the top-level container. A board groups related tables.
- Table — belongs to a board. A table defines a set of columns and holds rows.
- Row — a single record in a table. A row carries a cell value for each column, plus a markdown description and optional comments.
Board
└── Table (columns: Name, Status, Owner, Notes…)
├── Row #1 (cells: "Acme", "In progress", …)
├── Row #2
└── Row #3
Each row has two identifiers: an internal _id (a 24-character object id used in most endpoints) and a human-friendly row number (the small sequential number shown in the grid, e.g. 42). You can fetch a row by either one.
Columns and cells
A table's columns each have a columnId, a label, and a type. Column types include status, dropdown, labels, checkbox, text, paragraph, link, date, email, phone, website, number, duration, location, and users. Status/dropdown/labels columns expose their selectable options (each with an optionId, label, and color); status options also carry a statusGroup of TODO, IN_PROGRESS, or DONE.
When you read a row you get a columns array of cells, each keyed by columnId with a value. Link cells reference other rows; lookup cells surface values pulled from linked rows.
When you write a row you pass columns: [{ columnId, value }]. A few column types have special value semantics:
- Link columns take an array of row-id strings. Passing
[]clears the link. Linked rows must exist in the target table and the same workspace. - Rich-text (paragraph) columns take a markdown string. The content is seeded asynchronously, so it may appear a moment after the write returns.
- Duration columns take a JSON number representing seconds. For example,
1800represents 30 minutes. Formatted strings such as"00:30:00"are invalid.
Reading boards, tables, and rows
- List boards — returns every board your token can access, with an optional
qto filter by name or description. - Get a board / List tables / Get a table — drill into a board's structure and read a table's column definitions.
- List rows — returns the rows of a table. Supports three query parameters that compose together:
q(case-insensitive search across searchable columns),filter(a structured JSON filter, below), andsort. - Get a row — by internal id, or by row number using the dedicated row-number endpoint.
Filtering rows
The filter query parameter is a JSON string with this shape:
{
"match": "and",
"conditions": [
{ "column_id": "<columnId>", "operator": "contains", "value": "acme" },
{ "column_id": "<statusColumnId>", "operator": "includes", "value": ["<optionId>"] }
]
}
matchisand(default) oror.conditionsis an array of up to 20 conditions. Each names acolumn_id, anoperator, and (for most operators) avalue.- The valid operators depend on the column's type:
- Text:
equals,not_equals,contains,not_contains,starts_with,ends_with,is_empty,is_not_empty. - Number:
equals,not_equals,gt,gte,lt,lte,includes,not_includes,is_empty,is_not_empty. - Select (status / dropdown / labels):
equals,not_equals,includes,not_includes,is_empty,is_not_empty(values are option ids). - Checkbox:
equals,not_equals,is_empty,is_not_empty. - Date:
equals,before,after,between(value is a[startISO, endISO]pair), plus relative no-value operators liketoday,last_7_days,current_month, andis_empty/is_not_empty.
- Text:
An unknown column id or an operator that doesn't apply to the column type returns a 400.
Sorting rows
The sort query parameter is a comma-separated list of columnId:direction entries, where direction is asc (default) or desc:
?sort=statusColumnId:asc,createdColumnId:desc
The HTTP sort query string uses the columnId:direction form. Some SDK examples express sorting as an array of { column, dir } objects — both describe the same ordering, just in different shapes.
Writing rows
- Create a row —
POSTwith an optional markdowndescriptionand acolumnsarray (which may be empty). Returns the created row. - Update a row —
PATCHwith a non-emptycolumnsarray. Only the columns you send are changed; the rest are left untouched. - Delete a row — permanently removes the row.
Bots act like users: an action your token performs can trigger the board's automations, so programmatic writes integrate with the same workflows your team uses.
Row description and rich-text columns
Both a row's description and any rich-text column are markdown. Read them with the corresponding …/md GET endpoint, which returns { "content": "…" }.
To write them, POST an operation of replace, append, or prepend plus the markdown content. These writes are processed asynchronously and return HTTP 202 Accepted — the content lands a moment later.
File attachments
FILE columns store one or more private file references on the row cell. Through the Public API you can upload a new file directly to a FILE column cell with multipart/form-data, download an attached file by its fileId, or remove an attachment reference from the cell.
The upload endpoint appends the uploaded file to the existing cell value and returns the attachment metadata (fileId, name, MIME type, and size). The remove endpoint only detaches that fileId from the row cell; it does not delete the underlying private file record or bytes.
File-column cells and comment attachments can be downloaded directly. The download endpoints stream the raw file bytes (with the appropriate content-type and filename headers) rather than returning a signed URL.
Row comments
Rows support threaded comments, each with a visibility:
internal— visible only to workspace members.external— visible to everyone with row access, including guests.
List comments supports cursor pagination (after / before) and a visibility filter of all (default), internal, or external. Create a comment takes HTML content and a visibility that defaults to internal.
Authenticating a row
Tables can act as a lightweight credential store. The authenticate endpoint takes an identifier column + value and a password column + value, and returns the matching row (with password columns masked) when the credentials are valid:
400— no row matches the identifier.401— the password is incorrect.
This lets you build sign-in style checks against a board table without exposing the password cell.
Exporting a table view
The export endpoint renders a table view to a file. You POST the viewId and a format — CSV, XLSX, JSON, MARKDOWN, HTML, PDF, ZIP, or ICS — along with optional filters, sort, column selection, and per-format options.
Exports run synchronously or asynchronously depending on size:
- Small exports return 200 OK with the rendered file inline (text formats as UTF-8, binary formats base64-encoded).
- Large exports (and PDF/ZIP, which are always async, or any request with
forceAsync: true) return 202 Accepted with anasyncJobdescribing the job and, once ready, adownloadUrl. You can also supply awebhookUrlto be notified when the file is ready.
Authentication & scope
Board endpoints accept a full Personal Access Token (cp_pat_) or an integration API key (cp_key_), and require the access_boards scope. See Authentication for token types and Error Handling for the standard error shape.
Reference
- Boards introduction — orientation, Quick Start, and parity.
- Boards in the API Reference — every board, table, row, comment, authenticate, and export endpoint with request/response schemas.
- Pagination — cursor conventions used by row comments.
- Rate Limits — per-endpoint limits (exports are limited more tightly than reads).
- Copera CLI and MCP server — the same board operations from the command line and from AI clients.