# Kalender

Ein Kalender ist ein Abo-Kalender mit eigenem Feed, eigener Abo-Seite und eigenem Button. Viele Websites brauchen genau einen; du kannst aber auch einen pro Kategorie oder Ort anlegen.

## Felder

| Feld | Typ | Regeln | Standard |
|---|---|---|---|
| `name` | String | Pflicht beim Anlegen, 1 bis 100 Zeichen | |
| `timezone` | String | Pflicht beim Anlegen, IANA-Name wie `Europe/Berlin` | |
| `description` | String oder `null` | bis 1.000 Zeichen | `null` |
| `site_url` | String oder `null` | `https://`-URL, bis 2.000 Zeichen | `null` |
| `language` | `de` oder `en` | Sprache der Abo-Seite | `en` |
| `default_duration` | String | ISO-8601-Dauer bis `PT24H`, z. B. `PT1H`, `PT90M`, `PT1H30M`. Gilt für Termine ohne `end`. | `PT1H` |
| `brand_color` | String oder `null` | Hex-Farbe `#rrggbb`, wird kleingeschrieben gespeichert | `null` |
| `organizer` | Objekt oder `null` | `name` (Pflicht, bis 100 Zeichen), `url` (`https://`, optional), `email` (optional). Öffentlich auf der Abo-Seite. | `null` |
| `show_events` | Boolean | Kommende Termine auf der Abo-Seite zeigen | `true` |

Unbekannte Felder werden mit `400 invalid_request` abgelehnt.

## Kalender anlegen

```bash
curl https://api.kalender-sync.de/v1/publisher/calendars \
  -H "Authorization: Bearer $KS_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 3b0f6a52-6a1e-4c55-9d1b-0a7f2c9e1d44" \
  -d '{
    "name": "Riverside Yoga",
    "timezone": "Europe/Berlin",
    "language": "de",
    "site_url": "https://example.org",
    "default_duration": "PT1H",
    "brand_color": "#0f766e",
    "organizer": { "name": "Riverside Yoga e.V.", "url": "https://example.org", "email": "info@example.org" },
    "show_events": true
  }'
```

Antwort `201 Created` mit dem vollständigen Kalender (siehe unten). Der Modus (`live` oder `test`) ergibt sich aus dem Key.

Mit `Idempotency-Key` legt eine Wiederholung nach einem Netzwerkfehler keinen zweiten Kalender an. → [Idempotenz](https://docs.kalender-sync.de/api/idempotency/)

Mögliche Fehler: [`calendar_limit_reached`](https://docs.kalender-sync.de/api/errors/calendar_limit_reached/) (Limit deines Tarifs bzw. 3 Testkalender), [`plan_required`](https://docs.kalender-sync.de/api/errors/plan_required/) (Live-Kalender im Free-Tarif), [`insufficient_scope`](https://docs.kalender-sync.de/api/errors/insufficient_scope/) (Key ist auf bestimmte Kalender beschränkt).

## Die Antwort

Alle Kalender-Endpunkte liefern dieselbe Form:

```json
{
  "id": "cal_XXXXXXXXXXXX",
  "mode": "live",
  "name": "Riverside Yoga",
  "description": null,
  "timezone": "Europe/Berlin",
  "site_url": "https://example.org/",
  "language": "de",
  "default_duration": "PT1H",
  "brand_color": "#0f766e",
  "organizer": { "name": "Riverside Yoga e.V.", "url": "https://example.org/", "email": "info@example.org" },
  "show_events": true,
  "blocked": false,
  "logo_url": null,
  "banner_url": null,
  "subscribe_page_url": "https://app.kalender-sync.de/subscribe/cal_XXXXXXXXXXXX",
  "feed_url": "https://feeds.kalender-sync.de/f_….ics",
  "subscribe_links": {
    "google": "https://calendar.google.com/calendar/r?cid=webcal%3A%2F%2Ffeeds.kalender-sync.de%2Ff_….ics",
    "outlook": "https://outlook.live.com/calendar/0/addfromweb?url=…&name=Riverside%20Yoga",
    "office365": "https://outlook.office.com/calendar/0/addfromweb?url=…&name=Riverside%20Yoga",
    "apple": "webcal://feeds.kalender-sync.de/f_….ics"
  },
  "event_count": 0,
  "created_at": "2026-11-03T14:02:11.000Z",
  "updated_at": "2026-11-03T14:02:11.000Z"
}
```

| Feld | Bedeutung |
|---|---|
| `subscribe_page_url` | Die gehostete [Abo-Seite](https://docs.kalender-sync.de/abo-kalender/abo-seite/). Am besten verlinkst du diese oder baust den [Button](https://docs.kalender-sync.de/api/button/) ein. |
| `feed_url` | Der allgemeine ICS-Feed. Er ändert sich nie, solange der Kalender besteht. |
| `subscribe_links` | Direkte Abo-Links je App. Funktionieren, werden aber in der [Statistik](https://docs.kalender-sync.de/api/stats/) nicht gezählt. Abo-Seite und Button zählen. |
| `logo_url`, `banner_url` | Öffentliche Bild-URLs oder `null`. → [Bilder](https://docs.kalender-sync.de/api/images/) |
| `event_count` | Anzahl gespeicherter Termine; eine Serie zählt als einer. |
| `blocked` | `true`, wenn wir den Kalender wegen eines Verstoßes gegen unsere Bedingungen gesperrt haben. → [`calendar_blocked`](https://docs.kalender-sync.de/api/errors/calendar_blocked/) |

URLs speichern wir in normalisierter Form, daher kann zum Beispiel ein abschließender Schrägstrich dazukommen.

## Kalender auflisten und lesen

```bash
curl https://api.kalender-sync.de/v1/publisher/calendars -H "Authorization: Bearer $KS_KEY"
curl https://api.kalender-sync.de/v1/publisher/calendars/$KS_CAL -H "Authorization: Bearer $KS_KEY"
```

Die Liste steht unter `data`, sortiert nach Anlagedatum, und enthält nur Kalender im Modus des Keys.

## Kalender ändern

`PATCH` ändert nur die Felder, die du mitschickst. `null` leert ein Feld, das `null` erlaubt (zum Beispiel `"organizer": null`).

```bash
curl -X PATCH https://api.kalender-sync.de/v1/publisher/calendars/$KS_CAL \
  -H "Authorization: Bearer $KS_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "description": "Kurse, Workshops und Retreats.", "show_events": false }'
```

`organizer` wird immer als Ganzes ersetzt: Schickst du nur `name`, sind `url` und `email` danach leer.

Ändert sich die `timezone` eines Kalenders, gilt die neue Zone für alle Termine, die ohne eigene `timezone` gespeichert wurden, ab dem nächsten Schreiben dieser Termine.

## Kalender löschen

```bash
curl -X DELETE https://api.kalender-sync.de/v1/publisher/calendars/$KS_CAL -H "Authorization: Bearer $KS_KEY"
```

Antwort `204 No Content`. Die Termine werden dabei gelöscht. Damit sie auch bei bestehenden Abonnenten verschwinden, liefert der Feed noch **30 Tage** einen leeren Kalender mit einem Hinweis und danach `410 Gone`. Die Abo-Seite ist ab dann nicht mehr erreichbar.