# Calendars

A calendar is a subscribe calendar with its own feed, subscribe page and button. Many websites need exactly one; you can also create one per category or venue.

## Fields

| Field | Type | Rules | Default |
|---|---|---|---|
| `name` | string | Required on create, 1 to 100 characters | |
| `timezone` | string | Required on create, IANA name such as `America/New_York` | |
| `description` | string or `null` | up to 1,000 characters | `null` |
| `site_url` | string or `null` | `https://` URL, up to 2,000 characters | `null` |
| `language` | `de` or `en` | Language of the subscribe page | `en` |
| `default_duration` | string | ISO 8601 duration up to `PT24H`, e.g. `PT1H`, `PT90M`, `PT1H30M`. Used for events without `end`. | `PT1H` |
| `brand_color` | string or `null` | Hex color `#rrggbb`, stored in lower case | `null` |
| `organizer` | object or `null` | `name` (required, up to 100 characters), `url` (`https://`, optional), `email` (optional). Public on the subscribe page. | `null` |
| `show_events` | boolean | Show upcoming events on the subscribe page | `true` |

Unknown fields are refused with `400 invalid_request`.

## Create a calendar

```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": "America/New_York",
    "language": "en",
    "site_url": "https://example.org",
    "default_duration": "PT1H",
    "brand_color": "#0f766e",
    "organizer": { "name": "Riverside Yoga", "url": "https://example.org", "email": "info@example.org" },
    "show_events": true
  }'
```

Response `201 Created` with the full calendar (see below). The mode (`live` or `test`) follows from the key.

With an `Idempotency-Key`, a retry after a network error does not create a second calendar. → [Idempotency](https://docs.kalender-sync.de/en/api/idempotency/)

Possible errors: [`calendar_limit_reached`](https://docs.kalender-sync.de/en/api/errors/calendar_limit_reached/) (your plan's limit, or 3 sandbox calendars), [`plan_required`](https://docs.kalender-sync.de/en/api/errors/plan_required/) (live calendar on the Free plan), [`insufficient_scope`](https://docs.kalender-sync.de/en/api/errors/insufficient_scope/) (the key is limited to selected calendars).

## The response

Every calendar endpoint returns the same shape:

```json
{
  "id": "cal_XXXXXXXXXXXX",
  "mode": "live",
  "name": "Riverside Yoga",
  "description": null,
  "timezone": "America/New_York",
  "site_url": "https://example.org/",
  "language": "en",
  "default_duration": "PT1H",
  "brand_color": "#0f766e",
  "organizer": { "name": "Riverside Yoga", "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"
}
```

| Field | Meaning |
|---|---|
| `subscribe_page_url` | The hosted [subscribe page](https://docs.kalender-sync.de/en/abo-kalender/abo-seite/). Link to it or embed the [button](https://docs.kalender-sync.de/en/api/button/). |
| `feed_url` | The general ICS feed. It never changes while the calendar exists. |
| `subscribe_links` | Direct subscribe links per app. They work but are not counted in the [statistics](https://docs.kalender-sync.de/en/api/stats/). The subscribe page and the button are counted. |
| `logo_url`, `banner_url` | Public image URLs or `null`. → [Images](https://docs.kalender-sync.de/en/api/images/) |
| `event_count` | Number of stored events; a series counts as one. |
| `blocked` | `true` if we blocked the calendar for a breach of our terms. → [`calendar_blocked`](https://docs.kalender-sync.de/en/api/errors/calendar_blocked/) |

We store URLs in normalized form, so a trailing slash may be added, for example.

## List and get calendars

```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"
```

The list is under `data`, sorted by creation date, and only contains calendars in the key's mode.

## Update a calendar

`PATCH` only changes the fields you send. `null` clears a field that allows `null` (for example `"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": "Classes, workshops and retreats.", "show_events": false }'
```

`organizer` is always replaced as a whole: if you only send `name`, `url` and `email` are empty afterwards.

When a calendar's `timezone` changes, the new zone applies to events stored without their own `timezone` the next time those events are written.

## Delete a calendar

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

Response `204 No Content`. The events are deleted with it. So that they also disappear for existing subscribers, the feed keeps serving an empty calendar with a note for **30 days**, then `410 Gone`. The subscribe page goes offline at the same time.