Zum Inhalt springen
Zur App

Termine und Wiederholungen

Öffnen in ChatGPT

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.

PUT /v1/publisher/calendars/{calendar_id}/events/{external_id}
Terminal-Fenster
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
}
  • event ist der Termin so, wie wir ihn gespeichert haben: Zeiten als lokale Zeit in timezone, HTML als Text.
  • sequence zählt jede echte Änderung hoch.
  • Schickst du denselben Termin noch einmal, antwortet die API mit 200 und "changed": false. Es wird nichts geschrieben, der Feed bleibt unverändert. Du kannst also bei jedem Speichern in deinem System einfach PUT aufrufen.
  • PUT ersetzt den ganzen Termin. Felder, die du weglässt, sind danach leer bzw. auf dem Standardwert.

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.

FeldRegelnStandard
titlePflicht, 1 bis 255 Zeichen
startPflicht. Datum oder Datum mit Uhrzeit, siehe Zeiten
endoptional. Bei Terminen mit Uhrzeit nicht vor start; bei ganztägigen Terminen exklusivstart + default_duration des Kalenders bzw. ein Tag
timezoneIANA-Name, z. B. Europe/BerlinZeitzone des Kalenders
all_daytrue oder falsetrue, wenn start ein reines Datum ist
descriptionText oder HTML, bis 65.536 Zeichen. HTML wird zu Text; Links bleiben als Text (URL) erhalten. Gespeichert werden höchstens 8.000 Zeichen.
locationbis 500 Zeichen
urlhttps://-URL, bis 2.000 Zeichen. Wird in Apps als Link gezeigt und zusätzlich an die Beschreibung angehängt.
statusconfirmed, tentative oder cancelledconfirmed
show_asfree oder busy: ob der Termin bei Abonnenten als belegt giltfree
categoriesbis 10 Einträge à 1 bis 50 Zeichen[]
recurrenceSerie, siehe Wiederholungen
metadataBeliebiges 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.

start / endBedeutung
"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".

{
"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" }
]
}
}
FeldRegeln
rules1 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_datesbis 1.000 Termine, die ausfallen und spurlos verschwinden. Ein Datum genügt; bei Serien mit Uhrzeit ist damit der Termin an diesem Tag gemeint.
overridesbis 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“.

  • original_start bezeichnet 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, url und status. Weggelassene Felder übernehmen den Wert der Serie.
  • "status": "cancelled" sagt nur diesen einen Termin ab. Soll er spurlos verschwinden, nimm stattdessen except_dates.
  • Overrides sind Teil des Termins: Jedes PUT schickt 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).

So bildest du übliche Felder eines Event-Plugins (WordPress und ähnliche) ab:

Im PluginIn der API
Post-IDexternal_id, z. B. "wp-1482"
Titeltitle
Inhalt / Beschreibung (HTML)description. HTML geht direkt; besser ist ein kurzer Auszug plus url.
Permalink der Veranstaltungurl
Veranstaltungsort, Adresselocation, z. B. "Studio am Fluss, Hauptstraße 5, Berlin"
Startdatum + Startzeitstart: "2026-11-12T18:00:00"
Enddatum + Endzeitend: "2026-11-12T19:30:00"
Zeitzone der Website (timezone_string)timezone am Kalender, nur bei Abweichung am Termin
„Ganztägig“ angehaktstart/end als reines Datum, end + 1 Tag
Kategorien, Schlagwörtercategories
Status „abgesagt“"status": "cancelled"
Status „vorläufig“"status": "tentative"
Beitrag in den Papierkorb, Entwurf, privatDELETE auf den Termin
Eigene Felder (Preis, Ticket-ID …)metadata (nur für dich) oder als Text in description (für Abonnenten)

Wiederholungseinstellungen:

Einstellung im PluginRRULE
Täglich / wöchentlich / monatlich / jährlichFREQ=DAILY / WEEKLY / MONTHLY / YEARLY
Alle 2 WochenFREQ=WEEKLY;INTERVAL=2
Dienstags und donnerstagsFREQ=WEEKLY;BYDAY=TU,TH
Jeden zweiten Montag im MonatFREQ=MONTHLY;BYDAY=2MO
Letzter Freitag im MonatFREQ=MONTHLY;BYDAY=-1FR
Am 15. jedes MonatsFREQ=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 verschobenrecurrence.overrides

Für PHP baut und prüft die MIT-lizenzierte Bibliothek rlanvin/php-rrule solche Regeln.

Terminal-Fenster
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.

Terminal-Fenster
curl "https://api.kalender-sync.de/v1/publisher/calendars/$KS_CAL/events?limit=100" -H "Authorization: Bearer $KS_KEY"
ParameterBedeutung
limit1 bis 100, Standard 100
cursornext_cursor der vorigen Seite
updated_sinceISO-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.

Terminal-Fenster
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.

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[].codeBedeutung
invalid_timezoneUnbekannte Zeitzone
invalid_dateDatum unlesbar (ganztägige Termine, except_dates)
invalid_datetimeDatum mit Uhrzeit unlesbar, oder ein Override passt nicht zur Serie
end_before_startend liegt vor start (ganztägig: nicht danach)
invalid_rruleRegel syntaktisch falsch, z. B. COUNT und UNTIL zusammen
unsupported_rruleRegelteil oder Frequenz wird nicht unterstützt
duplicate_overrideEin Einzeltermin wurde doppelt überschrieben
too_largemetadata 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.