Für Entwickler:innen und KI-Agenten

Ballbuddy für Agenten.

Der Ballkalender von Ballbuddy ist als öffentliche, nur lesende API erreichbar – ohne Schlüssel, mit offenem CORS. Diese Seite beschreibt, welche Daten du bekommst, wie du sie abfragst und wo die maschinenlesbaren Beschreibungen liegen.

Was Ballbuddy ist und welche Daten es gibt

Ballbuddy ist die Plattform für Maturaball-Komitees in Österreich, ein Projekt von Framed aus Graz. Öffentlich sichtbar ist der Ballkalender mit einer eigenen Seite je Ball. Das Komitee arbeitet dahinter in einer eigenen App – die ist für Agenten nicht erreichbar und soll es auch nicht sein.

Die API führt drei Arten von Daten: Bälle (Motto, Termin, Schule, Location, Stadt, Bundesland, Ticket-Hinweis, Cover), Schulen und Locations. Alle drei sind öffentlich und redaktionell gepflegt: Komitees tragen ihren Ball selbst ein, wir prüfen und ergänzen. Wie viele Bälle gerade drin sind, sagt dir das Feld total in jeder Listenantwort – der Bestand ändert sich täglich, deshalb steht hier keine Zahl.

Die Discovery-Dateien und die API gibt es nur auf ballbuddy.at; die anderen Flächen (app., partner., admin.) antworten darauf mit 404.

Grundregeln für alle Endpunkte

  • Nur GET. Antworten sind JSON in UTF-8 (der Kalendereintrag ist text/calendar), CORS ist offen: Access-Control-Allow-Origin: *.
  • Zeiten sind ISO-8601 in UTC (starts_at, ends_at, doors_at, updated_at). Für die Anzeige gehört alles nach Europe/Vienna.
  • Eine Saison heißt nach ihrem Endjahr: season=2027 ist die Saison 2026/27.
  • Fehler kommen als JSON: 400 mit { "error": "invalid_params", "issues": [{ "path", "message" }] } bei ungültigen Parametern, 404 mit { "error": "not_found" } bei unbekanntem Slug.
  • Der Slug eines Balls ist ein Vertrag: Alte Slugs leiten mit 301 auf den aktuellen weiter – in der API wie auf der Ball-Seite unter https://ballbuddy.at/ball/<slug>.

Die sieben Endpunkte

Die Namen in Klammern sind die operationIds aus der OpenAPI-Beschreibung – dieselben stehen als capabilities im ai-catalog.

Bälle suchen GET /api/public/v1/balls (searchBalls)

Alle Parameter sind optional: season (Endjahr; ohne from, to und q gilt die laufende Saison), state (Bundesland, exakt einer von Burgenland, Kärnten, Niederösterreich, Oberösterreich, Salzburg, Steiermark, Tirol, Vorarlberg, Wien), city, school (Slug), venue (Slug), from und to (JJJJ-MM-TT als Wiener Kalendertag, from inklusive, to exklusive), q (Freitext über Motto, Schule und Stadt, mindestens zwei Zeichen), page (ab 1) und per (1–100, Standard 48). Sortiert wird nach starts_at aufsteigend. filters in der Antwort nennt die gültigen Werte für den nächsten Aufruf – Saisonen, Bundesländer, Städte, Schulen und Locations mit Zählern.

curl -s "https://ballbuddy.at/api/public/v1/balls?season=2027&state=Steiermark&per=10"

Antwort, gekürzt – Beispielwerte, kein Live-Abruf:

{
  "items": [
    {
      "id": "…",
      "slug": "hak-weiz-2027",
      "url": "https://ballbuddy.at/ball/hak-weiz-2027",
      "title": "Casino Royale",
      "starts_at": "2027-01-16T19:00:00+00:00",
      "ends_at": null,
      "doors_at": null,
      "season": 2027,
      "status": "scheduled",
      "city": "Weiz",
      "state": "Steiermark",
      "school": { "name": "HAK Weiz", "slug": "hak-weiz" },
      "venue": { "name": "Stadthalle Weiz", "slug": "stadthalle-weiz", "address": "…", "zip": "8160", "city": "Weiz" },
      "venue_text": null,
      "instagram": "hakweiz.ball",
      "tickets": { "mode": "link", "url": "https://…" },
      "cover": { "url640": "https://…/640.webp", "url1280": "https://…/1280.webp", "blur": "data:image/webp;base64,…" },
      "is_claimed": false,
      "updated_at": "2026-09-05T08:00:00+00:00",
      "has_motto": true,
      "time_known": true,
      "schools": [{ "name": "HAK Weiz", "slug": "hak-weiz" }]
    }
  ],
  "total": …,
  "page": 1,
  "per": 10,
  "filters": {
    "seasons": [2025, 2026, 2027, 2028, 2029],
    "states": ["Burgenland", "…"],
    "cities": ["Graz", "…"],
    "schools": [{ "slug": "hak-weiz", "name": "HAK Weiz", "count": 1 }],
    "venues": [{ "slug": "stadthalle-weiz", "name": "Stadthalle Weiz", "count": 2 }]
  }
}

status ist scheduled, cancelled oder postponed. tickets ist null, wenn das Komitee keine Ticket-Infos gesetzt hat; mode ist link, instagram oder info – bei instagram und info ist url null, die Details stehen auf der Ball-Seite. cover ist null, solange kein Bild da ist.

Ein Ball im Detail GET /api/public/v1/balls/{slug} (getBall)

Wie ein Listeneintrag, zusätzlich description, links ([{ type, label?, url }]), tanzschule, disco, band und ticket_types ([{ id, name, price_presale_cents, price_door_cents, sort }] – nur öffentliche Kategorien, Preise in Cent). Unbekannte Slugs antworten 404, alte Slugs leiten mit 301 auf die aktuelle Adresse weiter.

curl -s "https://ballbuddy.at/api/public/v1/balls/hak-weiz-2027"

Antwort, gekürzt – Beispielwerte, kein Live-Abruf:

{
  "slug": "hak-weiz-2027",
  "title": "Casino Royale",
  "starts_at": "2027-01-16T19:00:00+00:00",
  … (alle Felder des Listeneintrags), dazu:
  "description": "…",
  "links": [{ "type": "…", "label": "…", "url": "https://…" }],
  "tanzschule": "…",
  "disco": null,
  "band": null,
  "ticket_types": [{ "id": "…", "name": "…", "price_presale_cents": …, "price_door_cents": …, "sort": 0 }]
}

Schulen GET /api/public/v1/schools (listSchools)

Alle Schulen mit Slug, Schultyp, Stadt, Bundesland, Website, Instagram und ball_count (zählt nur veröffentlichte Bälle), sortiert nach Name. Der slug ist der Wert für den Filter school= bei der Ballsuche.

curl -s "https://ballbuddy.at/api/public/v1/schools"

Antwort, gekürzt – Beispielwerte, kein Live-Abruf:

{
  "items": [
    { "id": "…", "name": "HAK Weiz", "slug": "hak-weiz", "school_type": "…", "city": "Weiz", "state": "Steiermark", "website": "https://…", "instagram": null, "ball_count": 1 }
  ],
  "total": …
}

Locations GET /api/public/v1/venues (listVenues)

Alle Locations mit Adresse, Postleitzahl, Stadt, Bundesland, Website, Koordinaten (lat, lng) und ball_count. Der slug ist der Wert für den Filter venue=.

curl -s "https://ballbuddy.at/api/public/v1/venues"

Antwort, gekürzt – Beispielwerte, kein Live-Abruf:

{
  "items": [
    { "id": "…", "name": "Stadthalle Weiz", "slug": "stadthalle-weiz", "address": "…", "zip": "8160", "city": "Weiz", "state": "Steiermark", "website": null, "lat": …, "lng": …, "ball_count": 2 }
  ],
  "total": …
}

Alle Ball-Seiten GET /api/public/v1/sitemap (listBallUrls)

Die Adressen aller veröffentlichten Ball-Seiten mit lastModified. Wenn du Ball-Seiten verlinken oder gezielt abrufen willst, ist das die vollständige Liste – kürzer als die Ballsuche und ohne Paginierung.

curl -s "https://ballbuddy.at/api/public/v1/sitemap"

Antwort, gekürzt – Beispielwerte, kein Live-Abruf:

{
  "items": [
    { "url": "https://ballbuddy.at/ball/hak-weiz-2027", "lastModified": "2026-09-05T08:00:00+00:00" }
  ],
  "total": …
}

Kalendereintrag GET /api/ics/{slug} (getBallIcs)

Ein iCalendar-Eintrag (text/calendar) je Ball mit zwei Erinnerungen (eine Woche und ein Tag vorher), eine Stunde cachebar, 404 bei unbekanntem Slug. Bei has_motto: false heißt der Eintrag „Maturaball <Schule>“, bei time_known: false ist er ganztägig – du musst dafür nichts selbst rechnen.

curl -s "https://ballbuddy.at/api/ics/hak-weiz-2027"

Antwort, gekürzt – Beispielwerte, kein Live-Abruf:

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Ballbuddy//Ballkalender//DE
BEGIN:VEVENT
UID:…@ballbuddy.at
DTSTART:20270116T190000Z
DTEND:…
SUMMARY:Maturaball HAK Weiz: Casino Royale
LOCATION:Stadthalle Weiz\, 8160 Weiz
URL:https://ballbuddy.at/ball/hak-weiz-2027
BEGIN:VALARM
TRIGGER:-P7D
…
END:VEVENT
END:VCALENDAR

Status GET /api/health (getHealth)

Sagt nur, ob der Dienst läuft – Cache-Control: no-store, keine Daten. Zum Prüfen, ob sich Daten geändert haben, ist das der falsche Weg; dafür gibt es den ETag (unten).

curl -s "https://ballbuddy.at/api/health"

Antwort, gekürzt – Beispielwerte, kein Live-Abruf:

{ "ok": true, "service": "ballbuddy", "ts": "2026-09-05T08:00:00.000Z" }

Cache und ETag: höflich pollen

Alle Antworten der Public-API tragen Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400 und einen schwachen ETag. Merk dir den ETag und schick ihn beim nächsten Abruf als If-None-Match mit – hat sich nichts geändert, bekommst du 304 Not Modified ohne Body. Einmal pro Stunde je Abfrage reicht; die Daten ändern sich täglich, nicht minütlich. Ein sprechender User-Agent mit Kontaktmöglichkeit ist erwünscht, zum Beispiel MeinAgent/1.0 (+https://example.org/bot).

curl -s -i "https://ballbuddy.at/api/public/v1/balls/hak-weiz-2027" | grep -i '^etag'
# ETag: W/"…"
curl -s -o /dev/null -w '%{http_code}' -H 'If-None-Match: W/"…"' "https://ballbuddy.at/api/public/v1/balls/hak-weiz-2027"
# 304

Datenlücken: has_motto, time_known und schools

Drei Felder verhindern die typischen Fehler beim Anzeigen – dieselbe Erklärung steht im Skill ballbuddy-ballkalender, weil ein Agent selten beides liest:

  • has_motto: falsetitle ist generisch („HLW Weiz Maturaball“, „Maturaball HLW Weiz 2027“) und kein Motto. Zeig dann die Schule (schools[0].name, bei mehreren „A × B“) als Titel und „Motto folgt“ als Untertitel – so macht es ballbuddy.at selbst. title bleibt unverändert, url und slug gelten weiter.
  • time_known: false ⇒ die Uhrzeit ist unbekannt (starts_at trägt einen Platzhalter). Nur das Datum von starts_at in Europe/Vienna verwenden, keine Uhrzeit anzeigen, Kalender-Einträge ganztägig anlegen; ends_at und doors_at dann ignorieren.
  • schools: alle beteiligten Schulen, Hauptschule zuerst (nie leer, wenn school gesetzt ist). Gemeinsame Bälle (HTL × Modeschule) haben mehr als einen Eintrag; school bleibt die Hauptschule. Der Filter school=<slug> trifft jede beteiligte Schule.

Was die API nicht kann

  • Kein Schreiben. Keine Bälle anlegen oder ändern, keine Erinnerungen, keine Übernahme-Anfragen – das machen Menschen auf der Ball-Seite.
  • Keine Ticketkäufe. tickets.url und ticket_types sind Hinweise; verkauft wird beim Komitee.
  • Keine personenbezogenen Daten. Keine Mitglieder, Kontakte oder E-Mail-Adressen – die Komitee-App liegt hinter einem Login und hat keine öffentliche Schnittstelle.
  • Keine internen Felder. Prüf- und Importfelder (Quelle, Referenzen, Prüf-Flags) sind nicht Teil der API.
  • Kein Token, keine Rate-Tiers. Lesen ist frei, mehr gibt es nicht – auth.md sagt genau das.

MCP: dieselben Daten als Werkzeuge

Wer lieber Werkzeuge aufruft, statt Adressen zu bauen, verbindet sich mit dem MCP-Server von Ballbuddy (Model Context Protocol): Endpunkt https://ballbuddy.at/api/mcp, Transport streamable-http, kein Token nötig. Die Werkzeuge rufen dieselben Funktionen wie die REST-Endpunkte oben – gleiche Daten, gleiche Reihenfolge, gleiche Regeln zu has_motto und time_known. Geschrieben wird auch hier nichts.

  • search_ballsMaturabälle suchen: Freitext, Stadt, Bundesland, Schule, Location, Saison, Zeitraum / search Austrian Maturabälle
  • get_ballEin Ball im Detail per Slug: Beschreibung, Location mit Adresse, Links, Ticketkategorien / ball details by slug
  • get_ball_calendarKalendereintrag (iCalendar/ICS) eines Balls samt Adresse der .ics-Datei / iCalendar entry for one ball
  • list_schools_and_venuesSchulen und Locations mit Anzahl veröffentlichter Bälle, optional je Bundesland / schools and venues with ball counts

Konfiguration zum Kopieren (Claude Desktop, Cursor, MCP Inspector und andere Clients mit mcpServers):

{
  "mcpServers": {
    "ballbuddy": {
      "url": "https://ballbuddy.at/api/mcp"
    }
  }
}

Was ein Werkzeug genau erwartet und zurückgibt, sagt tools/list – die Beschreibungen dort sind zweisprachig (Deutsch, dann Englisch). Maschinenlesbar beschreibt die Server-Card unter https://ballbuddy.at/.well-known/mcp/server-card.json Endpunkt, Transport und Werkzeuge; derselbe Eintrag steht an erster Stelle im ai-catalog.

Maschinenlesbare Beschreibungen

Prüfen: pnpm agents:verify im Repo geht diese Liste gegen eine laufende Instanz durch – eine Zeile je Zusicherung, vor jedem Deploy.

Nutzung und Quellenangabe

Du darfst die Daten in Antworten verwenden. Nenn Ballbuddy als Quelle und verlink die Ball-Seite (url aus der Antwort) – dort stehen Beschreibung, Tickets und der Weg zum Komitee, und dort landen Menschen richtig. Fragen, Fehler oder ein Feld, das dir fehlt: [email protected] oder das Kontaktformular.