Anmeldung mit dem Token
Wie ein Schlüssel aufgebaut ist, welche Rechte er tragen kann, warum er im Query-String sofort widerrufen wird — und wann er über Nacht seine Rechte verliert.
Für EntwicklerVoraussetzungen
- Recht `apiSettings` im Dashboard, um Schlüssel anzulegen
Ein Token hat die Form `tt_<modus>_<kennung>_<geheimnis>`, zum Beispiel `tt_live_if3u5vp6maf67xmw_PT1x…`. Der Modus ist `live` oder `test`, die Kennung sind 16 Zeichen und ist öffentlich (sie steht in Protokollen und in der Schlüsselliste), das Geheimnis sind 43 Zeichen. Zerlegt wird von links über die ersten drei Unterstriche — das Geheimnis selbst darf `_` und `-` enthalten.
GESPEICHERT WIRD NUR EIN HMAC DES GEHEIMNISSES. Der Klartext existiert genau einmal: im Rumpf der Antwort, die ihn erzeugt hat. Auch wir können ihn danach nicht mehr anzeigen. Ein verlorener Schlüssel wird nicht wiederhergestellt, sondern ersetzt.
DER SCHLÜSSEL GEHÖRT AUSSCHLIESSLICH IN DIE KOPFZEILE `Authorization: Bearer <token>`. Steht er im Query-String oder in einem Cookie, antwortet die API mit 401 `key_in_query` UND WIDERRUFT IHN SOFORT UND DAUERHAFT. Das ist hart und gewollt: eine Adresse mit Schlüssel steht im Serverprotokoll, im Browserverlauf und in jedem `Referer` — sie ist damit kein Geheimnis mehr. Eine kaputte Integration fällt sofort auf, ein stilles Leck niemandem.
RECHTE SIND FLACHE ZEICHENKETTEN: `reservations:read`, `guests:write`, `menu:read`. Vergeben werden sie beim Anlegen des Schlüssels. Fehlt eines, antwortet die Route mit 403 `forbidden` und nennt im Körper `module` und `op` — nicht mit einer leeren Liste. Zuweisbar sind die sechzehn Datenmodule `reservations`, `guests`, `tables`, `areas`, `menu`, `rawMaterials`, `recipes`, `products`, `suppliers`, `mediathek`, `website`, `widget`, `tracking`, `marketing`, `vouchers` und `reports`.
SIEBEN MODULE KANN EIN SCHLÜSSEL NICHT EINMAL TRAGEN: `settings`, `billing`, `staff`, `apiSettings`, `inbox`, `emailAccounts` und `ai`. Daran hängen Eigentum, Geld, Rechtevergabe und fremde Postfächer. Die Sperre steht nicht in der Oberfläche, sondern im Wächter: selbst eine Route, die eines dieser Module verlangte, bekäme 403. Was von den Einstellungen wirklich gebraucht wird — Name, Anschrift, Zeitzone —, liefert `GET /api/v1/restaurant` unter `website:read`.
ZWEI HANDLUNGEN WERDEN GETRENNT VOM SCHREIBRECHT VERGEBEN, weil sie mehr bewirken als ein gewöhnliches Ändern. `action:reservations.status` erlaubt den Statuswechsel einer Reservierung: ein NOSHOW zieht die geblockte Gebühr ein und stuft den Gast betriebsübergreifend herab. `action:guests.personal` gibt Sperrvermerk, Sperrgrund, No-Show-Zähler und Notizen frei — beim Lesen wie beim Schreiben. Fehlt eine Handlung, antwortet die Route mit 403 `forbidden_action`.
DIE WIRKSAMEN RECHTE SIND EINE SCHNITTMENGE, UND SIE WIRD BEI JEDEM AUFRUF NEU GEBILDET: gespeicherte Rechte des Schlüssels ∩ die Rechte, die der ERSTELLER HEUTE hat. Verliert der Mitarbeiter, der den Schlüssel ausgestellt hat, das Recht `guests`, verliert es der Schlüssel in derselben Sekunde. Verlässt er den Betrieb, antwortet der Schlüssel mit 403 `key_owner_lost_access` — er authentifiziert noch, damit die Meldung im Supportfall lesbar bleibt, darf aber nichts mehr. Lassen Sie produktive Schlüssel deshalb vom Inhaber ausstellen, nicht vom Praktikanten.
EIN NEUES MODUL IST FÜR BESTEHENDE SCHLÜSSEL IMMER „NICHT GEWÄHRT". Die Rechtekarte wird wörtlich gelesen; es gibt keine Rolle und keine Vorgabe, aus der sich ein Recht ableiten würde. Ein Release kann einen Schlüssel in fremder Hand damit nicht nachträglich mächtiger machen.
Schritt für Schritt
Schlüssel ausstellen
Einstellungen → API-Einstellungen → neuen Schlüssel anlegen. Geben Sie ihm einen Namen, der sagt, WER ihn benutzt („Kassa Gastraum", „Newsletter-Tool") — im Zweifelsfall widerruft man sonst den falschen.
Nur die Haken setzen, die gebraucht werden
Ein Bildschirm im Gastraum braucht `tables:read` und `areas:read` — nicht `guests:read`. Ein Newsletter-Werkzeug braucht `guests:read`, aber nie `action:guests.personal`. Jeder überzählige Haken ist der Umfang des Schadens, wenn der Schlüssel abhandenkommt.
Ablaufdatum setzen, wo es passt
Ein Schlüssel für eine einmalige Migration darf in 30 Tagen ablaufen. Danach antwortet er mit 401 `key_expired` und nennt den Zeitpunkt — eine Meldung, die niemanden raten lässt.
Token in die Umgebung des aufrufenden Systems legen
Nicht ins Repository, nicht in eine Konfigurationsdatei, die im Backup liegt, nicht in ein Frontend. Die API weist jede Anfrage mit `Origin`-Kopfzeile ab, gerade damit dieser Weg gar nicht erst entsteht.
Widerrufen, sobald ein Verdacht besteht
Ein widerrufener Schlüssel antwortet ab der nächsten Sekunde mit 401 `key_revoked`. Es gibt kein Zurück und keine Karenzzeit — das ist der Sinn.
Codebeispiele
# RICHTIG: der Token steht in der Kopfzeile.curl -sS https://tactictable.com/api/v1/areas \ -H "Authorization: Bearer tt_live_if3u5vp6maf67xmw_PT1xP8EQF7reKUtLcZ7v9z5qaRKmoxeHmd27JpaDvfM" # FALSCH und deshalb hier nicht ausgeschrieben: derselbe Token als# Abfrageparameter. Diese Anfrage antwortet 401 key_in_query UND widerruft# den Schluessel dauerhaft — sie ist nicht zum Ausprobieren gedacht.{ "error": "unauthorized", "message": "Es fehlt die Kopfzeile „Authorization: Bearer <token>“. Den Token legt der Betrieb unter Einstellungen → API-Einstellungen an.", "docs": "https://tactictable.com/dokumentation/api/fehler/unauthorized", "requestId": "req_8f31c0a94d2b47e6ba05"}{ "error": "forbidden", "message": "Dem Schluessel fehlt das Recht „guests:read“. Der Betrieb setzt den Haken unter Einstellungen → API-Einstellungen.", "module": "guests", "op": "read", "docs": "https://tactictable.com/dokumentation/api/fehler/forbidden", "requestId": "req_8f31c0a94d2b47e6ba05"}interface ApiFehler { error: string message: string docs: string requestId: string} export class TacticTableFehler extends Error { constructor( readonly status: number, readonly koerper: ApiFehler, ) { super(koerper.error + ': ' + koerper.message) this.name = 'TacticTableFehler' }} export async function tt(pfad: string, init: RequestInit = {}): Promise<unknown> { const antwort = await fetch('https://tactictable.com/api/v1' + pfad, { ...init, headers: { Authorization: 'Bearer ' + (process.env.TACTICTABLE_TOKEN ?? ''), Accept: 'application/json', ...(init.headers ?? {}), }, }) const rumpf = await antwort.json() if (!antwort.ok) throw new TacticTableFehler(antwort.status, rumpf as ApiFehler) return rumpf} // Aufruf: await tt('/areas')import osimport requests sitzung = requests.Session()sitzung.headers.update({ "Authorization": "Bearer " + os.environ["TACTICTABLE_TOKEN"], "Accept": "application/json",}) class TacticTableFehler(Exception): def __init__(self, status, koerper): self.status = status self.koerper = koerper super().__init__("{}: {}".format(koerper.get("error"), koerper.get("message"))) def hole(pfad, **kwargs): antwort = sitzung.get("https://tactictable.com/api/v1" + pfad, timeout=15, **kwargs) rumpf = antwort.json() if antwort.status_code >= 400: raise TacticTableFehler(antwort.status_code, rumpf) return rumpf print(hole("/areas")["data"])<?php function tt_anfrage(string $pfad, array $optionen = []): array{ $ch = curl_init('https://tactictable.com/api/v1' . $pfad); $kopfzeilen = array_merge([ 'Authorization: Bearer ' . getenv('TACTICTABLE_TOKEN'), 'Accept: application/json', ], $optionen['headers'] ?? []); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $kopfzeilen, CURLOPT_TIMEOUT => 20, ]); $rumpf = curl_exec($ch); $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); $json = json_decode($rumpf, true, 512, JSON_THROW_ON_ERROR); if ($status >= 400) { throw new RuntimeException( $json['error'] . ': ' . $json['message'] . ' [' . $json['requestId'] . ']' ); } return $json;} print_r(tt_anfrage('/areas')['data']);Siehe auch
- Der erste Aufruf in fünf MinutenSchlüssel anlegen, Rechte setzen, die Stammdaten des Betriebs abrufen — in acht Sprachen, vom fertigen Befehl bis zur vollständigen Antwort.
- Die Form jedes FehlersEin Körper, ein Aufbau, ein geschlossener Katalog von Kennungen — und warum in `message` nie eine rohe Datenbankmeldung steht.
- Versionierung: was sich ändern darf und was nichtDie Fassung steht im Pfad. Was additiv ist, kommt jederzeit; was bricht, bekommt eine neue Fassung — und wie man eine Anbindung schreibt, die beides übersteht.