Publicado ·Calen Team

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.

PropiedadDetalles
Extensión de archivo.ics
MIME typetext/calendar
CodificaciónUTF-8
Terminaciones de líneaCRLF (\r\n)
Longitud máxima de línea75 octetos (luego folding)
EspecificaciónRFC 5545
Primera publicación1998 (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)

PropiedadObligatoriaDescripción
VERSIONSiempre 2.0
PRODIDIdentifica la aplicación que generó el archivo
CALSCALENoSistema de calendario, casi siempre GREGORIAN
METHODNoPUBLISH para feeds, REQUEST para invitaciones
X-WR-CALNAMENoNombre para mostrar (no estándar pero ampliamente soportado)

VEVENT (datos del evento)

PropiedadObligatoriaDescripción
DTSTARTFecha/hora de inicio del evento
DTENDSí*Fecha/hora de fin del evento (*o usa DURATION)
DTSTAMPMarca de tiempo de cuando se generó el ICS (UTC)
UIDIdentificador globalmente único del evento
SUMMARYNoTítulo del evento
DESCRIPTIONNoDetalles del evento (texto plano, escapado)
LOCATIONNoLugar o dirección
URLNoURL relacionada
STATUSNoTENTATIVE, CONFIRMED o CANCELLED
SEQUENCENoNú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:

  1. Reemplaza webcal:// por https://
  2. Obtiene el archivo ICS
  3. Añade todos los eventos del archivo
  4. 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:

CabeceraPropósito
Content-Type: text/calendar; charset=utf-8Indica al cliente que es un archivo de calendario
Cache-ControlControla con qué frecuencia los clientes vuelven a buscarlo
ETagPermite peticiones condicionales (304 Not Modified)
Cabeceras CORSRequeridas si se obtiene desde JavaScript del navegador

Google Calendar: suscribir vs importar

Esta distinción confunde a muchos desarrolladores:

ComportamientoImportar (subir .ics)Suscribir (URL)
ActualizacionesNunca — es una instantáneaConsulta periódicamente la URL
EliminacionesNo quita eventosElimina eventos que ya no están en el feed
DuplicadosPosibles al reimportarSe manejan por coincidencia de UID
Frecuencia de refrescoN/ACada ~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 .ics en 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:

EnfoqueEjemploVentajasDesventajas
UTCDTSTART:20260415T180000ZSimple, sin ambigüedadEl usuario ve UTC, no hora local
Referencia TZIDDTSTART;TZID=America/New_York:20260415T140000Hora local correctaRequiere bloque VTIMEZONE
FlotanteDTSTART:20260415T140000Sin manejo de zona horariaSe 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:

  1. Valida en icalendar.org/validator.html
  2. Verifica que el formato de DTSTART / DTEND coincida con el enfoque de zona horaria que elegiste
  3. Comprueba que las comas y puntos y comas estén escapados en los valores de texto
  4. Confirma que UID está presente y es único
  5. Confirma que DTSTAMP está presente y en UTC (sufijo Z)
  6. Revisa la cabecera Content-Type si sirves por HTTP
  7. 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

TareaEnfoque recomendado
Descarga de un único eventoGenerar archivo .ics (biblioteca o herramienta)
Calendario con actualización en vivoURL de suscripción webcal://
Enlace de Google CalendarPlantilla URL con action=TEMPLATE
Enlace de OutlookDeep link (ofrece tanto .live.com como .office.com)
Apple CalendarDescarga .ics o enlace webcal://
Cobertura multiplataformaOfrecer 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: