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 Entwickler

DER 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

  1. 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.

  2. 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.

  3. 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.

  4. Beim Schreiben `If-Match` mitschicken

    Unverändert, mit allen Anführungszeichen und dem `W/`-Präfix, falls vorhanden.

  5. 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 — anlegen, ohne etwas anzulegen
curl
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"  }'
Kommt ein 409 `conflict` mit `reason: "capacity"` zurück, wäre die echte Buchung gescheitert — und Sie wissen es, ohne dass jemand eine Probe-Reservierung aus dem Buch räumen muss.
Antwort 200 — Prüfmodus beim Anlegen einer Reservierung
JSON
{  "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"  }}
`would.tableIds` ist die Zuordnung, die der Tischplan gewählt hätte — hier zwei zusammengelegte Tische für sechs Personen. `durationMin` kommt aus den Einstellungen des Betriebs und lässt sich nicht von aussen setzen.
curl — schreiben nur, wenn sich nichts geändert hat
curl
# 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}'
Der ETag-Wert wird UNVERÄNDERT zurückgeschickt, samt `W/` und Anführungszeichen. In der Shell braucht die Kopfzeile deshalb einfache Anführungszeichen aussen.
Antwort 412 — jemand war schneller
JSON
{  "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"}
`etag` nennt den AKTUELLEN Stand. Lesen Sie die Zeile neu, prüfen Sie, ob Ihre Änderung noch sinnvoll ist, und schicken Sie sie mit diesem Wert erneut.
TypeScript — lesen, ändern, schreiben, bei 412 einmal neu
TypeScript
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.')}
Zwischen Lesen und Schreiben liegt der Zustand, den `If-Match` absichert. Wer den ETag aus einer älteren, zwischengespeicherten Antwort nimmt, bekommt bei jedem Versuch 412 — lesen Sie frisch.
Python — Prüfmodus für einen ganzen Import
Python
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.
Der Probelauf findet auch die doppelte Mailadresse (409 `conflict`) — die fände eine reine Schemaprüfung in Ihrem eigenen Code nie.