Cómo integrar la API de iCloud Calendar en tu aplicación

Publicado el

En un artículo anterior explicamos cómo integrar la API de Google Calendar en tu aplicación y repasamos todos los pasos necesarios para que la integración funcione.

Integrar iCloud Calendar no es tan sencillo como Google Calendar. La documentación es escasa y Apple no ha dedicado mucho esfuerzo a explicar lo que un desarrollador debe hacer para conectar iCloud Calendar a una aplicación.

En este artículo profundizamos en cómo integrar la API de iCloud Calendar en tu aplicación. Cubrimos la autenticación, las operaciones compatibles, las limitaciones, fragmentos de código y las herramientas que facilitan la integración.

¿Qué protocolos y estándares utiliza Apple iCloud Calendar?

Apple iCloud Calendar utiliza el estándar CalDAV para la comunicación de calendarios. CalDAV es una extensión de WebDAV que permite a los clientes gestionar calendarios y eventos en un servidor.

Los eventos se representan en el formato ICS (iCalendar), que es un formato de texto para datos de calendario. Esto significa que tu aplicación puede comunicarse con los calendarios de iCloud a través de HTTP usando solicitudes CalDAV y datos ICS.

La ventaja es que no se trata de una implementación específica de dispositivos u sistemas operativos de Apple; puedes usarla desde cualquier servidor que se ejecute en cualquier plataforma.

La mala noticia es que iCloud no proporciona una API REST para calendarios, por lo que CalDAV es la única forma de integrar iCloud Calendar en tu aplicación.

¿Cómo se autentica en iCloud?

Normalmente, cuando quieres integrar una plataforma en tu aplicación (Google Calendar, por ejemplo), debes crear una cuenta de desarrollador en esa plataforma, crear una aplicación, configurar alcances, añadir usuarios de prueba, completar la información de tu app y enviarla para aprobación.

Tras seguir todos estos pasos y obtener la aprobación, puedes dirigir al usuario final a la pantalla de OAuth de la plataforma, donde el usuario debe iniciar sesión en esa plataforma y conceder acceso explícito al alcance que solicita tu aplicación. El usuario final también ve el nombre de tu aplicación y toda la información que proporcionaste al configurar tu aplicación en esa plataforma.

Apple iCloud no funciona así. iCloud no tiene un flujo OAuth estándar como Google Calendar u Outlook. Para conectar el iCloud Calendar de un usuario, te autenticas con su Apple ID mediante Basic Authentication sobre SSL. Como la mayoría de las cuentas de iCloud tienen activada la autenticación de dos factores, el usuario debe crear una contraseña específica de la app para tu aplicación desde la configuración de su Apple ID, en lugar de usar su contraseña principal de Apple.

Tu aplicación solicitará al usuario su correo de iCloud/Apple ID y esta contraseña específica de la app de 16 caracteres. Con estas credenciales, podrás conectarte al servicio CalDAV de iCloud.

Además de los motivos mencionados, Apple exige contraseñas específicas de la app para el acceso de terceros al calendario con el fin de mejorar la seguridad. Compartir tu contraseña principal de Apple iCloud con una aplicación de terceros nunca es una buena idea.

En segundo plano, la cabecera de tu solicitud HTTP incluirá la contraseña específica de la app codificada en Base64, en este formato: Authorization: Basic <app-specific-password-here>

¿Qué métodos admite la API de iCloud Calendar?

El servicio CalDAV de iCloud se aloja en caldav.icloud.com. Tras autenticarte, están disponibles los siguientes métodos:

  • Listar los calendarios de un usuario
  • CRUD de eventos
  • Obtener eventos específicos

Para obtener más información sobre el estándar CalDAV, lee RFC 4791, que explica todos los métodos disponibles, filtros y más.

A continuación, resumo la información más importante que necesitas para tu integración con iCloud Calendar.

Verbos HTTP compatibles

Verbo HTTPQué hace en CalDAVCompatibilidad iCloudObservaciones
OPTIONSDescubre las capacidades del servidorÚtil para depurar; no requerido en producción.
PROPFINDBusca principals, calendar-home-set, lista calendarios, obtiene propiedadesAutentícate antes; usa Depth 0 o 1.
MKCALENDARCrea una nueva colección de calendarioRequiere permisos de escritura; ver Sección 5.3.1.
REPORTConsulta datos (calendar-query, calendar-multiget, free-busy-query)Los tres informes son obligatorios y están presentes en iCloud.
PUTSube/reemplaza un recurso .ics (evento/tarea)Debes enviar un VCALENDAR completo; no hay PATCH.
DELETEElimina un evento o calendarioUsa If-Match con ETag para mayor seguridad.
COPY / MOVECopia o mueve eventos entre calendariosSujetos a las mismas precondiciones de PUT.
GETObtiene un recurso .ics individualDevuelve text/calendar y ETag.

Propiedades de la colección de calendarios

PropiedadPropósitoNotas específicas de iCloud
CALDAV:calendar-descriptionDescripción legibleTotalmente compatible
CALDAV:calendar-timezoneZona horaria predeterminada para consultasCompatible
CALDAV:supported-calendar-component-setComponentes aceptados (VEVENT, VTODO)Eventos y tareas se guardan en calendarios separados
CALDAV:supported-calendar-dataMIME/versión permitida (text/calendar 2.0)Predeterminado
CALDAV:max-resource-sizeTamaño máximo por eventoLímite aproximado de 20 MB
CALDAV:min/max-date-time, max-instances, max-attendees-per-instanceLímites adicionales del servidorRespétalos para evitar errores 403/507

¿Qué bibliotecas puedes usar para simplificar la integración?

Trabajar con CalDAV, ICS, XML y otros detalles propios de iCloud Calendar no es lo ideal. Dedicarás mucho tiempo a entender cada método, convertir XML a JSON y encajarlo en tu código.

Para una integración más sencilla, recomendamos las siguientes bibliotecas:

  • tsdav : Imprescindible si usas JavaScript/TypeScript como lenguaje de programación. Con tsdav puedes comunicarte fácilmente con el servidor de iCloud sin usar la sintaxis ni la terminología propias de CalDAV. tsdav proporciona una API TypeScript de alto nivel que envuelve todos los verbos HTTP y el XML que de otra forma escribirías a mano (PROPFIND, REPORT, MKCALENDAR, PUT, DELETE, etc.). Consulta la documentación de tsdav para saber más sobre cómo funciona.
  • ical-generator : Cuando integras iCloud Calendar a través de CalDAV, tienes que subir y reemplazar archivos .ics completos cada vez que creas o actualizas un evento. Escribir esos archivos a mano es propenso a errores, y cada VEVENT necesita las cabeceras correctas, el UID, el formato de DTSTART/DTEND, las cadenas RRULE, las zonas horarias y más. ical-generator te ayuda con todos estos problemas.
  • ical.js es un analizador/motor de JavaScript puro creado por el equipo de Mozilla Calendar, usado para convertir las respuestas ICS en clases de JS.

La siguiente tabla explica el papel de tsdav en la integración con iCloud Calendar:

Rol en la pilaQué hace tsdavPor qué importa en iCloud
Cliente CalDAV/WebDAVProporciona una API TypeScript de alto nivel que envuelve los verbos HTTP y el XML necesario.Evita generar XML manualmente y parsear respuestas multistatus.
Ayudantes de descubrimientocreateDAVClient() sigue automáticamente el flujo de descubrimiento de CalDAV: contacta caldav.icloud.com, encuentra el principal del usuario, resuelve calendar-home-set y guarda la URL base pXX-caldav.icloud.com correcta para las llamadas posteriores.Elimina el código repetitivo del descubrimiento en dos pasos propio de iCloud.
Envoltorios de autenticaciónAyudantes integrados para autenticación Basic y OAuth 2. Para iCloud, pasas { username: 'user@icloud.com', password: '<app-specific-pw>', authMethod: 'Basic' }.No necesitas codificar las credenciales en Base64 ni inyectar cabeceras tú mismo.
Ayudantes tipados para tareas comunesfetchCalendars(), fetchCalendarObjects(), createCalendarObject(), updateCalendarObject() y deleteCalendarObject() devuelven/aceptan objetos JS planos en lugar de XML.Implementa CRUD rápidamente sin preocuparte por la sintaxis XML de la RFC 4791.
Soporte de sync tokensyncCollection() envuelve el REPORT sync-collection y devuelve solo cambios/eliminaciones.Permite sondear iCloud (no hay push) con una sola línea.
Compatibilidad navegador + NodeFunciona en Node y navegador gracias a fetch isomórfico.Útil si parte de tu app vive en una extensión o SPA.
Proyecto TS modernoTipos completos, módulos ES con soporte de tree-shaking y pocas dependencias.Fácil de integrar en flujos de trabajo modernos.

Ejemplo de integración de iCloud Calendar usando tsdav + TypeScript

En este ejemplo se asume que ya has obtenido el nombre de usuario y la contraseña específica de la app del usuario.

Método createClient que crea el DAVClient

import { DAVCalendar, DAVClient, DAVNamespaceShort, DAVObject } from "tsdav";

const APPLE_DAV_URL = "https://caldav.icloud.com";

function createClient({
  username,
  password,
}: {
  username: string;
  password: string;
}) {
  const client = new DAVClient({
    serverUrl: APPLE_DAV_URL,
    credentials: {
      username,
      password,
    },
    authMethod: "Basic",
    defaultAccountType: "caldav",
  });

  return client;
}

Archivo constants

/*
The prodId value is a required field that must appear on every calendar object.
The field is a globally-unique identifier for the software that 
produced the file. tsdav might automatically generate that information for you,
but it might be best if you provide it manually.

You might find it useful when you have edge cases, you can use it to tell which
program wrote the data.

*/

export const PROD_ID = {
  company: "your-company-name-here",
  product: "your-product-name-here",
};

Método getCalendars para obtener todos los calendarios

  async getCalendars(
    ...params: Parameters<typeof DAVClient.prototype.fetchCalendars>
  ): Promise<DAVCalendar[]> {
  
  // You can abstract this initialization into another method.
  // For the sake of simplicity, we'll initialize the client on each method.
   const client = createClient({
    username: <email-here>,
    password: <password-here>,
  });
  
    return client.fetchCalendars(...params);
  }

Método getCalendarById

  async getCalendarById(calendarUrl: string): Promise<DAVCalendar> {
    const calendars = await getCalendars();
    const calendar = calendars.find((el) => el.url === calendarUrl);
    if (!calendar) {
      throw new Error(`Apple Calendar with id ${calendarUrl} not found`);
    }

    return calendar;
  }

Método getCalendarEvents para listar eventos

  async getCalendarEvents(
    calendarUrl: string,
    query: GetCalendarEventsQuery = {}
  ) {
    const client = createClient({
      username: <email-here>,
      password: <password-here>,
    });

    const calendar = await getCalendarById(calendarUrl);

    const events = await client.fetchCalendarObjects({
        calendar,
        timeRange: query.dateRange ?? undefined,
      })

    return { events, nextSyncToken: calendar.syncToken };
  }

Método getEventById

  async getEventById(calendarUrl: string, eventId: string) {
    const client = createClient({
      username: <email-here>,
      password: <password-here>,
    });
    
    const eventUrl = new URL(`${eventId}.ics`, calendarUrl).pathname;

    const responses = await client.calendarMultiGet({
        url: calendarUrl,
        props: {
          [`${DAVNamespaceShort.DAV}:getetag`]: {},
          [`${DAVNamespaceShort.CALDAV}:calendar-data`]: {},
        },
        objectUrls: [eventUrl],
        depth: "1",
      })
    

    if (responses.length === 0) {
      throw new Error(
        `Received no response while fetching ${eventUrl}`,
        null
      );
    } else if (responses[0].status >= 400) {
      throw new Error(
        `Failed to get Apple event by id. Status: ${responses[0].statusText}`,
        responses[0]
      );
    }

    const response = responses[0];
    const calendarObject: DAVObject = {
      url: new URL(response.href ?? "", calendarUrl).href,
      etag: `${response.props?.getetag}`,
      data:
        response.props?.calendarData?._cdata ?? response.props?.calendarData,
    };

    try {
      return calendarObject;
    } catch (err: any) {
      this.logger.error("Failed to process Apple Response", {
        message: err.message,
        event: calendarObject,
      });
      return [];
    }
  }

Método createEvent

  async createEvent(calendarUrl: string, data: AppleEvent) {
    const client = createClient({
      username: <email-here>,
      password: <password-here>,
    });

    const eventId = data.id ?? <generate-id-here>

    const calendar = ical({
	    prodId: PROD_ID,
      method: ICalCalendarMethod.REQUEST,
    });
    
    const event = calendar.createEvent({ ...data, id: eventId });

    const response = await  client.createCalendarObject({
        calendar: {
          url: calendarUrl,
        },
        filename: `${event.id()}.ics`,
        iCalString: calendar.toString(),
      })
      
    if (!response.ok) {
      throw new Error(
        `Failed to create Apple event: ${response.statusText}`,
        response
      );
    }
    
    return { id: event.id(), eventWithExceptions };
  }

Método updateEvent

  async updateEvent(calendarUrl: string, eventId: string, data: AppleEvent) {
    const client = createClient({
      username: <email-here>,
      password: <password-here>,
    });

		
    const originalEventData = await getEventById(calendarUrl, eventId);

    const calendar = ical({
      prodId: PROD_ID,
      method: ICalCalendarMethod.REQUEST,
    });

    for (let event of originalEventData) {
      if (event.status === ICalEventStatus.CANCELLED) continue;

      if (event.id === eventId) {
        calendar.createEvent({ ...event, ...data, id: eventId, url: null });
      } else {
        calendar.createEvent({ ...event, id: eventId, url: null });
      }
    }

    const calendarObjectUrl = new URL(`${eventId}.ics`, calendarUrl);

    const response = await
      client.updateCalendarObject({
        calendarObject: {
          url: calendarObjectUrl.href,
          data: calendar.toString(),
        },
      })

    if (!response.ok) {
      throw new Error(
        `Failed to update Apple event: ${response.statusText}`,
        response
      );
    }

    return getEventById(calendarUrl, eventId);
  }

Método deleteEvent

  async deleteEvent(calendarUrl: string, eventId: string) {
    const client = createClient({
      username: <email-here>,
      password: <password-here>,
    });
    

    const calendarObjectUrl = new URL(`${eventId}.ics`, calendarUrl);

    const response = await client.deleteCalendarObject({
        calendarObject: {
          url: calendarObjectUrl.href,
        },
      })

    if (!response.ok) {
      throw new Error(
        `Failed to delete Apple event: ${response.statusText}`,
        response
      );
    }

    return { id: eventId };
  }

¿Qué limitaciones tiene Apple iCloud Calendar?

  1. Sin API REST ni respuestas JSON: Para comunicarte con la API de iCloud Calendar, necesitas usar CalDAV y trabajar con archivos .ics. No es lo ideal, ya que la API de Outlook Calendar y la API de Google Calendar tienen APIs REST que trabajan con JSON y son mucho más productivas. Puedes usar las bibliotecas que sugerí para facilitar la comunicación con la API de iCloud Calendar, pero al final dependerá del lenguaje y framework que uses, ya que no todos los frameworks tienen bibliotecas disponibles que faciliten la comunicación con la API de iCloud Calendar.
  2. Documentación escasa : Aunque hemos enlazado la documentación oficial de CalDAV, debes tener en cuenta que no todos los métodos funcionan al comunicarte con la API de iCloud Calendar, así que prepárate para sorpresas y muchas pruebas.
  3. Basic Auth con contraseña específica de la app : Como mencionamos, iCloud no sigue las convenciones estándar de OAuth 2.0; debes usar una contraseña específica de la app para autenticarte y comunicarte con iCloud Calendar.
  4. Sin soporte para webhooks/notificaciones push : A diferencia de Google Calendar u Outlook, iCloud no tiene la mejor API de calendario , ya que no puedes registrar webhooks para recibir notificaciones de cambios en los calendarios. Las aplicaciones de terceros no pueden suscribirse a actualizaciones en vivo. Una alternativa es sondear periódicamente y usar el informe sync-collection para obtener los cambios de forma eficiente.
  5. Sin soporte para el método PATCH : No puedes usar PATCH para actualizar eventos parcialmente; en su lugar, debes hacer un PUT completo para actualizar eventos.
  6. Sin control sobre las invitaciones : iCloud Calendar gestiona automáticamente las invitaciones a reuniones. Si creas o modificas un evento con asistentes, iCloud Calendar enviará las invitaciones y actualizará el estado de los asistentes. No puedes usar el Outbox/Inbox de programación de CalDAV para controlar manualmente las invitaciones.

¿Existe una forma más sencilla de integrar iCloud Calendar en mi aplicación?

Integrar iCloud Calendar no es una tarea pequeña. iCloud no sigue las convenciones habituales de calendario y muchos métodos y filtros de CalDAV que deberían funcionar simplemente no lo hacen.

Una forma más sencilla de integrar iCloud Calendar en tu aplicación es usar una API de calendario unificado.

Apiroc Unified Calendar API Illustration

Usar una API de calendario unificado como Apiroc tiene las siguientes ventajas:

  • Integra iCloud Calendar en tu aplicación a través de una API bien documentada y probada que sigue los estándares modernos. Con Apiroc, una cuenta de iCloud se conecta con el correo del Apple ID y una contraseña específica de la app mediante una sola llamada a la API, y los eventos se devuelven en JSON en lugar de ICS.
  • Dedica menos tiempo al desarrollo y mantenimiento de la integración. La mayor parte del trabajo ya está hecha: la API, el cliente y los casos extremos. Tampoco tendrás que mantener la integración ni corregir los nuevos casos extremos que aparezcan con el tiempo.
  • Integra otros proveedores de calendario además de iCloud Calendar sin trabajo adicional. Apiroc es compatible con Google Calendar y Microsoft Outlook a través de la misma API, así que añadir más proveedores a tu aplicación es fácil.
  • Webhooks para Google Calendar y Outlook, con soporte para iCloud planificado. Apiroc envía hoy notificaciones en tiempo real para los calendarios de Google y Microsoft. Los webhooks para calendarios de iCloud están planificados. Hasta que lleguen, todavía tienes que sondear los cambios de iCloud, algo que Apiroc simplifica con tokens de sincronización. Apiroc Unified Calendar API - Support for webhooks

Usa Apiroc para integrar iCloud Calendar en tu aplicación

No tienes que lidiar tú mismo con todas las complicaciones de integrar iCloud Calendar en tu aplicación. En su lugar, utiliza Apiroc, una API de calendario unificado creada por un equipo con años de experiencia en integraciones de calendario. El plan gratuito incluye hasta 10 End User Accounts y solicitudes ilimitadas a la API. Puedes registrarte gratis, sin tarjeta de crédito.

Preguntas frecuentes

¿Qué protocolo utiliza iCloud Calendar?

Usa CalDAV sobre HTTP y almacena los eventos en formato iCalendar (ICS).

¿iCloud Calendar ofrece una API REST?

No. CalDAV es la única forma de leer y escribir datos de iCloud Calendar.

¿Cómo se autentica en iCloud Calendar?

Se envía el Apple ID del usuario (normalmente el correo) y una contraseña específica de la app, de 16 caracteres, mediante Basic Auth sobre SSL.

¿iCloud Calendar admite notificaciones push o webhooks?

No. Debes sondear el servidor para detectar cambios.

¿Qué bibliotecas simplifican la integración?

tsdav (cliente CalDAV), ical-generator (creación de ICS) y ical.js (análisis de ICS) manejan la mayor parte de los detalles de bajo nivel.

¿Cómo evito tratar directamente con CalDAV e ICS?

Usa una API de calendario unificado como Apiroc, que envuelve iCloud, Google y Outlook en una sola interfaz JSON moderna.