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 |
|
|
Authentication |
Token authentication as a venue API user with upload permission ( |
|
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 |
|---|---|---|
|
|
|
Validate, deduplicate and enqueue a catalog apply |
|
|
|
Poll the status of a job ( |
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
sortfield anywhere. The order ofcategories[],menus[].categories[],modifiers[].articles[]andcategories[].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).priceis an integer in minor units. At most 10 entries; anorder_typemay 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 carryname/description/allergens/nutrition_facts; category and modifier translations carryname/description. Menus have no translations. -
Stock:
available/trackingset the absolute baseline on create. On update,availableis 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 inmodifiers[]; a modifier's or category'sarticles[]id not inarticles[]; a menu'scategories[]id not incategories[]. -
Duplicate ids within
articles/categories/modifiers, or a duplicate menutype. -
prices: more than 10 entries, or a repeatedorder_typewithin one article. -
mingreater thanmaxon a modifier. -
from_hour/to_hournot inHH:MMformat. -
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 |
|---|---|---|
|
|
New payload accepted and enqueued |
|
|
|
Identical payload already seen (deduplicated, no new work). Also returns header |
|
|
|
Validation failed |
Error object; nothing was written |
|
|
Missing/invalid token, or no upload permission |
— |
|
|
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
{
"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 |
|---|---|
|
|
Job id (matches the POST response) |
|
|
|
|
|
|
|
|
Human-facing result or error (a generic, translated message on failure) |
|
|
Per-entity counts after a successful apply (created/updated/links/etc.); |
|
|
Timestamps |
Recommended client flow
-
POSTthe catalog → keep the returnedid. -
On
400, stop and fix the payload (do not retry the same body — it will keep failing). -
On
202/200, pollGET …/{id}/untilstateisdoneorfailed. -
On
failed, surface themessageand 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
typeis ignored.
Relationship to the previous endpoints
This route replaces the previous endpoints POST /v1/web/pos/articles/, POST /v1/web/pos/menus/ and POST/PUT/PATCH /v1/web/pos/sync/articles/{plu}/. Those endpoints are now deprecated — use the Catalog Sync API exclusively for both new and existing integrations.
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 themessage, fix the cause and re-upload. Do not poll the status forever. -
404when polling status — The job id belongs to another venue or does not exist. Use theidfrom your own POST response.