Hilfe - Alle Produkte & Anleitungen

Catalog Sync (API)

Catalog Sync (API)

The Catalog Sync API lets you upload a venue's entire catalog — articles, categories, modifiers and menus — to bessa in one request. The request is validated immediately, then the data is written in the background in a single transaction. This interface is aimed at developers connecting a point-of-sale or third-party system to bessa.

You do not need this interface for manual catalog maintenance. Articles, categories and options are managed conveniently in the bessa Kassa Manager.

Overview

You upload the entire catalog in one request. The request is validated synchronously — a bad request is rejected immediately with 400. The actual writes then run as a background job. The client polls a status endpoint until the job is finished. An identical re-upload is detected (deduplication) and does no further work.

Base URL

/v1/web/pos/

Authentication

Token authentication as a venue API user with upload permission (Authorization: Token <key>). The venue is derived from the API user — there is no venue id in the URL.

Rate limit

60 requests per minute per upload user. An identical re-upload (same body hash) bypasses the limit and returns a dedup response.

Endpoints

Method

Path

Purpose

POST

/v1/web/pos/catalog/sync/

Validate, deduplicate and enqueue a catalog apply

GET

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

Poll the status of a job (id = job id from the POST response)

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

Validates the whole payload, stores it and enqueues the background apply.

Request body

{
  "mode": "update",                  // "update" (default) | "create"
  "articles": [
    {
      "id": "4001",                  // required, ≤128 chars — the article number (PLU/UUID)
      "name": "Pizza Salami",        // required
      "gross": 899,                  // required, integer in minor units → 8.99
      "tax": 1000,                   // required, integer → 10.00 (%)
      "is_active": true,             // optional, default true
      "description": "…",            // optional
      "allergens": "A.G.L",          // optional
      "type": 4,                     // optional ArticleType: 1 Unknown, 2 Other, 3 Drink, 4 Food
      "course_group": 1,             // optional, integer course-group number (upserted)
      "available": 10,               // optional, absolute stock baseline
      "tracking": true,              // optional, default false — enable stock tracking
      "dispenser_id": "D-42",        // optional, ≤32 chars
      "prices": [                    // optional, ≤10 entries, one per order type
        { "order_type": 1, "price": 899 },   // order_type = Order.Type, price = minor units
        { "order_type": 2, "price": 999 }
      ],
      "translations": {              // optional, keyed by language code (≤5 chars)
        "de": { "nutrition_facts": "…" },
        "en": { "name": "Pizza Salami", "description": "…", "allergens": "…", "nutrition_facts": "…" }
      },
      "modifiers": ["159434e3-…"]    // optional, ids from the top-level "modifiers" list
    }
  ],
  "categories": [
    {
      "id": "Pizza",                 // required, ≤64 chars — category reference (unique per venue)
      "name": "Pizza",               // required
      "from_hour": "11:00",          // optional, "HH:MM" or null
      "to_hour": null,               // optional, "HH:MM" or null
      "articles": ["4001", "4002"],  // ordered article ids in this category
      "translations": { "en": { "name": "Pizza", "description": "…" } }   // optional
    }
  ],
  "modifiers": [
    {
      "id": "159434e3-…",            // required, ≤128 chars — modifier number (unique per venue)
      "name": "Fleisch",             // required
      "min": 0,                      // optional, default 1
      "max": 3,                      // optional, default 1 (must be ≥ min)
      "ignore_price": true,          // optional, default false
      "articles": ["e400bb4a-…"],    // ordered article ids = the modifier's selectable options
      "translations": { "en": { "name": "Meat", "description": "…" } }    // optional
    }
  ],
  "menus": [
    {
      "name": "Abholung",            // required
      "description": "",             // optional
      "type": 1,                     // required, Order.Type (see below); unique per venue
      "categories": ["Pizza"]        // ordered category ids (== categories[].id)
    }
  ],
  "tables": [
    {
      "id": "T1",                    // required — table reference (unique per venue)
      "name": "Table 1",             // required
      "is_active": true              // optional, default true
    }
  ]
}

All five top-level lists (articles, categories, modifiers, menus, tables) are optional and default to empty.

Key rules

  • Ordering is implicit by array position — there is no sort field anywhere. The order of categories[], menus[].categories[], modifiers[].articles[] and categories[].articles[] defines the displayed or stored order.

  • mode:

    • update (default): upserts what's in the payload. Entities and links not mentioned are left untouched.

    • create: full rebuild — additionally removes structure absent from the payload (menus, categories, modifiers and their links). Articles are never deleted (orders and customer loyalty reference them); an article missing from the payload is only unlinked.

  • Prices use an explicit {order_type, price} list (one entry per order type). price is an integer in minor units. At most 10 entries; an order_type may not repeat within one article. Prices are upsert-only — a price for an order type dropped from a later payload is kept.

  • Translations are full-state per language per sync: send the complete field set for each language every time. A field omitted from a language entry is not preserved from a previous sync — it falls back to the article's flat value (and nutrition_facts, which has no flat field, becomes empty). Translations are upsert-only (a dropped language is kept). Article translations carry name/description/allergens/nutrition_facts; category and modifier translations carry name/description. Menus have no translations.

  • Stock: available/tracking set the absolute baseline on create. On update, available is applied only when the article is not stock-tracked — a tracked article's live count is owned by the separate stock-delta interface.

Validation (returns 400, no job, no writes)

The following cases are rejected immediately — nothing is written:

  • Unknown references inside the payload: an article's modifiers[] id not in modifiers[]; a modifier's or category's articles[] id not in articles[]; a menu's categories[] id not in categories[].

  • Duplicate ids within articles/categories/modifiers, or a duplicate menu type.

  • prices: more than 10 entries, or a repeated order_type within one article.

  • min greater than max on a modifier.

  • from_hour/to_hour not in HH:MM format.

  • A translation language key longer than 5 characters.

  • An out-of-range type/order_type/mode.

Errors are returned as a standard error object keyed by the offending section or field.

Responses

Status

When

Body

202 Accepted

New payload accepted and enqueued

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

200 OK

Identical payload already seen (deduplicated, no new work). Also returns header X-POS-Catalog-Dedup: hit

{ "id": "<existing job id>" } (id present when known)

400 Bad Request

Validation failed

Error object; nothing was written

401 / 403

Missing/invalid token, or no upload permission

429 Too Many Requests

Rate limit exceeded (non-deduped)

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

Returns the job status. Scoped to the caller's venue — another venue's job id returns 404.

Response 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"
}

Field

Meaning

id

Job id (matches the POST response)

mode

update or create

state

submittedin_progressdone | failed

message

Human-facing result or error (a generic, translated message on failure)

stats

Per-entity counts after a successful apply (created/updated/links/etc.); null until done

created / updated

Timestamps

  1. POST the catalog → keep the returned id.

  2. On 400, stop and fix the payload (do not retry the same body — it will keep failing).

  3. On 202/200, poll GET …/{id}/ until state is done or failed.

  4. On failed, surface the message and re-upload after correcting — do not poll forever.

Reference: Order.Type values

Value

Type

0

Dine in

1

Pickup

2

Delivery

3

Self service

4

Dispenser

5

Third party pickup

6

Third party delivery

7

Canteen

Behaviour driven by the point-of-sale system (informational)

The following points are applied automatically based on the venue's provider configuration; the client does not control them per request:

  • Ignore text: description and allergens from the payload are ignored (existing values are kept).

  • Ignore sort: on a pure reorder, the category→article order is left unchanged.

  • Ignore article type: for certain provider configurations the article type is ignored.

Relationship to the previous endpoints

This route is the successor to POST /v1/web/pos/articles/, POST /v1/web/pos/menus/ and POST/PUT/PATCH /v1/web/pos/sync/articles/{plu}/. Those endpoints remain available and unchanged during the migration and will be marked deprecated in a later release.

Frequently asked questions

How many requests may I send per minute? 60 requests per minute per upload user. An identical re-upload (same body hash) is deduplicated and does not count against the limit.

Are articles deleted in create mode? No. Articles are never deleted, because orders and customer loyalty reference them. In create mode, missing articles are only unlinked from categories and options; menus, categories and options absent from the payload are removed.

Why is a translated field empty after the sync? Translations are full-state per language: send the complete field set for each language every time. An omitted field falls back to the flat article value; nutrition_facts has no flat value and therefore becomes empty.

How do I set the stock level of a tracked article? On update, available is applied only to articles that are not stock-tracked. A tracked article's live count is maintained through the separate stock-delta interface.

Troubleshooting

  • 400 Bad Request — The payload failed validation. Check the returned error object (keyed by section/field), fix the payload and send it again. Do not resend the same body — it will keep failing.

  • 401 / 403 — The token is missing or invalid, or the API user has no upload permission for the venue.

  • 429 Too Many Requests — The rate limit was exceeded. Wait and throttle your send rate.

  • state: failed — The background apply failed. Surface the message, fix the cause and re-upload. Do not poll the status forever.

  • 404 when polling status — The job id belongs to another venue or does not exist. Use the id from your own POST response.