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 Entwickler

Voraussetzungen

  • 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

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

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

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

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

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

curl — die einzige richtige Art, den Token zu schicken
curl
# 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.
Der zweite Aufruf steht auskommentiert da, weil er nicht zum Ausprobieren gedacht ist: er kostet den Schlüssel. Auch ein abgeschnittener Token unter einem der üblichen Namen (`api_key`, `apikey`, `key`, `token`, `access_token`) wird gemeldet.
Antwort 401 — die Kopfzeile fehlt
JSON
{  "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"}
Alle Schlüsselfehler sind 401: `unauthorized` (Kopfzeile fehlt), `key_malformed` (Form stimmt nicht), `key_unknown` (Kennung oder Geheimnis passt nicht), `key_revoked`, `key_expired`, `key_in_query`. Abgelaufen ist bewusst 401 und nicht 403 — 403 hiesse „du, aber nicht das"; ein abgelaufener Schlüssel ist niemand mehr.
Antwort 403 — das Recht fehlt
JSON
{  "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"}
`module` und `op` stehen als eigene Felder im Körper — ein Einrichtungsassistent kann daraus genau den Haken benennen, der fehlt, statt den Benutzer die Meldung lesen zu lassen.
JavaScript / TypeScript — ein Klient, der den Token einmal kennt
TypeScript
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')
Der Token steht an EINER Stelle. Das ist nicht nur bequem: ein zweiter Aufrufpfad, der die Kopfzeile selbst zusammenbaut, ist der Pfad, der sie beim nächsten Umbau vergisst — und dann steht der Schlüssel plötzlich in einer Adresse.
Python — eine Session mit fester Kopfzeile
Python
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"])
Eine `Session` hält die Verbindung offen und setzt die Kopfzeile ein einziges Mal. Bei einem Abgleich über hunderte Seiten ist das auch der Unterschied zwischen einem und hundert TLS-Handshakes.
PHP — Kopfzeilen an einer Stelle
PHP
<?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']);
In einer WordPress- oder TYPO3-Erweiterung gehört diese Funktion in eine eigene Datei — und der Token in die Konfiguration der Installation, nie in den Erweiterungscode.

Wo im Dashboard

/restaurant/[slug]/settings/api

Im Dashboard öffnen