Pages API
All endpoints require authentication and pages tool permissions.
Core Pages
Section titled “Core Pages”List Pages
Section titled “List Pages”GET /api/pagesQuery parameters:
| Param | Type | Description |
|---|---|---|
search | string | Full-text search on title and content (returns up to 50 results) |
favourites | string | Set to true to return only the current user’s favourited pages |
entity_type | string | Filter pages linked to a specific entity type (used with entity_id) |
entity_id | string | Filter pages linked to a specific entity ID (used with entity_type) |
archived | string | Set to true to return archived (trashed) pages |
Response: { pages }
Each page includes id, title, icon, parent_id, position, entity_type, entity_id, created_by, updated_by, created_at, updated_at, created_by_name, updated_by_name, children_count, is_favourited (boolean), and content_preview (first 200 chars, only present in search results).
When no filters are applied, returns standalone (non-entity) unarchived pages ordered by parent_id, position, then title — suitable for building a sidebar tree.
Get Page
Section titled “Get Page”GET /api/pages/:idReturns the full page including content.
Response: { page }
The page object includes all list fields plus content, content_rich, cover_image, is_archived, full_width, small_text, lock_page, toc, text_style, is_published, and published_at.
Create Page
Section titled “Create Page”POST /api/pagesPermission: pages.update
Body:
{ "title": "string (optional, defaults to 'Untitled')", "content": "string (optional)", "content_rich": "string | null (optional, rich editor JSON)", "parent_id": "string | null (optional, parent page ID for nesting)", "entity_type": "string | null (optional, link to entity type)", "entity_id": "string | null (optional, link to entity ID)"}Position is auto-calculated as the next position under the given parent.
Response: { page } with status 201
Update Page
Section titled “Update Page”PATCH /api/pages/:idPermission: pages.update
Body: Any combination of:
| Field | Type | Description |
|---|---|---|
title | string | Page title (empty string becomes “Untitled”) |
content | string | Plain text content |
content_rich | string | null | Rich editor JSON content (also clears yjs_snapshot) |
icon | string | null | Page icon emoji or identifier |
cover_image | string | null | Cover image URL |
is_archived | number | 1 to archive, 0 to restore |
full_width | number | 1 for full-width layout |
small_text | number | 1 for small text mode |
lock_page | number | 1 to lock editing |
toc | number | 1 to show table of contents |
text_style | string | Text style preset |
is_published | number | 1 to publish (sets published_at), 0 to unpublish (clears published_at) |
parent_id | string | null | Move page to a new parent |
position | number | Reorder position within parent |
Response: { page }
Delete Page
Section titled “Delete Page”DELETE /api/pages/:idPermission: pages.manage
Soft-deletes the page by setting is_archived = 1. Child pages are orphaned to root level (their parent_id is set to NULL).
Response: { ok: true }
Toggle Favourite
Section titled “Toggle Favourite”POST /api/pages/:id/favouritePermission: pages.view
Toggles the favourite status for the current user. If already favourited, removes the favourite; otherwise adds it.
Response: { is_favourited: boolean }
AI Command (Streaming)
Section titled “AI Command (Streaming)”POST /api/pages/ai/commandPermission: pages.update
Streams AI-generated or edited content based on the editor context. Uses Claude Sonnet via the Anthropic SDK with server-sent events.
Body:
{ "ctx": { "children": "array (Slate editor children nodes)", "selection": "object | null (Slate selection with anchor/focus)", "toolName": "string (optional: 'edit' | 'generate' — auto-detected from selection)" }, "messages": "array of chat messages [{ role, parts: [{ type, text }] }]"}Behaviour:
- With selection +
edittool: Edits the selected text based on the user’s instruction - With selection (other tool): Transforms/rewrites the selected text
- Without selection: Generates new content based on the instruction
Response: Server-sent event stream (text/event-stream) using the Vercel AI SDK UIMessageStream format. Includes a data-toolName event indicating the tool used.
AI Copilot
Section titled “AI Copilot”POST /api/pages/ai/copilotPermission: pages.update
Generates short inline completions (max 50 tokens) for the copilot/autocomplete feature.
Body:
{ "prompt": "string (the text context for completion)", "system": "string (system prompt for the model)"}Response: Vercel AI SDK generateText result object.
Returns 408 if the request is aborted by the client.
Discussions
Section titled “Discussions”List Discussions
Section titled “List Discussions”GET /api/pages/:pageId/discussionsPermission: pages.view
Returns all discussions for a page, each with nested comments.
Response: { discussions }
Each discussion includes id, page_id, user_id, document_content, document_content_rich, is_resolved (boolean), created_at, updated_at, user_name, user_avatar, and a comments array. Each comment includes id, discussion_id, user_id, content, content_rich, is_edited, created_at, updated_at, user_name, user_avatar.
Create Discussion
Section titled “Create Discussion”POST /api/pages/:pageId/discussionsPermission: pages.update
Creates a new discussion thread, optionally with a first comment.
Body:
{ "id": "string (optional, client-generated UUID)", "document_content": "string (the highlighted text)", "document_content_rich": "string (optional, rich content JSON)", "comment": "string (optional, first comment text)", "comment_rich": "string (optional, first comment rich content)"}Response: { discussion: { id, page_id } } with status 201
Add Comment
Section titled “Add Comment”POST /api/pages/:pageId/discussions/:discussionId/commentsPermission: pages.update
Body:
{ "id": "string (optional, client-generated UUID)", "content": "string (required)", "content_rich": "string (optional)"}Response: { comment: { id } } with status 201
Edit Comment
Section titled “Edit Comment”PATCH /api/pages/:pageId/discussions/:discussionId/comments/:commentIdPermission: pages.update
Body:
{ "content": "string (required)", "content_rich": "string (optional)"}Sets is_edited = 1 on the comment.
Response: { ok: true }
Delete Comment
Section titled “Delete Comment”DELETE /api/pages/:pageId/discussions/:discussionId/comments/:commentIdPermission: pages.manage
Hard-deletes the comment.
Response: { ok: true }
Toggle Resolve Discussion
Section titled “Toggle Resolve Discussion”POST /api/pages/:pageId/discussions/:discussionId/resolvePermission: pages.update
Toggles the is_resolved status of a discussion.
Response: { is_resolved: boolean }
Delete Discussion
Section titled “Delete Discussion”DELETE /api/pages/:pageId/discussions/:discussionIdPermission: pages.manage
Hard-deletes the discussion and all its comments.
Response: { ok: true }
Uploads
Section titled “Uploads”Upload File
Section titled “Upload File”POST /api/pages/uploadPermission: pages.update
Uploads a media file to R2 storage. Accepts multipart/form-data.
Form fields:
| Field | Type | Description |
|---|---|---|
file | File | The file to upload (required) |
pageId | string | Page ID for organizing storage (optional, defaults to general) |
Constraints:
- Maximum file size: 10 MB
- Allowed MIME types:
image/jpeg,image/png,image/gif,image/webp,image/svg+xml,video/mp4,video/webm,audio/mpeg,audio/wav,audio/ogg,application/pdf - File content is validated against magic numbers for image types
Response:
{ "url": "/api/pages/uploads/pages/{pageId}/{uuid}.{ext}", "appUrl": "/api/pages/uploads/pages/{pageId}/{uuid}.{ext}", "name": "original-filename.png", "size": 123456, "type": "image/png", "key": "pages/{pageId}/{uuid}.{ext}"}Serve Uploaded File
Section titled “Serve Uploaded File”GET /api/pages/uploads/*Permission: None (public)
Serves files from R2 storage. Returns the file with appropriate Content-Type, one-year Cache-Control header, and ETag for caching.
Returns 404 if the file does not exist.
Versions
Section titled “Versions”List Versions
Section titled “List Versions”GET /api/pages/:pageId/versionsPermission: pages.view
Returns up to 50 most recent version snapshots for a page, ordered newest first.
Response: { versions }
Each version includes id, page_id, user_id, title, created_at, and user_name.
Create Version Snapshot
Section titled “Create Version Snapshot”POST /api/pages/:pageId/versionsPermission: pages.update
Captures the current page state (title and content_rich) as a version snapshot.
Response: { version: { id, page_id } } with status 201
Returns 404 if the page does not exist.
Get Version
Section titled “Get Version”GET /api/pages/:pageId/versions/:versionIdPermission: pages.view
Returns the full version content including content_rich.
Response: { version }
The version object includes id, page_id, user_id, title, content_rich, created_at, and user_name.
Restore Version
Section titled “Restore Version”POST /api/pages/:pageId/versions/:versionId/restorePermission: pages.update
Restores a page to a previous version. Before restoring, the current page state is automatically saved as a new version snapshot (so restores are non-destructive).
Response: { ok: true }
Returns 404 if the version does not exist.