Hilfe - Alle Produkte & Anleitungen

Katalog synchronisieren (API)

Katalog synchronisieren (API)

Über die Catalog-Sync-API übertragen Sie den kompletten Katalog eines Standorts — Artikel, Kategorien, Artikeloptionen und Menüs — in einer Anfrage an bessa. Die Anfrage wird sofort geprüft, anschließend werden die Daten im Hintergrund in einer einzigen Transaktion geschrieben. Diese Schnittstelle richtet sich an Entwickler, die ein Kassen- oder Fremdsystem an bessa anbinden.

Für die manuelle Pflege Ihres Katalogs benötigen Sie diese Schnittstelle nicht. Artikel, Kategorien und Optionen pflegen Sie komfortabel im bessa Kassa Manager.

Überblick

Sie laden den gesamten Katalog in einer Anfrage hoch. Die Anfrage wird synchron validiert — eine fehlerhafte Anfrage wird sofort mit 400 abgelehnt. Die eigentlichen Schreibvorgänge laufen danach als Hintergrund-Job. Der Client fragt anschließend einen Status-Endpunkt ab, bis der Job abgeschlossen ist. Ein identischer erneuter Upload wird erkannt (Deduplizierung) und löst keine weitere Arbeit aus.

Basis-URL

/v1/web/pos/

Authentifizierung

Token-Authentifizierung als API-Benutzer mit Upload-Berechtigung des Standorts (Authorization: Token <key>). Der Standort wird aus dem API-Benutzer abgeleitet — es gibt keine Standort-ID in der URL.

Rate-Limit

60 Anfragen pro Minute und Upload-Benutzer. Ein identischer erneuter Upload (gleicher Body-Hash) umgeht das Limit und liefert eine Dedup-Antwort.

Endpunkte

Methode

Pfad

Zweck

POST

/v1/web/pos/catalog/sync/

Validieren, deduplizieren und Katalog-Übernahme einreihen

GET

/v1/web/pos/catalog/sync/{id}/

Status eines Jobs abfragen (id = Job-ID aus der POST-Antwort)

POST /v1/web/pos/catalog/sync/

Validiert die gesamte Nutzlast, speichert sie und reiht die Hintergrund-Übernahme ein.

Anfrage-Body

{
  "mode": "update",                  // "update" (Standard) | "create"
  "articles": [
    {
      "id": "4001",                  // Pflicht, ≤128 Zeichen — die Artikelnummer (PLU/UUID)
      "name": "Pizza Salami",        // Pflicht
      "gross": 899,                  // Pflicht, ganzzahlig in kleinster Einheit → 8,99
      "tax": 1000,                   // Pflicht, ganzzahlig → 10,00 (%)
      "is_active": true,             // optional, Standard true
      "description": "…",            // optional
      "allergens": "A.G.L",          // optional
      "type": 4,                     // optional ArticleType: 1 Unbekannt, 2 Sonstiges, 3 Getränk, 4 Speise
      "course_group": 1,            // optional, ganzzahlige Gang-Nummer (wird angelegt/aktualisiert)
      "available": 10,               // optional, absoluter Lagerbestand als Basiswert
      "tracking": true,              // optional, Standard false — Lagerführung aktivieren
      "dispenser_id": "D-42",        // optional, ≤32 Zeichen
      "prices": [                    // optional, ≤10 Einträge, einer je Bestellart
        { "order_type": 1, "price": 899 },   // order_type = Order.Type, price = kleinste Einheit
        { "order_type": 2, "price": 999 }
      ],
      "translations": {              // optional, je Sprachcode (≤5 Zeichen)
        "de": { "nutrition_facts": "…" },
        "en": { "name": "Pizza Salami", "description": "…", "allergens": "…", "nutrition_facts": "…" }
      },
      "modifiers": ["159434e3-…"]    // optional, IDs aus der obersten Liste "modifiers"
    }
  ],
  "categories": [
    {
      "id": "Pizza",                 // Pflicht, ≤64 Zeichen — Kategorie-Referenz (je Standort eindeutig)
      "name": "Pizza",               // Pflicht
      "from_hour": "11:00",          // optional, "HH:MM" oder null
      "to_hour": null,               // optional, "HH:MM" oder null
      "articles": ["4001", "4002"],  // geordnete Artikel-IDs dieser Kategorie
      "translations": { "en": { "name": "Pizza", "description": "…" } }   // optional
    }
  ],
  "modifiers": [
    {
      "id": "159434e3-…",            // Pflicht, ≤128 Zeichen — Options-Nummer (je Standort eindeutig)
      "name": "Fleisch",             // Pflicht
      "min": 0,                      // optional, Standard 1
      "max": 3,                      // optional, Standard 1 (muss ≥ min sein)
      "ignore_price": true,          // optional, Standard false
      "articles": ["e400bb4a-…"],    // geordnete Artikel-IDs = die wählbaren Optionen
      "translations": { "en": { "name": "Meat", "description": "…" } }    // optional
    }
  ],
  "menus": [
    {
      "name": "Abholung",            // Pflicht
      "description": "",             // optional
      "type": 1,                     // Pflicht, Order.Type (siehe unten); je Standort eindeutig
      "categories": ["Pizza"]        // geordnete Kategorie-IDs (== categories[].id)
    }
  ],
  "tables": [
    {
      "id": "T1",                    // Pflicht — Tisch-Referenz (je Standort eindeutig)
      "name": "Table 1",             // Pflicht
      "is_active": true              // optional, Standard true
    }
  ]
}

Alle fünf obersten Listen (articles, categories, modifiers, menus, tables) sind optional und standardmäßig leer.

Wichtige Regeln

  • Reihenfolge ergibt sich aus der Array-Position — es gibt nirgends ein sort-Feld. Die Reihenfolge von categories[], menus[].categories[], modifiers[].articles[] und categories[].articles[] bestimmt die angezeigte bzw. gespeicherte Sortierung.

  • mode:

    • update (Standard): aktualisiert bzw. legt an, was in der Nutzlast enthalten ist. Nicht genannte Objekte und Verknüpfungen bleiben unangetastet.

    • create: vollständiger Neuaufbau — entfernt zusätzlich alle Strukturen, die in der Nutzlast fehlen (Menüs, Kategorien, Optionen und deren Verknüpfungen). Artikel werden nie gelöscht (Bestellungen und Kundenbindung verweisen darauf); ein in der Nutzlast fehlender Artikel wird lediglich entkoppelt.

  • Preise werden als explizite {order_type, price}-Liste angegeben (ein Eintrag je Bestellart). price ist ganzzahlig in der kleinsten Währungseinheit. Maximal 10 Einträge; ein order_type darf innerhalb eines Artikels nicht doppelt vorkommen. Preise werden ausschließlich aktualisiert/angelegt — ein Preis für eine Bestellart, die in einer späteren Nutzlast wegfällt, bleibt erhalten.

  • Übersetzungen sind Vollzustand je Sprache und je Sync: Senden Sie pro Sprache jedes Mal den vollständigen Feldsatz. Ein in einem Sprach-Eintrag weggelassenes Feld wird nicht aus einem vorherigen Sync übernommen — es fällt auf den flachen Wert des Artikels zurück (und nutrition_facts, das kein flaches Feld besitzt, wird leer). Übersetzungen werden nur aktualisiert/angelegt (eine weggelassene Sprache bleibt erhalten). Artikel-Übersetzungen enthalten name/description/allergens/nutrition_facts; Kategorie- und Options-Übersetzungen enthalten name/description. Menüs haben keine Übersetzungen.

  • Lagerbestand: available/tracking setzen beim Anlegen den absoluten Basiswert. Beim Aktualisieren wird available nur angewendet, wenn der Artikel nicht lagergeführt ist — der Live-Bestand eines lagergeführten Artikels gehört der separaten Lagerbestands-Schnittstelle (Stock-Delta).

Validierung (liefert 400, kein Job, keine Schreibvorgänge)

Folgende Fälle werden sofort abgelehnt — es wird nichts geschrieben:

  • Unbekannte Referenzen innerhalb der Nutzlast: eine modifiers[]-ID eines Artikels, die nicht in modifiers[] enthalten ist; eine articles[]-ID einer Option oder Kategorie, die nicht in articles[] enthalten ist; eine categories[]-ID eines Menüs, die nicht in categories[] enthalten ist.

  • Doppelte IDs innerhalb von articles/categories/modifiers oder ein doppelter Menü-type.

  • prices: mehr als 10 Einträge oder ein wiederholter order_type innerhalb eines Artikels.

  • min größer als max bei einer Option.

  • from_hour/to_hour nicht im Format HH:MM.

  • Ein Sprachschlüssel einer Übersetzung länger als 5 Zeichen.

  • Ein type/order_type/mode außerhalb des gültigen Bereichs.

Fehler werden als Standard-Fehlerobjekt zurückgegeben, geschlüsselt nach dem betroffenen Abschnitt bzw. Feld.

Antworten

Status

Wann

Body

202 Accepted

Neue Nutzlast angenommen und eingereiht

{ "id": "<job id>", "state": "submitted" }

200 OK

Identische Nutzlast bereits bekannt (dedupliziert, keine neue Arbeit). Zusätzlich Header X-POS-Catalog-Dedup: hit

{ "id": "<existing job id>" } (ID vorhanden, sofern bekannt)

400 Bad Request

Validierung fehlgeschlagen

Fehlerobjekt; es wurde nichts geschrieben

401 / 403

Token fehlt/ungültig oder keine Upload-Berechtigung

429 Too Many Requests

Rate-Limit überschritten (nicht dedupliziert)

GET /v1/web/pos/catalog/sync/{id}/

Liefert den Job-Status. Auf den Standort des Aufrufers beschränkt — die Job-ID eines anderen Standorts liefert 404.

Antwort 200

JSON
{
  "id": "csj_…",
  "mode": "update",
  "state": "done",
  "message": "",
  "stats": { "articles_created": 3, "articles_updated": 0, "...": 0 },
  "created": "2026-06-26T08:00:00Z",
  "updated": "2026-06-26T08:00:03Z"
}

Feld

Bedeutung

id

Job-ID (entspricht der POST-Antwort)

mode

update oder create

state

submittedin_progressdone | failed

message

Ergebnis- bzw. Fehlermeldung (bei Fehlern eine generische, übersetzte Meldung)

stats

Zähler je Objekttyp nach erfolgreicher Übernahme (angelegt/aktualisiert/verknüpft usw.); null, bis der Job fertig ist

created / updated

Zeitstempel

Empfohlener Ablauf für Clients

  1. Katalog per POST senden → zurückgegebene id merken.

  2. Bei 400 abbrechen und die Nutzlast korrigieren (denselben Body nicht erneut senden — er schlägt wieder fehl).

  3. Bei 202/200 den Endpunkt GET …/{id}/ abfragen, bis state gleich done oder failed ist.

  4. Bei failed die message anzeigen und nach Korrektur erneut hochladen — nicht endlos abfragen.

Referenz: Order.Type-Werte

Wert

Bestellart

0

Vor Ort

1

Abholung

2

Lieferung

3

Selbstbedienung

4

Schankanlage

5

Drittanbieter-Abholung

6

Drittanbieter-Lieferung

7

Kantine

Vom Kassensystem abhängiges Verhalten (informativ)

Die folgenden Punkte werden automatisch anhand der Anbieter-Konfiguration des Standorts angewendet; der Client steuert sie nicht je Anfrage:

  • Text ignorieren: Beschreibung und Allergene aus der Nutzlast werden ignoriert (bestehende Werte bleiben erhalten).

  • Sortierung ignorieren: Bei einer reinen Umsortierung bleibt die Reihenfolge Kategorie→Artikel unverändert.

  • Artikeltyp ignorieren: Bei bestimmten Anbieter-Konfigurationen wird der type des Artikels ignoriert.

Verhältnis zu den bisherigen Endpunkten

Diese Route ersetzt die bisherigen Endpunkte POST /v1/web/pos/articles/, POST /v1/web/pos/menus/ und POST/PUT/PATCH /v1/web/pos/sync/articles/{plu}/. Diese Endpunkte sind veraltet (deprecated) — verwenden Sie für neue wie bestehende Anbindungen ausschließlich die Catalog-Sync-API.

Häufige Fragen

Wie viele Anfragen darf ich pro Minute senden? 60 Anfragen pro Minute und Upload-Benutzer. Ein identischer erneuter Upload (gleicher Body-Hash) wird dedupliziert und zählt nicht gegen das Limit.

Werden Artikel im Modus create gelöscht? Nein. Artikel werden nie gelöscht, weil Bestellungen und die Kundenbindung darauf verweisen. Im Modus create werden fehlende Artikel lediglich aus Kategorien und Optionen entkoppelt; Menüs, Kategorien und Optionen, die in der Nutzlast fehlen, werden hingegen entfernt.

Warum ist ein übersetztes Feld nach dem Sync leer? Übersetzungen sind Vollzustand je Sprache: Senden Sie jedes Mal den vollständigen Feldsatz pro Sprache. Ein weggelassenes Feld fällt auf den flachen Artikelwert zurück; nutrition_facts hat keinen flachen Wert und wird daher leer.

Wie setze ich den Lagerbestand eines lagergeführten Artikels? Beim Aktualisieren wird available nur für nicht lagergeführte Artikel angewendet. Der Live-Bestand eines lagergeführten Artikels wird über die separate Lagerbestands-Schnittstelle (Stock-Delta) gepflegt.

Fehlerbehebung

  • 400 Bad Request — Die Nutzlast hat die Validierung nicht bestanden. Prüfen Sie das zurückgegebene Fehlerobjekt (geschlüsselt nach Abschnitt/Feld), korrigieren Sie die Nutzlast und senden Sie sie erneut. Senden Sie nicht denselben Body erneut — er schlägt wieder fehl.

  • 401 / 403 — Token fehlt oder ist ungültig, oder der API-Benutzer hat keine Upload-Berechtigung für den Standort.

  • 429 Too Many Requests — Das Rate-Limit wurde überschritten. Warten Sie und drosseln Sie die Sendefrequenz.

  • state: failed — Die Hintergrund-Übernahme ist fehlgeschlagen. Zeigen Sie die message an, korrigieren Sie die Ursache und laden Sie erneut hoch. Fragen Sie den Status nicht endlos ab.

  • 404 beim Statusabruf — Die Job-ID gehört zu einem anderen Standort oder existiert nicht. Verwenden Sie die id aus Ihrer eigenen POST-Antwort.

Verwandte Themen