Der erste Aufruf in fünf Minuten

Schlüssel anlegen, Rechte setzen, die Stammdaten des Betriebs abrufen — in acht Sprachen, vom fertigen Befehl bis zur vollständigen Antwort.

Für Entwickler

Voraussetzungen

  • Ein API-Schlüssel des Betriebs (Einstellungen → API-Einstellungen)
  • Recht `website:read` für diesen ersten Aufruf
  • Aufruf von einem Server, nicht aus dem Browser

Die TacticTable-API ist eine gewöhnliche REST-Schnittstelle über HTTPS. Jede Adresse beginnt mit `https://tactictable.com/api/v1`, jede Antwort ist JSON in UTF-8, und jede Anfrage weist sich mit der Kopfzeile `Authorization: Bearer <token>` aus. Mehr braucht der erste Aufruf nicht.

EIN SCHLÜSSEL GEHÖRT ZU GENAU EINEM BETRIEB, und der Betrieb steht nirgends in der Anfrage: kein Pfadabschnitt, kein `?restaurantId=`, kein Feld im Rumpf. Die Zuordnung steckt ausschliesslich in der Schlüsselzeile. Wer mehrere Häuser bedient, hält mehrere Schlüssel — und verwechseln kann er sie nicht, weil jede Antwort im Feld `meta.restaurantId` bzw. in `data.id` sagt, zu welchem Haus sie gehört.

DIE API ANTWORTET KEINEM BROWSER. Sie setzt keine CORS-Kopfzeilen, und eine Anfrage, die einen `Origin`-Header mitbringt, wird mit 403 `origin_not_allowed` abgewiesen. Das ist kein Versehen, sondern der Zweck: ein Schlüssel, der in Frontend-JavaScript liegt, liegt im Quelltext jeder Besucherin. Rufen Sie die API von Ihrem eigenen Server aus auf.

DIESE SEITE FÜHRT DEN AUFRUF VOR, SIE IST NICHT SEIN VERTRAG. Was `GET /api/v1/restaurant` an Rechten verlangt, welche Felder es liefert und welche Fehler es kennt, steht vollständig auf der Seite „Betrieb und Marke“. Dort wird jede Route mit Parametern, Werten und Fehlern beschrieben — hier geht es nur darum, dass der erste Aufruf gelingt.

ZWEI MODI, EIN VERHALTEN: ein Token beginnt mit `tt_live_` oder `tt_test_`. Beide laufen gegen dieselben Daten desselben Betriebs — `tt_test_` ist eine Kennzeichnung für Ihre eigene Betriebsführung (welcher Schlüssel steckt im Testsystem?), keine zweite, leere Datenbank. Schreiben Sie mit einem Testschlüssel, entsteht eine echte Reservierung. Nutzen Sie für gefahrloses Ausprobieren stattdessen den Prüfmodus (`X-TacticTable-Dry-Run: 1`).

Schritt für Schritt

  1. Schlüssel anlegen

    Im Dashboard unter Einstellungen → API-Einstellungen. Der Token wird GENAU EINMAL angezeigt — beim Anlegen. Danach kennt ihn auch TacticTable nicht mehr (gespeichert ist nur ein HMAC davon). Verloren heisst: neu ausstellen.

  2. Rechte setzen — so wenige wie möglich

    Die Haken sind Rechte der Form `modul:read` bzw. `modul:write`, dazu die zwei getrennt vergebenen Handlungen `action:reservations.status` und `action:guests.personal`. Für diesen ersten Aufruf genügt `website:read`. Ein Schlüssel ohne das passende Recht bekommt 403 `forbidden` — nicht etwa eine leere Liste.

  3. Token in die Umgebung legen, nicht in den Quelltext

    Alle Beispiele unten lesen ihn aus einer Umgebungsvariablen (`TACTICTABLE_TOKEN`). Ein Schlüssel in einem Repository ist ein Schlüssel in der Hand jedes Menschen, der jemals Lesezugriff hatte — auch nach dem Löschen des Commits.

  4. Den Aufruf absetzen

    Das curl-Beispiel unten ist vollständig. Ersetzen Sie nur den Token. Antwortet die API mit 200, steht in `data.timezone` die Zeitzone Ihres Betriebs — und die brauchen Sie für alles Weitere, weil jeder Kalendertag der API sich darauf bezieht.

  5. Die Kopfzeilen der Antwort lesen

    `TT-Request-Id` ist die Kennung dieses einen Aufrufs; nennen Sie sie im Supportfall. `X-RateLimit-Remaining` sagt, wie viele Aufrufe in dieser Minute noch offen sind. `TT-Api-Version` steht auf `v1`.

Codebeispiele

curl — der vollständige erste Aufruf
curl
curl -sS https://tactictable.com/api/v1/restaurant \
  -H "Authorization: Bearer tt_live_if3u5vp6maf67xmw_PT1xP8EQF7reKUtLcZ7v9z5qaRKmoxeHmd27JpaDvfM" \
  -H "Accept: application/json"
`-sS` unterdrückt den Fortschrittsbalken, zeigt aber Fehler. Der Token ist erfunden — setzen Sie Ihren eigenen ein. Er gehört AUSSCHLIESSLICH in diese Kopfzeile: ein Schlüssel im Query-String wird erkannt und sofort dauerhaft widerrufen.
Antwort 200 — GET /api/v1/restaurant
JSON
{  "data": {    "id": "35122c8e-d842-5b49-814c-0e8f2aaed157",    "slug": "zur-alten-post",    "name": "Zur Alten Post",    "tagline": "Wirtshaus seit 1897",    "description": "Regionale Küche im Herzen der Altstadt.",    "email": "office@zur-alten-post.at",    "phone": "+4366412345678",    "website": "https://zur-alten-post.at",    "address": {      "street": "Hauptplatz 4",      "street2": null,      "zipCode": "5020",      "city": "Salzburg",      "state": "Salzburg",      "country": "AT"    },    "logoUrl": "https://cdn.tactictable.com/logo/zur-alten-post.svg",    "iconUrl": null,    "timezone": "Europe/Vienna",    "createdAt": "2026-02-11T08:14:22.301Z",    "updatedAt": "2026-09-02T16:41:05.887Z"  }}
`timezone` ist eine IANA-Kennung und der Bezugsrahmen für ALLE Kalendertage und naiven Uhrzeiten der API. `address.state` ist das Bundesland, `country` ein Ländercode nach ISO 3166-1 alpha-2. Felder, die der Betrieb nicht gepflegt hat, sind `null` — nie ein leerer Text.
Kopfzeilen jeder Antwort
Text
HTTP/2 200content-type: application/jsoncache-control: no-store, privatevary: Authorizationreferrer-policy: no-referrertt-api-version: v1tt-request-id: req_8f31c0a94d2b47e6ba05x-ratelimit-limit: 600x-ratelimit-remaining: 599x-ratelimit-reset: 1789412460
`Vary: Authorization` und `Cache-Control: no-store, private` hängen an JEDER Antwort, auch an der fehlerhaften: ohne sie dürfte ein Zwischenspeicher die Antwort für Kunde A an Kunde B ausliefern. Legen Sie die API nie hinter einen CDN-Cache, der diese Kopfzeilen ignoriert.
JavaScript / TypeScript (fetch)
TypeScript
const token = process.env.TACTICTABLE_TOKEN ?? '' const antwort = await fetch('https://tactictable.com/api/v1/restaurant', {    headers: {        Authorization: 'Bearer ' + token,        Accept: 'application/json',    },}) const rumpf = await antwort.json() if (!antwort.ok) {    // Der Fehlerkoerper hat IMMER dieselbe Form: error, message, docs, requestId.    throw new Error(rumpf.error + ': ' + rumpf.message + ' [' + rumpf.requestId + ']')} console.log(rumpf.data.name, rumpf.data.timezone)
Läuft ohne Abhängigkeit in Node 18 und neuer. Werfen Sie NICHT bei `!antwort.ok`, bevor Sie den Rumpf gelesen haben — der Fehlerkörper ist die einzige Stelle, an der `requestId` und `docs` stehen.
Python (requests)
Python
import osimport requests token = os.environ["TACTICTABLE_TOKEN"] antwort = requests.get(    "https://tactictable.com/api/v1/restaurant",    headers={"Authorization": "Bearer " + token, "Accept": "application/json"},    timeout=15,) rumpf = antwort.json() if antwort.status_code != 200:    raise SystemExit("{}: {} [{}]".format(rumpf["error"], rumpf["message"], rumpf["requestId"])) print(rumpf["data"]["name"], rumpf["data"]["timezone"])
Setzen Sie IMMER ein `timeout`. Ohne es hängt ein Abgleich, dessen Gegenstelle nicht antwortet, unbegrenzt — und ein Cron-Lauf, der nie endet, überholt beim nächsten Start sich selbst.
PHP (cURL)
PHP
<?php $token = getenv('TACTICTABLE_TOKEN'); $ch = curl_init('https://tactictable.com/api/v1/restaurant');curl_setopt_array($ch, [    CURLOPT_RETURNTRANSFER => true,    CURLOPT_HTTPHEADER => [        'Authorization: Bearer ' . $token,        'Accept: application/json',    ],]); $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 !== 200) {    throw new RuntimeException($json['error'] . ': ' . $json['message']);} echo $json['data']['name'], ' ', $json['data']['timezone'], PHP_EOL;
`JSON_THROW_ON_ERROR` statt eines stillen `null` von `json_decode`: eine abgeschnittene Antwort soll auffallen und nicht als leeres Ergebnis durchgehen.
Ruby (net/http)
Ruby
require 'json'require 'net/http' ziel = URI('https://tactictable.com/api/v1/restaurant') anfrage = Net::HTTP::Get.new(ziel)anfrage['Authorization'] = 'Bearer ' + ENV.fetch('TACTICTABLE_TOKEN')anfrage['Accept'] = 'application/json' antwort = Net::HTTP.start(ziel.hostname, ziel.port, use_ssl: true, read_timeout: 15) do |http|  http.request(anfrage)end rumpf = JSON.parse(antwort.body) raise rumpf['error'] + ': ' + rumpf['message'] unless antwort.code == '200' puts rumpf['data']['name']puts rumpf['data']['timezone']
`use_ssl: true` ist Pflicht — ohne das versucht `net/http` einen Klartextaufruf auf Port 443 und bekommt nie eine Antwort.
Go (net/http)
Go
package main import (    "encoding/json"    "fmt"    "net/http"    "os"    "time") type betriebsAntwort struct {    Data struct {        Name     string `json:"name"`        Timezone string `json:"timezone"`    } `json:"data"`} func main() {    anfrage, err := http.NewRequest("GET", "https://tactictable.com/api/v1/restaurant", nil)    if err != nil {        panic(err)    }    anfrage.Header.Set("Authorization", "Bearer "+os.Getenv("TACTICTABLE_TOKEN"))    anfrage.Header.Set("Accept", "application/json")     klient := &http.Client{Timeout: 15 * time.Second}    antwort, err := klient.Do(anfrage)    if err != nil {        panic(err)    }    defer antwort.Body.Close()     if antwort.StatusCode != http.StatusOK {        panic(fmt.Sprintf("HTTP %d", antwort.StatusCode))    }     var gelesen betriebsAntwort    if err := json.NewDecoder(antwort.Body).Decode(&gelesen); err != nil {        panic(err)    }    fmt.Println(gelesen.Data.Name, gelesen.Data.Timezone)}
Nur die Felder in die Struktur aufnehmen, die gebraucht werden: `encoding/json` überliest alles andere stillschweigend, und damit bricht ein neu hinzugekommenes Feld der API Ihre Anbindung nicht.
C# (HttpClient)
C#
using System;using System.Net.Http;using System.Net.Http.Headers;using System.Text.Json;using System.Threading.Tasks; public static class TacticTable{    public static async Task Main()    {        using var klient = new HttpClient();        klient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(            "Bearer",            Environment.GetEnvironmentVariable("TACTICTABLE_TOKEN"));         using var antwort = await klient.GetAsync("https://tactictable.com/api/v1/restaurant");        var rumpf = await antwort.Content.ReadAsStringAsync();        using var json = JsonDocument.Parse(rumpf);         if (!antwort.IsSuccessStatusCode)        {            var fehler = json.RootElement;            throw new HttpRequestException(                fehler.GetProperty("error").GetString() + ": " +                fehler.GetProperty("message").GetString());        }         var daten = json.RootElement.GetProperty("data");        Console.WriteLine(daten.GetProperty("name").GetString());        Console.WriteLine(daten.GetProperty("timezone").GetString());    }}
Einen `HttpClient` wiederverwenden und nicht je Aufruf einen neuen anlegen — sonst gehen unter Last die Sockets aus. In einer echten Anwendung gehört er in den `IHttpClientFactory`.
Java (java.net.http)
Java
import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;import java.time.Duration; public final class TacticTable {    public static void main(String[] args) throws Exception {        HttpClient klient = HttpClient.newBuilder()                .connectTimeout(Duration.ofSeconds(15))                .build();         HttpRequest anfrage = HttpRequest.newBuilder()                .uri(URI.create("https://tactictable.com/api/v1/restaurant"))                .header("Authorization", "Bearer " + System.getenv("TACTICTABLE_TOKEN"))                .header("Accept", "application/json")                .GET()                .build();         HttpResponse<String> antwort = klient.send(anfrage, HttpResponse.BodyHandlers.ofString());         if (antwort.statusCode() != 200) {            throw new IllegalStateException("HTTP " + antwort.statusCode() + ": " + antwort.body());        }         System.out.println(antwort.body());    }}
Ab Java 11 ohne Fremdbibliothek. Für das Auslesen einzelner Felder kommt eine JSON-Bibliothek dazu (Jackson, Gson) — die Anfrage selbst braucht keine.