Betrieb und Marke

Name, Anschrift, Zeitzone — und Logo, Farben, Schriften sowie die Links, die eine Bewertungsanfrage braucht.

Für Entwickler

Voraussetzungen

  • Recht `website:read`
  • Kein Plan-Merkmal — auf jedem Plan verfügbar

ZWEI ROUTEN, ZWEI ZWECKE. `GET /api/v1/restaurant` liefert die Stammdaten des Betriebs, zu dem der Schlüssel gehört — vor allem `timezone`, den Bezugsrahmen jeder anderen Antwort der API. `GET /api/v1/branding` liefert das Erscheinungsbild: Logo, Farben, Schriften, Sozial- und Rechtslinks.

BEIDE HÄNGEN AN `website:read` UND AN KEINEM PLAN-MERKMAL. Das Recht sitzt an `website` und nicht an `settings`, weil `settings` einem Schlüssel gar nicht zuweisbar ist — eine Route, die es verlangte, liefe unweigerlich in ein 403. Ausgeliefert wird hier genau das, was ohnehin öffentlich auf der Website steht. Und die fehlende Plan-Schranke ist Absicht: hinge `timezone` an einem Plan, wäre die halbe API für eine Kassenanbindung unbrauchbar.

ES GIBT KEINEN PFADABSCHNITT UND KEIN `?restaurantId=`. Der Betrieb steht ausschliesslich in der Schlüsselzeile. Damit entsteht die Fehlerklasse „fremder Mandant im Pfad" gar nicht erst, statt geprüft zu werden.

DIE ANSCHRIFT IST EIN UNTEROBJEKT. `address.street`, `address.street2`, `address.zipCode`, `address.city`, `address.state` (Bundesland) und `address.country` (ISO 3166-1 alpha-2, z. B. `AT`). Nicht gepflegte Felder sind `null`, nie ein leerer Text.

BRANDING ANTWORTET AUCH DANN MIT 200, WENN ES NOCH KEINS GIBT. Dann stehen überall `null`-Werte, und Name, Untertitel, Beschreibung sowie Logo fallen auf die Stammdaten zurück. „Dieser Betrieb hat noch kein Branding gepflegt" ist eine ANTWORT, kein Fehler — ein 404 zwänge jede Website-Einbindung zu einer Fallunterscheidung, die sie ohnehin gleich behandelte.

DER ANWENDUNGSFALL HINTER `branding` IST DIE BEWERTUNGSANFRAGE. Unter `social.googleReviewUrl` und `social.tripAdvisorUrl` stehen die Adressen, ohne die ein Werkzeug den Gast nirgendwohin schickt. Unter `legal` stehen Impressum, Datenschutzerklärung, AGB und die Umsatzsteuer-Identifikationsnummer.

Die Routen

GET/api/v1/restaurant

Stammdaten des Betriebs: Name, Kennung, Anschrift, Kontakt, Logo — und die Zeitzone.

Rechte

website:read

Mögliche Fehler

  • forbiddenDem Schlüssel fehlt `website:read`.
  • not_foundDen Betrieb zu diesem Schlüssel gibt es nicht mehr.
  • Antwortet mit `{ "data": { … } }`.
  • `slug` ist die öffentliche Kennung des Betriebs — sie steht in der Adresse seiner Website und in jedem Link aus einer Bestätigungsmail.

GET/api/v1/branding

Erscheinungsbild und Verweise: Logo, Farben, Schriften, Kontakt, soziale Netze, Bewertungsportale, Rechtstexte.

Rechte

website:read

Mögliche Fehler

  • forbiddenDem Schlüssel fehlt `website:read`.
  • not_foundDen Betrieb zu diesem Schlüssel gibt es nicht mehr.
  • Antwortet mit `{ "data": { … } }`. `updatedAt` ist `null`, solange nie ein Branding gepflegt wurde.
  • Farben sind Hex-Zeichenketten, wie sie im Theme stehen. Prüfen Sie sie, bevor Sie sie in CSS einsetzen — es ist Eingabe des Wirts.

Codebeispiele

curl — Stammdaten und Marke
curl
curl -sS https://tactictable.com/api/v1/restaurant \  -H "Authorization: Bearer tt_live_if3u5vp6maf67xmw_PT1xP8EQF7reKUtLcZ7v9z5qaRKmoxeHmd27JpaDvfM" curl -sS https://tactictable.com/api/v1/branding \  -H "Authorization: Bearer tt_live_if3u5vp6maf67xmw_PT1xP8EQF7reKUtLcZ7v9z5qaRKmoxeHmd27JpaDvfM"
Beide Antworten ändern sich selten. Holen Sie sie einmal beim Start Ihrer Anwendung und halten Sie sie im Speicher, statt sie bei jedem Bildaufbau abzufragen.
Antwort 200 — GET /api/v1/branding
JSON
{  "data": {    "name": "Zur Alten Post",    "tagline": "Wirtshaus seit 1897",    "description": "Regionale Küche im Herzen der Altstadt.",    "logoUrl": "https://cdn.tactictable.com/logo/zur-alten-post.svg",    "logoWidth": 220,    "faviconUrl": "https://cdn.tactictable.com/icon/zur-alten-post.png",    "coverImageUrl": "https://cdn.tactictable.com/cover/zur-alten-post.jpg",    "colors": {      "primary": "#7A2E1E",      "secondary": "#E8DCC8",      "accent": "#C8892F",      "background": "#FFFDF8",      "text": "#231C16"    },    "fonts": { "body": "Inter", "heading": "Playfair Display" },    "contact": {      "street": "Hauptplatz 4",      "zipCode": "5020",      "city": "Salzburg",      "country": "AT",      "phone": "+4366212345",      "email": "office@zur-alten-post.at",      "website": "https://zur-alten-post.at"    },    "social": {      "facebookUrl": "https://facebook.com/zuraltenpost",      "instagramUrl": "https://instagram.com/zuraltenpost",      "twitterUrl": null,      "googleReviewUrl": "https://g.page/r/abcdef/review",      "tripAdvisorUrl": null    },    "legal": {      "imprintUrl": "https://zur-alten-post.at/impressum",      "privacyPolicyUrl": "https://zur-alten-post.at/datenschutz",      "termsUrl": null,      "vatNumber": "ATU12345678"    },    "timezone": "Europe/Vienna",    "updatedAt": "2026-06-11T14:03:27.551Z"  }}
Ein `null` in `social` heisst: der Betrieb hat diesen Kanal nicht gepflegt. Blenden Sie den Knopf dann aus, statt auf eine leere Adresse zu verlinken.
TypeScript — Marke einmal laden und halten
TypeScript
interface Branding {    name: string | null    logoUrl: string | null    colors: { primary: string | null; accent: string | null; text: string | null }    social: { googleReviewUrl: string | null; tripAdvisorUrl: string | null }    legal: { imprintUrl: string | null; privacyPolicyUrl: string | null }    timezone: string    updatedAt: string | null} let zwischenspeicher: { daten: Branding; geholt: number } | null = nullconst HALTBAR_MS = 15 * 60 * 1000 export async function marke(token: string): Promise<Branding> {    const jetzt = Date.now()    if (zwischenspeicher && jetzt - zwischenspeicher.geholt < HALTBAR_MS) {        return zwischenspeicher.daten    }     const antwort = await fetch('https://tactictable.com/api/v1/branding', {        headers: { Authorization: 'Bearer ' + token, Accept: 'application/json' },    })    if (!antwort.ok) throw new Error('HTTP ' + antwort.status)     const daten = ((await antwort.json()) as { data: Branding }).data    zwischenspeicher = { daten, geholt: jetzt }    return daten} /** Wohin der Gast fuer eine Bewertung geschickt wird — oder nirgendwohin. */export function bewertungsZiel(b: Branding): string | null {    return b.social.googleReviewUrl ?? b.social.tripAdvisorUrl ?? null}
Der Zwischenspeicher liegt in IHREM Prozess. Die API selbst antwortet mit `Cache-Control: no-store, private` — ein geteilter Cache dürfte diese Antwort nie zwischenspeichern, weil sie am Schlüssel hängt.
Python — Zeitzone des Betriebs holen
Python
import requests  def zeitzone(sitzung: requests.Session) -> str:    """Der Bezugsrahmen fuer JEDEN Kalendertag und jede naive Uhrzeit der API."""    antwort = sitzung.get("https://tactictable.com/api/v1/restaurant", timeout=15)    antwort.raise_for_status()    return antwort.json()["data"]["timezone"]  # Einmal beim Start holen und behalten. Sie aendert sich praktisch nie —# aber ohne sie laesst sich "2026-09-20" + "19:30" nicht in einen echten# Zeitpunkt umrechnen.
Diese Route trägt kein Plan-Merkmal und antwortet auch nach Ablauf der Testphase. Sie muss es — sonst wäre eine Anbindung beim ersten 402 vollständig blind.