Verfügbarkeit: welche Uhrzeiten anbieten?

Dieselbe Rechnung wie im Buchungs-Widget — damit eine Telefonanlage genau die Zeiten anbietet, die der Gast online auch sähe.

Für Entwickler

Voraussetzungen

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

DIESE ROUTE RECHNET GENAU DAS, WAS DAS WIDGET RECHNET: Öffnungszeiten, Saison, Sperren, Vorlauffristen, Kontingente je Uhrzeit und freie Tische. Ein Mitarbeiter am Telefon bietet damit dieselben Zeiten an wie die Buchungsstrecke — und tippt keine Uhrzeit ein, die beim Anlegen abgelehnt wird.

VERBINDLICH IST TROTZDEM ERST DAS ANLEGEN. Zwischen Abfrage und Buchung können Minuten liegen, in denen der letzte Tisch weggeht oder der Betrieb den Tag sperrt. Behandeln Sie die Liste als Vorschlag und ein 409 beim Anlegen als normalen Ausgang, nicht als Ausfall.

JEDER SLOT SAGT, OB ER BUCHBAR IST UND WARUM NICHT. `available: false` kommt mit einem `reason`: `closed` (an diesem Tag keine Online-Reservierung), `blocked` (Sperre des Betriebs), `limit` (das Kontingent dieser Uhrzeit ist vergeben), `capacity` (kein Tisch für diese Personenzahl), `advance` (ausserhalb des Buchungszeitraums — zu früh oder zu spät dran).

`endTimeRequired` IST DER PUNKT, DEN MAN LEICHT ÜBERSIEHT. Arbeitet der Betrieb mit festen Zeitfenstern, muss beim Anlegen zusätzlich `endTime` mitgeschickt werden, und zwar einer der Werte aus `endTimes`. Fehlt er, antwortet `POST /api/v1/reservations` mit 400 und `reason: "end_time_required"`. Ist `endTimeRequired` gleich `false`, bestimmt der Betrieb die Dauer selbst — dann `endTime` weglassen.

`meta` BESCHREIBT DIE REGELN DES HAUSES: `minPartySizeOnline` und `maxPartySizeOnline` sind die Grenzen der Online-Buchung, `groupSizeThreshold` die Personenzahl, ab der der Betrieb eine Gruppenanfrage statt einer Buchung will, `defaultDurationMin` die Standarddauer, `slotGranularityMin` der Abstand zwischen zwei angebotenen Uhrzeiten. `closed: true` heisst: für diesen Tag und diese Personenzahl gibt es keine einzige Uhrzeit — geschlossen, gesperrt oder Personenzahl ausserhalb der Grenzen.

ANZAHLUNGEN UND GEBÜHREN STEHEN NICHT IN DER ANTWORT, obwohl die Widget-Antwort sie trägt. Das sind Finanzdaten, und `reservations:read` ist dafür zu grob — jede Rolle mit Leserecht sähe sie mit. Solange es kein eigenes Modul für Zahlungen gibt, gehen Beträge über diese Schnittstelle nicht hinaus.

Die Route

GET/api/v1/availability

Die buchbaren Uhrzeiten eines Tages für eine bestimmte Personenzahl — mit Begründung für jede Uhrzeit, die nicht geht.

Rechte

reservations:read

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

Abfrageparameter

NameTypBedeutung
datePflichtYYYY-MM-DDDer Kalendertag des Betriebs, für den gerechnet wird.
partySizePflichtinteger 1–200Personenzahl. Sie entscheidet über Kapazität UND über die Online-Grenzen des Betriebs.
areaIdUUIDNur Uhrzeiten, die in diesem Bereich buchbar sind. Eine Kennung, die nicht zu diesem Betrieb gehört, ist 400.

Mögliche Fehler

  • validationFehlendes oder unlesbares `date`, `partySize` ausserhalb 1–200, unbekannter Parameter oder ein `areaId`, der nicht zu diesem Betrieb gehört.
  • forbiddenDem Schlüssel fehlt `reservations:read`.
  • plan_upgrade_requiredDer Plan des Betriebs enthält Reservierungen nicht.
  • Diese Route zählt NICHT gegen das Kontingent gelesener Datensätze — Uhrzeiten sind keine Gastdatensätze.
  • Der Modus „Gruppenanfrage", der im Widget auch unbuchbare Zeiten auflistet, gibt es hier nicht: über die API wäre er nur ein Weg, die Antwort „nicht buchbar" zu umgehen.

Codebeispiele

curl — freie Zeiten für vier Personen
curl
curl -sS -G https://tactictable.com/api/v1/availability \  -H "Authorization: Bearer tt_live_if3u5vp6maf67xmw_PT1xP8EQF7reKUtLcZ7v9z5qaRKmoxeHmd27JpaDvfM" \  --data-urlencode "date=2026-09-20" \  --data-urlencode "partySize=4"
Ohne `areaId` wird über alle online buchbaren Bereiche gerechnet. Mit `areaId` bekommen Sie die Zeiten genau dieses Bereichs — sinnvoll, wenn der Gast ausdrücklich auf die Terrasse will.
Antwort 200 — GET /api/v1/availability
JSON
{  "date": "2026-09-20",  "partySize": 4,  "areaId": null,  "timezone": "Europe/Vienna",  "slots": [    { "time": "11:30", "available": false, "reason": "advance", "endTimes": null, "endTimeRequired": false },    { "time": "12:00", "available": true, "reason": null, "endTimes": null, "endTimeRequired": false },    { "time": "18:00", "available": true, "reason": null, "endTimes": ["20:00", "20:30"], "endTimeRequired": true },    { "time": "18:30", "available": false, "reason": "capacity", "endTimes": null, "endTimeRequired": false },    { "time": "19:00", "available": false, "reason": "limit", "endTimes": null, "endTimeRequired": false },    { "time": "19:30", "available": true, "reason": null, "endTimes": ["21:30"], "endTimeRequired": true }  ],  "meta": {    "minPartySizeOnline": 1,    "maxPartySizeOnline": 8,    "groupSizeThreshold": 9,    "defaultDurationMin": 120,    "slotGranularityMin": 30,    "closed": false  }}
Bei `18:00` verlangt der Betrieb eine Endzeit — beim Anlegen muss dann `endTime` einer der beiden Werte sein. Bei `12:00` steht `endTimeRequired: false`: dort `endTime` weglassen, die Dauer bestimmt der Betrieb.
Antwort 200 — an diesem Tag geht gar nichts
JSON
{  "date": "2026-12-24",  "partySize": 4,  "areaId": null,  "timezone": "Europe/Vienna",  "slots": [],  "meta": {    "minPartySizeOnline": 1,    "maxPartySizeOnline": 8,    "groupSizeThreshold": 9,    "defaultDurationMin": 120,    "slotGranularityMin": 30,    "closed": true  }}
`closed: true` mit leerer `slots`-Liste: geschlossen, gesperrt oder die Personenzahl liegt ausserhalb der Online-Grenzen. Es ist KEIN Fehler — der Status ist 200.
TypeScript — nur die buchbaren Zeiten anbieten
TypeScript
interface Slot {    time: string    available: boolean    reason: string | null    endTimes: string[] | null    endTimeRequired: boolean} interface Verfuegbarkeit {    date: string    partySize: number    timezone: string    slots: Slot[]    meta: { closed: boolean; groupSizeThreshold: number; maxPartySizeOnline: number }} export async function freieZeiten(    token: string,    tag: string,    personen: number,): Promise<Verfuegbarkeit> {    const parameter = new URLSearchParams({ date: tag, partySize: String(personen) })     const antwort = await fetch('https://tactictable.com/api/v1/availability?' + parameter, {        headers: { Authorization: 'Bearer ' + token, Accept: 'application/json' },    })    if (!antwort.ok) throw new Error('HTTP ' + antwort.status)     return (await antwort.json()) as Verfuegbarkeit} /** Was dem Anrufer vorgelesen wird — und was er mitgeben muss. */export function vorschlaege(v: Verfuegbarkeit): { zeit: string; endzeiten: string[] }[] {    return v.slots        .filter((s) => s.available)        .map((s) => ({ zeit: s.time, endzeiten: s.endTimeRequired ? (s.endTimes ?? []) : [] }))}
Übersteigt `partySize` den Wert `meta.groupSizeThreshold`, will der Betrieb eine Gruppenanfrage statt einer Buchung — dann gehört der Gast ans Telefon und nicht in die Buchungsstrecke.
Python — Zeit anbieten und sofort buchen
Python
import jsonimport uuid import requests  def erste_freie_zeit(sitzung: requests.Session, tag: str, personen: int):    antwort = sitzung.get(        "https://tactictable.com/api/v1/availability",        params={"date": tag, "partySize": personen},        timeout=20,    )    antwort.raise_for_status()    daten = antwort.json()     for slot in daten["slots"]:        if slot["available"]:            return slot     return None  def buchen(sitzung: requests.Session, tag: str, personen: int, name: str):    slot = erste_freie_zeit(sitzung, tag, personen)    if slot is None:        raise ValueError("An diesem Tag ist nichts frei.")     rumpf = {"guestName": name, "partySize": personen, "date": tag, "time": slot["time"]}     # Nur mitschicken, wenn der Betrieb eine Endzeit VERLANGT.    if slot["endTimeRequired"] and slot["endTimes"]:        rumpf["endTime"] = slot["endTimes"][0]     erg = sitzung.post(        "https://tactictable.com/api/v1/reservations",        data=json.dumps(rumpf),        headers={"Content-Type": "application/json", "Idempotency-Key": str(uuid.uuid4())},        timeout=30,    )    if erg.status_code == 409:        raise ValueError("In der Zwischenzeit vergeben — erneut abfragen.")    erg.raise_for_status()    return erg.json()["reservation"]
Das 409 dazwischen ist keine Ausnahme, sondern der erwartete Ausgang eines Wettlaufs. Fragen Sie in dem Fall die Verfügbarkeit neu ab, statt den Fehler durchzureichen.
Ruby — Zeiten für den Telefondienst aufbereiten
Ruby
require 'json'require 'net/http' GRUENDE = {  'closed'   => 'an diesem Tag geschlossen',  'blocked'  => 'vom Betrieb gesperrt',  'limit'    => 'Kontingent vergeben',  'capacity' => 'kein Tisch dieser Größe frei',  'advance'  => 'außerhalb des Buchungszeitraums'}.freeze def verfuegbarkeit(token, tag, personen)  ziel = URI('https://tactictable.com/api/v1/availability')  ziel.query = URI.encode_www_form(date: tag, partySize: personen)   anfrage = Net::HTTP::Get.new(ziel)  anfrage['Authorization'] = 'Bearer ' + token   antwort = Net::HTTP.start(ziel.hostname, ziel.port, use_ssl: true) do |http|    http.request(anfrage)  end   raise 'HTTP ' + antwort.code unless antwort.code == '200'   JSON.parse(antwort.body)end def vorlesen(daten)  frei = daten['slots'].select { |s| s['available'] }  return 'Heute ist leider nichts mehr frei.' if frei.empty?   'Frei wären: ' + frei.map { |s| s['time'] }.join(', ')end # Ein unbekannter Grund wird DURCHGEREICHT, nicht auf einen anderen abgebildet.def grund_text(slot)  GRUENDE.fetch(slot['reason'], slot['reason'].to_s)end
`GRUENDE.fetch` mit Rückfall auf den Rohwert: die Liste der Gründe kann wachsen, und ein unbekannter Grund, der als „kein Tisch frei" vorgelesen wird, ist eine Falschauskunft an den Gast.