Besuchshistorie eines Gastes

Welche Reservierungen zu einem Gast gehören — und warum dieser eine Endpunkt zwei Rechte verlangt.

Für Entwickler

Voraussetzungen

  • Recht `guests:read` UND `reservations:read`
  • Plan-Merkmal „Gäste / CRM"

DIESE ROUTE VERLANGT ZWEI RECHTE, und das ist keine Umständlichkeit. Die Antwort besteht aus REServierungszeilen, ausgewählt nach einem GAST. Wer sie liest, liest beides. Ohne die zweite Frage wäre `/guests/{id}/visits` der Weg, mit einem reinen Gästeschlüssel das Reservierungsbuch des Hauses auszulesen — Gast für Gast, vollständig, am Recht `reservations:read` vorbei.

EINE FREMDE KENNUNG ERGIBT 404, NICHT EINE LEERE LISTE. Das klingt nach einem Detail, ist aber der Unterschied zwischen einer Ressource und einem Nachschlagewerk: „leer" hiesse sonst „kennen wir nicht ODER hat nie gebucht", und der Aufrufer könnte beides nicht auseinanderhalten.

DIE ZEILEN SIND BESUCHSNACHWEISE, KEINE VOLLEN RESERVIERUNGEN. Bewusst nicht enthalten: `changeHash` (der Stornolink aus unseren Mails — gäbe die API ihn heraus, könnte jeder Halter eines LESEschlüssels jede Reservierung des Hauses stornieren), `internalNotes` (dort steht „zahlt schlecht"), der Gastwunsch `notes` (er steht in der DSGVO-Auskunft, aber nicht in der Besuchsliste) sowie alle Zahlungen und Gebühren.

`anonymizedAt` IST DAS FELD, DAS MAN NICHT ÜBERSEHEN DARF. Ist es gesetzt, wurden die Personendaten dieser Buchung geschrubbt. Ein Werkzeug, das daraus eine Bewertungsanfrage oder eine Geburtstagsmail baut, MUSS diese Zeile überspringen.

SORTIERT WIRD AUFSTEIGEND NACH `updatedAt`, dann `id` — dieselbe Ordnung wie bei der Gästeliste, und aus demselben Grund: eine Zeile, die während des Blätterns geändert wird, wandert ans Ende und fällt nicht durch.

Die Route

GET/api/v1/guests/{id}/visits

Alle Reservierungen, die diesem Gast zugeordnet sind — als Besuchsnachweis, nicht als vollständige Reservierung.

Rechte

guests:readreservations:read

Alle genannten Rechte zusammen, nicht wahlweise.

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

Pfad

NameTypBedeutung
{id}PflichtUUIDKennung des Gastes. Gibt es ihn in diesem Betrieb nicht, ist die Antwort 404 — nicht eine leere Liste.

Abfrageparameter

NameTypBedeutung
limitinteger 1–200Vorgabe: 50Zeilen je Seite.
cursorundurchsichtiger Zeiger`pagination.nextCursor` der vorigen Antwort, unverändert.

Mögliche Fehler

  • forbiddenDem Schlüssel fehlt `guests:read` — oder `reservations:read`. Die Meldung sagt ausdrücklich, dass `guests:read` allein das Reservierungsbuch nicht öffnet.
  • not_foundZu dieser Kennung gibt es in diesem Betrieb keinen Gast.
  • validationUnbekannter Parameter oder unlesbarer Cursor.
  • Umschlag: `{ "guestId": …, "data": [...], "pagination": { … }, "meta": { … } }`.
  • Zählt nicht gegen das Stundenkontingent für zeilenliefernde Gästeaufrufe — es sind Reservierungs-, keine Gastdatensätze.

Codebeispiele

curl — Besuche eines Gastes
curl
curl -sS \
  "https://tactictable.com/api/v1/guests/fb69190b-e35f-5a2e-be8a-b7c272782903/visits?limit=50" \
  -H "Authorization: Bearer tt_live_if3u5vp6maf67xmw_PT1xP8EQF7reKUtLcZ7v9z5qaRKmoxeHmd27JpaDvfM"
Der Schlüssel braucht hier BEIDE Leserechte. Fehlt `reservations:read`, ist die Antwort 403 mit einer Meldung, die genau das sagt.
Antwort 200 — GET /api/v1/guests/{id}/visits
JSON
{  "guestId": "fb69190b-e35f-5a2e-be8a-b7c272782903",  "data": [    {      "reservationId": "31061d44-ac33-550d-b4d0-a16973e270f5",      "date": "2026-08-29",      "time": "18:00",      "endTime": "20:00",      "partySize": 3,      "status": "COMPLETED",      "source": "WIDGET",      "areaId": "100baca4-f01d-5c3c-8947-65e5a5061c6b",      "areaName": "Terrasse",      "seatedAt": "2026-08-29T16:04:12.000Z",      "finishedAt": "2026-08-29T18:11:50.000Z",      "anonymizedAt": null,      "confirmedAt": "2026-08-20T09:15:02.000Z",      "canceledAt": null,      "createdAt": "2026-08-20T09:15:01.000Z",      "updatedAt": "2026-08-29T18:11:50.000Z"    }  ],  "pagination": { "limit": 50, "nextCursor": null, "hasMore": false },  "meta": {    "restaurantId": "35122c8e-d842-5b49-814c-0e8f2aaed157",    "timezone": "Europe/Vienna"  }}
`date` ist der Kalendertag, `seatedAt` und `finishedAt` sind echte Zeitpunkte in UTC. Der Unterschied zwischen `date`+`time` und `seatedAt` ist die Verspätung des Gastes — genau das, was eine Auswertung wissen will.
Antwort 403 — ein Recht fehlt
JSON
{  "error": "forbidden",  "message": "Die Besuchshistorie besteht aus Reservierungen. Dem Schluessel fehlt dafuer das Recht „reservations:read“ — „guests:read“ allein oeffnet das Reservierungsbuch nicht.",  "module": "reservations",  "op": "read",  "docs": "https://tactictable.com/dokumentation/api/fehler/forbidden",  "requestId": "req_8f31c0a94d2b47e6ba05"}
Die Meldung nennt genau den fehlenden Haken. Ein Einrichtungsassistent kann daraus einen Satz bauen, ohne dass jemand die Dokumentation aufschlägt.
TypeScript — Stammgast oder nicht?
TypeScript
interface Besuch {    reservationId: string    date: string    time: string    partySize: number    status: string    anonymizedAt: string | null} interface Besuchsseite {    guestId: string    data: Besuch[]    pagination: { limit: number; nextCursor: string | null; hasMore: boolean }} export async function besuche(token: string, gastId: string): Promise<Besuch[]> {    const antwort = await fetch(        'https://tactictable.com/api/v1/guests/' + encodeURIComponent(gastId) + '/visits?limit=200',        { headers: { Authorization: 'Bearer ' + token, Accept: 'application/json' } },    )    if (!antwort.ok) throw new Error('HTTP ' + antwort.status)     const seite = (await antwort.json()) as Besuchsseite    return seite.data} /** * Wie viele Besuche WIRKLICH stattgefunden haben. * Anonymisierte Zeilen bleiben als Zaehlung stehen, tragen aber keinen * Personenbezug mehr — fuer eine Bewertungsanfrage sind sie tabu. */export function abgeschlosseneBesuche(liste: Besuch[]): Besuch[] {    return liste.filter((b) => b.status === 'COMPLETED' && b.anonymizedAt === null)}
`status === "COMPLETED"` ist die einzige Zusage, dass der Gast wirklich da war. `CONFIRMED` heisst nur, dass der Tisch zugesagt war — auch eine No-Show-Buchung war einmal `CONFIRMED`.
Python — Besuchshistorie mit Blätterung
Python
import requests  def alle_besuche(sitzung: requests.Session, gast_id: str) -> list:    gesammelt = []    cursor = None     for _ in range(200):        parameter = {"limit": 200}        if cursor:            parameter["cursor"] = cursor         antwort = sitzung.get(            "https://tactictable.com/api/v1/guests/" + gast_id + "/visits",            params=parameter,            timeout=30,        )         if antwort.status_code == 404:            raise LookupError("Diesen Gast gibt es in diesem Betrieb nicht.")         antwort.raise_for_status()        rumpf = antwort.json()        gesammelt.extend(rumpf["data"])         cursor = rumpf["pagination"]["nextCursor"]        if cursor is None:            return gesammelt     raise RuntimeError("Unerwartet viele Seiten.")
Der 404 wird eigens behandelt: er bedeutet „diesen Gast gibt es hier nicht" und nicht „hat nie gebucht". Beides zu vermischen wäre genau die Unschärfe, die diese Route vermeidet.