Desarrolladores
API de páginas
Con la API de páginas puedes listar las páginas de un espacio de trabajo, crear páginas nuevas, leer o escribir su título y su contenido, y mandarlas a la papelera desde tu propio código. Cada petición actúa en tu nombre: ves exactamente las páginas que ves en la app.
Antes de empezar
- URL base:
https://app.tylode.com/api/v1 - Manda tu token en la cabecera
Authorization: Bearer tylode_pat_xxx. Los tokens de acceso personal necesitan el plan Pro. Consulta Autenticación. - Casi todas las llamadas piden el ID del espacio. Lo obtienes con
GET /api/v1/workspaceso en la app, en Ajustes → Workspace → Datos del espacio → ID del espacio → Copiar. - Las respuestas devuelven el resultado dentro de
{ "data": ... }y los fallos,{ "error": "..." }. Los errores que puede dar cualquier endpoint (401token no válido,403plan o permiso,429más de 60 peticiones por minuto) se explican en Errores y convenciones; en cada endpoint solo verás los suyos.
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
}
]
}Los ejemplos en JavaScript de esta página usan esta pequeña función auxiliar (Node.js 18 o posterior, o cualquier entorno moderno con 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;
}El objeto página
| Campo | Tipo | Descripción |
|---|---|---|
id | string (UUID) | ID de la página. |
workspace_id | string (UUID) | Espacio al que pertenece la página. |
parent_page_id | string o null | Página padre, si es una subpágina; null en las páginas de primer nivel. |
title | string | Título de la página. |
icon | string o null | El emoji de la página, si tiene. |
position | number | Orden entre sus páginas hermanas. |
content | array | El cuerpo de la página en JSON de bloques. Solo lo devuelve GET /pages/:id. |
El listado nunca incluye content, así que sigue siendo rápido aunque el espacio sea enorme. Para leer el cuerpo de una página, pídela por su ID.
Endpoints
| Método | Ruta | Qué hace | Permiso (scope) |
|---|---|---|---|
| GET | /api/v1/pages | Lista las páginas de un espacio (sin contenido) | pages:read |
| POST | /api/v1/pages | Crea una página | pages:write |
| GET | /api/v1/pages/:id | Devuelve una página, con su contenido | pages:read |
| PATCH | /api/v1/pages/:id | Cambia el título y/o el contenido | pages:write |
| DELETE | /api/v1/pages/:id | Mueve una página a la papelera, con sus subpáginas | pages:delete |
Los permisos solo cuentan para las aplicaciones OAuth. Un token de acceso personal puede llamar a todos los endpoints.
Listar páginas
GET /api/v1/pages
Devuelve todas las páginas que puedes ver en un espacio, en una lista plana ordenada por position. Las páginas de la papelera no aparecen.
Permiso (scope): pages:read
| Parámetro | Dónde | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
workspace_id | query | string | Sí | El espacio que quieres listar. |
La lista incluye las subpáginas. Para reconstruir el árbol, agrupa por parent_page_id y ordena cada grupo por 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);Respuesta 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
}
]
}Errores
| Estado | Cuándo |
|---|---|
400 | Falta workspace_id. |
Si no perteneces a ese espacio, recibes una lista vacía, no un error.
Crear una página
POST /api/v1/pages
Crea una página vacía, en el primer nivel o como subpágina.
Permiso (scope): pages:write
| Parámetro | Dónde | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
workspace_id | body | string | Sí | Espacio donde se crea la página. |
title | body | string | No | Título. Si lo omites o va vacío, la página se guarda con el título Sin título. |
parent_page_id | body | string | No | Crea la página como subpágina de esta. Omítelo (o manda null) para una página de primer nivel. |
La página nace vacía. Para darle contenido, manda un PATCH justo después de crearla (mira Crear un acta de reunión con contenido).
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);Respuesta 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
}
}Errores
| Estado | Cuándo |
|---|---|
400 | Falta workspace_id o no se ha podido crear la página ahí (por ejemplo, porque no tienes permiso para crear páginas en ese espacio). |
Crear una página dispara el evento de webhook page.created.
Obtener una página
GET /api/v1/pages/:id
Devuelve una página con su content en JSON de bloques.
Si la página es una página tipo panel, trae también nevera: un objeto con imanes (cada imán con su id, tipo —texto, imagen, reloj o secciones—, su posición x, y, w, h en una cuadrícula de 12 columnas y, si tiene, su tono) y textos, el texto de cada imán de texto en el mismo JSON de bloques, por su id. El texto principal es content. En una página normal, nevera es null.
Permiso (scope): pages:read
| Parámetro | Dónde | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string | Sí | ID de la página. |
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");Respuesta 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
}
}Errores
| Estado | Cuándo |
|---|---|
404 | La página no existe o no puedes verla. |
Actualizar una página
PATCH /api/v1/pages/:id
Cambia el título de una página, sustituye su contenido o ambas cosas.
Permiso (scope): pages:write
| Parámetro | Dónde | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string | Sí | ID de la página. |
title | body | string | No* | Título nuevo. |
content | body | array | No* | Cuerpo nuevo en JSON de bloques. Sustituye el cuerpo entero; no se fusiona con lo que había. |
* Manda al menos title o content.
La API guarda content tal cual lo mandas, sin comprobar su forma, así que construye cada bloque con los cinco campos que se explican en Formato del contenido de una página. La respuesta es la página actualizada sin 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" },
});Respuesta 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
}
}Errores
| Estado | Cuándo |
|---|---|
400 | El cuerpo no es JSON válido, no trae ni title ni content, o se ha rechazado el cambio. |
404 | La página no existe o no puedes editarla. |
Cambiar el título dispara el evento de webhook page.updated. Cambiar solo el contenido, no.
Mover una página a la papelera
DELETE /api/v1/pages/:id
Manda una página a la papelera junto con sus subpáginas, igual que al borrarla en la app. Desde la papelera se puede recuperar.
Permiso (scope): pages:delete
| Parámetro | Dónde | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string | Sí | ID de la página. |
curl -X DELETE https://app.tylode.com/api/v1/pages/PAGE_ID \
-H "Authorization: Bearer tylode_pat_xxx"Respuesta 200
{ "data": { "id": "3f8a1c2e-7b4d-4e9a-8c61-2d5f0e9b7a14", "papelera": true } }Una página en la papelera deja de salir en Listar páginas. Mandarla a la papelera no dispara el webhook page.deleted: ese salta cuando se borra para siempre. Lo que tienen en común todos los DELETE está en Borrar: papelera o para siempre.
Errores
| Estado | Cuándo |
|---|---|
400 | El ID no es válido. |
404 | La página no existe, ya está en la papelera o no puedes borrarla. |
Formato del contenido de una página
El content de una página es un array de bloques: el mismo JSON de bloques que usa el editor. Cada bloque es un objeto con exactamente estos cinco campos, y el editor no carga un bloque al que le falte alguno:
| Campo | Tipo | Descripción |
|---|---|---|
id | string | Un ID único para el bloque. Genera uno por bloque, por ejemplo con crypto.randomUUID(). |
type | string | Tipo de bloque, como paragraph, heading o bulletListItem. |
props | object | Ajustes del bloque. Los bloques de texto llevan textColor, backgroundColor y textAlignment; algunos tipos añaden los suyos (mira la tabla de abajo). |
content | array | El texto del bloque como lista de fragmentos: { "type": "text", "text": "...", "styles": {} }. Usa [] para un bloque vacío. |
children | array | Bloques anidados, como una viñeta dentro de otra. Usa [] si no hay. |
Aquí tienes un encabezado, un párrafo con una palabra en negrita y una viñeta con otra anidada:
[
{
"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": []
}
]
}
]Tipos de bloque de texto más habituales:
type | Props extra | Notas |
|---|---|---|
paragraph | — | Texto normal. |
heading | level: 1–6 | Encabezado 1 a Encabezado 6. |
bulletListItem | — | Elemento de lista con viñetas. |
numberedListItem | — | Elemento de lista numerada. |
checkListItem | checked: true / false | Tarea con casilla. |
toggleListItem | — | Elemento plegable; sus children son lo que se oculta. |
quote | — | Cita. |
Los fragmentos de texto admiten estilos como bold, italic, underline, strike y code (todos con valor true), por ejemplo "styles": { "bold": true, "italic": true }.
Al leer páginas creadas en la app te encontrarás otros tipos de bloque (tablas, imágenes, bases de datos incrustadas, tableros y más). La forma más fácil de conocer su estructura es crear uno en la app y leer la página con GET /pages/:id. En Bloques tienes todo lo que admite el editor.
Ejemplo: crear un acta de reunión con contenido
Este script crea una página de acta y la rellena. Como la página se crea desde la API, sustituir su content justo después es precisamente para lo que sirve PATCH.
const WORKSPACE_ID = "WORKSPACE_ID";
// Crea un bloque de texto con los cinco campos obligatorios
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. Crea la página
const page = await tylode("/pages", {
method: "POST",
body: { workspace_id: WORKSPACE_ID, title: `Meeting notes ${today}` },
});
// 2. Escribe su contenido
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}`);Las mismas dos llamadas con curl:
# 1. Crea la página y quédate con su 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. Escribe el contenido (usa el id del paso 1 como 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": []
}
]
}'Lo que la API de páginas no hace
- No hay endpoint para mover, duplicar ni restaurar páginas, ni para cambiarles el icono, ni para borrarlas para siempre. Eso se hace en la app (mira Organizar páginas y Papelera).
- No hay paginación: el listado devuelve todas las páginas que puedes ver en el espacio.
- Para descargar una página en Markdown o PDF, usa Exportar en la app.
- Los imanes de una página tipo panel se leen (
nevera), pero no se crean, mueven ni cambian por la API:PATCHconcontentcambia solo el texto principal.
Relacionado
- Autenticación y Errores y convenciones
- API de búsqueda para encontrar una página por su texto
- Recetas, con un script para hacer copia de todas tus páginas
- MCP para que un asistente de IA lea y escriba páginas