Zum Inhalt springen
Abstimmung starten

Mittagsmenü-API

Tagesmenüs und Wochenhits von Schweizer Restaurants abrufen, auf der eigenen Website zeigen oder direkt aus dem Kassensystem übertragen. Offenes Format, ohne Anmeldung zum Lesen.

Format: schema.org, kein eigenes

Es gibt keinen eigenen Standard für Mittagsmenüs, aber einen allgemeinen, der sie vollständig abdeckt: schema.org Menu, MenuItem und Offer als JSON-LD. Google liest es, viele Website-Baukästen erzeugen es schon. Wir legen nur fest, welche Felder Pflicht sind und wie sie gelesen werden:

FeldBedeutung
MenuSection.nameAngebotsart, z. B. «Mittagsmenü», «Wochenhit», «Vegi-Menü». Ohne Section gilt Menu.name, sonst «Mittagsmenü».
MenuItem.name PflichtDas Gericht, höchstens 160 Zeichen.
MenuItem.descriptionBeilagen, Hinweise, höchstens 500 Zeichen.
MenuItem.suitableForDietVegetarianDiet, VeganDiet, GlutenFreeDiet (als URL oder Kurzname, einzeln oder als Liste). Andere Werte werden ignoriert.
Offer.priceZahl von 0 bis 999, z. B. "24.50".
Offer.priceCurrencyISO 4217, Standard CHF.
Offer.validFrom, Offer.validThrough Pflicht ISO 8601. Nur ein Datum (2026-10-06) heisst: der ganze Tag. Mit Uhrzeit (2026-10-06T11:30:00+02:00) zählt die Uhrzeit als Servicezeit. Ohne Zeitzonenangabe gilt Schweizer Zeit. Höchstens 31 Tage pro Angebot.

Angenommen werden ein Menu, ein Restaurant mit hasMenu oder ein @graph, der eines davon enthält – so, wie es viele Websites schon ausgeben.

Menü lesen

Ohne Anmeldung. Liefert die heutigen und kommenden Angebote eines Restaurants. Die Restaurant-ID steht in der Adresse der Menüseite in Tavatella.

curl https://app.tavatella.com/api/v1/restaurants/12345/menu

Antwort (Content-Type: application/ld+json):

{
    "@context": "https://schema.org",
    "@type": "Restaurant",
    "@id": "https://app.tavatella.com/api/v1/restaurants/12345/menu",
    "identifier": "12345",
    "name": "Restaurant Beispiel",
    "address": "Marktgasse 1, 9000 St. Gallen",
    "hasMenu": {
        "@type": "Menu",
        "name": "Mittagsangebote",
        "hasMenuSection": [
            {
                "@type": "MenuSection",
                "name": "Mittagsmenü",
                "hasMenuItem": [
                    {
                        "@type": "MenuItem",
                        "identifier": "881",
                        "name": "Zürcher Geschnetzeltes mit Rösti",
                        "description": "mit Saisonsalat",
                        "offers": {
                            "@type": "Offer",
                            "price": "24.50",
                            "priceCurrency": "CHF",
                            "validFrom": "2026-10-06T11:30:00+02:00",
                            "validThrough": "2026-10-06T14:00:00+02:00"
                        }
                    }
                ]
            }
        ]
    }
}

In JavaScript:

const res = await fetch('https://app.tavatella.com/api/v1/restaurants/12345/menu');
const restaurant = await res.json();
for (const section of restaurant.hasMenu.hasMenuSection) {
    for (const item of section.hasMenuItem) {
        console.log(section.name, item.name, item.offers.price);
    }
}

Menü schreiben

Für Kassensysteme und Websites, die das Menü ohnehin pflegen. Das Token erstellt der Betreiber in Tavatella: im Restaurant auf «Betreibst du dieses Restaurant?», nach der Bestätigung unter «Automatisch übertragen (API)». Ein Token gilt für genau ein Restaurant.

curl -X PUT https://app.tavatella.com/api/v1/restaurants/12345/menu \
  -H "Authorization: Bearer tvt_…" \
  -H "Content-Type: application/ld+json" \
  --data @menu.json

menu.json:

{
    "@context": "https://schema.org",
    "@type": "Menu",
    "hasMenuSection": [
        {
            "@type": "MenuSection",
            "name": "Mittagsmenü",
            "hasMenuItem": [
                {
                    "@type": "MenuItem",
                    "name": "Zürcher Geschnetzeltes mit Rösti",
                    "description": "mit Saisonsalat",
                    "offers": {
                        "@type": "Offer",
                        "price": "24.50",
                        "priceCurrency": "CHF",
                        "validFrom": "2026-10-06T11:30:00+02:00",
                        "validThrough": "2026-10-06T14:00:00+02:00"
                    }
                },
                {
                    "@type": "MenuItem",
                    "name": "Älplermagronen mit Apfelmus",
                    "suitableForDiet": "https://schema.org/VegetarianDiet",
                    "offers": {
                        "@type": "Offer",
                        "price": "21.00",
                        "validFrom": "2026-10-06",
                        "validThrough": "2026-10-06"
                    }
                }
            ]
        },
        {
            "@type": "MenuSection",
            "name": "Wochenhit",
            "hasMenuItem": [
                {
                    "@type": "MenuItem",
                    "name": "Thai-Curry mit Jasminreis",
                    "suitableForDiet": [
                        "https://schema.org/VeganDiet"
                    ],
                    "offers": {
                        "@type": "Offer",
                        "price": "23.00",
                        "validFrom": "2026-10-05",
                        "validThrough": "2026-10-09"
                    }
                }
            ]
        }
    ]
}

PUT ersetzt alle heutigen und kommenden Angebote, die über die API kamen. Was der Betreiber von Hand in Tavatella erfasst hat, bleibt stehen. Die Antwort ist dieselbe wie beim Lesen. Zum Leeren, etwa an einem Ruhetag, ein Menu ohne Gerichte schicken: {"@context":"https://schema.org","@type":"Menu","hasMenuItem":[]}

StatusBedeutung
200Übernommen, Antwort enthält das neue Menü.
400Kein JSON im Body.
401Token fehlt oder ist ungültig (widerrufen?).
403Token gehört zu einem anderen Restaurant.
404Restaurant unbekannt oder geschlossen.
422Ungültige Daten. Jeder Fehler steht mit seinem JSON-Pfad da, nichts wurde gespeichert.
429Zu viele Anfragen, siehe Retry-After.

Beispiel 422:

{
    "message": "validFrom ist Pflicht (ISO 8601, z. B. \"2026-10-06\" oder \"2026-10-06T11:30:00+02:00\"). (and 1 more error)",
    "errors": {
        "hasMenuSection.0.hasMenuItem.1.offers.validFrom": [
            "validFrom ist Pflicht (ISO 8601, z. B. \"2026-10-06\" oder \"2026-10-06T11:30:00+02:00\")."
        ],
        "hasMenuSection.1.hasMenuItem.0.offers.price": [
            "price ist eine Zahl zwischen 0 und 999, z. B. \"24.50\"."
        ]
    }
}

Widget für die eigene Website

Zwei Zeilen, kein Framework, keine Cookies. Zeigt das heutige Menü; mit data-tavatella-show="all" auch die kommenden Angebote. Die Darstellung übernimmt die Schrift der Website.

<div data-tavatella-menu="12345"></div>
<script src="https://app.tavatella.com/widget/menu.js" async></script>

Grenzen und Caching

  • Lesen: 120 Anfragen pro Minute und IP. Antworten dürfen 5 Minuten gecacht werden (Cache-Control).
  • Schreiben: 30 Anfragen pro Minute, höchstens 50 Angebote, je höchstens 31 Tage gültig.
  • Lesen ist von jeder Website aus erlaubt (CORS).
  • Fragen und Wünsche: app@tavatella.com