Pages
Creating, moving, trashing and restoring pages; their Markdown surface, field values, relations, backlinks, search, and the pages a person pins.
26 operations. Every path is relative to the instance origin; every response body is JSON unless stated. See API for authentication, errors and pagination.
GET /api/v1/workspaces/{workspaceId}/pages
List child pages of a parent page, or the top level of a collection. Each entry carries hasChildren — whether this CALLER would find at least one live child below it, resolved through the same permission filter as the listing (ADR-0016 D9), so a chevron can never disclose a child the caller may not read.
listPages · token scope: content:read
Path parameters
workspaceId· string (uuid) — required
Query parameters
cursor· string — length 1–∞limit· integer — 1–100collectionId· string (uuid)parentPageId· string (uuid)
Response 200 — application/json
items· object[] — requiredid· string (uuid) — requiredworkspaceId· string (uuid) — requiredcollectionId· string (uuid) — requiredparentPageId· string (uuid) | null — requiredposition· string — requiredtitle· string — requiredicon· string | null — requiredcover· string | null — requiredcreatedBy· string (uuid) | null — requiredupdatedBy· string (uuid) | null — requiredcreatedAt· string (date-time) — requiredupdatedAt· string (date-time) — requireddeletedAt· string (date-time) | null — requiredrestricted· boolean — requiredhasChildren· boolean — required
nextCursor· string | null — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
POST /api/v1/workspaces/{workspaceId}/pages
Create a page in a collection, optionally under a parent page.
createPage · token scope: content:write
Path parameters
workspaceId· string (uuid) — required
Request body — application/json, required
collectionId· string (uuid) — requiredparentPageId· string (uuid)title· string — required, length 0–1000icon· string — length 0–2048
Response 200 — application/json
id· string (uuid) — requiredworkspaceId· string (uuid) — requiredcollectionId· string (uuid) — requiredparentPageId· string (uuid) | null — requiredposition· string — requiredtitle· string — requiredicon· string | null — requiredcover· string | null — requiredcreatedBy· string (uuid) | null — requiredupdatedBy· string (uuid) | null — requiredcreatedAt· string (date-time) — requiredupdatedAt· string (date-time) — requireddeletedAt· string (date-time) | null — requiredrestricted· boolean — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
GET /api/v1/workspaces/{workspaceId}/pages/{pageId}
Get a single page. Default representation is JSON; ?format=markdown returns the page body as canonical Markdown (text/markdown) with an ETag (ADR-0012).
getPage · token scope: content:read
Path parameters
workspaceId· string (uuid) — requiredpageId· string (uuid) — required
Query parameters
format·"markdown"
Response 200 — application/json
id· string (uuid) — requiredworkspaceId· string (uuid) — requiredcollectionId· string (uuid) — requiredparentPageId· string (uuid) | null — requiredposition· string — requiredtitle· string — requiredicon· string | null — requiredcover· string | null — requiredcreatedBy· string (uuid) | null — requiredupdatedBy· string (uuid) | null — requiredcreatedAt· string (date-time) — requiredupdatedAt· string (date-time) — requireddeletedAt· string (date-time) | null — requiredrestricted· boolean — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
PUT /api/v1/workspaces/{workspaceId}/pages/{pageId}
Replace a page body from canonical Markdown (text/markdown; ADR-0012). Parses to blocks and applies them through the doc mutator in one Yjs transaction. Supports If-Match/ETag optimistic concurrency (412 on mismatch); returns the stored canonical Markdown.
updatePageMarkdown · token scope: content:write
Path parameters
workspaceId· string (uuid) — requiredpageId· string (uuid) — required
Query parameters
format·"markdown"
Request body — application/json, required
- string
Response 200 — application/json
- string
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
PATCH /api/v1/workspaces/{workspaceId}/pages/{pageId}
Update page title and/or icon.
updatePage · token scope: content:write
Path parameters
workspaceId· string (uuid) — requiredpageId· string (uuid) — required
Request body — application/json, required
title· string — length 0–1000icon· string | null
Response 200 — application/json
id· string (uuid) — requiredworkspaceId· string (uuid) — requiredcollectionId· string (uuid) — requiredparentPageId· string (uuid) | null — requiredposition· string — requiredtitle· string — requiredicon· string | null — requiredcover· string | null — requiredcreatedBy· string (uuid) | null — requiredupdatedBy· string (uuid) | null — requiredcreatedAt· string (date-time) — requiredupdatedAt· string (date-time) — requireddeletedAt· string (date-time) | null — requiredrestricted· boolean — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
POST /api/v1/workspaces/{workspaceId}/pages/{pageId}/move
Move a page to a new canonical parent and/or sibling position. Returns the moved page plus an ADVISORY accessDiff (ADR-0016 D12): the subjects who gained and lost access, computed AFTER the move. A move is never refused on permission grounds — the UI surfaces the diff as a notice.
movePage · token scope: content:write
Path parameters
workspaceId· string (uuid) — requiredpageId· string (uuid) — required
Request body — application/json, required
parentPageId· string (uuid) | null — requiredbeforeId· string (uuid)afterId· string (uuid)
Response 200 — application/json
id· string (uuid) — requiredworkspaceId· string (uuid) — requiredcollectionId· string (uuid) — requiredparentPageId· string (uuid) | null — requiredposition· string — requiredtitle· string — requiredicon· string | null — requiredcover· string | null — requiredcreatedBy· string (uuid) | null — requiredupdatedBy· string (uuid) | null — requiredcreatedAt· string (date-time) — requiredupdatedAt· string (date-time) — requireddeletedAt· string (date-time) | null — requiredrestricted· boolean — requiredaccessDiff· object — requiredgained· object[] — requiredsubjectType·"user"|"group"— requiredsubjectId· string (uuid) — requiredname· string — requiredimage· string | null — required
lost· object[] — requiredsubjectType·"user"|"group"— requiredsubjectId· string (uuid) — requiredname· string — requiredimage· string | null — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
POST /api/v1/workspaces/{workspaceId}/pages/{pageId}/duplicate
Duplicate a page as a new sibling placed right after it: title, icon, field values (props) and body doc are copied, and the whole canonical page subtree comes along. The root copy is titled by the request title when given (verbatim — localize it client-side); otherwise the server falls back to the source title suffixed "(copy)". Every copied block gets a FRESH id — the copy shares nothing with the original. Databases, views and relation links inside the subtree are NOT copied; database memberships of the duplicated page itself are (the copy stays a row of the same databases). Refuses (400) above DUPLICATE_PAGE_MAX_PAGES / DUPLICATE_PAGE_MAX_DEPTH. Returns the new root page.
duplicatePage · token scope: content:write
Path parameters
workspaceId· string (uuid) — requiredpageId· string (uuid) — required
Request body — application/json, required
title· string — length 0–1000
Response 200 — application/json
id· string (uuid) — requiredworkspaceId· string (uuid) — requiredcollectionId· string (uuid) — requiredparentPageId· string (uuid) | null — requiredposition· string — requiredtitle· string — requiredicon· string | null — requiredcover· string | null — requiredcreatedBy· string (uuid) | null — requiredupdatedBy· string (uuid) | null — requiredcreatedAt· string (date-time) — requiredupdatedAt· string (date-time) — requireddeletedAt· string (date-time) | null — requiredrestricted· boolean — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
POST /api/v1/workspaces/{workspaceId}/pages/{pageId}/trash
Soft-delete a page (move to trash).
trashPage · token scope: content:write
Path parameters
workspaceId· string (uuid) — requiredpageId· string (uuid) — required
Response 200 — application/json
id· string (uuid) — requiredworkspaceId· string (uuid) — requiredcollectionId· string (uuid) — requiredparentPageId· string (uuid) | null — requiredposition· string — requiredtitle· string — requiredicon· string | null — requiredcover· string | null — requiredcreatedBy· string (uuid) | null — requiredupdatedBy· string (uuid) | null — requiredcreatedAt· string (date-time) — requiredupdatedAt· string (date-time) — requireddeletedAt· string (date-time) | null — requiredrestricted· boolean — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
POST /api/v1/workspaces/{workspaceId}/pages/{pageId}/restore
Restore a page from the trash.
restorePage · token scope: content:write
Path parameters
workspaceId· string (uuid) — requiredpageId· string (uuid) — required
Response 200 — application/json
id· string (uuid) — requiredworkspaceId· string (uuid) — requiredcollectionId· string (uuid) — requiredparentPageId· string (uuid) | null — requiredposition· string — requiredtitle· string — requiredicon· string | null — requiredcover· string | null — requiredcreatedBy· string (uuid) | null — requiredupdatedBy· string (uuid) | null — requiredcreatedAt· string (date-time) — requiredupdatedAt· string (date-time) — requireddeletedAt· string (date-time) | null — requiredrestricted· boolean — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
GET /api/v1/workspaces/{workspaceId}/trash
List the workspace's trash (deleted_at set), most-recently trashed first. Only the roots of each trashed subtree are returned — entries whose canonical parent is itself trashed are omitted, so restoring a listed entry brings back its whole batch. Covers trashed PAGES and trashed DATABASES in one list; resourceType says which one an entry is (and therefore which restore / permanent-delete route applies). A database entry carries its name in title. Workspace-scoped; auth + membership guarded.
listTrashedPages · token scope: content:read
Path parameters
workspaceId· string (uuid) — required
Query parameters
cursor· string — length 1–∞limit· integer — 1–100
Response 200 — application/json
items· object[] — requiredid· string (uuid) — requiredworkspaceId· string (uuid) — requiredcollectionId· string (uuid) — requiredparentPageId· string (uuid) | null — requiredposition· string — requiredtitle· string — requiredicon· string | null — requiredcover· string | null — requiredcreatedBy· string (uuid) | null — requiredupdatedBy· string (uuid) | null — requiredcreatedAt· string (date-time) — requiredupdatedAt· string (date-time) — requireddeletedAt· string (date-time) | null — requiredrestricted· boolean — requiredresourceType·"page"|"database"— required
nextCursor· string | null — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
GET /api/v1/workspaces/{workspaceId}/trash/summary
Count the workspace trash: how many trashed subtree ROOTS listTrashedPages would return in total (same predicate, unpaginated). emptyTrash purges all of them, so a confirmation must not count the one paginated page the client happens to hold. Workspace-scoped; auth + membership guarded.
getTrashSummary · token scope: content:read
Path parameters
workspaceId· string (uuid) — required
Response 200 — application/json
entries· integer — required, 0–9007199254740991
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
DELETE /api/v1/workspaces/{workspaceId}/pages/{pageId}/permanent
Permanently delete a TRASHED page and everything below it — irreversible. Refuses with 409 when the page is not in the trash (trash it first). The cascade removes the whole canonical subtree: db_row memberships (as row and, for purged databases, as container), relation_link rows in BOTH directions (including the symmetric mirror), database_field bindings of purged databases, block_link edges in both directions, permissions/public shares, the search projection, the stored Yjs doc + update tail, and every structure- and doc-plane block row. Views targeting a purged database are removed too, wherever in the workspace they live — a saved query whose table is gone. Returns what was deleted, broken out by pages/databases/views.
deletePagePermanently · token scope: content:write
Path parameters
workspaceId· string (uuid) — requiredpageId· string (uuid) — required
Response 200 — application/json
roots· integer — required, 0–9007199254740991pages· integer — required, 0–9007199254740991databases· integer — required, 0–9007199254740991views· integer — required, 0–9007199254740991blocks· integer — required, 0–9007199254740991
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
POST /api/v1/workspaces/{workspaceId}/trash/empty
Permanently delete every trashed subtree root in the workspace — the trash is emptied and nothing is recoverable. Applies exactly the cascade of deletePagePermanently to each root, including trashed databases and views (which never appear in the trash listing but would otherwise linger forever). Idempotent: emptying an empty trash deletes nothing and returns zeroes. Use getTrashSummary for the entry count to confirm against — the trash listing is paginated, this route is not scoped to a page of it.
emptyTrash · token scope: content:write
Path parameters
workspaceId· string (uuid) — required
Response 200 — application/json
roots· integer — required, 0–9007199254740991pages· integer — required, 0–9007199254740991databases· integer — required, 0–9007199254740991views· integer — required, 0–9007199254740991blocks· integer — required, 0–9007199254740991
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
GET /api/v1/workspaces/{workspaceId}/search
Full-text search over page titles and bodies in a workspace.
searchWorkspace · token scope: content:read
Path parameters
workspaceId· string (uuid) — required
Query parameters
q· string — required, length 1–500limit· integer — 1–50
Response 200 — application/json
items· object[] — requiredid· string (uuid) — requiredtitle· string — requiredsnippet· string — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
GET /api/v1/workspaces/{workspaceId}/favorites
The calling user's pinned pages in this workspace, in order. Filtered to what they may currently read, so a page whose access was withdrawn stops being listed without the pin itself being lost.
listFavorites · token scope: session only
Path parameters
workspaceId· string (uuid) — required
Response 200 — application/json
items· object[] — requiredpageId· string (uuid) — requiredtitle· string — requiredicon· string | null — requiredposition· string — requiredcreatedAt· string (date-time) — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
POST /api/v1/workspaces/{workspaceId}/favorites
Pin a page for the calling user. Requires read on the page — pinning is a bookmark, not a grant. Refuses past 5 pinned pages.
addFavorite · token scope: session only
Path parameters
workspaceId· string (uuid) — required
Request body — application/json, required
pageId· string (uuid) — requiredbeforeId· string (uuid)
Response 200 — application/json
pageId· string (uuid) — requiredtitle· string — requiredicon· string | null — requiredposition· string — requiredcreatedAt· string (date-time) — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
DELETE /api/v1/workspaces/{workspaceId}/favorites/{pageId}
Unpin a page for the calling user.
removeFavorite · token scope: session only
Path parameters
workspaceId· string (uuid) — requiredpageId· string (uuid) — required
Response 200 — application/json
ok·true— required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
POST /api/v1/workspaces/{workspaceId}/favorites/{pageId}/move
Reorder a pinned page within the calling user's own list.
moveFavorite · token scope: session only
Path parameters
workspaceId· string (uuid) — requiredpageId· string (uuid) — required
Request body — application/json, required
beforeId· string (uuid)afterId· string (uuid)
Response 200 — application/json
pageId· string (uuid) — requiredtitle· string — requiredicon· string | null — requiredposition· string — requiredcreatedAt· string (date-time) — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
GET /api/v1/workspaces/{workspaceId}/pages/{pageId}/backlinks
List the pages that link to this page ("Linked from"). Resolves inline block_link rows targeting the page back to their source pages (distinct, trashed sources skipped). Workspace-scoped; auth + membership guarded.
getPageBacklinks · token scope: content:read
Path parameters
workspaceId· string (uuid) — requiredpageId· string (uuid) — required
Response 200 — application/json
items· object[] — requiredpageId· string (uuid) — requiredtitle· string — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
GET /api/v1/workspaces/{workspaceId}/blocks/{blockId}/references
List where a single block is referenced: the distinct readable pages that transclude it (block_ref) — and, when the block is a page, the pages that link or embed it — each with the number of references it carries, plus the readable total. Requires read on the target block; the source pages are filtered inside the same statement that finds them (ADR-0016 D9/D9a), and the counts come from that filtered set.
getBlockReferences · token scope: content:read
Path parameters
workspaceId· string (uuid) — requiredblockId· string (uuid) — required
Response 200 — application/json
items· object[] — requiredpageId· string (uuid) — requiredtitle· string — requiredcount· integer — required, −∞–9007199254740991
count· integer — required, 0–9007199254740991
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
GET /api/v1/workspaces/{workspaceId}/pages/{pageId}/referenced-blocks
List the blocks OF this page that are referenced from elsewhere, with a short excerpt and how often each is referenced ("Referenced blocks"). Requires read on the page; the referencing blocks are permission-filtered before they are counted, so both the per-block counts and the total are what the CALLER may see (ADR-0016 D9) — a count taken over unreadable sources would be a counting oracle for restricted pages.
getPageReferencedBlocks · token scope: content:read
Path parameters
workspaceId· string (uuid) — requiredpageId· string (uuid) — required
Response 200 — application/json
items· object[] — requiredblockId· string (uuid) — requiredtype·"page"|"database"|"db_view"|"page_ref"|"outline"|"paragraph"|"heading"|"list_item"|"todo"|"toggle"|"callout"|"quote"|"code"|"divider"|"image"|"table"|"block_ref"— requiredexcerpt· string — requiredcount· integer — required, −∞–9007199254740991
count· integer — required, 0–9007199254740991
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
GET /api/v1/workspaces/{workspaceId}/blocks/{blockId}/ref-target
Resolve a block reference: the page that canonically owns the block, plus whether the viewer may read it and whether it is trashed. Powers read-only block transclusion (block_ref). Workspace-scoped; auth + membership guarded.
resolveBlockRef · token scope: content:read
Path parameters
workspaceId· string (uuid) — requiredblockId· string (uuid) — required
Response 200 — application/json
pageId· string (uuid) | null — requiredaccessible· boolean — requireddeleted· boolean — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
GET /api/v1/workspaces/{workspaceId}/pages/{pageId}/props
Read a page's field values (props), including read-only relation/formula cells.
getPageProps · token scope: content:read
Path parameters
workspaceId· string (uuid) — requiredpageId· string (uuid) — required
Response 200 — application/json
pageId· string (uuid) — requiredcells· object — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
PATCH /api/v1/workspaces/{workspaceId}/pages/{pageId}/props
Set a page's field values (a null value clears a cell). Validates option ids and member ids (M1). Uniform across tree pages and every database.
updatePageProps · token scope: content:write
Path parameters
workspaceId· string (uuid) — requiredpageId· string (uuid) — required
Request body — application/json, required
cells· object — required
Response 200 — application/json
pageId· string (uuid) — requiredcells· object — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
GET /api/v1/workspaces/{workspaceId}/pages/{pageId}/relations
Read a page's relation values, grouped by relation field id.
getPageRelations · token scope: content:read
Path parameters
workspaceId· string (uuid) — requiredpageId· string (uuid) — required
Response 200 — application/json
pageId· string (uuid) — requiredrelations· object — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required
PUT /api/v1/workspaces/{workspaceId}/pages/{pageId}/relations/{fieldId}
Replace the target set of one relation field on a page. Writes the symmetric back-reference when the field config declares one.
setPageRelations · token scope: content:write
Path parameters
workspaceId· string (uuid) — requiredpageId· string (uuid) — requiredfieldId· string (uuid) — required
Request body — application/json, required
targetPageIds· string (uuid)[] — required
Response 200 — application/json
pageId· string (uuid) — requiredfieldId· string (uuid) — requiredtargets· object[] — requiredpageId· string (uuid) — requiredtitle· string — required
Response default — application/json
error· object — requiredcode·"bad_request"|"validation_failed"|"unauthorized"|"forbidden"|"not_found"|"conflict"|"rate_limited"|"internal"— requiredmessage· string — required