Termine und Wiederholungen
Ein Termin ist ein einzelner Termin oder eine ganze Serie. Du adressierst ihn mit deiner eigenen external_id, zum Beispiel der Post-ID aus WordPress. Eine ID von uns musst du nicht speichern.
Termin anlegen oder ändern
Abschnitt betitelt „Termin anlegen oder ändern“PUT /v1/publisher/calendars/{calendar_id}/events/{external_id}curl -X PUT https://api.kalender-sync.de/v1/publisher/calendars/$KS_CAL/events/wp-1482 \ -H "Authorization: Bearer $KS_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Sunset Yoga im Park", "description": "<p>Bring eine <strong>Matte</strong> mit. Für Mitglieder kostenlos.</p>", "location": "Riverside Park, Steg 2, Berlin", "url": "https://example.org/kurse/sunset-yoga/", "start": "2026-11-12T18:00:00", "end": "2026-11-12T19:30:00", "timezone": "Europe/Berlin", "status": "confirmed", "categories": ["Outdoor"], "metadata": { "post_id": 1482 } }'external_id darf 1 bis 128 Zeichen lang sein: Buchstaben, Ziffern und . _ : -. Bewährt hat sich ein Präfix, etwa wp-1482.
Antwort: 201 Created beim ersten Schreiben, 200 OK bei einer Änderung.
{ "external_id": "wp-1482", "calendar_id": "cal_XXXXXXXXXXXX", "sequence": 0, "created_at": "2026-11-03T14:05:40.000Z", "updated_at": "2026-11-03T14:05:40.000Z", "event": { "title": "Sunset Yoga im Park", "all_day": false, "start": "2026-11-12T18:00:00", "end": "2026-11-12T19:30:00", "timezone": "Europe/Berlin", "status": "confirmed", "show_as": "free", "categories": ["Outdoor"], "description": "Bring eine Matte mit. Für Mitglieder kostenlos.", "location": "Riverside Park, Steg 2, Berlin", "url": "https://example.org/kurse/sunset-yoga/", "metadata": { "post_id": 1482 } }, "changed": true}eventist der Termin so, wie wir ihn gespeichert haben: Zeiten als lokale Zeit intimezone, HTML als Text.sequencezählt jede echte Änderung hoch.- Schickst du denselben Termin noch einmal, antwortet die API mit
200und"changed": false. Es wird nichts geschrieben, der Feed bleibt unverändert. Du kannst also bei jedem Speichern in deinem System einfachPUTaufrufen. PUTersetzt den ganzen Termin. Felder, die du weglässt, sind danach leer bzw. auf dem Standardwert.
Optimistisches Sperren mit If-Match
Abschnitt betitelt „Optimistisches Sperren mit If-Match“Optional schickst du die zuletzt gesehene sequence mit:
If-Match: "3"Hat sich der Termin inzwischen geändert (oder gibt es ihn noch nicht), kommt 409 sequence_mismatch statt eines Überschreibens.
| Feld | Regeln | Standard |
|---|---|---|
title | Pflicht, 1 bis 255 Zeichen | |
start | Pflicht. Datum oder Datum mit Uhrzeit, siehe Zeiten | |
end | optional. Bei Terminen mit Uhrzeit nicht vor start; bei ganztägigen Terminen exklusiv | start + default_duration des Kalenders bzw. ein Tag |
timezone | IANA-Name, z. B. Europe/Berlin | Zeitzone des Kalenders |
all_day | true oder false | true, wenn start ein reines Datum ist |
description | Text oder HTML, bis 65.536 Zeichen. HTML wird zu Text; Links bleiben als Text (URL) erhalten. Gespeichert werden höchstens 8.000 Zeichen. | |
location | bis 500 Zeichen | |
url | https://-URL, bis 2.000 Zeichen. Wird in Apps als Link gezeigt und zusätzlich an die Beschreibung angehängt. | |
status | confirmed, tentative oder cancelled | confirmed |
show_as | free oder busy: ob der Termin bei Abonnenten als belegt gilt | free |
categories | bis 10 Einträge à 1 bis 50 Zeichen | [] |
recurrence | Serie, siehe Wiederholungen | |
metadata | Beliebiges JSON-Objekt bis 2 KB. Kommt in API-Antworten zurück, erscheint aber nie im Feed oder auf der Abo-Seite. |
show_as steht standardmäßig auf free: Öffentliche Termine sollen den Kalender deiner Abonnenten nicht als belegt markieren.
Ein abgesagter Termin ("status": "cancelled") bleibt im Feed und ist dort als abgesagt markiert; wie das aussieht, hängt von der App ab. Soll er ganz verschwinden, lösche ihn.
Zeiten und Zeitzonen
Abschnitt betitelt „Zeiten und Zeitzonen“start / end | Bedeutung |
|---|---|
"2026-11-12T18:00:00" | Lokale Zeit in timezone (bzw. der Zeitzone des Kalenders). Empfohlen. |
"2026-11-12T17:00:00Z" oder "2026-11-12T18:00:00+01:00" | Zeitpunkt mit Offset. Wird in timezone umgerechnet und so gespeichert. |
"2026-11-12" | Reines Datum: ganztägiger Termin. |
Lokale Zeit plus IANA-Zeitzone ist der sicherste Weg: Sommer- und Winterzeit stimmen dann auch bei Serien über die Zeitumstellung hinweg.
Ganztägige Termine: end ist exklusiv. Ein Termin nur am 12. November hat "start": "2026-11-12" und "end": "2026-11-13" oder gar kein end. Ein dreitägiges Festival vom 12. bis 14. November hat "end": "2026-11-15".
Wiederholungen
Abschnitt betitelt „Wiederholungen“{ "title": "Morning Flow", "start": "2026-11-03T07:30:00", "end": "2026-11-03T08:30:00", "timezone": "Europe/Berlin", "recurrence": { "rules": ["FREQ=WEEKLY;BYDAY=TU;UNTIL=20270330"], "except_dates": ["2026-12-22", "2026-12-29"], "overrides": [ { "original_start": "2026-11-17T07:30:00", "start": "2026-11-17T08:00:00", "end": "2026-11-17T09:00:00", "location": "Studio 2" }, { "original_start": "2026-12-01T07:30:00", "status": "cancelled" } ] }}| Feld | Regeln |
|---|---|
rules | 1 bis 5 RRULE-Strings nach RFC 5545, je bis 500 Zeichen. Das Präfix RRULE: ist optional. Mehrere Regeln werden im Feed zu getrennten Serien. |
except_dates | bis 1.000 Termine, die ausfallen und spurlos verschwinden. Ein Datum genügt; bei Serien mit Uhrzeit ist damit der Termin an diesem Tag gemeint. |
overrides | bis 500 geänderte Einzeltermine, siehe unten. |
Erlaubte RRULE-Teile: FREQ (DAILY, WEEKLY, MONTHLY, YEARLY), INTERVAL (1 bis 1.000), COUNT (1 bis 1.000), UNTIL, BYDAY, BYMONTHDAY, BYMONTH, BYSETPOS, WKST. COUNT und UNTIL schließen sich aus. Andere Teile wie BYHOUR oder FREQ=HOURLY werden mit unsupported_rrule abgelehnt.
UNTIL darf YYYYMMDD, YYYYMMDDTHHMMSS (lokale Zeit in der Zeitzone des Termins) oder YYYYMMDDTHHMMSSZ (UTC) sein. Wir rechnen es so um, wie RFC 5545 es verlangt. Ein reines Datum bei einem Termin mit Uhrzeit heißt „bis einschließlich diesen Tag“.
Einzeltermine ändern: overrides
Abschnitt betitelt „Einzeltermine ändern: overrides“original_startbezeichnet den Termin so, wie die Regel ihn erzeugt, in der Zeitzone des Termins (bei ganztägigen Serien ein Datum).- Ändern kannst du
start,end,title,description,location,urlundstatus. Weggelassene Felder übernehmen den Wert der Serie. "status": "cancelled"sagt nur diesen einen Termin ab. Soll er spurlos verschwinden, nimm stattdessenexcept_dates.- Overrides sind Teil des Termins: Jedes
PUTschickt die vollständige Liste. Ein Override, den du weglässt, fällt auf die Serie zurück. - Jeder Termin der Serie darf nur einmal überschrieben werden (sonst
duplicate_override).
Mapping typischer Plugin-Felder
Abschnitt betitelt „Mapping typischer Plugin-Felder“So bildest du übliche Felder eines Event-Plugins (WordPress und ähnliche) ab:
| Im Plugin | In der API |
|---|---|
| Post-ID | external_id, z. B. "wp-1482" |
| Titel | title |
| Inhalt / Beschreibung (HTML) | description. HTML geht direkt; besser ist ein kurzer Auszug plus url. |
| Permalink der Veranstaltung | url |
| Veranstaltungsort, Adresse | location, z. B. "Studio am Fluss, Hauptstraße 5, Berlin" |
| Startdatum + Startzeit | start: "2026-11-12T18:00:00" |
| Enddatum + Endzeit | end: "2026-11-12T19:30:00" |
Zeitzone der Website (timezone_string) | timezone am Kalender, nur bei Abweichung am Termin |
| „Ganztägig“ angehakt | start/end als reines Datum, end + 1 Tag |
| Kategorien, Schlagwörter | categories |
| Status „abgesagt“ | "status": "cancelled" |
| Status „vorläufig“ | "status": "tentative" |
| Beitrag in den Papierkorb, Entwurf, privat | DELETE auf den Termin |
| Eigene Felder (Preis, Ticket-ID …) | metadata (nur für dich) oder als Text in description (für Abonnenten) |
Wiederholungseinstellungen:
| Einstellung im Plugin | RRULE |
|---|---|
| Täglich / wöchentlich / monatlich / jährlich | FREQ=DAILY / WEEKLY / MONTHLY / YEARLY |
| Alle 2 Wochen | FREQ=WEEKLY;INTERVAL=2 |
| Dienstags und donnerstags | FREQ=WEEKLY;BYDAY=TU,TH |
| Jeden zweiten Montag im Monat | FREQ=MONTHLY;BYDAY=2MO |
| Letzter Freitag im Monat | FREQ=MONTHLY;BYDAY=-1FR |
| Am 15. jedes Monats | FREQ=MONTHLY;BYMONTHDAY=15 |
| Endet nach 10 Terminen | ;COUNT=10 |
| Endet am 25. März 2027 | ;UNTIL=20270325 |
| Ausgenommene Tage (Ferien, Feiertage) | recurrence.except_dates |
| Einzelner Termin verschoben | recurrence.overrides |
Für PHP baut und prüft die MIT-lizenzierte Bibliothek rlanvin/php-rrule solche Regeln.
Termin lesen
Abschnitt betitelt „Termin lesen“curl https://api.kalender-sync.de/v1/publisher/calendars/$KS_CAL/events/wp-1482 -H "Authorization: Bearer $KS_KEY"Gibt es den Termin nicht: 404 event_not_found.
Termine auflisten
Abschnitt betitelt „Termine auflisten“curl "https://api.kalender-sync.de/v1/publisher/calendars/$KS_CAL/events?limit=100" -H "Authorization: Bearer $KS_KEY"| Parameter | Bedeutung |
|---|---|
limit | 1 bis 100, Standard 100 |
cursor | next_cursor der vorigen Seite |
updated_since | ISO-Zeitpunkt mit Offset, z. B. 2026-11-01T00:00:00Z: nur Termine, die danach geändert wurden |
Die Antwort enthält data (Termine wie oben) und next_cursor (null auf der letzten Seite). Sortiert wird nach letzter Änderung, sodass updated_since plus Cursor jede Änderung genau einmal liefert. Ein ungültiger Cursor ergibt 400 invalid_cursor.
Termin löschen
Abschnitt betitelt „Termin löschen“curl -X DELETE https://api.kalender-sync.de/v1/publisher/calendars/$KS_CAL/events/wp-1482 -H "Authorization: Bearer $KS_KEY"Antwort immer 204 No Content, auch wenn der Termin schon weg war. Wiederholungen sind also unbedenklich.
Validierungsfehler
Abschnitt betitelt „Validierungsfehler“Ungültige Termine werden komplett abgelehnt, mit 400 invalid_request und allen Problemen auf einmal in errors:
{ "type": "https://docs.kalender-sync.de/en/api/errors/invalid_request", "title": "The request is invalid", "status": 400, "code": "invalid_request", "request_id": "req_…", "errors": [ { "field": "end", "code": "end_before_start", "message": "end must not be before start." }, { "field": "recurrence.rules[0]", "code": "unsupported_rrule", "message": "BYHOUR is not supported." } ]}errors[].code | Bedeutung |
|---|---|
invalid_timezone | Unbekannte Zeitzone |
invalid_date | Datum unlesbar (ganztägige Termine, except_dates) |
invalid_datetime | Datum mit Uhrzeit unlesbar, oder ein Override passt nicht zur Serie |
end_before_start | end liegt vor start (ganztägig: nicht danach) |
invalid_rrule | Regel syntaktisch falsch, z. B. COUNT und UNTIL zusammen |
unsupported_rrule | Regelteil oder Frequenz wird nicht unterstützt |
duplicate_override | Ein Einzeltermin wurde doppelt überschrieben |
too_large | metadata größer als 2 KB |
Verstöße gegen das Schema (fehlendes Pflichtfeld, unbekanntes Feld, zu langer Text) tragen den Code des Validators, z. B. invalid_type, too_big oder unrecognized_keys.
Weitere mögliche Fehler beim Schreiben: event_limit_reached, calendar_not_found, plan_required, calendar_blocked.