{
  "openapi": "3.1.0",
  "info": {
    "title": "THILO SKYE — Website API",
    "version": "1.0.0",
    "summary": "Einsatzbereiche, Projekte und ein vorbereiteter Kontakt-Link.",
    "description": "Die öffentliche Oberfläche der Website von THILO SKYE, einem Drohnen-Foto- und Filmstudio in Wiesbaden. Kein Produkt-API: zwei Leselisten und ein Endpunkt, der eine Anfrage prüft und einen vorausgefüllten Formular-Link zurückgibt. Es wird nichts gespeichert und nichts verschickt. Dieselben Werkzeuge gibt es als MCP-Server unter /mcp.\n\n## Versionierung\nDie aktuelle Version ist 1. Jede Antwort trägt sie im Header `API-Version`. Die Version steht im Header und nicht im Pfad, weil die Pfade bereits veröffentlicht sind — sie zu verschieben würde genau das brechen, wofür Versionierung da ist.\n\n## Abkündigung\nEine brechende Änderung bekommt eine neue Versionsnummer. Die alte antwortet danach mindestens 90 Tage weiter und trägt dabei die Header `Deprecation` (RFC 9745) und `Sunset` (RFC 8594) mit dem Abschalttermin. Beide Header sind an jedem Vorgang deklariert, damit ein Client sie kennt, bevor sie das erste Mal auftauchen. Heute fehlen sie, weil nichts abgekündigt ist; die Politik selbst steht dauerhaft unter /api#versionierung und ist aus jeder Antwort über den Link-Header mit rel=\"sunset\" erreichbar.\n\n## Katalog\nAlle Entwickler-Dokumente dieses Origins stehen als Linkset nach RFC 9727 unter /.well-known/api-catalog: diese Datei, die Prosa-Seite /api, das MCP-Manifest und die Abkündigungspolitik.\n\n## Rate-Limit\n`POST /api/kontakt-link` und `/mcp` sind auf 60 Anfragen pro Minute und IP begrenzt; die statischen Listen nicht. Jede Antwort trägt `RateLimit-Policy` und `RateLimit-Limit`, eine 429 zusätzlich `Retry-After`. Ein `RateLimit-Remaining` gibt es bewusst nicht: der Zähler liegt in Cloudflares Limiter und wird nicht herausgegeben, und eine erfundene Zahl wäre schlimmer als keine.\n\n## Fehler\nJeder Fehler ist ein `application/problem+json` nach RFC 9457 — siehe das Schema `Problem`.",
    "contact": {
      "name": "THILO SKYE",
      "email": "thilo@thiloskye.de",
      "url": "https://web.thiloskye.workers.dev/kontakt"
    }
  },
  "servers": [
    {
      "url": "https://web.thiloskye.workers.dev"
    }
  ],
  "components": {
    "headers": {
      "API-Version": {
        "description": "Die Version, die geantwortet hat. Aktuell immer 1.",
        "schema": {
          "type": "string",
          "example": "1"
        }
      },
      "RateLimit-Policy": {
        "description": "RFC 9331. Die Politik, nicht der Reststand.",
        "schema": {
          "type": "string",
          "example": "\"api\";q=60;w=60"
        }
      },
      "Retry-After": {
        "description": "Sekunden bis zum nächsten erlaubten Versuch. Nur bei 429.",
        "schema": {
          "type": "integer",
          "example": 60
        }
      },
      "Deprecation": {
        "description": "RFC 9745. Erscheint erst, wenn diese Version abgekündigt ist, und nennt als IMF-fixdate den Zeitpunkt, ab dem sie als veraltet gilt. Solange die Version aktuell ist, fehlt der Header — ein erfundenes Datum wäre schlimmer als keines.",
        "required": false,
        "schema": {
          "type": "string",
          "example": "Wed, 11 Nov 2026 00:00:00 GMT"
        }
      },
      "Sunset": {
        "description": "RFC 8594. Erscheint zusammen mit `Deprecation` und nennt den Termin, ab dem diese Version nicht mehr antwortet — mindestens 90 Tage nach der Abkündigung. Die Politik dazu steht dauerhaft unter /api#versionierung und ist von jeder Antwort aus als `Link` mit rel=\"sunset\" erreichbar.",
        "required": false,
        "schema": {
          "type": "string",
          "example": "Wed, 10 Feb 2027 00:00:00 GMT"
        }
      },
      "Link": {
        "description": "RFC 8288. Immer vorhanden: rel=\"sunset\" zeigt auf die Abkündigungspolitik, rel=\"api-catalog\" auf den Katalog nach RFC 9727, rel=\"service-desc\" auf diese Datei und rel=\"service-doc\" auf /api.",
        "schema": {
          "type": "string",
          "example": "</api#versionierung>; rel=\"sunset\", </.well-known/api-catalog>; rel=\"api-catalog\""
        }
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "Ein Fehler nach RFC 9457. Content-Type ist application/problem+json, nicht application/json — daran ist ein Fehler ohne Statuscode-Prüfung erkennbar.",
        "required": [
          "type",
          "title",
          "status",
          "detail",
          "code"
        ],
        "properties": {
          "type": {
            "type": "string",
            "format": "uri",
            "description": "Stabile URI, die auf den Abschnitt unter /api zeigt."
          },
          "title": {
            "type": "string",
            "description": "Kurzfassung, für Menschen."
          },
          "status": {
            "type": "integer",
            "description": "Der HTTP-Status, wiederholt."
          },
          "detail": {
            "type": "string",
            "description": "Was zu tun ist."
          },
          "code": {
            "type": "string",
            "description": "Maschinenlesbar und stabil. Hierauf lässt sich verzweigen.",
            "enum": [
              "invalid_json",
              "incomplete_draft",
              "method_not_allowed",
              "not_acceptable",
              "not_found",
              "rate_limited",
              "internal_error"
            ]
          },
          "problems": {
            "type": "array",
            "description": "Nur bei incomplete_draft: welches Feld, und was fehlt daran.",
            "items": {
              "type": "object",
              "required": [
                "field",
                "problem"
              ],
              "properties": {
                "field": {
                  "type": "string"
                },
                "problem": {
                  "type": "string"
                }
              }
            }
          },
          "required": {
            "type": "array",
            "description": "Nur bei incomplete_draft: die Pflichtfelder.",
            "items": {
              "type": "string"
            }
          }
        }
      }
    },
    "responses": {
      "Problem": {
        "description": "Fehler (RFC 9457).",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Zu viele Anfragen.",
        "headers": {
          "Retry-After": {
            "$ref": "#/components/headers/Retry-After"
          }
        },
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      }
    }
  },
  "externalDocs": {
    "description": "API und MCP-Server — THILO SKYE",
    "url": "https://web.thiloskye.workers.dev/api"
  },
  "paths": {
    "/api/use-cases.json": {
      "get": {
        "operationId": "listUseCases",
        "parameters": [
          {
            "name": "Api-Version",
            "in": "header",
            "required": false,
            "description": "Die gewünschte API-Version. Zurzeit gibt es nur \"1\". Weggelassen heißt: die aktuelle. Die Antwort nennt im Header `API-Version`, welche Version geantwortet hat.",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ],
              "default": "1"
            }
          }
        ],
        "summary": "Einsatzbereiche auflisten",
        "description": "Die Branchen, für die THILO SKYE fliegt. `id` ist zugleich der Seiten-Slug und der Wert, den das Feld `thema` einer Anfrage annimmt.",
        "responses": {
          "200": {
            "description": "Die Liste.",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/API-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              },
              "Link": {
                "$ref": "#/components/headers/Link"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "useCases"
                  ],
                  "properties": {
                    "useCases": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "title",
                          "status",
                          "headline",
                          "description"
                        ],
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "live",
                              "planned"
                            ]
                          },
                          "headline": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string",
                            "format": "uri",
                            "description": "Fehlt, solange die Seite noch nicht existiert."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/api/projects.json": {
      "get": {
        "operationId": "listProjects",
        "parameters": [
          {
            "name": "Api-Version",
            "in": "header",
            "required": false,
            "description": "Die gewünschte API-Version. Zurzeit gibt es nur \"1\". Weggelassen heißt: die aktuelle. Die Antwort nennt im Header `API-Version`, welche Version geantwortet hat.",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ],
              "default": "1"
            }
          }
        ],
        "summary": "Projekte auflisten",
        "description": "Veröffentlichte Projekte. Fehlende Felder fehlen absichtlich — `year` ist nur gesetzt, wenn das Jahr bekannt ist, und darf nicht ergänzt werden.",
        "responses": {
          "200": {
            "description": "Die Liste.",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/API-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              },
              "Link": {
                "$ref": "#/components/headers/Link"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "projects"
                  ],
                  "properties": {
                    "projects": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "slug",
                          "title",
                          "category",
                          "location",
                          "intro",
                          "url"
                        ],
                        "properties": {
                          "slug": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string"
                          },
                          "category": {
                            "type": "string"
                          },
                          "location": {
                            "type": "string"
                          },
                          "intro": {
                            "type": "string"
                          },
                          "year": {
                            "type": "integer"
                          },
                          "challenge": {
                            "type": "string"
                          },
                          "url": {
                            "type": "string",
                            "format": "uri"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/Problem"
          }
        }
      }
    },
    "/api/kontakt-link": {
      "post": {
        "operationId": "createContactLink",
        "parameters": [
          {
            "name": "Api-Version",
            "in": "header",
            "required": false,
            "description": "Die gewünschte API-Version. Zurzeit gibt es nur \"1\". Weggelassen heißt: die aktuelle. Die Antwort nennt im Header `API-Version`, welche Version geantwortet hat.",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ],
              "default": "1"
            }
          }
        ],
        "summary": "Anfrage vorbereiten",
        "description": "Prüft einen Anfrage-Entwurf und gibt einen Link auf das Kontaktformular zurück, in dem die Felder bereits ausgefüllt sind. **Verschickt nichts.** Das Formular verlangt eine Datenschutz-Einwilligung, und die kann nur die anfragende Person selbst geben — sie prüft den Entwurf, setzt den Haken und schickt ab. POST statt GET, damit Name, Adresse und Nachricht nicht in einer Request-URL und damit in Zugriffs-Logs landen. Es wird nichts gespeichert. Pflichtfelder: name, email, ort, nachricht.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name",
                  "email",
                  "ort",
                  "nachricht"
                ],
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 5000,
                    "description": "Wer anfragt. Voller Name."
                  },
                  "email": {
                    "type": "string",
                    "maxLength": 5000,
                    "description": "Antwortadresse der anfragenden Person."
                  },
                  "ort": {
                    "type": "string",
                    "maxLength": 5000,
                    "description": "Objekt und Ort — was aufgenommen werden soll und wo es steht, z. B. \"Stadtvilla in Wiesbaden-Sonnenberg\"."
                  },
                  "nachricht": {
                    "type": "string",
                    "maxLength": 5000,
                    "description": "Worum es geht. Deutsch, aus Sicht der anfragenden Person. Kein erfundenes Budget, kein erfundener Termin."
                  },
                  "telefon": {
                    "type": "string",
                    "maxLength": 5000,
                    "description": "Optional."
                  },
                  "thema": {
                    "type": "string",
                    "maxLength": 5000,
                    "description": "Optional. Eine id aus /api/use-cases.json, oder \"sonstiges\"."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Der Entwurf ist vollständig; der Link ist vorbereitet.",
            "headers": {
              "API-Version": {
                "$ref": "#/components/headers/API-Version"
              },
              "RateLimit-Policy": {
                "$ref": "#/components/headers/RateLimit-Policy"
              },
              "Deprecation": {
                "$ref": "#/components/headers/Deprecation"
              },
              "Sunset": {
                "$ref": "#/components/headers/Sunset"
              },
              "Link": {
                "$ref": "#/components/headers/Link"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "url",
                    "sent"
                  ],
                  "properties": {
                    "url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "sent": {
                      "type": "boolean",
                      "enum": [
                        false
                      ],
                      "description": "Immer false. Dieser Endpunkt verschickt nichts."
                    },
                    "detail": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Problem"
          },
          "405": {
            "$ref": "#/components/responses/Problem"
          },
          "422": {
            "description": "Der Entwurf ist unvollständig — welches Feld, steht in `problems`.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    }
  }
}