Archivos ICS e integración de calendarios: la guía completa para desarrolladores [2026]
Si alguna vez has necesitado añadir un evento al calendario de alguien de forma programática, te has topado con el formato de archivo ICS. Es el estándar universal que todas las plataformas principales soportan: Google Calendar, Outlook, Apple Calendar y decenas más.
Esta guía cubre todo lo que un desarrollador necesita saber: la especificación del archivo ICS, cómo generarlos en múltiples lenguajes, URLs de suscripción a calendarios, deep links específicos por plataforma y los errores comunes que te costarán horas de depuración si no los conoces de antemano.
¿Qué es un archivo ICS?
Un archivo ICS (.ics) es un formato de datos de calendario en texto plano definido por la RFC 5545 (originalmente RFC 2445). "ICS" significa iCalendar Specification. Toda aplicación moderna de calendario puede leer y escribir este formato.
| Propiedad | Detalles |
|---|---|
| Extensión de archivo | .ics |
| MIME type | text/calendar |
| Codificación | UTF-8 |
| Terminaciones de línea | CRLF (\r\n) |
| Longitud máxima de línea | 75 octetos (luego folding) |
| Especificación | RFC 5545 |
| Primera publicación | 1998 (RFC 2445), actualizada en 2009 |
El formato es sorprendentemente simple: es un archivo de texto estructurado con pares clave-valor envueltos en bloques BEGIN: y END:. Pero "simple" no significa "fácil de hacer bien". Los detalles importan, y los clientes de calendario son implacables con los archivos mal formados.
Estructura de un archivo ICS
Aquí tienes un archivo ICS completo y válido con cada campo de uso común:
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Your Company//Your App//EN
CALSCALE:GREGORIAN
METHOD:PUBLISH
X-WR-CALNAME:My Event Calendar
X-WR-TIMEZONE:America/New_York
BEGIN:VTIMEZONE
TZID:America/New_York
BEGIN:DAYLIGHT
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
TZNAME:EDT
DTSTART:19700308T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
TZNAME:EST
DTSTART:19701101T020000
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID=America/New_York:20260415T140000
DTEND;TZID=America/New_York:20260415T160000
DTSTAMP:20260401T120000Z
UID:550e8400-e29b-41d4-a716-446655440000@yourdomain.com
SUMMARY:Developer Conference 2026
DESCRIPTION:Annual developer conference.\nTopics: ICS integration\, calen
dar APIs\, and more.
LOCATION:456 Tech Blvd\, San Francisco\, CA 94105
URL:https://example.com/devconf2026
ORGANIZER;CN=Jane Smith:mailto:jane@example.com
ATTENDEE;RSVP=TRUE;CN=John Doe:mailto:john@example.com
STATUS:CONFIRMED
SEQUENCE:0
BEGIN:VALARM
TRIGGER:-PT30M
ACTION:DISPLAY
DESCRIPTION:Event starts in 30 minutes
END:VALARM
END:VEVENT
END:VCALENDAR
Desglosemos las partes críticas:
VCALENDAR (contenedor)
| Propiedad | Obligatoria | Descripción |
|---|---|---|
VERSION | Sí | Siempre 2.0 |
PRODID | Sí | Identifica la aplicación que generó el archivo |
CALSCALE | No | Sistema de calendario, casi siempre GREGORIAN |
METHOD | No | PUBLISH para feeds, REQUEST para invitaciones |
X-WR-CALNAME | No | Nombre para mostrar (no estándar pero ampliamente soportado) |
VEVENT (datos del evento)
| Propiedad | Obligatoria | Descripción |
|---|---|---|
DTSTART | Sí | Fecha/hora de inicio del evento |
DTEND | Sí* | Fecha/hora de fin del evento (*o usa DURATION) |
DTSTAMP | Sí | Marca de tiempo de cuando se generó el ICS (UTC) |
UID | Sí | Identificador globalmente único del evento |
SUMMARY | No | Título del evento |
DESCRIPTION | No | Detalles del evento (texto plano, escapado) |
LOCATION | No | Lugar o dirección |
URL | No | URL relacionada |
STATUS | No | TENTATIVE, CONFIRMED o CANCELLED |
SEQUENCE | No | Número de revisión (incrementa al actualizar) |
VALARM (recordatorio)
El TRIGGER:-PT30M significa "30 minutos antes del evento". Puedes usar PT1H para una hora, P1D para un día, etc. ACTION:DISPLAY le dice a la app de calendario que muestre una notificación.
Cómo generar archivos ICS
Opción 1: construcción manual de cadenas
Para casos simples, puedes construir la cadena ICS directamente. Un ejemplo funcional en varios lenguajes:
Python:
from datetime import datetime, timezone
def generate_ics(title, start, end, description="", location=""):
uid = f"{start.strftime('%Y%m%d%H%M%S')}-{id(title)}@yourdomain.com"
now = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
# Escapar caracteres especiales según RFC 5545
def escape(text):
return (text
.replace("\\", "\\\\")
.replace(";", "\\;")
.replace(",", "\\,")
.replace("\n", "\\n"))
ics = (
"BEGIN:VCALENDAR\r\n"
"VERSION:2.0\r\n"
"PRODID:-//YourApp//EN\r\n"
"BEGIN:VEVENT\r\n"
f"DTSTART:{start.strftime('%Y%m%dT%H%M%SZ')}\r\n"
f"DTEND:{end.strftime('%Y%m%dT%H%M%SZ')}\r\n"
f"DTSTAMP:{now}\r\n"
f"UID:{uid}\r\n"
f"SUMMARY:{escape(title)}\r\n"
f"DESCRIPTION:{escape(description)}\r\n"
f"LOCATION:{escape(location)}\r\n"
"STATUS:CONFIRMED\r\n"
"END:VEVENT\r\n"
"END:VCALENDAR\r\n"
)
return ics
JavaScript / Node.js:
function generateICS({ title, start, end, description = '', location = '' }) {
const formatDate = (d) => d.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '');
const escape = (s) => s.replace(/\\/g, '\\\\').replace(/;/g, '\\;').replace(/,/g, '\\,').replace(/\n/g, '\\n');
const uid = `${formatDate(start)}-${Math.random().toString(36).slice(2)}@yourdomain.com`;
return [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//YourApp//EN',
'BEGIN:VEVENT',
`DTSTART:${formatDate(start)}`,
`DTEND:${formatDate(end)}`,
`DTSTAMP:${formatDate(new Date())}`,
`UID:${uid}`,
`SUMMARY:${escape(title)}`,
`DESCRIPTION:${escape(description)}`,
`LOCATION:${escape(location)}`,
'STATUS:CONFIRMED',
'END:VEVENT',
'END:VCALENDAR',
].join('\r\n');
}
La construcción manual funciona para casos básicos, pero rápidamente te toparás con casos límite —manejo de zona horaria, line folding, eventos recurrentes— que hacen que una biblioteca valga la pena.
Opción 2: usando bibliotecas
Python — icalendar:
from icalendar import Calendar, Event, Alarm
from datetime import datetime, timedelta
import pytz
cal = Calendar()
cal.add('prodid', '-//Your App//EN')
cal.add('version', '2.0')
event = Event()
event.add('summary', 'Developer Conference 2026')
event.add('dtstart', datetime(2026, 4, 15, 14, 0, 0, tzinfo=pytz.timezone('America/New_York')))
event.add('dtend', datetime(2026, 4, 15, 16, 0, 0, tzinfo=pytz.timezone('America/New_York')))
event.add('dtstamp', datetime.now(pytz.utc))
event.add('uid', '550e8400-e29b-41d4-a716-446655440000@yourdomain.com')
event.add('location', '456 Tech Blvd, San Francisco, CA 94105')
event.add('description', 'Annual developer conference covering ICS integration and calendar APIs.')
alarm = Alarm()
alarm.add('action', 'DISPLAY')
alarm.add('trigger', timedelta(minutes=-30))
alarm.add('description', 'Event starts in 30 minutes')
event.add_component(alarm)
cal.add_component(event)
with open('event.ics', 'wb') as f:
f.write(cal.to_ical())
JavaScript — ical-generator:
import icalGenerator from 'ical-generator';
const calendar = icalGenerator({ name: 'My Event Calendar' });
calendar.createEvent({
start: new Date('2026-04-15T14:00:00-04:00'),
end: new Date('2026-04-15T16:00:00-04:00'),
summary: 'Developer Conference 2026',
description: 'Annual developer conference.',
location: '456 Tech Blvd, San Francisco, CA 94105',
url: 'https://example.com/devconf2026',
alarms: [
{ type: 'display', trigger: 30 * 60 } // 30 minutos antes
]
});
// Enviar como respuesta HTTP
res.setHeader('Content-Type', 'text/calendar; charset=utf-8');
res.setHeader('Content-Disposition', 'attachment; filename="event.ics"');
res.send(calendar.toString());
Las bibliotecas manejan el escape de caracteres, el line folding, la incorporación de zona horaria y la generación de UID: todo lo que es fácil equivocarse al construir cadenas manualmente.
Opción 3: generadores de archivos ICS online
Si no necesitas generación programática, las herramientas online funcionan bien para eventos puntuales. El generador de enlaces de calendario de Calen produce archivos ICS junto a enlaces de Google Calendar, Outlook y Apple Calendar, todo desde un único formulario. Maneja la codificación, la conversión de zona horaria y el cumplimiento de la RFC automáticamente.
URLs de suscripción a calendarios
Hay una diferencia importante entre importar un archivo ICS y suscribirse a un feed de calendario. Importar es una copia única. Suscribirse crea una conexión en vivo que se actualiza cuando la fuente cambia.
Cómo funciona el protocolo webcal://
El protocolo webcal:// es simplemente https:// con un esquema distinto. Cuando una app de calendario ve un enlace webcal://, sabe que debe suscribirse a la URL en lugar de descargarla una vez.
webcal://example.com/calendar/feed.ics
Por debajo, la app de calendario:
- Reemplaza
webcal://porhttps:// - Obtiene el archivo ICS
- Añade todos los eventos del archivo
- Consulta la URL periódicamente buscando actualizaciones (normalmente cada 1-24 horas)
Configurar un feed de calendario suscribible
Tu servidor debe devolver un archivo ICS válido con las cabeceras correctas:
# Ejemplo con Flask
from flask import Flask, Response
app = Flask(__name__)
@app.route('/calendar/feed.ics')
def calendar_feed():
cal = generate_calendar() # Tu lógica de generación ICS
return Response(
cal.to_ical(),
mimetype='text/calendar',
headers={
'Content-Disposition': 'attachment; filename="calendar.ics"',
'Cache-Control': 'no-cache, no-store, must-revalidate',
'ETag': compute_etag(cal),
}
)
Requisitos clave del lado del servidor:
| Cabecera | Propósito |
|---|---|
Content-Type: text/calendar; charset=utf-8 | Indica al cliente que es un archivo de calendario |
Cache-Control | Controla con qué frecuencia los clientes vuelven a buscarlo |
ETag | Permite peticiones condicionales (304 Not Modified) |
| Cabeceras CORS | Requeridas si se obtiene desde JavaScript del navegador |
Google Calendar: suscribir vs importar
Esta distinción confunde a muchos desarrolladores:
| Comportamiento | Importar (subir .ics) | Suscribir (URL) |
|---|---|---|
| Actualizaciones | Nunca — es una instantánea | Consulta periódicamente la URL |
| Eliminaciones | No quita eventos | Elimina eventos que ya no están en el feed |
| Duplicados | Posibles al reimportar | Se manejan por coincidencia de UID |
| Frecuencia de refresco | N/A | Cada ~12-24 horas (no configurable) |
Si tus eventos cambian (y la mayoría lo hace), suscribirse es casi siempre lo que quieres. La desventaja es que el intervalo de refresco de Google Calendar es lento (12-24 horas) y no hay forma de forzar un refresco vía API.
Integración específica por plataforma
API de Google Calendar: crear enlaces de evento de forma programática
El enfoque más simple de enlace de evento de la API de Google Calendar no requiere claves API: usa la plantilla basada en URL:
https://calendar.google.com/calendar/render?action=TEMPLATE
&text=Developer+Conference+2026
&dates=20260415T180000Z/20260415T200000Z
&details=Annual+developer+conference
&location=456+Tech+Blvd%2C+San+Francisco
&ctz=America/New_York
Para creación del lado del servidor vía la API de Google Calendar (requiere OAuth):
const { google } = require('googleapis');
const calendar = google.calendar({ version: 'v3', auth: oauthClient });
const event = await calendar.events.insert({
calendarId: 'primary',
requestBody: {
summary: 'Developer Conference 2026',
location: '456 Tech Blvd, San Francisco, CA 94105',
start: {
dateTime: '2026-04-15T14:00:00',
timeZone: 'America/New_York',
},
end: {
dateTime: '2026-04-15T16:00:00',
timeZone: 'America/New_York',
},
},
});
console.log('Event created:', event.data.htmlLink);
El htmlLink en la respuesta es un enlace directo al evento en Google Calendar. Para una inmersión más profunda construyendo estos enlaces manualmente, consulta la guía de enlaces para añadir al calendario.
Outlook: deep links y enfoque de adjunto .ics
Para añadir un evento al calendario de Outlook mediante un enlace tienes dos caminos:
Outlook.com (personal):
https://outlook.live.com/calendar/0/action/compose?
subject=Developer+Conference+2026
&startdt=2026-04-15T18:00:00Z
&enddt=2026-04-15T20:00:00Z
&body=Annual+developer+conference
&location=456+Tech+Blvd%2C+San+Francisco
Office 365 (trabajo/escuela):
https://outlook.office.com/calendar/0/action/compose?
subject=Developer+Conference+2026
&startdt=2026-04-15T18:00:00Z
&enddt=2026-04-15T20:00:00Z
&body=Annual+developer+conference
&location=456+Tech+Blvd%2C+San+Francisco
El problema: no puedes saber cuál necesita tu usuario. El enfoque multiplataforma más confiable es ofrecer una descarga de archivo .ics. Outlook (escritorio y web) maneja archivos ICS de forma nativa: hacer doble clic abre el diálogo de "Añadir al calendario".
Para invitaciones por correo, adjunta el archivo .ics con Content-Type: text/calendar; method=REQUEST y Outlook lo renderizará como una invitación a reunión con botones de Aceptar/Rechazar.
Apple Calendar: soporte de webcal y .ics
Apple Calendar tiene el mejor soporte de ICS de cualquier cliente principal. Maneja:
- Apertura directa de archivos .ics: hacer clic en un archivo
.icsen macOS o iOS abre Calendar.app con un aviso de añadir - Enlaces webcal://: se suscribe automáticamente
- Detector de datos: extrae información del evento del texto en Mail y Safari
Para deep linking en iOS:
webcal://example.com/calendar/feed.ics
O para disparar una descarga:
<a href="data:text/calendar;charset=utf-8,BEGIN%3AVCALENDAR%0D%0A..."
download="event.ics">
Añadir a Apple Calendar
</a>
El enfoque de URI data: funciona para eventos únicos pero está limitado por la longitud de URL. Para cualquier cosa más allá de un evento trivial, sirve el archivo ICS desde tu servidor.
Errores comunes y depuración
Estos son los problemas que consumirán tu tiempo de depuración si no los conoces de antemano.
1. Manejo de zona horaria (VTIMEZONE)
La fuente más común de bugs en ICS. Hay tres enfoques:
| Enfoque | Ejemplo | Ventajas | Desventajas |
|---|---|---|---|
| UTC | DTSTART:20260415T180000Z | Simple, sin ambigüedad | El usuario ve UTC, no hora local |
| Referencia TZID | DTSTART;TZID=America/New_York:20260415T140000 | Hora local correcta | Requiere bloque VTIMEZONE |
| Flotante | DTSTART:20260415T140000 | Sin manejo de zona horaria | Se interpreta como hora local del dispositivo |
Mejor práctica: usa siempre TZID con un bloque VTIMEZONE correspondiente, o convierte a UTC. Nunca uses horas flotantes para eventos que tengan una zona horaria real.
2. Codificación y escape de caracteres
La RFC 5545 requiere un escape específico dentro de los valores de texto:
Backslash → \\
Punto y coma → \;
Coma → \,
Salto de línea → \n
No escapar las comas es el error más común. Una ubicación como 456 Tech Blvd, San Francisco, CA romperá los parsers si las comas no se escapan a 456 Tech Blvd\, San Francisco\, CA.
3. Line folding (límite de 75 octetos)
La RFC 5545 requiere que las líneas de contenido no superen los 75 octetos. Las líneas más largas deben "plegarse" insertando un CRLF seguido de un único carácter de espacio en blanco (espacio o tab):
DESCRIPTION:This is a very long description that exceeds seventy-five octe
ts and must be folded onto the next line using a CRLF followed by a singl
e space character.
Muchos clientes de calendario son laxos con esto, pero el importador ICS de Google Calendar no lo es. Si construyes cadenas manualmente, implementa el folding:
def fold_line(line, max_len=75):
if len(line.encode('utf-8')) <= max_len:
return line
result = []
while len(line.encode('utf-8')) > max_len:
# Buscar un punto seguro de corte (no romper caracteres multi-byte)
cut = max_len if not result else max_len - 1
encoded = line.encode('utf-8')
chunk = encoded[:cut].decode('utf-8', errors='ignore')
result.append(chunk)
line = line[len(chunk):]
result.append(line)
return '\r\n '.join(result)
4. Requisitos de PRODID y UID
PRODID es obligatorio en el VCALENDAR. Identifica tu aplicación. Formato: //-//Company//Product//Language. Ejemplo: -//Acme Corp//Event Manager 1.0//EN.
UID es obligatorio en cada VEVENT y debe ser globalmente único. La recomendación estándar es {timestamp}-{random}@{domain}. Si usas UUIDs, añade tu dominio: 550e8400-e29b-41d4-a716-446655440000@yourdomain.com.
El UID es la forma en que los clientes de calendario identifican eventos para actualizaciones. Si dos archivos ICS tienen el mismo UID y un número de SEQUENCE mayor, el cliente lo trata como una actualización. Si reutilizas UIDs por accidente, los eventos se sobrescribirán entre sí.
5. Terminaciones de línea CRLF
La especificación requiere terminaciones de línea \r\n, no \n. La mayoría de los clientes toleran solo \n, pero algunos (notablemente versiones antiguas de Outlook de escritorio) rechazarán o corromperán archivos que no usen CRLF. Usa siempre \r\n en producción.
Lista de depuración
Cuando un archivo ICS no funciona, revisa esto en orden:
- Valida en icalendar.org/validator.html
- Verifica que el formato de
DTSTART/DTENDcoincida con el enfoque de zona horaria que elegiste - Comprueba que las comas y puntos y comas estén escapados en los valores de texto
- Confirma que
UIDestá presente y es único - Confirma que
DTSTAMPestá presente y en UTC (sufijoZ) - Revisa la cabecera
Content-Typesi sirves por HTTP - Verifica las terminaciones de línea CRLF con un editor hexadecimal si todo lo demás falla
La forma fácil: deja que una herramienta lo maneje
Construir archivos ICS correctos, mantener URLs de suscripción a calendarios y construir deep links para cada plataforma es mucha superficie para bugs. Si estás añadiendo una función de eventos a tu producto, no tienes que hacer todo esto desde cero.
Calen genera archivos ICS válidos, enlaces de Google Calendar, deep links de Outlook y enlaces de Apple Calendar, todo a partir de una única definición de evento. El generador de enlaces de calendario maneja la conversión de zona horaria, el cumplimiento de RFC 5545, el escape de caracteres y el line folding automáticamente. También puedes añadir un botón de añadir al calendario a tu sitio web para que los visitantes guarden eventos directamente en su calendario preferido.
Para desarrolladores que necesitan integrar funcionalidad de calendario sin lidiar con los detalles de la especificación, este suele ser el camino más rápido a producción.
Resumen
| Tarea | Enfoque recomendado |
|---|---|
| Descarga de un único evento | Generar archivo .ics (biblioteca o herramienta) |
| Calendario con actualización en vivo | URL de suscripción webcal:// |
| Enlace de Google Calendar | Plantilla URL con action=TEMPLATE |
| Enlace de Outlook | Deep link (ofrece tanto .live.com como .office.com) |
| Apple Calendar | Descarga .ics o enlace webcal:// |
| Cobertura multiplataforma | Ofrecer todo lo anterior |
El formato ICS existe desde 1998 y no va a desaparecer. Toda nueva app de calendario lo soporta. Invertir en una generación correcta de ICS se amortiza en cada plataforma donde puedan estar tus usuarios, y cuando necesites cobertura multiplataforma sin la sobrecarga de implementación, herramientas como el generador de enlaces de calendario de Calen pueden llevarte allí en minutos en lugar de días.
Lectura relacionada:
- Cómo crear un enlace para añadir al calendario - Guía completa
- Cómo añadir un botón de añadir al calendario a tu sitio web
- Cómo compartir eventos de Google Calendar - Guía completa
- Invitaciones de calendario para webinars: cómo conseguir el doble de asistentes
- 7 formas probadas de reducir las ausencias en eventos