Desarrolladores
API de calendario
La API de calendario te permite leer los eventos del calendario de un espacio de trabajo, con los eventos recurrentes ya desplegados en fechas reales, crear eventos nuevos, cambiarlos y borrarlos. Sirve para montar agendas, recordatorios o sincronizar eventos desde otra herramienta.
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. - Obtén el ID del espacio con
GET /api/v1/workspaceso en la app, en Ajustes → Workspace → Datos del espacio → ID del espacio → Copiar. - Los errores que puede dar cualquier endpoint (
401,403,429) se explican en Errores y convenciones. - Los ejemplos en JavaScript usan la función
tylode()de la API de páginas.
La API trabaja con el calendario del espacio. Tu calendario personal no está disponible por la API. En Calendario tienes cómo funcionan los dos en la app.
Endpoints
| Método | Ruta | Qué hace | Permiso (scope) |
|---|---|---|---|
| GET | /api/v1/calendar/events | Lista eventos, opcionalmente en un rango de fechas | calendar:read |
| POST | /api/v1/calendar/events | Crea un evento | calendar:write |
| PATCH | /api/v1/calendar/events/:id | Cambia un evento | calendar:write |
| DELETE | /api/v1/calendar/events/:id | Borra un evento | calendar:delete |
El objeto evento
| Campo | Tipo | Descripción |
|---|---|---|
id | string | ID del evento. Las repeticiones de un evento recurrente comparten el ID del evento original. |
calendar_id | string | Calendario al que pertenece. |
workspace_id | string | Espacio de ese calendario. |
title | string | Título del evento. |
description | string | Descripción (puede ir vacía). |
start_at | string | Inicio, como marca de tiempo ISO 8601 en UTC. |
end_at | string o null | Fin, o null si el evento no tiene hora de fin (los de todo el día no suelen tenerla). |
all_day | boolean | Si es un evento de todo el día. |
recurrence_rule | string o null | Regla de repetición de los eventos recurrentes; null en los eventos sueltos. Mira Eventos recurrentes. |
excluded_at | array o null | Horas de inicio de las repeticiones sueltas que se han borrado de un evento recurrente. |
Listar eventos
GET /api/v1/calendar/events
Devuelve los eventos del calendario del espacio ordenados por start_at. Si indicas un rango de fechas, los eventos recurrentes llegan como un elemento por cada repetición.
Permiso (scope): calendar:read
| Parámetro | Dónde | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
workspace_id | query | string | Sí | El espacio cuyo calendario quieres. |
from | query | string (ISO 8601) | No | Inicio del rango. |
to | query | string (ISO 8601) | No | Fin del rango. |
Cómo funcionan from y to:
| Si mandas | Eventos sueltos | Eventos recurrentes |
|---|---|---|
| Ninguno | Todos. | Una vez cada uno, como el evento original con su recurrence_rule (sin desplegar). |
from y to | Los que cumplen from ≤ start_at < to. | Cada repetición que empieza entre from y to (ambos incluidos). |
Solo from | Los que tienen start_at ≥ from. | Las repeticiones desde from hasta 365 días después. |
Solo to | Los que tienen start_at < to, por antiguos que sean. | Las repeticiones desde ahora hasta to. |
Para vistas de un día, manda los dos extremos. No hay paginación.
Codifica las marcas de tiempo en la URL: el + de un desfase como +02:00 se convierte en un espacio si lo pegas tal cual. Con curl, -G junto con --data-urlencode lo hace por ti:
curl -G https://app.tylode.com/api/v1/calendar/events \
-H "Authorization: Bearer tylode_pat_xxx" \
--data-urlencode "workspace_id=WORKSPACE_ID" \
--data-urlencode "from=2026-09-28T00:00:00+02:00" \
--data-urlencode "to=2026-10-05T00:00:00+02:00"const params = new URLSearchParams({
workspace_id: WORKSPACE_ID,
from: "2026-09-28T00:00:00+02:00",
to: "2026-10-05T00:00:00+02:00",
});
const events = await tylode(`/calendar/events?${params}`);Respuesta 200
Un evento suelto seguido de dos repeticiones de una reunión semanal. Las repeticiones comparten el mismo id.
{
"data": [
{
"id": "a3c6f9b2-5e8d-4a1c-b4f7-0d3a6c9e2b58",
"calendar_id": "f1e4b7a0-3c6f-4d9a-8e2b-5a8d1c4f7e03",
"workspace_id": "7c1e2a4b-3f5d-4e8a-9b21-6d0f5c8e1a37",
"title": "Dentist",
"description": "",
"start_at": "2026-09-28T07:30:00+00:00",
"end_at": "2026-09-28T08:15:00+00:00",
"all_day": false,
"recurrence_rule": null,
"excluded_at": null
},
{
"id": "b8e1d4a7-0f3c-4e6b-9a2d-5c8f1b4e7a30",
"calendar_id": "f1e4b7a0-3c6f-4d9a-8e2b-5a8d1c4f7e03",
"workspace_id": "7c1e2a4b-3f5d-4e8a-9b21-6d0f5c8e1a37",
"title": "Team sync",
"description": "Weekly check-in.",
"start_at": "2026-09-28T08:00:00.000Z",
"end_at": "2026-09-28T08:30:00.000Z",
"all_day": false,
"recurrence_rule": "FREQ=WEEKLY;BYDAY=MO,TH,FR",
"excluded_at": ["2026-10-01T08:00:00+00:00"]
},
{
"id": "b8e1d4a7-0f3c-4e6b-9a2d-5c8f1b4e7a30",
"calendar_id": "f1e4b7a0-3c6f-4d9a-8e2b-5a8d1c4f7e03",
"workspace_id": "7c1e2a4b-3f5d-4e8a-9b21-6d0f5c8e1a37",
"title": "Team sync",
"description": "Weekly check-in.",
"start_at": "2026-10-02T08:00:00.000Z",
"end_at": "2026-10-02T08:30:00.000Z",
"all_day": false,
"recurrence_rule": "FREQ=WEEKLY;BYDAY=MO,TH,FR",
"excluded_at": ["2026-10-01T08:00:00+00:00"]
}
]
}Aquí la repetición del jueves 1 de octubre se borró en la app, así que aparece en excluded_at y no sale en los resultados.
Errores
| Estado | Cuándo |
|---|---|
400 | Falta workspace_id. |
Crear un evento
POST /api/v1/calendar/events
Crea un evento suelto en el calendario del espacio.
Permiso (scope): calendar:write
| Parámetro | Dónde | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
workspace_id | body | string | Sí | Espacio en cuyo calendario se crea el evento. |
title | body | string | Sí | Título del evento. |
start_at | body | string (ISO 8601) | Sí | Inicio, con desfase horario o Z. |
end_at | body | string (ISO 8601) | No | Fin. Por defecto, sin hora de fin. |
all_day | body | boolean | No | Evento de todo el día. Por defecto, false. |
description | body | string | No | Descripción. Por defecto, vacía. |
La API no puede crear eventos recurrentes ni añadir participantes; eso se hace en la app. Para un evento de todo el día, haz lo mismo que la app: manda como start_at la medianoche de ese día en tu zona horaria, pon all_day a true y no mandes end_at.
curl -X POST https://app.tylode.com/api/v1/calendar/events \
-H "Authorization: Bearer tylode_pat_xxx" \
-H "Content-Type: application/json" \
-d '{
"workspace_id": "WORKSPACE_ID",
"title": "Quarterly review",
"start_at": "2026-10-06T10:00:00+02:00",
"end_at": "2026-10-06T11:30:00+02:00",
"description": "Room 2 and video call."
}'const event = await tylode("/calendar/events", {
method: "POST",
body: {
workspace_id: WORKSPACE_ID,
title: "Quarterly review",
start_at: "2026-10-06T10:00:00+02:00",
end_at: "2026-10-06T11:30:00+02:00",
description: "Room 2 and video call.",
},
});Respuesta 201
La respuesta no incluye recurrence_rule ni excluded_at, porque los eventos creados por la API nunca se repiten.
{
"data": {
"id": "c9f2e5b8-1a4d-4f7c-b0e3-6d9a2c5f8b41",
"calendar_id": "f1e4b7a0-3c6f-4d9a-8e2b-5a8d1c4f7e03",
"workspace_id": "7c1e2a4b-3f5d-4e8a-9b21-6d0f5c8e1a37",
"title": "Quarterly review",
"description": "Room 2 and video call.",
"start_at": "2026-10-06T08:00:00+00:00",
"end_at": "2026-10-06T09:30:00+00:00",
"all_day": false
}
}Errores
| Estado | Cuándo |
|---|---|
400 | Falta workspace_id, title o start_at. |
400 | El espacio todavía no tiene calendario. |
400 | No se ha podido crear el evento (por ejemplo, una fecha no es válida o no puedes añadir eventos en ese espacio). |
Cambiar un evento
PATCH /api/v1/calendar/events/:id
Cambia el título, la descripción, el inicio, el fin o si es de todo el día. Solo cambia lo que mandas: si otra persona cambia otro campo mientras tanto, su cambio se queda.
Permiso (scope): calendar:write
| Parámetro | Dónde | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string | Sí | ID del evento, de Listar eventos. |
title | body | string | No | Título nuevo. No puede ir en blanco. |
description | body | string | No | Descripción nueva. Con "" se queda vacía. |
start_at | body | string (ISO 8601) | No | Inicio nuevo, con desfase horario o Z. |
end_at | body | string (ISO 8601) o null | No | Fin nuevo, o null para quitar la hora de fin. |
all_day | body | boolean | No | Si es un evento de todo el día. |
Manda al menos uno de los campos del cuerpo. El fin no puede quedar antes del inicio; si solo cambias uno de los dos, se compara con el que ya tenía el evento.
En un evento recurrente, el cambio vale para todas sus repeticiones, porque todas salen del mismo evento (su start_at es el de la primera). La regla de repetición no se cambia por la API.
curl -X PATCH https://app.tylode.com/api/v1/calendar/events/EVENT_ID \
-H "Authorization: Bearer tylode_pat_xxx" \
-H "Content-Type: application/json" \
-d '{
"start_at": "2026-10-06T11:00:00+02:00",
"end_at": "2026-10-06T12:30:00+02:00"
}'// Retrasa una hora la revisión trimestral
const event = await tylode(`/calendar/events/${EVENT_ID}`, {
method: "PATCH",
body: {
start_at: "2026-10-06T11:00:00+02:00",
end_at: "2026-10-06T12:30:00+02:00",
},
});Respuesta 200
El evento tal como queda. Trae recurrence_rule, pero no excluded_at.
{
"data": {
"id": "c9f2e5b8-1a4d-4f7c-b0e3-6d9a2c5f8b41",
"calendar_id": "f1e4b7a0-3c6f-4d9a-8e2b-5a8d1c4f7e03",
"workspace_id": "7c1e2a4b-3f5d-4e8a-9b21-6d0f5c8e1a37",
"title": "Quarterly review",
"description": "Room 2 and video call.",
"start_at": "2026-10-06T09:00:00+00:00",
"end_at": "2026-10-06T10:30:00+00:00",
"all_day": false,
"recurrence_rule": null
}
}Errores
| Estado | Cuándo |
|---|---|
400 | El ID del evento no es válido. |
400 | No has mandado nada que cambiar, o title va en blanco. |
400 | start_at o end_at no son fechas ISO 8601 válidas (end_at también admite null). |
400 | end_at quedaría antes de start_at. |
404 | El evento no existe o no puedes cambiarlo. |
Borrar un evento
DELETE /api/v1/calendar/events/:id
Borra un evento. Si se repite, se borran todas sus repeticiones, porque todas salen del mismo evento. Los eventos no tienen papelera: no se puede recuperar.
Permiso (scope): calendar:delete
| Parámetro | Dónde | Tipo | Obligatorio | Descripción |
|---|---|---|---|---|
id | path | string | Sí | ID del evento. |
curl -X DELETE https://app.tylode.com/api/v1/calendar/events/EVENT_ID \
-H "Authorization: Bearer tylode_pat_xxx"Respuesta 200
{ "data": { "id": "c9f2e5b8-1a4d-4f7c-b0e3-6d9a2c5f8b41", "papelera": false } }Para quitar una sola repetición de un evento recurrente, hazlo en la app. 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 | El evento no existe o no puedes borrarlo. |
Eventos recurrentes
Los eventos recurrentes se crean en la app (mira Calendario). La API te da dos formas de leerlos.
Con rango (from y/o to), la API los despliega por ti: cada repetición es un elemento aparte con su propio start_at y end_at (en UTC, como 2026-10-02T08:00:00.000Z) y todo lo demás copiado del evento original, incluido su id. Usa id más start_at como clave única. Las repeticiones borradas ya vienen quitadas.
Sin rango, recibes cada evento recurrente una sola vez, con su primer start_at y estos dos campos para desplegarlo tú:
recurrence_rule: una regla RRULE de iCalendar, sin el prefijoRRULE:. La primera repetición es elstart_atdel evento. La app escribe reglas como estas:Regla Significado FREQ=DAILYTodos los días, sin fin. FREQ=WEEKLY;BYDAY=MO,WE,FRCada lunes, miércoles y viernes. FREQ=WEEKLY;INTERVAL=2Cada dos semanas, el mismo día de la semana que el inicio. FREQ=MONTHLY;COUNT=6Cada mes, seis veces. FREQ=YEARLY;UNTIL=20301231T235959ZCada año hasta finales de 2030. excluded_at: las horas de inicio de las repeticiones que se borraron una a una. Sáltatelas. Compáralas como instantes, no como texto: el mismo momento puede escribirse con+00:00o conZ, con milisegundos o sin ellos.
Un esbozo con el paquete rrule, que despliega las reglas de la misma forma:
import { RRule } from "rrule";
function occurrences(event, from, to) {
const rule = new RRule({
...RRule.parseString(event.recurrence_rule),
dtstart: new Date(event.start_at),
});
const excluded = new Set((event.excluded_at ?? []).map((d) => new Date(d).getTime()));
return rule.between(from, to, true).filter((d) => !excluded.has(d.getTime()));
}Si una regla no se puede leer, el evento llega como un único elemento, sin desplegar, aunque mandes un rango.
Zonas horarias
- Cada marca de tiempo es un instante. Cuando mandes una (
start_at,end_at,from,to), incluye el desfase (2026-10-06T10:00:00+02:00) oZ(2026-10-06T08:00:00Z), para que no haya dudas sobre el momento del que hablas. - Las respuestas siempre van en UTC. Los eventos guardados tienen la forma
2026-10-06T08:00:00+00:00y las repeticiones desplegadas,2026-10-06T08:00:00.000Z. Las dos se leen connew Date(). Pásalas a la zona horaria de quien las vaya a ver al mostrarlas. - Para "hoy" o "esta semana", calcula el inicio y el fin del día en tu zona horaria y mándalos como
fromyto.
Ejemplo: un script con la agenda del día
Este script de Node.js muestra los eventos de hoy en tu zona horaria. Ejecútalo con TZ definida para que "hoy" sea tu día, por ejemplo TZ=Europe/Madrid node agenda.mjs.
// agenda.mjs (pega la función tylode() de la API de páginas encima de esta línea)
const WORKSPACE_ID = process.env.TYLODE_WORKSPACE_ID;
const start = new Date();
start.setHours(0, 0, 0, 0); // medianoche local
const end = new Date(start);
end.setDate(end.getDate() + 1); // la medianoche local siguiente
const params = new URLSearchParams({
workspace_id: WORKSPACE_ID,
from: start.toISOString(),
to: end.toISOString(),
});
const events = (await tylode(`/calendar/events?${params}`))
// las repeticiones incluyen el límite final; nos quedamos solo con las de hoy
.filter((e) => new Date(e.start_at) < end);
const hhmm = (iso) =>
new Date(iso).toLocaleTimeString("en-GB", { hour: "2-digit", minute: "2-digit" });
console.log(`Agenda for ${start.toDateString()}`);
if (events.length === 0) console.log("Nothing scheduled.");
for (const e of events) {
const when = e.all_day ? "All day" : e.end_at ? `${hhmm(e.start_at)}–${hhmm(e.end_at)}` : hhmm(e.start_at);
console.log(`${when} ${e.title}`);
}Agenda for Mon Sep 28 2026
09:30–10:15 Dentist
10:00–10:30 Team syncPara mandarla a Slack cada mañana junto con las tarjetas que vencen hoy, mira la receta de la agenda diaria.
Relacionado
- Calendario en la app
- API de tableros, para las tarjetas con fecha de vencimiento
- MCP: la herramienta
list_calendar_eventsusa este mismo endpoint, así que un asistente ve los eventos recurrentes en sus fechas correctas