Developers
Pages API
The Pages API lets you list the pages in a workspace, create new ones, read or write their title and content, and move them to the trash from your own code. Every request acts as you, so you see exactly the pages you can see in the app.
Before you start
- Base URL:
https://app.tylode.com/api/v1 - Send your token in the
Authorization: Bearer tylode_pat_xxxheader. Personal access tokens need the Pro plan. See Authentication. - Most calls need a workspace ID. Get it with
GET /api/v1/workspaces, or in the app under Settings → Workspace → Workspace details → Workspace ID → Copy. - Responses wrap results in
{ "data": ... }; failures return{ "error": "..." }. Errors that any endpoint can return (401bad token,403plan or scope,429more than 60 requests a minute) are covered in Errors and conventions; each endpoint below lists only its own.
curl https://app.tylode.com/api/v1/workspaces \
-H "Authorization: Bearer tylode_pat_xxx"{
"data": [
{
"id": "7c1e2a4b-3f5d-4e8a-9b21-6d0f5c8e1a37",
"name": "Acme",
"slug": "acme",
"plan": "pro",
"role": "owner",
"frozen": false
}
]
}The JavaScript examples on this page use this small helper (Node.js 18 or later, or any modern runtime with fetch):
const BASE = "https://app.tylode.com/api/v1";
const TOKEN = process.env.TYLODE_TOKEN; // tylode_pat_xxx
async function tylode(path, { method = "GET", body } = {}) {
const res = await fetch(BASE + path, {
method,
headers: {
Authorization: `Bearer ${TOKEN}`,
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const json = await res.json();
if (!res.ok) throw new Error(`${res.status}: ${json.error}`);
return json.data;
}The page object
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Page ID. |
workspace_id | string (UUID) | Workspace the page belongs to. |
parent_page_id | string or null | Parent page for subpages; null for top-level pages. |
title | string | Page title. |
icon | string or null | The page's emoji icon, if it has one. |
position | number | Order among its sibling pages. |
content | array | The page body as block JSON. Only returned by GET /pages/:id. |
The list endpoint never includes content, so listing stays fast even in large workspaces. To read a page's body, fetch it individually.
Endpoints
| Method | Path | What it does | Scope |
|---|---|---|---|
| GET | /api/v1/pages | List the pages in a workspace (without content) | pages:read |
| POST | /api/v1/pages | Create a page | pages:write |
| GET | /api/v1/pages/:id | Get one page, including its content | pages:read |
| PATCH | /api/v1/pages/:id | Update a page's title and/or content | pages:write |
| DELETE | /api/v1/pages/:id | Move a page, with its subpages, to the trash | pages:delete |
Scopes only matter for OAuth apps. Personal access tokens can call every endpoint.
List pages
GET /api/v1/pages
Returns every page you can see in a workspace, as a flat list ordered by position. Pages in the trash are left out.
Scope: pages:read
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
workspace_id | query | string | Yes | The workspace to list. |
The list includes subpages. To rebuild the tree, group pages by parent_page_id and sort each group by position.
curl "https://app.tylode.com/api/v1/pages?workspace_id=WORKSPACE_ID" \
-H "Authorization: Bearer tylode_pat_xxx"const pages = await tylode(`/pages?workspace_id=${WORKSPACE_ID}`);
const topLevel = pages.filter((p) => p.parent_page_id === null);Response 200
{
"data": [
{
"id": "3b9f6c2e-1d4a-4f7b-8e6c-0a5d2f9b7c14",
"workspace_id": "7c1e2a4b-3f5d-4e8a-9b21-6d0f5c8e1a37",
"parent_page_id": null,
"title": "Roadmap",
"icon": "🗺️",
"position": 0
},
{
"id": "e4a8d1f3-6b2c-4a9e-b7d5-3c1f8e0a2d69",
"workspace_id": "7c1e2a4b-3f5d-4e8a-9b21-6d0f5c8e1a37",
"parent_page_id": "3b9f6c2e-1d4a-4f7b-8e6c-0a5d2f9b7c14",
"title": "Q4 planning",
"icon": null,
"position": 0
}
]
}Errors
| Status | When |
|---|---|
400 | workspace_id is missing. |
A workspace you don't belong to returns an empty list, not an error.
Create a page
POST /api/v1/pages
Creates an empty page, at the top level or as a subpage.
Scope: pages:write
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
workspace_id | body | string | Yes | Workspace to create the page in. |
title | body | string | No | Page title. If empty or omitted, the page is stored with the default title Sin título ("Untitled"). |
parent_page_id | body | string | No | Create the page as a subpage of this page. Omit it (or send null) for a top-level page. |
The page starts empty. To give it content, send a PATCH right after creating it (see Create meeting notes with content).
curl -X POST https://app.tylode.com/api/v1/pages \
-H "Authorization: Bearer tylode_pat_xxx" \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "WORKSPACE_ID",
"title": "Launch checklist",
"parent_page_id": "PAGE_ID"
}'const page = await tylode("/pages", {
method: "POST",
body: { workspace_id: WORKSPACE_ID, title: "Launch checklist", parent_page_id: PAGE_ID },
});
console.log(page.id);Response 201
{
"data": {
"id": "9d2c5e7a-4f1b-4c8d-a6e3-5b0f7d1c3e82",
"workspace_id": "7c1e2a4b-3f5d-4e8a-9b21-6d0f5c8e1a37",
"parent_page_id": "3b9f6c2e-1d4a-4f7b-8e6c-0a5d2f9b7c14",
"title": "Launch checklist",
"icon": null,
"position": 0
}
}Errors
| Status | When |
|---|---|
400 | workspace_id is missing, or the page couldn't be created there (for example, you don't have permission to create pages in that workspace). |
Creating a page fires the page.created webhook event.
Get a page
GET /api/v1/pages/:id
Returns one page, including its content as block JSON.
If the page is a dashboard page, it also includes nevera: an object with imanes (each magnet with its id, tipo —texto, imagen, reloj or secciones—, its position x, y, w, h on a 12-column grid and, if it has one, its tono color) and textos, each text magnet's text in the same block JSON, by its id. The main text is content. On a regular page, nevera is null.
Scope: pages:read
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Page ID. |
curl https://app.tylode.com/api/v1/pages/PAGE_ID \
-H "Authorization: Bearer tylode_pat_xxx"const page = await tylode(`/pages/${PAGE_ID}`);
console.log(page.title, page.content.length, "blocks");Response 200
{
"data": {
"id": "3b9f6c2e-1d4a-4f7b-8e6c-0a5d2f9b7c14",
"workspace_id": "7c1e2a4b-3f5d-4e8a-9b21-6d0f5c8e1a37",
"parent_page_id": null,
"title": "Roadmap",
"icon": "🗺️",
"content": [
{
"id": "a1f3c9d2-7b4e-4e1a-9c2d-5f6b8a0e3d71",
"type": "paragraph",
"props": { "textColor": "default", "backgroundColor": "default", "textAlignment": "left" },
"content": [{ "type": "text", "text": "What we're shipping this quarter.", "styles": {} }],
"children": []
}
],
"position": 0,
"nevera": null
}
}Errors
| Status | When |
|---|---|
404 | The page doesn't exist, or you can't see it. |
Update a page
PATCH /api/v1/pages/:id
Changes a page's title, replaces its content, or both.
Scope: pages:write
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Page ID. |
title | body | string | No* | New title. |
content | body | array | No* | New page body as block JSON. It replaces the whole body; it isn't merged. |
* Send at least one of title or content.
The API stores content as you send it, without checking its shape, so build every block with all five fields described in Page content format. The response is the updated page without content.
curl -X PATCH https://app.tylode.com/api/v1/pages/PAGE_ID \
-H "Authorization: Bearer tylode_pat_xxx" \
-H "Content-Type: application/json" \
-d '{ "title": "Roadmap 2027" }'await tylode(`/pages/${PAGE_ID}`, {
method: "PATCH",
body: { title: "Roadmap 2027" },
});Response 200
{
"data": {
"id": "3b9f6c2e-1d4a-4f7b-8e6c-0a5d2f9b7c14",
"workspace_id": "7c1e2a4b-3f5d-4e8a-9b21-6d0f5c8e1a37",
"parent_page_id": null,
"title": "Roadmap 2027",
"icon": "🗺️",
"position": 0
}
}Errors
| Status | When |
|---|---|
400 | The body isn't valid JSON, it has neither title nor content, or the update was rejected. |
404 | The page doesn't exist, or you can't edit it. |
Changing the title fires the page.updated webhook event. Changing only the content doesn't.
Move a page to the trash
DELETE /api/v1/pages/:id
Moves a page to the trash together with its subpages, the same as deleting it in the app. It can be recovered from the trash.
Scope: pages:delete
| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
id | path | string | Yes | Page ID. |
curl -X DELETE https://app.tylode.com/api/v1/pages/PAGE_ID \
-H "Authorization: Bearer tylode_pat_xxx"Response 200
{ "data": { "id": "3f8a1c2e-7b4d-4e9a-8c61-2d5f0e9b7a14", "papelera": true } }A page in the trash no longer appears in List pages. Moving it to the trash doesn't fire the page.deleted webhook: that one fires when it's deleted for good. What all DELETE endpoints have in common is in Deleting: trash or permanent.
Errors
| Status | When |
|---|---|
400 | The ID isn't valid. |
404 | The page doesn't exist, is already in the trash, or you can't delete it. |
Page content format
A page's content is an array of blocks: the same block JSON the editor uses. Each block is an object with exactly these five fields, and the editor won't load a block that's missing any of them:
| Field | Type | Description |
|---|---|---|
id | string | A unique ID for the block. Generate one per block, for example with crypto.randomUUID(). |
type | string | Block type, such as paragraph, heading or bulletListItem. |
props | object | Block settings. Text blocks take textColor, backgroundColor and textAlignment; some types add their own (see below). |
content | array | The block's text as a list of text runs: { "type": "text", "text": "...", "styles": {} }. Use [] for an empty block. |
children | array | Nested blocks, such as a sub-bullet under a bullet. Use [] when there are none. |
Here's a heading, a paragraph with a bold word, and a bullet list item with a nested bullet:
[
{
"id": "5f0c7a52-8a3e-4c55-9d7e-2b1f4f0e6a10",
"type": "heading",
"props": { "textColor": "default", "backgroundColor": "default", "textAlignment": "left", "level": 2 },
"content": [{ "type": "text", "text": "Agenda", "styles": {} }],
"children": []
},
{
"id": "c7d1e4b8-2a9f-4b6c-8e3d-1f0a5c9b2e47",
"type": "paragraph",
"props": { "textColor": "default", "backgroundColor": "default", "textAlignment": "left" },
"content": [
{ "type": "text", "text": "Weekly sync with the ", "styles": {} },
{ "type": "text", "text": "design team", "styles": { "bold": true } }
],
"children": []
},
{
"id": "2e8b6f1a-9c3d-4a7e-b5f2-8d0c4e1a6b93",
"type": "bulletListItem",
"props": { "textColor": "default", "backgroundColor": "default", "textAlignment": "left" },
"content": [{ "type": "text", "text": "Review the new onboarding", "styles": {} }],
"children": [
{
"id": "f4a9c2d7-6e1b-4d8a-a3c5-7b2e0f9d1c58",
"type": "bulletListItem",
"props": { "textColor": "default", "backgroundColor": "default", "textAlignment": "left" },
"content": [{ "type": "text", "text": "Check the copy in Spanish", "styles": {} }],
"children": []
}
]
}
]Common text block types:
type | Extra props | Notes |
|---|---|---|
paragraph | — | Plain text. |
heading | level: 1–6 | Heading 1 to Heading 6. |
bulletListItem | — | Bulleted list item. |
numberedListItem | — | Numbered list item. |
checkListItem | checked: true / false | To-do item. |
toggleListItem | — | Collapsible item; its children are the hidden content. |
quote | — | Quote. |
Text runs accept styles such as bold, italic, underline, strike and code (all true), for example "styles": { "bold": true, "italic": true }.
When you read pages created in the app, you'll also find other block types (tables, images, embedded databases, boards and more). The easiest way to learn their shape is to create one in the app and read the page with GET /pages/:id. See Blocks for everything the editor supports.
Example: create meeting notes with content
This script creates a meeting-notes page and fills it in. Because the page is created through the API, replacing its content right away is exactly what PATCH is for.
const WORKSPACE_ID = "WORKSPACE_ID";
// Build a text block with all five required fields
function block(type, text = "", props = {}) {
return {
id: crypto.randomUUID(),
type,
props: { textColor: "default", backgroundColor: "default", textAlignment: "left", ...props },
content: text ? [{ type: "text", text, styles: {} }] : [],
children: [],
};
}
const today = new Date().toISOString().slice(0, 10);
// 1. Create the page
const page = await tylode("/pages", {
method: "POST",
body: { workspace_id: WORKSPACE_ID, title: `Meeting notes ${today}` },
});
// 2. Write its content
await tylode(`/pages/${page.id}`, {
method: "PATCH",
body: {
content: [
block("heading", "Attendees", { level: 2 }),
block("bulletListItem", "Ana"),
block("bulletListItem", "Marc"),
block("heading", "Agenda", { level: 2 }),
block("numberedListItem", "Status of the launch"),
block("numberedListItem", "Open questions"),
block("heading", "Action items", { level: 2 }),
block("checkListItem", "Send the recap to the team", { checked: false }),
],
},
});
console.log(`https://app.tylode.com/dashboard/${page.id}`);The same two calls with curl:
# 1. Create the page and keep its id
curl -X POST https://app.tylode.com/api/v1/pages \
-H "Authorization: Bearer tylode_pat_xxx" \
-H "Content-Type: application/json" \
-d '{ "workspace_id": "WORKSPACE_ID", "title": "Meeting notes 2026-09-23" }'
# 2. Write the content (use the id from step 1 as PAGE_ID)
curl -X PATCH https://app.tylode.com/api/v1/pages/PAGE_ID \
-H "Authorization: Bearer tylode_pat_xxx" \
-H "Content-Type: application/json" \
-d '{
"content": [
{
"id": "0b6e2d4f-8a1c-4e9b-b3d7-5c2f9a1e0d64",
"type": "heading",
"props": { "textColor": "default", "backgroundColor": "default", "textAlignment": "left", "level": 2 },
"content": [{ "type": "text", "text": "Action items", "styles": {} }],
"children": []
},
{
"id": "d3a7f1c9-5e2b-4b8d-9f6a-1c4e8b0d2a75",
"type": "checkListItem",
"props": { "textColor": "default", "backgroundColor": "default", "textAlignment": "left", "checked": false },
"content": [{ "type": "text", "text": "Send the recap to the team", "styles": {} }],
"children": []
}
]
}'What the Pages API doesn't do
- There's no endpoint to move, duplicate or restore pages, to change their icon, or to delete them for good. Do that in the app (see Organize pages and Trash).
- There's no pagination: the list returns every page you can see in the workspace.
- To download a page as Markdown or PDF, use Export in the app.
- A dashboard page's magnets can be read (
nevera), but not created, moved or changed through the API:PATCHwithcontentonly changes the main text.
Related
- Authentication and Errors and conventions
- Search API to find a page by text
- Recipes, including a backup script for all your pages
- MCP to let an AI assistant read and write pages