book2eat Supplier API

Schnittstelle für die Übergabe von Speiseplänen an book2eat und den Abruf von Bestellzahlen.
Version 1.

Basis-URL: https://supplier-api.book2eat.eu/v1 ⭳ Postman-Collection (Live) herunterladen

Authentifizierung

Alle Endpoints außer /v1/health erfordern HTTP Basic Auth über HTTPS. Die Zugangsdaten werden von tech2solutions vergeben — Anfragen dazu bitte an info@book2eat.eu. Jeder API-User ist für einen oder mehrere Standorte freigeschaltet — Requests für nicht freigeschaltete Standorte werden mit 403 abgelehnt.

curl -u benutzername:passwort https://supplier-api.book2eat.eu/v1/locations

Damit lässt sich der eigene Zugang testen, ohne bereits eine echte Speiseplan-ID zu kennen — die Antwort zeigt die freigeschalteten Standorte (siehe unten).

Datenformat

Alle Request- und Response-Bodies sind JSON (application/json; charset=utf-8). Datumsangaben: YYYY-MM-DD, Zeitstempel: YYYY-MM-DD HH:MM:SS (lokale Zeit, Europa/Wien).

Deadlines: orderDeadline kann pro menuItem mitgegeben werden und übersteuert dann den Wert auf Speiseplan-Ebene. cancelDeadline ist optional — fehlt sie, wird automatisch orderDeadline + 7 Tage gesetzt. minOrderQuantity wird entgegengenommen, aber derzeit nicht ausgewertet. Sobald bei einem menuItem sowohl orderDeadline als auch cancelDeadline verstrichen sind, gilt es als final (final: true) und kann über die API nicht mehr geändert oder gelöscht werden (409 ITEM_LOCKED) — die Bestellzahl steht dann endgültig fest.

Preis: price ist auf jedem menuItem optional — und wird nur übernommen, wenn der Standort in book2eat dafür freigeschaltet ist. Standardmäßig ist die Übernahme aus: der Preis kommt dann wie bisher aus dem in book2eat gepflegten Menütyp, ein mitgeschicktes price wird ignoriert. Ist die Übernahme aktiv, gilt zusätzlich die Standort-Einstellung, ob die gesendeten Beträge netto oder brutto sind — bei netto rechnet book2eat mit der im Menütyp hinterlegten Umsatzsteuer auf brutto hoch. Fehlende, nicht numerische oder negative Beträge fallen immer auf den Menütyp-Preis zurück. Umsatzsteuer (vat) kommt immer aus book2eat und kann nicht per API gesetzt werden.

Gesamtmenü: In book2eat kann ein buchbares Menü aus mehreren Menülinien zusammengesetzt sein — etwa Suppe und Nachspeise, die immer zum Hauptgericht gehören. Solche Bestandteile werden nicht separat bestellt: Ihre Bestellzahl ist die Summe der Menüs, denen sie zugeordnet sind. Ist die Preisübernahme aktiv, werden sie mit 0 € ausgewiesen, und ob ihr gesendeter Preis zusätzlich auf das Menü aufgeschlagen wird oder der Menüpreis unverändert gilt, ist je Menü eingestellt. Ohne Preisübernahme gelten — wie bei allen Linien — die in book2eat gepflegten Preise. Die Berechnung umfasst immer den ganzen Speiseplan: Wird ein einzelner Eintrag über /items angelegt, geändert oder gelöscht, rechnet book2eat die Gesamtmenüs neu durch — mit den zuletzt gesendeten Preisen der übrigen Einträge. Eine Linie kann Bestandteil mehrerer Menüs sein — dieselbe Suppe etwa von Menü 1 und Menü 2, während ein Allergiker- oder Glutenmenü eine eigene hat. Am Payload ändert das nichts: Alle Linien werden weiterhin ganz normal mitgeschickt, die Zuordnung pflegt tech2solutions und sie ist im Menülinien-Mapping einsehbar.

Freigeschaltete Standorte abrufen

GET/v1/locations

Listet die Standorte, für die der authentifizierte API-User freigeschaltet ist — damit lässt sich vorab prüfen, ob die eigene locationId korrekt mit book2eat verknüpft ist, bevor ein Speiseplan gesendet wird. linked: false bedeutet, dass der Standort zwar freigeschaltet, aber noch keiner locationId zugeordnet ist — in dem Fall bitte tech2solutions kontaktieren. active spiegelt das bestehende Aktiv-Flag der Einrichtung in book2eat: ist eine Einrichtung hier false, schlägt POST/PUT mit 422 UNKNOWN_LOCATION fehl, auch wenn linked: true ist — book2eat findet die deaktivierte Einrichtung über locationId dann nicht mehr.

Response · 200 OK
[
  {
    "canteen_ID": 271,
    "canteenName": "Kindergarten Obersulz",
    "locationId": "642",
    "active": true,
    "linked": true
  }
]

Menülinien-Mapping abrufen

GET/v1/menus/lines?locationId={locationId}

Listet die für diesen Standort in book2eat gepflegten Menülinien-Mappings (menuLines[].id → book2eat-Menütyp inkl. aktuellem Preis/USt.) — damit lässt sich vorab prüfen, ob eine ADAM-Menülinie korrekt verknüpft ist, bevor ein Speiseplan gesendet wird. Fehlt eine extLineId hier, liefert POST/PUT für genau diese Linie später 422 UNKNOWN_MENU_LINE — in dem Fall bitte tech2solutions kontaktieren, damit die Zuordnung ergänzt wird.

Die Liste zeigt außerdem, wie book2eat die jeweilige Linie behandelt: ob ein mitgeschickter Preis überhaupt übernommen wird und ob die Linie Bestandteil eines Gesamtmenüs ist. Damit lässt sich ein unerwarteter Preis im Bestellsystem hier nachvollziehen, ohne nachfragen zu müssen.

Response · 200 OK
[
  {
    "extLineId": "4056",
    "lineName": "Tagessuppe",
    "typ_ID": 1,
    "name": "Suppe",
    "standard_price": "0.00",
    "vat": 10,
    "component_price_mode": "menu_only",
    "component_quantity_mode": "sum_of_menus",
    "part_of_typ_IDs": [2, 3],
    "supplier_price_use": true,
    "supplier_price_is_net": true
  },
  {
    "extLineId": "4057",
    "lineName": "Hauptspeise",
    "typ_ID": 2,
    "name": "Hauptgericht",
    "standard_price": "8.90",
    "vat": 10,
    "component_price_mode": "sum",
    "component_quantity_mode": "sum_of_menus",
    "part_of_typ_IDs": [],
    "supplier_price_use": true,
    "supplier_price_is_net": true
  },
  {
    "extLineId": "4058",
    "lineName": "Alternativmenü",
    "typ_ID": 3,
    "name": "Alternativmenü",
    "standard_price": "8.90",
    "vat": 10,
    "component_price_mode": "sum",
    "component_quantity_mode": "sum_of_menus",
    "part_of_typ_IDs": [],
    "supplier_price_use": true,
    "supplier_price_is_net": true
  }
]

Im Beispiel ist die Tagessuppe Bestandteil beider Menüs (part_of_typ_IDs: [2, 3]): Sie wird nicht einzeln bestellt, kostet nichts extra, und als Bestellmenge wird ihr die Summe aus Hauptspeise und Alternativmenü gemeldet.

Die Felder im Einzelnen
FeldBedeutung
extLineIdDie ID, die im Speiseplan als menuLines[].id gesendet wird — der Anker für alle weiteren Requests.
lineNameLetzter bekannter Name der Linie, nur zur Orientierung — maßgeblich ist die extLineId.
typ_ID · nameDer book2eat-Menütyp, auf den die Linie verweist. Alle Preis- und Fristen-Einstellungen hängen an diesem Typ.
standard_price · vatDer in book2eat gepflegte Bruttopreis und die Umsatzsteuer. Dieser Preis gilt immer dann, wenn kein price übernommen wird. vat kann nie per API gesetzt werden.
part_of_typ_IDsDie Menüs, deren Bestandteil diese Linie ist (Gesamtmenü), als Liste von typ_IDs. Leere Liste = eigenständige Linie. Mehrere Einträge bedeuten, dass dieselbe Linie zu mehreren Menüs gehört — die Suppe im Beispiel zu Hauptspeise und Alternativmenü.
component_price_modeGilt am buchbaren Menü: sum = die Preise seiner Bestandteile werden auf den Menüpreis aufgeschlagen, der Gast zahlt einen Gesamtpreis. menu_only = der Menüpreis gilt unverändert, die Bestandteile sind darin schon enthalten. In beiden Fällen wird der Bestandteil selbst mit 0,00 ausgewiesen. Wirkt nur bei supplier_price_use: true — sonst gilt je Linie standard_price.
component_quantity_modeGilt am Bestandteil: sum_of_menus = gemeldet wird die Summe der Bestellmengen aller Menüs aus part_of_typ_IDs. own_orders = gemeldet werden nur die Bestellungen, die wirklich auf dieser Linie liegen.
supplier_price_usefalse (Standard) = ein mitgeschicktes price wird ignoriert, es gilt standard_price. true = der gesendete Preis wird übernommen.
supplier_price_is_netNur bei supplier_price_use: true relevant: true = die gesendeten Beträge sind netto und werden mit vat auf brutto hochgerechnet, false = sie sind bereits brutto.
Die Zusammensetzung (part_of_typ_IDs), die beiden component_*-Modi und die supplier_price_*-Schalter werden von tech2solutions gepflegt und können nicht per API geändert werden — diese Liste gibt sie nur zum Nachlesen aus. Weicht ein Preis oder eine Bestellzahl von der Erwartung ab, steht hier die Erklärung.

Speiseplan anlegen

POST/v1/menus

Legt einen Wochenspeiseplan an. Die Speiseplan-ID (id) sowie alle menuItem-IDs sind die IDs des sendenden Systems und bleiben in allen weiteren Requests die Referenz. Da es sich um einen neuen Speiseplan handelt, existieren noch keine Bestellungen dazu — es wird keine Benachrichtigung verschickt.

Request-Body (gekürzt)
{
  "id": "717304",
  "locationId": "642",
  "locationName": "Kindergarten Obersulz",
  "week": "17",
  "year": "2026",
  "startDate": "2026-04-20",
  "endDate": "2026-04-26",
  "orderDeadline": "2026-04-06 23:59:59",
  "menuLines": [
    {
      "id": "4056",
      "name": "Tagessuppe",
      "minOrderQuantity": "0",
      "menuItems": [
        {
          "id": "15323144",
          "name": "Klare Hühnersuppe mit Reibteig",
          "date": "2026-04-20",
          "isExtraMenu": false,
          "allergens": ["A", "C"],
          "additives": [],
          "attributes": ["enthält Gluten"],
          "price": 3.50,
          "nutritionValues": { "kJ": 203, "kcal": 49 }
        }
      ]
    }
  ]
}
Response · 201 Created
{ "id": "717304", "status": "created", "itemsImported": 10 }

Existiert die Speiseplan-ID bereits, antwortet die API mit 409 Conflict — Änderungen laufen über PUT.

Speiseplan abrufen

GET/v1/menus/{id}

Liefert den aktuell in book2eat hinterlegten Stand des Speiseplans {id} — Name, Datum, Allergene und Deadlines je menuItem, so wie sie aktuell gespeichert sind (nicht zwingend identisch mit dem ursprünglich gesendeten Payload, falls es zwischenzeitlich Änderungen gab). additives, attributes und nutritionValues werden von book2eat nicht gespeichert und sind daher hier nicht enthalten; der Preis ist ebenfalls nicht Teil der Antwort.

Response · 200 OK
{
  "id": "717304",
  "locationId": "642",
  "week": 17,
  "year": 2026,
  "startDate": "2026-04-20",
  "endDate": "2026-04-26",
  "menuLines": [
    {
      "id": "4056",
      "name": "Tagessuppe",
      "menuItems": [
        {
          "id": 15323144,
          "name": "Klare Hühnersuppe mit Reibteig",
          "date": "2026-04-20",
          "allergens": ["A", "C"],
          "orderDeadline": "2026-04-06 23:59:59",
          "cancelDeadline": "2026-04-13 23:59:59",
          "orderDeadlinePassed": true,
          "cancelDeadlinePassed": true,
          "final": true
        }
      ]
    }
  ]
}

Bestellzahlen abrufen

GET/v1/menus/{id}/quantities

Liefert die Bestellzahlen je menuItem für den Speiseplan {id}. Einträge ohne Bestellungen werden mit quantity: 0 ausgegeben. Für Bestandteile eines Gesamtmenüs wird die Summe der zugeordneten Menüs desselben Tages gemeldet (bzw. bei component_quantity_mode: own_orders die eigenen Bestellungen der Linie). final zeigt an, ob die Zahl schon endgültig ist — also sowohl orderDeadline als auch cancelDeadline bereits verstrichen sind und sich die Bestellzahl nicht mehr ändern kann.

Response · 200 OK
[
  {
    "id": 15323144,
    "date": "2026-04-20",
    "quantity": 15,
    "orderDeadline": "2026-04-06 23:59:59",
    "cancelDeadline": "2026-04-13 23:59:59",
    "orderDeadlinePassed": true,
    "cancelDeadlinePassed": true,
    "final": true
  }
]

Bestellzahlen über Woche/Zeitraum abrufen

GET/v1/menus/quantities

Liefert Bestellzahlen kantinenweit über mehrere Speisepläne hinweg — wahlweise für eine ganze Woche oder einen freien Datumsbereich, ohne dass die einzelne Speiseplan-ID bekannt sein muss. Erfordert locationId sowie entweder week+year oder from+to (YYYY-MM-DD) als Query-Parameter. planId im Ergebnis verweist auf die zugehörige Speiseplan-ID.

Zeitraum-Grenzen: Der angefragte Zeitraum darf höchstens 400 Tage in der Vergangenheit beginnen und darf insgesamt höchstens 180 Tage umfassen (bei week+year anhand der tatsächlichen Kalenderwoche geprüft). Außerhalb dieser Grenzen antwortet die API mit 400 VALIDATION_ERROR.
Beispiel
curl -u benutzername:passwort \
  "https://supplier-api.book2eat.eu/v1/menus/quantities?locationId=642&week=17&year=2026"

curl -u benutzername:passwort \
  "https://supplier-api.book2eat.eu/v1/menus/quantities?locationId=642&from=2026-04-20&to=2026-04-26"
Response · 200 OK
[
  {
    "id": 15323144,
    "planId": "717304",
    "date": "2026-04-20",
    "quantity": 15,
    "orderDeadline": "2026-04-06 23:59:59",
    "cancelDeadline": "2026-04-13 23:59:59",
    "orderDeadlinePassed": true,
    "cancelDeadlinePassed": true,
    "final": true
  }
]

Speiseplan komplett ersetzen

PUT/v1/menus/{id}

Ersetzt den Speiseplan durch den mitgeschickten Body (gleiche Struktur wie POST). Neue Einträge werden angelegt, bestehende (gleiche menuItem-ID) aktualisiert. Einträge, die im Body fehlen, werden entfernt — vorhandene Bestellungen dazu werden storniert. menuItems, die bereits final sind (siehe oben), bleiben unangetastet — weder Änderung noch Entfernung — und zählen in der Antwort als protected. Der restliche Speiseplan wird ganz normal verarbeitet.

Benachrichtigung: Für sowohl aktualisierte als auch entfernte menuItems werden Besteller mit einer aktiven Bestellung (nicht bereits stornierte) automatisch von book2eat per E-Mail benachrichtigt. Für neu angelegte Einträge sowie für protected Einträge gibt es keine Benachrichtigung.

Die Mail nennt die Einrichtung, den Namen des Bestellers, Datum und Gericht — und sagt, was passiert ist: Bei einem aktualisierten Eintrag bleibt die Bestellung bestehen und soll geprüft werden, bei einem entfernten wurde sie storniert. Das ist relevant, weil die Mail bei Kinderbuchungen an das Elternkonto geht: Ohne den Namen wäre bei mehreren Kindern nicht erkennbar, wessen Bestellung betroffen ist.
Response · 200 OK
{ "id": "717304", "status": "replaced", "created": 1, "updated": 8, "removed": 1, "protected": 0, "cancelledOrders": 12 }

Einzelnen Eintrag anlegen

POST/v1/menus/{id}/items

Fügt einem bestehenden Speiseplan ein einzelnes neues menuItem hinzu — Alternative zu PUT auf den ganzen Plan, wenn nur ein einzelner Eintrag dazukommt (z. B. nachträglich ergänztes Angebot für einen Tag), statt den kompletten Speiseplan erneut zu senden.

Pflichtfelder: menuLineId (die bestehende Menülinie, der das Item zugeordnet wird — muss für den Standort bereits über ein Menülinien-Mapping verknüpft sein), id, name, date (muss innerhalb der Speiseplanwoche liegen). Optional: menuLineName, isExtraMenu, allergens, additives, attributes, orderDeadline, cancelDeadline, price (nur bei freigeschalteter Preisübernahme), nutritionValues. Wie bei POST /v1/menus muss die menuItem-ID global eindeutig sein — sonst 409 ITEM_ID_EXISTS.

Benachrichtigung: Da es zu einem brandneuen Eintrag noch keine Bestellungen geben kann, wird — wie bei POST /v1/menus — keine Benachrichtigung verschickt.
Request-Body (Beispiel)
{
  "menuLineId": "2120",
  "menuLineName": "Tagessuppe",
  "id": "15323199",
  "name": "Kürbiscremesuppe mit Kernöl",
  "date": "2026-04-25",
  "allergens": ["G"],
  "orderDeadline": "2026-04-11 23:59:59"
}
Response · 201 Created
{ "id": "15323199", "status": "created" }

Einzelnen Eintrag ändern

PUT/v1/menus/{id}/items/{itemId}

Detailänderung eines einzelnen Speiseplaneintrags — z. B. Komponententausch oder geändertes Bestellende. Erlaubte Felder: name, date, allergens, orderDeadline, cancelDeadline, price (optional, nur bei freigeschalteter Preisübernahme). Nur mitgeschickte Felder werden geändert — fehlt price, bleibt der zuletzt gesendete Preis bestehen. Besteller mit einer aktiven Bestellung für diesen Eintrag werden automatisch von book2eat per E-Mail benachrichtigt. Ist das Item bereits final, antwortet die API mit 409 ITEM_LOCKED.

Request-Body (Beispiel: Komponententausch + neues Bestellende)
{
  "name": "Kürbiscremesuppe mit Kernöl",
  "allergens": ["G"],
  "orderDeadline": "2026-04-08 23:59:59"
}
Response · 200 OK
{ "id": "15323144", "status": "updated" }

Löschen

DELETE/v1/menus/{id}

Löscht den kompletten Speiseplan. Alle offenen Bestellungen werden storniert. Bereits finale menuItems (siehe oben) werden dabei übersprungen statt gelöscht/storniert und zählen in der Antwort als protected.

Benachrichtigung: Besteller mit einer aktiven Bestellung für ein betroffenes menuItem werden automatisch von book2eat per E-Mail benachrichtigt. Für protected Einträge gibt es keine Benachrichtigung.
Response · 200 OK
{ "id": "717304", "deleted": true, "items": 9, "protected": 1, "cancelledOrders": 33 }
DELETE/v1/menus/{id}/items/{itemId}

Löscht einen einzelnen Speiseplaneintrag inklusive Storno der zugehörigen Bestellungen. Besteller mit einer aktiven Bestellung für diesen Eintrag werden automatisch von book2eat per E-Mail benachrichtigt. Ist das Item bereits final, antwortet die API mit 409 ITEM_LOCKED.

Response · 200 OK
{ "id": "15323144", "deleted": true, "cancelledOrders": 4 }

Fehlercodes

StatusCodeBedeutung
400VALIDATION_ERROR / INVALID_JSONBody entspricht nicht dem Schema bzw. ist kein gültiges JSON. details enthält die Feldfehler.
401UNAUTHORIZEDFehlende oder falsche Zugangsdaten.
403CANTEEN_NOT_ALLOWEDDer API-User ist für diesen Standort nicht freigeschaltet.
404MENU_NOT_FOUND / ITEM_NOT_FOUNDSpeiseplan bzw. Eintrag existiert nicht.
409MENU_EXISTSSpeiseplan-ID bereits vorhanden — PUT verwenden.
409ITEM_ID_EXISTSDie menuItem-ID wird bereits in einem anderen Speiseplan verwendet — menuItem-IDs müssen global eindeutig sein, nicht nur innerhalb eines Speiseplans.
409ITEM_LOCKEDorder- und cancelDeadline des menuItems sind bereits verstrichen — keine Änderung/Löschung mehr möglich.
422UNKNOWN_LOCATION, UNKNOWN_MENU_LINE, UNKNOWN_MENU_TYPE, DATE_OUT_OF_RANGE, DUPLICATE_ITEM_IDFachlich ungültig — Meldung beschreibt das Problem. UNKNOWN_MENU_LINE: menuLineId/menuLines[].id ist für diesen Standort keinem book2eat-Menütyp zugeordnet, siehe Menülinien-Mapping. UNKNOWN_MENU_TYPE: die Linie ist zugeordnet, der Menütyp existiert in book2eat aber nicht (mehr) — bitte tech2solutions kontaktieren.
429—Rate-Limit erreicht. Kurz warten und erneut senden.
Fehlerformat
{
  "error": {
    "code": "UNKNOWN_LOCATION",
    "message": "Der Standort mit locationId 699 ist in book2eat nicht angelegt.",
    "details": null
  }
}

Health-Check

GET/v1/health

Ohne Authentifizierung erreichbar, für Monitoring.

{ "status": "ok", "db": "ok", "time": "2026-07-14T09:00:00.000Z" }