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 EntwicklerVoraussetzungen
- 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
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.
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.
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.
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.
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 -sS https://tactictable.com/api/v1/restaurant \
-H "Authorization: Bearer tt_live_if3u5vp6maf67xmw_PT1xP8EQF7reKUtLcZ7v9z5qaRKmoxeHmd27JpaDvfM" \
-H "Accept: application/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" }}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: 1789412460const 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)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"])<?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;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']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)}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()); }}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()); }}Siehe auch
- Anmeldung mit dem TokenWie 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.
- Die Form jedes FehlersEin Körper, ein Aufbau, ein geschlossener Katalog von Kennungen — und warum in `message` nie eine rohe Datenbankmeldung steht.
- Betrieb und MarkeName, Anschrift, Zeitzone — und Logo, Farben, Schriften sowie die Links, die eine Bewertungsanfrage braucht.
- 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.