{
  "openapi": "3.1.0",
  "info": {
    "title": "Adventskonzerte Berlin – Termine-API",
    "version": "1.0.0",
    "summary": "Öffentliche, kostenlose Lese-API für die Termine der Adventskonzerte in Berlin.",
    "description": "Liefert alle veröffentlichten Konzerttermine von adventskonzerte-berlin.de als JSON. Kostenlos, ohne Anmeldung und ohne API-Schlüssel, nur lesend (GET/HEAD/OPTIONS).\n\n**Rate Limits (pro IP-Adresse):** 5 Anfragen pro Minute, 30 pro Stunde, 100 pro Tag. Die Daten ändern sich selten – bitte höchstens **einmal pro Stunde** abfragen und `ETag`/`If-None-Match` nutzen. Bei Überschreitung antwortet die API mit `429` und `Retry-After`.\n\n**Datenschutz:** IP-Adressen (IPv6 gekürzt auf das /64-Netz) werden nur als gesalzener SHA-256-Hash (Tagessalz) gezählt; die Zähler werden spätestens zum Tagesende (UTC), also nach höchstens 24 Stunden, gelöscht. Es werden keine Zugriffsprotokolle geführt. Details: [Datenschutzerklärung](https://adventskonzerte-berlin.de/datenschutz/), Anbieter: [Impressum](https://adventskonzerte-berlin.de/impressum/).",
    "contact": {
      "name": "Adventskonzerte Berlin",
      "url": "https://adventskonzerte-berlin.de/impressum/"
    },
    "license": {
      "name": "Nutzung der Termindaten mit Quellenangabe „adventskonzerte-berlin.de“",
      "url": "https://adventskonzerte-berlin.de/impressum/"
    }
  },
  "externalDocs": {
    "description": "Ausführliche Dokumentation (HTML)",
    "url": "https://api.adventskonzerte-berlin.de/"
  },
  "servers": [
    {
      "url": "https://api.adventskonzerte-berlin.de",
      "description": "Produktion"
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "Termine",
      "description": "Konzerttermine abrufen"
    },
    {
      "name": "Dienst",
      "description": "Übersicht, Gesundheitsprüfung und Dokumentation"
    }
  ],
  "paths": {
    "/v1/termine": {
      "get": {
        "tags": [
          "Termine"
        ],
        "operationId": "termineAuflisten",
        "summary": "Alle Termine auflisten",
        "description": "Alle veröffentlichten Termine, sortiert nach `beginn`, bei Gleichstand nach `reihenfolge`. Optional gefiltert nach Datum und Livestream. Ein leeres Ergebnis ist kein Fehler (`anzahl: 0`).",
        "parameters": [
          {
            "$ref": "#/components/parameters/Ab"
          },
          {
            "$ref": "#/components/parameters/Bis"
          },
          {
            "$ref": "#/components/parameters/Livestream"
          },
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "Liste der Termine",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              },
              "Access-Control-Allow-Origin": {
                "$ref": "#/components/headers/AccessControlAllowOrigin"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TerminListe"
                },
                "examples": {
                  "alle": {
                    "summary": "Antwort (gekürzt)",
                    "value": {
                      "termine": [
                        {
                          "id": "friedrichshain",
                          "name": "Friedrichshain",
                          "reihenfolge": 3,
                          "datum": "2026-12-19",
                          "uhrzeit": "17:30",
                          "beginn": "2026-12-19T17:30:00+01:00",
                          "zeitzone": "Europe/Berlin",
                          "veranstalter": "Eckstein-Gemeinde",
                          "adresse": "An der Schillingbrücke 4, 10243 Berlin",
                          "website": {
                            "url": "https://eckstein.online",
                            "text": "eckstein.online"
                          },
                          "bild": {
                            "url": "https://adventskonzerte-berlin.de/bilder/konzerte_05.webp",
                            "beschreibung": "Konzert Friedrichshain"
                          },
                          "koordinaten": {
                            "breitengrad": 52.509829,
                            "laengengrad": 13.429333
                          },
                          "livestream": false,
                          "links": {
                            "konzertseite": "https://adventskonzerte-berlin.de/konzerte/#termin-friedrichshain",
                            "karte": "https://www.google.com/maps/search/?api=1&query=Eckstein-Gemeinde%2C%20An%20der%20Schillingbr%C3%BCcke%204%2C%2010243%20Berlin"
                          }
                        }
                      ],
                      "anzahl": 1,
                      "stand": "2026-11-09T10:15:00.000Z",
                      "quelle": "https://adventskonzerte-berlin.de/konzerte/"
                    }
                  }
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NichtGeaendert"
          },
          "400": {
            "$ref": "#/components/responses/UngueltigerParameter"
          },
          "405": {
            "$ref": "#/components/responses/MethodeNichtErlaubt"
          },
          "429": {
            "$ref": "#/components/responses/ZuVieleAnfragen"
          },
          "503": {
            "$ref": "#/components/responses/QuelleNichtErreichbar"
          }
        }
      }
    },
    "/v1/termine/{id}": {
      "get": {
        "tags": [
          "Termine"
        ],
        "operationId": "terminAbrufen",
        "summary": "Einen Termin abrufen",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Kennung des Termins (z. B. `friedrichshain`), siehe Feld `id`.",
            "schema": {
              "type": "string",
              "pattern": "^[a-z0-9-]+$",
              "examples": [
                "friedrichshain"
              ]
            }
          },
          {
            "$ref": "#/components/parameters/IfNoneMatch"
          }
        ],
        "responses": {
          "200": {
            "description": "Der Termin",
            "headers": {
              "ETag": {
                "$ref": "#/components/headers/ETag"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/CacheControl"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              },
              "Access-Control-Allow-Origin": {
                "$ref": "#/components/headers/AccessControlAllowOrigin"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Termin"
                },
                "examples": {
                  "friedrichshain": {
                    "value": {
                      "id": "friedrichshain",
                      "name": "Friedrichshain",
                      "reihenfolge": 3,
                      "datum": "2026-12-19",
                      "uhrzeit": "17:30",
                      "beginn": "2026-12-19T17:30:00+01:00",
                      "zeitzone": "Europe/Berlin",
                      "veranstalter": "Eckstein-Gemeinde",
                      "adresse": "An der Schillingbrücke 4, 10243 Berlin",
                      "website": {
                        "url": "https://eckstein.online",
                        "text": "eckstein.online"
                      },
                      "bild": {
                        "url": "https://adventskonzerte-berlin.de/bilder/konzerte_05.webp",
                        "beschreibung": "Konzert Friedrichshain"
                      },
                      "koordinaten": {
                        "breitengrad": 52.509829,
                        "laengengrad": 13.429333
                      },
                      "livestream": false,
                      "links": {
                        "konzertseite": "https://adventskonzerte-berlin.de/konzerte/#termin-friedrichshain",
                        "karte": "https://www.google.com/maps/search/?api=1&query=Eckstein-Gemeinde%2C%20An%20der%20Schillingbr%C3%BCcke%204%2C%2010243%20Berlin"
                      }
                    }
                  }
                }
              }
            }
          },
          "304": {
            "$ref": "#/components/responses/NichtGeaendert"
          },
          "404": {
            "$ref": "#/components/responses/NichtGefunden"
          },
          "405": {
            "$ref": "#/components/responses/MethodeNichtErlaubt"
          },
          "429": {
            "$ref": "#/components/responses/ZuVieleAnfragen"
          },
          "503": {
            "$ref": "#/components/responses/QuelleNichtErreichbar"
          }
        }
      }
    },
    "/v1": {
      "get": {
        "tags": [
          "Dienst"
        ],
        "operationId": "uebersicht",
        "summary": "Übersicht mit Links",
        "description": "Kleine Einstiegsseite der Version 1 mit Links zu Endpunkten und Dokumentation.",
        "responses": {
          "200": {
            "description": "Übersicht",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              },
              "Access-Control-Allow-Origin": {
                "$ref": "#/components/headers/AccessControlAllowOrigin"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true
                },
                "examples": {
                  "uebersicht": {
                    "value": {
                      "name": "Adventskonzerte Berlin – Termine-API",
                      "version": "v1",
                      "links": {
                        "termine": "https://api.adventskonzerte-berlin.de/v1/termine",
                        "dokumentation": "https://api.adventskonzerte-berlin.de/",
                        "openapi": "https://api.adventskonzerte-berlin.de/openapi.json"
                      }
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodeNichtErlaubt"
          },
          "429": {
            "$ref": "#/components/responses/ZuVieleAnfragen"
          },
          "503": {
            "$ref": "#/components/responses/QuelleNichtErreichbar"
          }
        }
      }
    },
    "/gesundheit": {
      "get": {
        "tags": [
          "Dienst"
        ],
        "operationId": "gesundheit",
        "summary": "Gesundheitsprüfung",
        "description": "Prüft, ob der Dienst antwortet. Zählt ebenfalls zum Rate Limit.",
        "responses": {
          "200": {
            "description": "Dienst läuft",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              },
              "Access-Control-Allow-Origin": {
                "$ref": "#/components/headers/AccessControlAllowOrigin"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "status"
                  ],
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "ok"
                    }
                  }
                },
                "examples": {
                  "ok": {
                    "value": {
                      "status": "ok"
                    }
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodeNichtErlaubt"
          },
          "429": {
            "$ref": "#/components/responses/ZuVieleAnfragen"
          },
          "503": {
            "$ref": "#/components/responses/QuelleNichtErreichbar"
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "tags": [
          "Dienst"
        ],
        "operationId": "openapi",
        "summary": "Diese OpenAPI-Spezifikation",
        "responses": {
          "200": {
            "description": "OpenAPI 3.1 als JSON",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              },
              "Access-Control-Allow-Origin": {
                "$ref": "#/components/headers/AccessControlAllowOrigin"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodeNichtErlaubt"
          },
          "429": {
            "$ref": "#/components/responses/ZuVieleAnfragen"
          },
          "503": {
            "$ref": "#/components/responses/QuelleNichtErreichbar"
          }
        }
      }
    },
    "/": {
      "get": {
        "tags": [
          "Dienst"
        ],
        "operationId": "dokumentation",
        "summary": "Dokumentation (HTML)",
        "description": "Menschenlesbare Dokumentation. Auch unter `/doku/` erreichbar.",
        "responses": {
          "200": {
            "description": "HTML-Seite",
            "headers": {
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimitPolicy"
              },
              "RateLimit": {
                "$ref": "#/components/headers/RateLimit"
              },
              "X-RateLimit-Limit": {
                "$ref": "#/components/headers/XRateLimitLimit"
              },
              "X-RateLimit-Remaining": {
                "$ref": "#/components/headers/XRateLimitRemaining"
              },
              "X-RateLimit-Reset": {
                "$ref": "#/components/headers/XRateLimitReset"
              },
              "Access-Control-Allow-Origin": {
                "$ref": "#/components/headers/AccessControlAllowOrigin"
              }
            },
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodeNichtErlaubt"
          },
          "429": {
            "$ref": "#/components/responses/ZuVieleAnfragen"
          },
          "503": {
            "$ref": "#/components/responses/QuelleNichtErreichbar"
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "Ab": {
        "name": "ab",
        "in": "query",
        "required": false,
        "description": "Nur Termine an oder nach diesem Datum (einschließlich), Format `YYYY-MM-DD`.",
        "schema": {
          "type": "string",
          "format": "date",
          "examples": [
            "2026-12-01"
          ]
        }
      },
      "Bis": {
        "name": "bis",
        "in": "query",
        "required": false,
        "description": "Nur Termine an oder vor diesem Datum (einschließlich), Format `YYYY-MM-DD`.",
        "schema": {
          "type": "string",
          "format": "date",
          "examples": [
            "2026-12-24"
          ]
        }
      },
      "Livestream": {
        "name": "livestream",
        "in": "query",
        "required": false,
        "description": "`true`: nur Termine mit Livestream, `false`: nur Termine ohne Livestream.",
        "schema": {
          "type": "string",
          "enum": [
            "true",
            "false"
          ]
        }
      },
      "IfNoneMatch": {
        "name": "If-None-Match",
        "in": "header",
        "required": false,
        "description": "Wert des zuletzt erhaltenen `ETag`. Unveränderte Daten → `304 Not Modified` ohne Inhalt.",
        "schema": {
          "type": "string",
          "examples": [
            "\"a1b2c3d4\""
          ]
        }
      }
    },
    "headers": {
      "ETag": {
        "description": "Kennung des Inhalts. Für bedingte Anfragen mit `If-None-Match` verwenden.",
        "schema": {
          "type": "string"
        }
      },
      "CacheControl": {
        "description": "Caching-Vorgabe, standardmäßig `public, max-age=900` (15 Minuten).",
        "schema": {
          "type": "string",
          "examples": [
            "public, max-age=900"
          ]
        }
      },
      "RateLimitPolicy": {
        "description": "Geltende Begrenzungen nach IETF-Entwurf „RateLimit header fields for HTTP“.",
        "schema": {
          "type": "string",
          "examples": [
            "\"minute\";q=5;w=60, \"stunde\";q=30;w=3600, \"tag\";q=100;w=86400"
          ]
        }
      },
      "RateLimit": {
        "description": "Aktueller Stand nach IETF-Entwurf: verbleibende Anfragen (`r`) und Sekunden bis zum Zurücksetzen (`t`).",
        "schema": {
          "type": "string",
          "examples": [
            "\"stunde\";r=27;t=1834"
          ]
        }
      },
      "XRateLimitLimit": {
        "description": "Erlaubte Anfragen im laufenden Stundenfenster.",
        "schema": {
          "type": "integer",
          "examples": [
            30
          ]
        }
      },
      "XRateLimitRemaining": {
        "description": "Verbleibende Anfragen im laufenden Stundenfenster.",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "examples": [
            27
          ]
        }
      },
      "XRateLimitReset": {
        "description": "Sekunden, bis das Stundenfenster zurückgesetzt wird.",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "examples": [
            1834
          ]
        }
      },
      "RetryAfter": {
        "description": "Sekunden, die vor der nächsten Anfrage gewartet werden müssen.",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "examples": [
            60
          ]
        }
      },
      "AccessControlAllowOrigin": {
        "description": "CORS: Abruf aus jedem Browser-Ursprung erlaubt.",
        "schema": {
          "type": "string",
          "const": "*"
        }
      }
    },
    "responses": {
      "NichtGeaendert": {
        "description": "Nicht geändert – der zwischengespeicherte Stand ist aktuell. Kein Inhalt. Zählt zum Rate Limit.",
        "headers": {
          "ETag": {
            "$ref": "#/components/headers/ETag"
          },
          "Cache-Control": {
            "$ref": "#/components/headers/CacheControl"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/XRateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/XRateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/XRateLimitReset"
          },
          "Access-Control-Allow-Origin": {
            "$ref": "#/components/headers/AccessControlAllowOrigin"
          }
        }
      },
      "UngueltigerParameter": {
        "description": "Ungültiger Filterparameter (z. B. falsches Datumsformat).",
        "headers": {
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/XRateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/XRateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/XRateLimitReset"
          },
          "Access-Control-Allow-Origin": {
            "$ref": "#/components/headers/AccessControlAllowOrigin"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Fehler"
            },
            "examples": {
              "datum": {
                "value": {
                  "fehler": {
                    "code": "ungueltiger_parameter",
                    "nachricht": "Parameter „ab“ muss das Format YYYY-MM-DD haben."
                  }
                }
              }
            }
          }
        }
      },
      "NichtGefunden": {
        "description": "Termin oder Adresse nicht gefunden.",
        "headers": {
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/XRateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/XRateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/XRateLimitReset"
          },
          "Access-Control-Allow-Origin": {
            "$ref": "#/components/headers/AccessControlAllowOrigin"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Fehler"
            },
            "examples": {
              "termin": {
                "value": {
                  "fehler": {
                    "code": "nicht_gefunden",
                    "nachricht": "Termin „mitte“ nicht gefunden."
                  }
                }
              }
            }
          }
        }
      },
      "MethodeNichtErlaubt": {
        "description": "Nur GET, HEAD und OPTIONS sind erlaubt.",
        "headers": {
          "Allow": {
            "description": "Erlaubte Methoden",
            "schema": {
              "type": "string",
              "const": "GET, HEAD, OPTIONS"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Fehler"
            },
            "examples": {
              "post": {
                "value": {
                  "fehler": {
                    "code": "methode_nicht_erlaubt",
                    "nachricht": "Erlaubt sind nur GET, HEAD und OPTIONS."
                  }
                }
              }
            }
          }
        }
      },
      "ZuVieleAnfragen": {
        "description": "Rate Limit überschritten (5/Minute, 30/Stunde oder 100/Tag pro IP). Erst nach `Retry-After` Sekunden erneut anfragen.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/XRateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/XRateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/XRateLimitReset"
          },
          "Access-Control-Allow-Origin": {
            "$ref": "#/components/headers/AccessControlAllowOrigin"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Fehler"
            },
            "examples": {
              "stunde": {
                "value": {
                  "fehler": {
                    "code": "zu_viele_anfragen",
                    "nachricht": "Zu viele Anfragen. Bitte in 1834 Sekunden erneut versuchen."
                  }
                }
              }
            }
          }
        }
      },
      "QuelleNichtErreichbar": {
        "description": "Dienst vorübergehend nicht verfügbar: Datenquelle (Website) nicht erreichbar (`quelle_nicht_erreichbar`) oder API nicht vollständig eingerichtet (`konfiguration_fehlt`). `Retry-After` gibt die Wartezeit in Sekunden an.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          },
          "RateLimit-Policy": {
            "$ref": "#/components/headers/RateLimitPolicy"
          },
          "RateLimit": {
            "$ref": "#/components/headers/RateLimit"
          },
          "X-RateLimit-Limit": {
            "$ref": "#/components/headers/XRateLimitLimit"
          },
          "X-RateLimit-Remaining": {
            "$ref": "#/components/headers/XRateLimitRemaining"
          },
          "X-RateLimit-Reset": {
            "$ref": "#/components/headers/XRateLimitReset"
          },
          "Access-Control-Allow-Origin": {
            "$ref": "#/components/headers/AccessControlAllowOrigin"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Fehler"
            },
            "examples": {
              "quelle": {
                "value": {
                  "fehler": {
                    "code": "quelle_nicht_erreichbar",
                    "nachricht": "Die Termindaten sind gerade nicht abrufbar."
                  }
                }
              },
              "konfiguration": {
                "value": {
                  "fehler": {
                    "code": "konfiguration_fehlt",
                    "nachricht": "Die API ist gerade nicht richtig eingerichtet."
                  }
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "TerminListe": {
        "type": "object",
        "required": [
          "termine",
          "anzahl",
          "stand",
          "quelle"
        ],
        "properties": {
          "termine": {
            "type": "array",
            "description": "Termine, sortiert nach `beginn`, dann `reihenfolge`.",
            "items": {
              "$ref": "#/components/schemas/Termin"
            }
          },
          "anzahl": {
            "type": "integer",
            "minimum": 0,
            "description": "Anzahl der Termine in `termine`."
          },
          "stand": {
            "type": "string",
            "format": "date-time",
            "description": "Zeitpunkt, zu dem die Daten auf der Website erzeugt wurden (ISO 8601)."
          },
          "quelle": {
            "type": "string",
            "format": "uri",
            "description": "Seite, auf der die Termine veröffentlicht sind.",
            "examples": [
              "https://adventskonzerte-berlin.de/konzerte/"
            ]
          }
        }
      },
      "Termin": {
        "type": "object",
        "description": "Ein veröffentlichter Konzerttermin. Versteckte Termine erscheinen nicht.",
        "required": [
          "id",
          "name",
          "reihenfolge",
          "datum",
          "uhrzeit",
          "beginn",
          "zeitzone",
          "veranstalter",
          "adresse",
          "website",
          "bild",
          "koordinaten",
          "livestream",
          "links"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[a-z0-9-]+$",
            "description": "Eindeutige, stabile Kennung.",
            "examples": [
              "friedrichshain"
            ]
          },
          "name": {
            "type": "string",
            "description": "Name des Termins, meist der Bezirk.",
            "examples": [
              "Friedrichshain"
            ]
          },
          "reihenfolge": {
            "type": "integer",
            "description": "Redaktionelle Reihenfolge, zweites Sortierkriterium.",
            "examples": [
              3
            ]
          },
          "datum": {
            "type": "string",
            "format": "date",
            "description": "Konzerttag (`YYYY-MM-DD`).",
            "examples": [
              "2026-12-19"
            ]
          },
          "uhrzeit": {
            "type": "string",
            "pattern": "^\\d{2}:\\d{2}$",
            "description": "Beginn als Ortszeit (`HH:MM`).",
            "examples": [
              "17:30"
            ]
          },
          "beginn": {
            "type": "string",
            "format": "date-time",
            "description": "Beginn als ISO 8601 mit Berliner Zeitversatz (+01:00 / +02:00).",
            "examples": [
              "2026-12-19T17:30:00+01:00"
            ]
          },
          "zeitzone": {
            "type": "string",
            "const": "Europe/Berlin",
            "description": "IANA-Zeitzone von `datum`/`uhrzeit`."
          },
          "veranstalter": {
            "type": "string",
            "description": "Gemeinde oder Veranstalter.",
            "examples": [
              "Eckstein-Gemeinde"
            ]
          },
          "adresse": {
            "type": "string",
            "description": "Anschrift des Veranstaltungsorts.",
            "examples": [
              "An der Schillingbrücke 4, 10243 Berlin"
            ]
          },
          "website": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/Link"
              },
              {
                "type": "null"
              }
            ],
            "description": "Website des Veranstalters oder `null`."
          },
          "bild": {
            "$ref": "#/components/schemas/Bild"
          },
          "koordinaten": {
            "$ref": "#/components/schemas/Koordinaten"
          },
          "livestream": {
            "type": "boolean",
            "description": "`true`, wenn das Konzert live übertragen wird."
          },
          "links": {
            "$ref": "#/components/schemas/TerminLinks"
          }
        }
      },
      "Link": {
        "type": "object",
        "required": [
          "url",
          "text"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "examples": [
              "https://eckstein.online"
            ]
          },
          "text": {
            "type": "string",
            "description": "Anzeigetext für den Link.",
            "examples": [
              "eckstein.online"
            ]
          }
        }
      },
      "Bild": {
        "type": "object",
        "required": [
          "url",
          "beschreibung"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Absolute Bildadresse (WebP)."
          },
          "beschreibung": {
            "type": "string",
            "description": "Alternativtext für das Bild."
          }
        }
      },
      "Koordinaten": {
        "type": "object",
        "required": [
          "breitengrad",
          "laengengrad"
        ],
        "properties": {
          "breitengrad": {
            "type": "number",
            "minimum": -90,
            "maximum": 90,
            "description": "Breitengrad (WGS 84).",
            "examples": [
              52.509829
            ]
          },
          "laengengrad": {
            "type": "number",
            "minimum": -180,
            "maximum": 180,
            "description": "Längengrad (WGS 84).",
            "examples": [
              13.429333
            ]
          }
        }
      },
      "TerminLinks": {
        "type": "object",
        "required": [
          "konzertseite",
          "karte"
        ],
        "properties": {
          "konzertseite": {
            "type": "string",
            "format": "uri",
            "description": "Dieser Termin auf der Konzertseite der Website (Anker `#termin-{id}`).",
            "examples": [
              "https://adventskonzerte-berlin.de/konzerte/#termin-friedrichshain"
            ]
          },
          "karte": {
            "type": "string",
            "format": "uri",
            "description": "Link zum Ort in der Karten-App (Google Maps)."
          }
        }
      },
      "Fehler": {
        "type": "object",
        "required": [
          "fehler"
        ],
        "properties": {
          "fehler": {
            "type": "object",
            "required": [
              "code",
              "nachricht"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "nicht_gefunden",
                  "zu_viele_anfragen",
                  "methode_nicht_erlaubt",
                  "ungueltiger_parameter",
                  "quelle_nicht_erreichbar",
                  "konfiguration_fehlt",
                  "interner_fehler"
                ],
                "description": "Maschinenlesbarer Fehlercode – für Programmlogik verwenden."
              },
              "nachricht": {
                "type": "string",
                "description": "Menschenlesbare Beschreibung auf Deutsch."
              }
            }
          }
        }
      }
    }
  }
}