So integrieren Sie die iCloud Calendar API in Ihre App

Veröffentlicht am

In einem früheren Artikel haben wir erläutert, wie Sie die Google Calendar API in Ihre App integrieren, und dabei alle nötigen Schritte für eine erfolgreiche Integration gezeigt.

Die Integration von iCloud Calendar ist nicht so geradlinig wie bei Google Calendar. Die Dokumentation ist dünn, und Apple erklärt nicht genau, was Entwickler tun müssen, um iCloud Calendar in eine Anwendung einzubinden.

In diesem Artikel schauen wir uns die Integration der iCloud Calendar API im Detail an. Wir behandeln Authentifizierung, unterstützte Funktionen, Einschränkungen, Codebeispiele und Tools, die die Integration vereinfachen.

Welche Protokolle und Standards verwendet Apple iCloud Calendar?

Apple iCloud Calendar nutzt den CalDAV-Standard (eine Erweiterung von WebDAV) für die Kalenderkommunikation.

Ereignisse werden im ICS-Format (iCalendar) gespeichert, einem textbasierten Format für Kalenderdaten. Ihre Anwendung kann also über HTTP mit CalDAV-Anfragen und ICS-Daten mit iCloud-Kalendern kommunizieren.

Vorteil: Die Implementierung ist nicht an Apple-Geräte oder -Betriebssysteme gebunden und kann auf jedem Server unter jedem Betriebssystem eingesetzt werden.

Nachteil: iCloud stellt keine REST-API für Kalender bereit; CalDAV ist daher der einzige Weg, iCloud Calendar in Ihre App einzubinden.

Wie authentifiziert man sich bei iCloud?

Normalerweise legt man bei einer Plattform (z. B. Google Calendar) ein Entwicklerkonto an, erstellt eine App, konfiguriert Scopes, fügt Testnutzer hinzu, trägt App-Informationen ein und lässt alles genehmigen. Danach leitet man den Endnutzer über den OAuth-Dialog zur Einwilligung.

Bei Apple iCloud funktioniert das anders. iCloud bietet keinen standardisierten OAuth-Flow wie Google Calendar oder Outlook. Um den Kalender eines Nutzers zu verbinden, authentifizieren Sie sich mit dessen Apple-ID über Basic Authentication (SSL). Da die meisten iCloud-Konten die Zwei-Faktor-Authentifizierung aktiviert haben, muss der Nutzer in seinen Apple-ID-Einstellungen ein app-spezifisches Passwort erstellen, statt sein Hauptpasswort zu verwenden.

Ihre Anwendung fragt also die iCloud-E-Mail/Apple-ID sowie dieses 16-stellige app-spezifische Passwort ab. Mit diesen Daten verbinden Sie sich mit dem iCloud-CalDAV-Dienst.

Neben den oben genannten Gründen verlangt Apple app-spezifische Passwörter für den Kalenderzugriff durch Drittanbieter, um die Sicherheit zu erhöhen. Das Haupt-Passwort Ihres Apple-iCloud-Kontos mit einer Drittanbieter-Anwendung zu teilen ist nie eine gute Idee.

Im Hintergrund enthält der Header Ihrer HTTP-Anfrage ein Base64-kodiertes app-spezifisches Passwort in diesem Format: Authorization: Basic <app-specific-password-here>

Welche Methoden unterstützt die iCloud Calendar API?

Der iCloud-CalDAV-Dienst ist unter caldav.icloud.com erreichbar. Nach der Authentifizierung stehen diese Operationen bereit:

  • Auflisten der Kalender eines Nutzers
  • CRUD-Operationen für Ereignisse
  • Abrufen einzelner Ereignisse

Mehr über den CalDAV-Standard erfahren Sie in RFC 4791, wo alle verfügbaren Methoden, Filter und mehr erklärt werden.

Nachfolgend fasse ich die wichtigsten Informationen zusammen, die Sie für Ihre iCloud-Calendar-Integration wissen müssen.

Unterstützte HTTP-Verben:

HTTP-VerbFunktion in CalDAViCloud-UnterstützungBesonderheiten
OPTIONSServer-Fähigkeiten ermittelnGut zum Debuggen; zur Laufzeit meist nicht nötig
PROPFINDPrincipals ermitteln, calendar-home-set abrufen, Kalender auflisten, Eigenschaften lesenAuthentifizierung erforderlich; Depth 0 oder 1
MKCALENDARNeue Kalender-Collection anlegenSchreibrechte erforderlich; siehe Abschnitt 5.3.1
REPORTDaten abfragen (calendar-query, calendar-multiget, free-busy-query)Alle drei Reports werden unterstützt
PUTEine .ics-Ressource (Ereignis/Aufgabe) hochladen/ersetzenKomplettes VCALENDAR senden; kein PATCH
DELETEEreignis oder Kalender löschenMit If-Match-ETag kombinieren
COPY / MOVEEreignisse zwischen Kalendern kopieren/verschiebenGleiche Voraussetzungen wie PUT
GETEine .ics-Ressource abrufenLiefert text/calendar und ETag

Eigenschaften einer Kalender-Collection

EigenschaftZweckiCloud-spezifische Hinweise
CALDAV:calendar-descriptionMenschlich lesbare BeschreibungVollständig unterstützt
CALDAV:calendar-timezoneStandard-Zeitzone für AbfragenUnterstützt
CALDAV:supported-calendar-component-setAkzeptierte Komponenten (VEVENT, VTODO)Ereignisse und Aufgaben liegen in separaten Kalendern
CALDAV:supported-calendar-dataErlaubte MIME/Version (text/calendar 2.0)Standardwert bei iCloud
CALDAV:max-resource-sizeMax. Größe pro EreignisiCloud-Limit ca. 20 MB
CALDAV:min/max-date-time, max-instances, max-attendees-per-instanceDiverse ServerlimitsEinhaltung verhindert 403/507-Fehler

Welche Bibliotheken erleichtern die Integration?

Der Umgang mit CalDAV, ICS, XML und den anderen iCloud-spezifischen Besonderheiten ist bei der Integration von iCloud in Ihre Anwendung nicht ideal. Sie verbringen viel Zeit damit, jede Methode zu verstehen, XML in JSON umzuwandeln und alles in Ihren Code einzubinden.

Für eine angenehmere Integration empfehlen wir die folgenden Bibliotheken:

  • tsdav : Ein Muss, wenn Sie JavaScript/TypeScript als Programmiersprache verwenden. Mit tsdav kommunizieren Sie einfach mit dem iCloud-Server, ohne CalDAV-spezifische Syntax oder Terminologie zu verwenden. tsdav bietet eine High-Level-TypeScript-API, die alle HTTP-Verben und das XML kapselt, das Sie sonst manuell schreiben müssten (PROPFIND, REPORT, MKCALENDAR, PUT, DELETE usw.). In der tsdav-Dokumentation erfahren Sie mehr über die Funktionsweise.
  • ical-generator : Wenn Sie iCloud Calendar über CalDAV integrieren, müssen Sie bei jedem Erstellen oder Aktualisieren eines Ereignisses ganze .ics-Dateien hochladen und ersetzen . Diese Dateien von Hand zu schreiben ist fehleranfällig, und jedes VEVENT braucht die richtigen Header, UID, DTSTART/DTEND-Formatierung, RRULE-Strings, Zeitzonen und mehr. ical-generator hilft Ihnen bei all diesen Punkten.
  • ical.js ist ein reiner JavaScript-Parser/-Engine vom Mozilla-Calendar-Team, mit dem ICS-Antworten in JS-Klassen geparst werden.

Die folgende Tabelle erklärt die Rolle von tsdav bei der iCloud-Calendar-Integration:

Rolle im StackWas tsdav machtWarum das für iCloud wichtig ist
CalDAV/WebDAV-ClientBietet eine High-Level-TypeScript-API, die alle HTTP-Verben und das XML kapselt, das Sie sonst manuell schreiben und konvertieren müssten.Sie konzentrieren sich auf die Geschäftslogik statt auf rohe XML-Strings und das Parsen von Multistatus-Antworten.
Discovery-HelfercreateDAVClient() folgt automatisch dem CalDAV-Discovery-Flow: Verbindung zu caldav.icloud.com, Principal des Nutzers ermitteln, calendar-home-set auflösen und die richtige pXX-caldav.icloud.com-Basis-URL für spätere Aufrufe speichern.Spart den Boilerplate für die zweistufige Discovery, die nur bei iCloud nötig ist.
Auth-WrapperEingebaute Helfer für Basic- und OAuth 2-Auth. Für iCloud übergeben Sie { username: 'user@icloud.com', password: '<app-specific-pw>', authMethod: 'Basic' }.Kein manuelles Base64-Kodieren der Zugangsdaten oder Setzen von Headern nötig.
Typisierte Helfer für häufige AufgabenfetchCalendars(), fetchCalendarObjects(), createCalendarObject(), updateCalendarObject(), deleteCalendarObject() liefern und akzeptieren reine JS-Objekte statt XML.Schnelle CRUD-Implementierung ohne Sorge um die RFC-4791-XML-Syntax.
Sync-Token-UnterstützungsyncCollection() kapselt den CalDAV-REPORT sync-collection, verfolgt Tokens und liefert nur geänderte/gelöschte Elemente.Ermöglicht Polling für iCloud (das kein Push hat) mit einem Einzeiler.
Browser- und Node-KompatibilitätLäuft im Server-Code (Node) oder im Browser dank isomorphic fetch.Praktisch, wenn ein Teil Ihrer App in einer Browser-Extension oder SPA läuft.
Typisiertes, modernes TS-ProjektWird mit vollständigen Typdefinitionen, tree-shakable ES-Modulen und minimalen Abhängigkeiten ausgeliefert.Leicht in moderne Build-Pipelines zu integrieren.

Beispiel einer iCloud-Calendar-Integration mit tsdav + ts

In diesem Beispiel gehe ich davon aus, dass Sie den Benutzernamen und das app-spezifische Apple-Passwort des Nutzers bereits haben.

createClient Methode, die den DAVClient erstellt:

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;
}

constants Datei mit Konstanten

/*
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",
};

getCalendars Methode, die alle Kalender abruft

  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);
  }

getCalendarById Methode, die einen Kalender per ID abruft

  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;
  }

getCalendarEvents Methode, die alle Kalenderereignisse auflistet

  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 };
  }

getEventById Methode, die ein Kalenderereignis per ID zurückgibt

  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 [];
    }
  }

createEvent Methode, die ein Ereignis erstellt

  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 };
  }

updateEvent Methode, die ein Ereignis aktualisiert

  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);
  }

deleteEvent Methode, die ein Ereignis per ID löscht

  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 };
  }

Welche Einschränkungen hat Apple iCloud Calendar?

  1. Keine REST-API und keine JSON-Antworten: Um mit der iCloud Calendar API zu kommunizieren, müssen Sie CalDAV verwenden und mit .ics-Dateien arbeiten. Das ist nicht ideal, denn die Outlook Calendar API und die Google Calendar API bieten REST-APIs, die mit JSON arbeiten und deutlich produktiver sind. Sie können die vorgeschlagenen Bibliotheken nutzen, um die Kommunikation mit der iCloud Calendar API zu erleichtern. Letztlich hängt es aber von Ihrer Sprache und Ihrem Framework ab, denn nicht für alle Frameworks gibt es Bibliotheken, die die Kommunikation mit der iCloud Calendar API vereinfachen.
  2. Mangelhafte Dokumentation : Wir haben zwar auf die offizielle CalDAV-Dokumentation verlinkt, aber Sie sollten wissen, dass nicht alle Methoden bei der Kommunikation mit der iCloud Calendar API funktionieren. Stellen Sie sich also auf Stolperfallen und viel Testen ein.
  3. Basic Auth mit app-spezifischem Passwort : Wie erwähnt folgt iCloud nicht den üblichen OAuth-2.0-Konventionen. Sie müssen ein app-spezifisches Passwort verwenden, um sich zu authentifizieren und mit iCloud Calendar zu kommunizieren.
  4. Keine Unterstützung für Webhooks/Push-Benachrichtigungen : Anders als Google Calendar oder Outlook hat iCloud nicht die beste Kalender-API , denn Sie können keine Webhooks registrieren, um über Änderungen in Kalendern informiert zu werden. Drittanbieter-Apps können keine Live-Updates abonnieren. Ein Workaround ist regelmäßiges Polling mit dem sync-collection -Report, um Änderungen effizient abzurufen.
  5. Keine Unterstützung für PATCH-Methoden : Sie können Ereignisse nicht per PATCH teilweise aktualisieren. Stattdessen müssen Sie ein vollständiges PUT ausführen.
  6. Keine Kontrolle über Einladungen : iCloud Calendar verarbeitet Meeting-Einladungen automatisch. Wenn Sie ein Ereignis mit Teilnehmern erstellen oder ändern, versendet iCloud Calendar Einladungen und aktualisiert den Status der Teilnehmer. Die CalDAV-Scheduling-Outbox/-Inbox können Sie nicht verwenden, um Einladungen manuell zu steuern.

Gibt es einen einfacheren Weg, iCloud Calendar in meine Anwendung zu integrieren?

Die Integration von iCloud Calendar in Ihre Anwendung ist keine kleine Aufgabe. iCloud folgt nicht den üblichen Kalender-Konventionen, und viele CalDAV-Methoden und -Filter, die eigentlich funktionieren sollten, tun es schlicht nicht.

Ein einfacherer Weg, iCloud Calendar in Ihre Anwendung zu integrieren, ist eine Einheitliche Kalender-API.

Apiroc Unified Calendar API Illustration

Eine Einheitliche Kalender-API wie Apiroc bietet folgende Vorteile:

  • Integrieren Sie iCloud Calendar in Ihre Anwendung über eine gut dokumentierte und getestete API, die modernen Standards folgt. Mit Apiroc wird ein iCloud-Konto mit der Apple-ID-E-Mail und einem app-spezifischen Passwort über einen einzigen API-Aufruf verbunden, und Ereignisse kommen als JSON statt als ICS zurück.
  • Verbringen Sie weniger Zeit mit der Entwicklung und Wartung der Integration. Der größte Teil der Arbeit ist bereits erledigt: die API, der Client und die Sonderfälle. Sie müssen die Integration auch nicht pflegen oder neue Sonderfälle beheben, die im Laufe der Zeit auftreten.
  • Integrieren Sie neben iCloud Calendar auch andere Kalenderanbieter ohne zusätzlichen Aufwand. Apiroc unterstützt Google Calendar und Microsoft Outlook über dieselbe API, sodass Sie weitere Anbieter problemlos in Ihre Anwendung einbinden können.
  • Webhooks für Google Calendar und Outlook, iCloud-Unterstützung ist geplant. Apiroc sendet heute Echtzeit-Benachrichtigungen für Google- und Microsoft-Kalender. Webhooks für iCloud-Kalender sind geplant. Bis dahin müssen Sie iCloud-Änderungen weiterhin per Polling abfragen, was Apiroc mit Sync-Tokens einfach macht. Apiroc Unified Calendar API - Unterstützung für Webhooks

Nutzen Sie Apiroc, um iCloud Calendar in Ihre Anwendung zu integrieren

Sie müssen sich nicht selbst mit allen Fallstricken der iCloud-Calendar-Integration auseinandersetzen. Nutzen Sie stattdessen Apiroc, eine Einheitliche Kalender-API von einem Team mit jahrelanger Erfahrung in Kalenderintegrationen. Der kostenlose Plan umfasst bis zu 10 End User Accounts und unbegrenzte API-Anfragen. Sie können sich kostenlos registrieren, keine Kreditkarte erforderlich.

Häufig gestellte Fragen

Welches Protokoll verwendet iCloud Kalender?

iCloud Kalender nutzt CalDAV über HTTP und speichert Ereignisse im iCalendar-(ICS-)Format.

Bietet iCloud Kalender eine REST-API?

Nein. CalDAV ist die einzige Möglichkeit, auf iCloud Kalenderdaten zuzugreifen und sie zu ändern.

Wie authentifiziert man sich bei iCloud Kalender?

Man übergibt die Apple-ID des Nutzers (meist die E-Mail) und ein 16-stelliges app-spezifisches Passwort per Basic Auth über SSL.

Unterstützt iCloud Kalender Push-Benachrichtigungen oder Webhooks?

Nein. Änderungen müssen durch Polling erkannt werden.

Welche Bibliotheken erleichtern die Integration?

tsdav (CalDAV-Client), ical-generator (ICS erstellen) und ical.js (ICS parsen) übernehmen die meisten Low-Level-Aufgaben.

Wie kann ich CalDAV und ICS umgehen?

Nutzen Sie eine Einheitliche Kalender-API wie Apiroc, die iCloud, Google und Outlook über eine moderne JSON-Schnittstelle vereint.