Prüfmodus und optimistisches Sperren
`X-TacticTable-Dry-Run: 1` rechnet alles durch, ohne etwas zu hinterlassen — und `ETag`/`If-Match` verhindert, dass Ihr Schreibvorgang eine fremde Änderung überfährt.
Für EntwicklerDER PRÜFMODUS BEANTWORTET EINE FRAGE: „Ginge das durch?" Schicken Sie `X-TacticTable-Dry-Run: 1` an einen schreibenden Aufruf, dann läuft die gesamte Arbeit — Rechte, Schema, fremde Kennungen, Verfügbarkeitsrechnung, Tischsuche, Eindeutigkeitsprüfungen — und wird am Ende zurückgerollt. Die Antwort trägt die Kopfzeile `TT-Dry-Run: 1` und im Körper `"dryRun": true`.
ER IST KEINE VORSCHAU, SONDERN EIN PROBELAUF. Eine Vorschau, die die Arbeit gar nicht erst ausführt, fände weder den belegten Tisch noch die doppelte Mailadresse — also genau das nicht, was man wissen will. Deshalb läuft hier alles wirklich, nur in einer Transaktion, die bewusst zurückgerollt wird.
WAS DIE ANTWORT ENTHÄLT, hängt von der Route ab. Beim Anlegen einer Reservierung steht unter `would` die Buchung, die entstanden wäre — mit der vom Tischplan gewählten Zeit, der berechneten Dauer und den zugeordneten Tischen. Beim Anlegen einer Sperre steht `data` mit `id: null` (es gibt nichts, worauf eine Kennung zeigen könnte) plus `affectedReservations`. Bei den Warenwirtschaftsrouten ist es der vollständige Datensatz, wie er entstanden wäre.
DER PRÜFMODUS VERBRAUCHT KEINEN IDEMPOTENZSCHLÜSSEL. Sie dürfen denselben Wert danach für den echten Aufruf verwenden.
OPTIMISTISCHES SPERREN IST DIE ZWEITE HÄLFTE. Reservierungen und Gäste liefern beim Einzelabruf eine `ETag`-Kopfzeile. Schicken Sie diesen Wert beim nächsten Schreiben als `If-Match` zurück, dann führt die API den Vorgang NUR aus, wenn sich die Zeile seither nicht geändert hat. Sonst antwortet sie mit 412 `precondition_failed` und nennt den aktuellen Wert.
OHNE `If-Match` GEWINNT DER LETZTE SCHREIBER. Das ist häufig richtig (ein Abgleich, der die Wahrheit kennt) und manchmal fatal: ein Kassenabgleich, der die Zeile vor zehn Minuten gelesen hat, überschreibt damit die Änderung, die der Kellner vor einer Minute im Dashboard gemacht hat. `If-Match` ist optional — wer es schickt, bekommt die Zusicherung.
DIE BEIDEN FORMEN UNTERSCHEIDEN SICH. Eine Reservierung liefert ein SCHWACHES ETag (`W/"1789412..."`), weil der Rumpf von den Rechten des Schlüssels abhängt (`internalNotes`). Ein Gast liefert ein starkes (`"g-1789412..."`). Behandeln Sie den Wert als undurchsichtig: lesen, aufheben, unverändert zurückschicken. Der Sonderwert `*` heisst „nur, wenn es die Zeile überhaupt gibt".
Schritt für Schritt
Einbindung zuerst im Prüfmodus fahren
Setzen Sie in der Testphase `X-TacticTable-Dry-Run: 1` auf jeden Schreibaufruf. Sie sehen dieselben Fehler wie im Ernstfall — nur entsteht keine Reservierung, die jemand wieder löschen muss.
Vor dem Anlegen einer Sperre prüfen, wen es trifft
`POST /api/v1/blocks` im Prüfmodus meldet `affectedReservations`. Ein Kalenderabgleich, der aus einem falsch benannten Termin den 24. Dezember sperren würde, kann so Alarm schlagen statt still den Online-Verkauf abzuschalten.
Beim Lesen das ETag aufheben
Aus der Kopfzeile `ETag` der Einzelabrufe `GET /api/v1/reservations/{id}` und `GET /api/v1/guests/{id}`. Listen liefern kein ETag.
Beim Schreiben `If-Match` mitschicken
Unverändert, mit allen Anführungszeichen und dem `W/`-Präfix, falls vorhanden.
Auf 412 mit Neulesen reagieren
Holen Sie den aktuellen Stand, wenden Sie Ihre Änderung darauf an und wiederholen Sie — mit dem neuen ETag. Nicht einfach ohne `If-Match` wiederholen: damit überfahren Sie genau die fremde Änderung, die der 412 gemeldet hat.
Codebeispiele
curl -sS -X POST https://tactictable.com/api/v1/reservations \ -H "Authorization: Bearer tt_live_if3u5vp6maf67xmw_PT1xP8EQF7reKUtLcZ7v9z5qaRKmoxeHmd27JpaDvfM" \ -H "Content-Type: application/json" \ -H "X-TacticTable-Dry-Run: 1" \ -d '{ "guestName": "Probe", "partySize": 6, "date": "2026-12-24", "time": "19:00" }'{ "dryRun": true, "would": { "date": "2026-12-24", "time": "19:00", "endTime": "21:00", "durationMin": 120, "partySize": 6, "areaId": null, "tableIds": [ "653d49c8-cd70-53ea-839f-d3131a574417", "bfdfcf1b-8d9b-5708-8a65-9f553849dfae" ], "status": "CONFIRMED", "timezone": "Europe/Vienna" }}# 1. Lesen, ETag merken (steht in der Kopfzeile).curl -sS -D - -o antwort.json \ https://tactictable.com/api/v1/reservations/31061d44-ac33-550d-b4d0-a16973e270f5 \ -H "Authorization: Bearer tt_live_if3u5vp6maf67xmw_PT1xP8EQF7reKUtLcZ7v9z5qaRKmoxeHmd27JpaDvfM" # 2. Schreiben mit genau diesem Wert.curl -sS -X PATCH https://tactictable.com/api/v1/reservations/31061d44-ac33-550d-b4d0-a16973e270f5 \ -H "Authorization: Bearer tt_live_if3u5vp6maf67xmw_PT1xP8EQF7reKUtLcZ7v9z5qaRKmoxeHmd27JpaDvfM" \ -H "Content-Type: application/json" \ -H 'If-Match: W/"1789412460123"' \ -d '{"partySize": 5}'{ "error": "precondition_failed", "message": "Die Reservierung wurde inzwischen geaendert. Holen Sie den aktuellen Stand und wiederholen Sie die Aenderung.", "etag": "W/\"1789413001777\"", "docs": "https://tactictable.com/dokumentation/api/fehler/precondition_failed", "requestId": "req_8f31c0a94d2b47e6ba05"}async function leseMitEtag(id: string, token: string) { const antwort = await fetch('https://tactictable.com/api/v1/reservations/' + id, { headers: { Authorization: 'Bearer ' + token }, }) if (!antwort.ok) throw new Error('HTTP ' + antwort.status) return { etag: antwort.headers.get('etag'), rumpf: await antwort.json() }} export async function aenderePartySize(id: string, token: string, neueGroesse: number) { for (let versuch = 0; versuch < 2; versuch++) { const { etag } = await leseMitEtag(id, token) const antwort = await fetch('https://tactictable.com/api/v1/reservations/' + id, { method: 'PATCH', headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json', ...(etag ? { 'If-Match': etag } : {}), }, body: JSON.stringify({ partySize: neueGroesse }), }) if (antwort.ok) return await antwort.json() const fehler = await antwort.json() // Nur 412 ist wiederholbar — und nur EINMAL, sonst kreist man mit // einem Prozess, der jede Sekunde schreibt, endlos. if (fehler.error !== 'precondition_failed') { throw new Error(fehler.error + ': ' + fehler.message) } } throw new Error('Die Reservierung wird gerade von anderer Seite geaendert.')}import json import requests def import_pruefen(token: str, zeilen: list, probelauf: bool = True) -> list: """Fuehrt den Import aus — oder rechnet ihn nur durch.""" kopf = { "Authorization": "Bearer " + token, "Content-Type": "application/json", } if probelauf: kopf["X-TacticTable-Dry-Run"] = "1" ergebnisse = [] for zeile in zeilen: antwort = requests.post( "https://tactictable.com/api/v1/guests", data=json.dumps(zeile), headers=kopf, timeout=30, ) ergebnisse.append({ "eingabe": zeile.get("email"), "status": antwort.status_code, "antwort": antwort.json(), }) return ergebnisse # Erst pruefen, die Fehlerzeilen korrigieren, dann mit probelauf=False laufen.Siehe auch
- Idempotenz: derselbe Aufruf, dasselbe ErgebnisWie `Idempotency-Key` einen Netzabbruch von einer zweiten Buchung trennt, wo er Pflicht ist und was bei Wiedergabe, Kollision und laufender Anfrage passiert.
- Reservierungen lesen, anlegen und ändernDas Reservierungsbuch: Liste mit zwanzig Filtern, Einzelabruf mit ETag, Anlegen durch dieselbe Torwache wie das Widget und Teiländerung ohne Datenverlust.
- Gästekartei lesen, anlegen und pflegenDie Stammdaten der Gäste: exakte Suche statt Auskunftei, drei Sichtbarkeitsstufen, Merkmale additiv ändern und der Newsletter-Zustand als eine einzige verlässliche Frage.