Öffnungszeiten

Die Zeitsätze des Betriebs mit ihren sieben Tagen, zwei Fenstern je Tag, Saison und Vorrang — inklusive der Falle „Schluss vor Beginn".

Für Entwickler

Voraussetzungen

  • Recht `reservations:read`
  • Plan-Merkmal „Reservierungen"

EIN BETRIEB HAT MEHRERE ZEITSÄTZE, nicht einen. Ein Satz gilt für den ganzen Betrieb oder für bestimmte Bereiche (`areaIds`), kann ein Saisonfenster tragen (`season`) und hat einen `priority`. Überlappen sich zwei Sätze, gewinnt der mit dem höheren Wert. Deshalb ist die Liste absteigend nach `priority` sortiert: wer nur die erste Seite liest, sieht den Satz, der heute wirklich gilt.

EINE LEERE `areaIds`-LISTE HEISST „FÜR DEN GANZEN BETRIEB", nicht „für keinen Bereich". Das ist die eine Stelle, an der eine leere Liste nicht das Offensichtliche bedeutet — und die Unterscheidung entscheidet darüber, ob Ihre Anzeige die Öffnungszeiten des Hauses oder gar keine zeigt.

`closeTime` KLEINER ALS `openTime` IST ERLAUBT und bedeutet ein Fenster ÜBER MITTERNACHT: `openTime: "18:00"`, `closeTime: "02:00"` heisst „von sechs Uhr abends bis zwei Uhr früh". Wer die beiden Werte naiv vergleicht und den Satz verwirft, blendet den ganzen Nachtbetrieb aus. Das ist die häufigste Falle dieser Route.

JEDER TAG HAT ZWEI FENSTER. `openTime`/`closeTime` ist das erste, `openTime2`/`closeTime2` das zweite — so bildet der Betrieb die Mittagspause ab. Ist `isOpen` gleich `false`, sind alle vier Werte bedeutungslos.

DIE SIEBEN TAGE KOMMEN IMMER VOLLSTÄNDIG UND IMMER IN DERSELBEN REIHENFOLGE (`MONDAY` bis `SUNDAY`). Das ist kein Zufall, sondern Absicht: ein Abgleich, der Reihenfolgen vergleicht, meldet sonst bei jedem Lauf „geändert".

DIE ROUTE HÄNGT AM RECHT `reservations:read` UND NICHT AN EINEM EIGENEN MODUL — die Öffnungszeiten steuern die Verfügbarkeitsrechnung, und ein eigenes Modul wäre ein zweites Vokabular für dieselbe Sache. Die Folge gehört benannt: eine reine Website- oder Bildschirm-Einbindung braucht dafür `reservations:read` und kann damit auch die Reservierungsliste lesen. Wer das nicht will, nimmt die Öffnungszeiten von der Website statt aus der API.

Die Route

GET/api/v1/opening-hours

Alle Öffnungszeiten-Sätze des Betriebs mit ihren sieben Tagen, dem Saisonfenster und den Bereichen, für die sie gelten.

Rechte

reservations:read

Plan-Merkmal reservations — fehlt es dem Betrieb, antwortet die Route mit 402 plan_upgrade_required.

Abfrageparameter

NameTypBedeutung
activetrue | falseNur aktive bzw. nur abgeschaltete Sätze. Ohne Angabe: alle.
updatedSinceISO-8601 mit ZoneNur Sätze, die seit diesem Zeitpunkt geändert wurden.
limitinteger 1–200Vorgabe: 50Zeilen je Seite.
cursorundurchsichtiger Zeiger`nextCursor` der vorigen Antwort, unverändert.
includeTotaltrue | falseVorgabe: falseErgänzt `total`; kostet eine zweite Abfrage.

Mögliche Fehler

  • validationUnbekannter Parameter, ein Parameter steht mehrfach in der Adresse, oder der Cursor gehört zu einer anderen Abfrage.
  • forbiddenDem Schlüssel fehlt `reservations:read`.
  • plan_upgrade_requiredDer Plan des Betriebs enthält Reservierungen nicht.
  • Sortiert nach `priority` ABSTEIGEND, dann `id`. Der wichtigste Satz steht oben.
  • Flacher Umschlag: `data`, `nextCursor`, `hasMore`, `limit`, optional `total`.

Codebeispiele

curl — aktive Zeitsätze
curl
curl -sS "https://tactictable.com/api/v1/opening-hours?active=true" \
  -H "Authorization: Bearer tt_live_if3u5vp6maf67xmw_PT1xP8EQF7reKUtLcZ7v9z5qaRKmoxeHmd27JpaDvfM"
Ein Parameter darf nur EINMAL in der Adresse stehen. `?active=true&active=false` ist 400 — welcher Wert gelten soll, ist nicht bestimmbar, und Raten wäre ein Schmuggelweg.
Antwort 200 — GET /api/v1/opening-hours
JSON
{  "data": [    {      "id": "1e7271b6-4486-5481-b33b-aa937048b8dd",      "title": "Sommer — Terrasse",      "isDefault": false,      "isActive": true,      "priority": 20,      "season": { "startMonth": 5, "startDay": 1, "endMonth": 9, "endDay": 30 },      "areaIds": ["100baca4-f01d-5c3c-8947-65e5a5061c6b"],      "days": [        { "dayOfWeek": "MONDAY", "isOpen": false, "openTime": null, "closeTime": null, "openTime2": null, "closeTime2": null },        { "dayOfWeek": "TUESDAY", "isOpen": true, "openTime": "11:30", "closeTime": "14:00", "openTime2": "17:30", "closeTime2": "23:00" },        { "dayOfWeek": "WEDNESDAY", "isOpen": true, "openTime": "11:30", "closeTime": "14:00", "openTime2": "17:30", "closeTime2": "23:00" },        { "dayOfWeek": "THURSDAY", "isOpen": true, "openTime": "11:30", "closeTime": "14:00", "openTime2": "17:30", "closeTime2": "23:00" },        { "dayOfWeek": "FRIDAY", "isOpen": true, "openTime": "11:30", "closeTime": "14:00", "openTime2": "17:30", "closeTime2": "01:00" },        { "dayOfWeek": "SATURDAY", "isOpen": true, "openTime": "17:30", "closeTime": "01:00", "openTime2": null, "closeTime2": null },        { "dayOfWeek": "SUNDAY", "isOpen": true, "openTime": "11:30", "closeTime": "21:00", "openTime2": null, "closeTime2": null }      ],      "timezone": "Europe/Vienna",      "createdAt": "2026-04-02T08:11:00.000Z",      "updatedAt": "2026-05-14T16:20:41.000Z"    }  ],  "nextCursor": null,  "hasMore": false,  "limit": 50}
Freitag und Samstag haben `closeTime: "01:00"` — ein Fenster über Mitternacht. Ein Vergleich `closeTime > openTime` würde diese beiden Tage als „geschlossen" darstellen. `season` gilt einschliesslich: 1. Mai bis 30. September.
TypeScript — Öffnungszeiten richtig lesen
TypeScript
interface Tag {    dayOfWeek: string    isOpen: boolean    openTime: string | null    closeTime: string | null    openTime2: string | null    closeTime2: string | null} interface Zeitsatz {    id: string    title: string    isActive: boolean    isDefault: boolean    priority: number    season: { startMonth: number; startDay: number; endMonth: number; endDay: number } | null    areaIds: string[]    days: Tag[]    timezone: string} /** Minuten seit Mitternacht — "25:00" gibt es hier nicht, dafuer Ueberlauf. */function minuten(hhmm: string): number {    const [h, m] = hhmm.split(':').map(Number)    return (h ?? 0) * 60 + (m ?? 0)} /** * Ein Fenster kann UEBER MITTERNACHT laufen: closeTime < openTime heisst * „bis zum naechsten Morgen". Wer das nicht beruecksichtigt, blendet den * ganzen Nachtbetrieb aus. */export function istOffen(tag: Tag, uhrzeit: string): boolean {    if (!tag.isOpen) return false    const jetzt = minuten(uhrzeit)     const fenster: [string, string][] = []    if (tag.openTime && tag.closeTime) fenster.push([tag.openTime, tag.closeTime])    if (tag.openTime2 && tag.closeTime2) fenster.push([tag.openTime2, tag.closeTime2])     return fenster.some(([von, bis]) => {        const a = minuten(von)        const b = minuten(bis)        return a <= b ? jetzt >= a && jetzt < b : jetzt >= a || jetzt < b    })} /** Leeres areaIds heisst: gilt fuer den GANZEN Betrieb. */export function giltFuerBereich(satz: Zeitsatz, areaId: string | null): boolean {    if (satz.areaIds.length === 0) return true    return areaId !== null && satz.areaIds.includes(areaId)}
Die beiden Funktionen sind der ganze Unterschied zwischen einer Anzeige, die stimmt, und einer, die Freitagabend „geschlossen" behauptet.
Python — die heute gültigen Zeiten bestimmen
Python
from datetime import date import requests WOCHENTAGE = [    "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY",]  def in_saison(satz: dict, tag: date) -> bool:    saison = satz.get("season")    if saison is None:        return True  # ganzjaehrig     start = (saison["startMonth"], saison["startDay"])    ende = (saison["endMonth"], saison["endDay"])    heute = (tag.month, tag.day)     # Ein Fenster, das ueber den Jahreswechsel laeuft (z. B. Nov -> Feb).    if start <= ende:        return start <= heute <= ende    return heute >= start or heute <= ende  def satz_fuer_heute(sitzung: requests.Session, tag: date) -> dict | None:    antwort = sitzung.get(        "https://tactictable.com/api/v1/opening-hours",        params={"active": "true", "limit": 200},        timeout=20,    )    antwort.raise_for_status()     # Bereits nach priority ABSTEIGEND sortiert: der erste passende gewinnt.    for satz in antwort.json()["data"]:        if in_saison(satz, tag):            return satz     return None  def zeiten_heute(satz: dict, tag: date) -> dict:    name = WOCHENTAGE[tag.weekday()]    return next(t for t in satz["days"] if t["dayOfWeek"] == name)
Das Saisonfenster kann über den Jahreswechsel laufen (November bis Februar). Ein naiver Vergleich `start <= heute <= ende` meldet dann ganzjährig „ausserhalb der Saison".
PHP — Öffnungszeiten für eine Website ausgeben
PHP
<?php const WOCHENTAGE = [    'MONDAY' => 'Montag',    'TUESDAY' => 'Dienstag',    'WEDNESDAY' => 'Mittwoch',    'THURSDAY' => 'Donnerstag',    'FRIDAY' => 'Freitag',    'SATURDAY' => 'Samstag',    'SUNDAY' => 'Sonntag',]; function zeiten_zeile(array $tag): string{    if (!$tag['isOpen']) {        return 'geschlossen';    }     $fenster = [];    if ($tag['openTime'] && $tag['closeTime']) {        $fenster[] = $tag['openTime'] . '–' . $tag['closeTime'];    }    if ($tag['openTime2'] && $tag['closeTime2']) {        $fenster[] = $tag['openTime2'] . '–' . $tag['closeTime2'];    }     return $fenster === [] ? 'geschlossen' : implode(' und ', $fenster);} function oeffnungszeiten_html(string $token): string{    $ch = curl_init('https://tactictable.com/api/v1/opening-hours?active=true&limit=200');    curl_setopt_array($ch, [        CURLOPT_RETURNTRANSFER => true,        CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],        CURLOPT_TIMEOUT => 20,    ]);    $rumpf = curl_exec($ch);    curl_close($ch);     $json = json_decode($rumpf, true, 512, JSON_THROW_ON_ERROR);    $satz = $json['data'][0] ?? null;    if ($satz === null) {        return '';    }     $zeilen = [];    foreach ($satz['days'] as $tag) {        $zeilen[] = '<tr><th>' . WOCHENTAGE[$tag['dayOfWeek']] . '</th><td>'            . htmlspecialchars(zeiten_zeile($tag), ENT_QUOTES, 'UTF-8')            . '</td></tr>';    }     return '<table>' . implode('', $zeilen) . '</table>';}
`htmlspecialchars` auch hier: die Werte kommen aus der Datenbank des Betriebs und sind Eingabe eines Menschen. Eine Anbindung, die fremde Zeichenketten ungeprüft in HTML schreibt, ist der Anfang jedes Cross-Site-Scripting.