Desarrolladores
Apps OAuth 2.0
Con OAuth, otras personas pueden conectar tu app a su cuenta de Tylode con solo los permisos que necesita. Lo aprueban en una pantalla de consentimiento y pueden desconectarla cuando quieran sin tocar nada más.
Cuándo usar OAuth
| Usa un token de acceso personal si… | Usa OAuth si… |
|---|---|
| El código es tuyo y funciona con tu propia cuenta. | Otras personas van a conectar su cuenta a tu app. |
| Necesitas acceso a todo, sin acotar permisos. | La app necesita un conjunto de permisos limitado y a la vista. |
| Es un script, una tarea programada o tu propio asistente de IA. | Es un producto, una herramienta interna para tu equipo o una app móvil, de escritorio o de línea de comandos. |
No pidas nunca a otros usuarios que peguen su token personal en tu app: te estarían dando la cuenta entera. Los tokens personales se explican en Autenticación.
Registrar una app
- Abre Ajustes → API y webhooks → API y plugins (ir allí).
- En Aplicaciones OAuth, pulsa Registrar aplicación.
- Rellena el formulario:
| Campo | Obligatorio | Detalles |
|---|---|---|
| Nombre de la aplicación | Sí | Se muestra en la pantalla de consentimiento: «{nombre} quiere acceder a tu cuenta». |
| Qué hace | No | Una descripción corta que se enseña al usuario al pedirle permiso. |
URL de la web (el campo https://tu-app.com (opcional)) | No | Aparece como enlace Sobre esta aplicación en la pantalla de consentimiento. |
| URLs de retorno | Sí, al menos una | Una por línea. Se comparan exactas. Tienen que usar https, salvo http://localhost y http://127.0.0.1 para desarrollar. No pueden llevar fragmento (#…). |
| Permisos que podrá pedir | Sí, al menos uno | Los permisos (scopes) que tu app puede solicitar. Cada autorización puede pedirlos todos o solo una parte. |
| Aplicación pública | No | Márcalo para apps móviles, de una sola página (SPA) o de línea de comandos. La app no recibe secreto y tiene que usar PKCE. |
- Pulsa Registrar aplicación.
Tylode te enseña entonces las credenciales de la app:
client_id:tylode_app_seguido de 24 caracteres hexadecimales, comotylode_app_3f9a1c2b7d4e5f60a1b2c3d4. Es público y aparece en la lista de apps.client_secret(solo en apps que no son públicas):tylode_secret_seguido de 43 caracteres aleatorios. Cópialo en ese momento: solo se muestra una vez.
Registrar una app es gratis en cualquier plan. Lo que pide Pro es usarla: cada persona que autorice tu app tiene que estar en Pro o Premium, o sus llamadas a la API devolverán 403.
Rotar el secreto
En la lista de apps, pulsa Nuevo secreto junto a la app. Verás el secreto nuevo una sola vez y el anterior deja de funcionar al instante, así que actualízalo en tu servidor lo primero. Los tokens ya emitidos siguen funcionando; solo los nuevos canjes de código necesitan el secreto nuevo. Las apps públicas no tienen este botón porque no tienen secreto.
Eliminar una app
Pulsa el icono de la papelera (Eliminar aplicación) y vuelve a pulsar para confirmar. Esto corta el acceso de todos los usuarios a la vez: todos los tokens emitidos a la app dejan de funcionar.
Las apps no se pueden editar después de registrarlas. Para cambiar las URLs de retorno o los permisos, registra una app nueva y pasa a tus usuarios a ella.
El flujo de código de autorización
- Tu app manda el navegador del usuario a la URL de autorización de Tylode.
- Si no ha iniciado sesión, Tylode se lo pide y luego lo trae de vuelta.
- Ve la pantalla de consentimiento y pulsa Autorizar (o Cancelar).
- Tylode redirige a tu
redirect_uricon uncodey tustate. - Tu servidor canjea el código por un token de acceso en
POST /api/oauth/token. - Tu app llama a la API con
Authorization: Bearer tylode_oat_….
Paso 1: manda al usuario a la URL de autorización
GET https://app.tylode.com/oauth/authorize
Es una página que el usuario abre en su navegador, no una llamada a la API.
| Parámetro | Obligatorio | Descripción |
|---|---|---|
client_id | Sí | El client_id de tu app. |
redirect_uri | Sí | Adónde devolver al usuario. Tiene que coincidir exactamente, carácter a carácter, con una de las URLs de retorno de la app. |
response_type | Sí | Siempre code. |
scope | Sí | Lista de permisos separados por espacios (al menos uno). Todos tienen que estar entre los permisos registrados de la app. |
state | Recomendado | Un valor aleatorio que generas tú y compruebas cuando el usuario vuelve. Protege contra la falsificación de peticiones entre sitios (CSRF). Se devuelve tal cual. |
code_challenge | En apps públicas | El reto de PKCE. Obligatorio en apps públicas; recomendado en todas. |
code_challenge_method | Con code_challenge | Tiene que ser S256. plain no se admite. |
App confidencial (con secreto):
https://app.tylode.com/oauth/authorize?response_type=code&client_id=tylode_app_3f9a1c2b7d4e5f60a1b2c3d4&redirect_uri=https%3A%2F%2Fyour-app.com%2Fcallback&scope=pages%3Aread%20pages%3Awrite&state=k8Jd2mQx7LpApp pública con PKCE:
https://app.tylode.com/oauth/authorize?response_type=code&client_id=tylode_app_3f9a1c2b7d4e5f60a1b2c3d4&redirect_uri=http%3A%2F%2Flocalhost%3A8765%2Fcallback&scope=boards%3Aread%20boards%3Awrite&state=k8Jd2mQx7Lp&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256No olvides codificar los valores para la URL: los espacios de scope pasan a ser %20 (o +) y los :, %3A.
Paso 2: la pantalla de consentimiento
El usuario ve:
- «{Tu app} quiere acceder a tu cuenta» y el correo con el que ha iniciado sesión.
- La descripción de tu app, si pusiste una.
- En Podrá, una línea por permiso pedido, como «Leer tus páginas
pages:read». - Un aviso de que la app podrá actuar en su nombre hasta que revoque el acceso, y de que no podrá ver nada a lo que él no tenga acceso.
- «Volverás a tu-app.com» (el dominio de tu
redirect_uri) y, si pusiste una web, el enlace Sobre esta aplicación. - Los botones Cancelar y Autorizar.
El usuario aprueba todos los permisos pedidos o ninguno, salvo los de borrar (…:delete): esos salen aparte, en También puede borrar, cada uno con una casilla sin marcar y un aviso de qué se puede recuperar y qué no. Solo se conceden los que marca, así que comprueba en el scope de la respuesta con el token cuáles tienes de verdad. Pide solo lo que necesites.
Paso 3: recoge la redirección
Si el usuario pulsa Autorizar:
https://your-app.com/callback?state=k8Jd2mQx7Lp&code=tylode_code_Vn3pXq8sR2tY6uW1zA4bC7dE0fG5hJ9kL2mN6oP3qR8Comprueba que state coincide con el que enviaste y canjea el code. Los códigos valen 10 minutos y solo se pueden usar una vez.
Si algo sale mal, el usuario vuelve con un error:
https://your-app.com/callback?error=access_denied&error_description=...&state=k8Jd2mQx7Lperror | Causa |
|---|---|
access_denied | El usuario pulsó Cancelar. |
unsupported_response_type | response_type no es code. |
invalid_scope | scope está vacío, lleva un permiso desconocido o pide uno que la app no registró. |
invalid_request | code_challenge_method no es S256, o una app pública no envió code_challenge. |
error_description es una explicación para leer en tus registros, no para enseñársela a tus usuarios.
Hay problemas que no se pueden devolver con seguridad, así que Tylode enseña una página de error y no redirige: falta client_id o redirect_uri, el client_id no existe, o la redirect_uri no está registrada en la app. Si tus usuarios ven «URL de retorno no registrada», comprueba que la URL de tu código coincide exactamente con la registrada, incluida la barra final y el puerto.
Canjear el código por un token
POST https://app.tylode.com/api/oauth/token
Llámalo desde tu servidor (o desde la propia app, si es pública). El cuerpo puede ir como application/x-www-form-urlencoded o en JSON. Las credenciales de la app pueden ir en el cuerpo o en una cabecera Authorization HTTP Basic.
| Parámetro | Obligatorio | Descripción |
|---|---|---|
grant_type | Sí | Siempre authorization_code. |
code | Sí | El código de la redirección. |
redirect_uri | Sí | La misma redirect_uri del paso de autorización. |
client_id | Sí | En el cuerpo o como usuario de Basic. |
client_secret | Sin PKCE | En el cuerpo o como contraseña de Basic. |
code_verifier | Con PKCE | El verificador original. Obligatorio siempre que enviaras un code_challenge. |
App confidencial, secreto en el cuerpo:
curl -X POST https://app.tylode.com/api/oauth/token \
-d grant_type=authorization_code \
-d code=tylode_code_Vn3pXq8sR2tY6uW1zA4bC7dE0fG5hJ9kL2mN6oP3qR8 \
--data-urlencode redirect_uri=https://your-app.com/callback \
-d client_id=tylode_app_3f9a1c2b7d4e5f60a1b2c3d4 \
-d client_secret=tylode_secret_xxxLo mismo con HTTP Basic:
curl -X POST https://app.tylode.com/api/oauth/token \
-u "tylode_app_3f9a1c2b7d4e5f60a1b2c3d4:tylode_secret_xxx" \
-d grant_type=authorization_code \
-d code=tylode_code_Vn3pXq8sR2tY6uW1zA4bC7dE0fG5hJ9kL2mN6oP3qR8 \
--data-urlencode redirect_uri=https://your-app.com/callbackApp pública con PKCE:
curl -X POST https://app.tylode.com/api/oauth/token \
-d grant_type=authorization_code \
-d code=tylode_code_Vn3pXq8sR2tY6uW1zA4bC7dE0fG5hJ9kL2mN6oP3qR8 \
--data-urlencode redirect_uri=http://localhost:8765/callback \
-d client_id=tylode_app_3f9a1c2b7d4e5f60a1b2c3d4 \
-d code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXkEn JavaScript:
const res = await fetch("https://app.tylode.com/api/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "authorization_code",
code,
redirect_uri: "https://your-app.com/callback",
client_id: process.env.TYLODE_CLIENT_ID,
client_secret: process.env.TYLODE_CLIENT_SECRET,
}),
});
const token = await res.json();
if (!res.ok) throw new Error(`${token.error}: ${token.error_description}`);Respuesta con el token
{
"access_token": "tylode_oat_mZ4pQ9sT2vX7yB1cE6gH0jK3nR8uW5aD2fL7oS1tV4x",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "pages:read pages:write"
}scopelista los permisos concedidos de verdad, separados por espacios.- La respuesta llega con
Cache-Control: no-store; no la guardes en caché ni en los registros. - Guarda el token de acceso cifrado y asociado al usuario.
Errores del canje
Los errores siguen el formato de OAuth 2.0 (RFC 6749, sección 5.2), no el { "error": "…" } del resto de la API:
{
"error": "invalid_grant",
"error_description": "..."
}error | Estado | Causa |
|---|---|---|
invalid_request | 400 | No se puede leer el cuerpo, falta code o redirect_uri, o falta code_verifier cuando se envió un code_challenge. |
invalid_client | 401 | Falta client_id o no existe, el secreto es incorrecto, o una app pública no usó PKCE. |
invalid_grant | 400 | El código es incorrecto, ha caducado (a los 10 minutos), ya se usó o es de otra app; la redirect_uri no es la del paso de autorización; o el code_verifier no corresponde al reto. |
unsupported_grant_type | 400 | grant_type no es authorization_code. |
slow_down | 429 | Más de 30 peticiones en un minuto desde tu dirección IP. |
server_error | 500 | Algo ha fallado en Tylode. Vuelve a pasar al usuario por el flujo. |
Generar el verificador y el reto de PKCE
PKCE protege el código si alguien lo intercepta. Genera un code_verifier aleatorio, envía su hash SHA-256 como code_challenge en el paso 1 y manda el verificador tal cual en el canje. El reto es el hash codificado en base64url, sin el relleno =.
Node.js:
import crypto from "node:crypto";
const codeVerifier = crypto.randomBytes(32).toString("base64url");
const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url");Navegador (Web Crypto):
function base64url(bytes) {
return btoa(String.fromCharCode(...bytes))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
const codeVerifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier));
const codeChallenge = base64url(new Uint8Array(digest));Python:
import base64, hashlib, secrets
code_verifier = secrets.token_urlsafe(32)
code_challenge = (
base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest())
.rstrip(b"=")
.decode()
)Guarda el verificador hasta que el usuario vuelva (en la sesión del servidor, o en memoria en una app de línea de comandos) y usa uno nuevo en cada autorización.
Llamar a la API con el token
curl "https://app.tylode.com/api/v1/pages?workspace_id=WORKSPACE_ID" \
-H "Authorization: Bearer tylode_oat_xxx"Empieza por GET /api/v1/workspaces para saber en qué espacios está el usuario. Funciona con cualquier permiso, así que toda app autorizada puede llamarlo. Mira Listar tus espacios de trabajo.
Cada llamada se hace como el usuario que autorizó la app: ve solo lo que esa persona ve en Tylode, y además limitado a los permisos concedidos. El usuario tiene que estar en Pro o Premium; si pasa a Free, las llamadas devuelven 403 hasta que vuelva a subir de plan. Se aplica el límite de uso habitual de 60 peticiones por minuto, compartido con todo lo demás que ese usuario haga a través de la API.
Permisos
| Permiso | Lo que ve el usuario | Endpoints |
|---|---|---|
pages:read | Leer tus páginas | GET /pages, GET /pages/:id |
pages:write | Crear y editar páginas | POST /pages, PATCH /pages/:id |
pages:delete | Mover páginas a la papelera | DELETE /pages/:id |
boards:read | Leer tus tableros | GET /boards, GET /boards/:id/lists, GET /boards/:id/cards |
boards:write | Crear, cambiar y mover tarjetas | POST /boards/:id/cards, PATCH /boards/:id/cards/:cardId |
boards:delete | Borrar tarjetas y mover tableros a la papelera | DELETE /boards/:id, DELETE /boards/:id/cards/:cardId |
calendar:read | Leer tu calendario | GET /calendar/events |
calendar:write | Crear y editar eventos | POST /calendar/events, PATCH /calendar/events/:id |
calendar:delete | Borrar eventos | DELETE /calendar/events/:id |
whiteboards:read | Leer tus pizarras | GET /whiteboards, GET /whiteboards/:id/elements |
whiteboards:write | Crear y editar pizarras | POST /whiteboards, POST /whiteboards/:id/elements, PATCH /whiteboards/:id/elements/:elementId |
whiteboards:delete | Borrar elementos y mover pizarras a la papelera | DELETE /whiteboards/:id, DELETE /whiteboards/:id/elements/:elementId |
presentations:read | Leer tus presentaciones | GET /presentations, GET /presentations/:id/slides, GET /presentations/:id/slides/:slideId/elements |
presentations:write | Crear presentaciones y diapositivas | POST /presentations, POST /presentations/:id/slides, POST /presentations/:id/slides/:slideId/elements, PATCH /presentations/:id/slides/:slideId/elements/:elementId |
presentations:delete | Borrar diapositivas y elementos, y mover presentaciones a la papelera | DELETE /presentations/:id, DELETE /presentations/:id/slides/:slideId, DELETE /presentations/:id/slides/:slideId/elements/:elementId |
databases:read | Leer tus bases de datos | GET /databases, GET /databases/:id/rows |
databases:write | Añadir filas y cambiar celdas de tus bases de datos | POST /databases/:id/rows, PATCH /databases/:id/rows/:rowId |
databases:delete | Borrar filas de tus bases de datos | DELETE /databases/:id/rows/:rowId |
chats:read | Leer tus conversaciones y sus mensajes | GET /chats, GET /chats/:id/messages |
chats:write | Escribir mensajes en tu nombre | POST /chats/:id/messages |
mail:read | Leer tu correo interno | GET /mail, GET /mail/:id |
mail:write | Escribir y responder correo en tu nombre, y marcarlo como leído o archivarlo | POST /mail, POST /mail/:id, PATCH /mail/:id |
storage:read | Leer y descargar archivos de Mi unidad | GET /storage, GET /storage/:id |
storage:write | Subir archivos a Mi unidad | POST /storage |
storage:delete | Mover archivos de Mi unidad a la papelera | DELETE /storage/:id |
notifications:read | Leer tus notificaciones | GET /notifications |
notifications:write | Marcar tus notificaciones como leídas | POST /notifications |
search:read | Buscar en todo tu workspace | GET /search |
GET /workspaces no necesita permiso. Llamar a un endpoint sin su permiso devuelve 403.
Borrar es un permiso aparte: :write no incluye borrar, y cualquier DELETE necesita el permiso :delete de su recurso. Los chats, el correo, las notificaciones y la búsqueda no tienen permiso de borrar. Qué va a la papelera y qué se borra para siempre está en Borrar: papelera o para siempre.
Permisos nuevos
databases:read, databases:write, storage:write y los siete permisos de borrar (…:delete) son los más recientes. Un token ya emitido conserva los permisos con los que se autorizó: para usar uno nuevo, manda al usuario otra vez por el flujo con el scope ampliado. Además, tu app tiene que poder pedirlo. Como las apps no se pueden editar, una app registrada en Ajustes solo puede pedir los permisos que marcaste al registrarla; si necesitas uno más, registra una app nueva. Los asistentes que se conectan solos al servidor MCP, como Claude o ChatGPT, pueden pedir todos los permisos de esta tabla.
Lo que no está disponible para apps OAuth
Los tokens de OAuth llegan a todos los endpoints de la tabla, bases de datos, subida de archivos y borrado incluidos, y al servidor MCP, donde el asistente solo ve las herramientas de los permisos concedidos. Una ruta que ningún permiso cubra devuelve 403 a los tokens de OAuth, tengan los permisos que tengan.
Los usuarios revocan el acceso
Los usuarios ven todas las apps que han autorizado en Ajustes → API y webhooks → API y plugins → Aplicaciones conectadas a tu cuenta, con la fecha en que la conectaron y la de su último uso. Al pulsar Revocar y luego Confirmar, el acceso se corta en el acto: los tokens de la app dejan de funcionar en la siguiente petición y recibes 401. Para volver a conectarla, el usuario tiene que pasar otra vez por el flujo.
Lo que no se admite
- Refresh tokens. Un token de acceso dura 90 días desde que se emite; después, la persona vuelve a autorizar la app.
- Otros tipos de concesión, como client credentials o device code. Solo funciona
authorization_code. - OpenID Connect. Sí hay descubrimiento de OAuth (
/.well-known/oauth-authorization-servery/.well-known/oauth-protected-resource), pero no de OpenID Connect. - Introspección de tokens y endpoint de revocación. Los usuarios revocan desde Ajustes; para cortar a todo el mundo, borra la app.
- Registro dinámico de apps con secreto. Los asistentes (Claude, ChatGPT…) se registran solos como apps públicas (
/api/oauth/register); una app con secreto se registra en Ajustes. - El método
plainde PKCE. SoloS256.
Páginas relacionadas
- Autenticación: tokens personales y límites de uso.
- Errores y convenciones: códigos de estado de las llamadas a la API.
- Crear un plugin: los plugins usan los mismos permisos.