Change an addon group
Partner key + restaurant key. The fields from creation, all optional (at least one). addons is replaced as a set when sent; an addon sent with its id keeps its id and its size prices. The sizes (tiers) are changed only in the restaurant dashboard. Send the ids from GET to keep the rows: a row with id is updated in place, a row without id is new (new id), and rows that are left out are deleted. New ids can make orders fail for up to six minutes, because the website shows the cached menu. Size prices (tierPrices) cannot be set through the API: a new addon in a group with sizes costs the same for all sizes (unless a deleted addon with the same name had prices), and a new size without addonTierKey gives the addons their base price. notice says so. isRequired: false without minSelect gives minSelect 0, and minSelect without isRequired makes the group required exactly when the minimum is above 0. The checkout requires minSelect among the available addons regardless of isRequired, so isRequired: false with minSelect above 0, and minSelect above the number of available addons (isAvailable not false), give 422 (paths isRequired and minSelect). warnings says when a GLOBAL group is required. A stored group that already breaks the rules can be saved unchanged, but not made worse. Through LettBestilt MCP an addon is never deleted: if addons lacks an addon that exists (also when it is sent without id, as a new one), the response is 422 VALIDATION_FAILED with the path addons and the names, and nothing is changed. New addons and changes with id are allowed. A person removes addons in the restaurant dashboard or with the partner API directly. The scope targets are checked again when scope, categoryId or productIds is sent. Optimistic concurrency: GET (and every write response) has version, a hash of the row and its sizes (product) or its sizes and addons (addon group). Send it back as expectedVersion: if anything changed since you read it, nothing is written and the response is 409 CHANGED; read it again and retry. Without expectedVersion the write is only conditional on the row as it was read inside the request, so a change made after your GET is overwritten. Always send it when you work from an earlier read (an AI tool working from an old get_menu). expectedUpdatedAt is rejected with 422. Names and descriptions are trimmed and cleaned (links, email addresses, HTML, javascript: and invisible direction marks are removed); the count is in textCleaned, and a name that ends up empty gives 422. GET /api/v1/menu is CDN-cached (s-maxage=30, stale-while-revalidate=300, at most 330 seconds), so the website can show the old menu for up to six minutes; the response has notice about it.
Authentication
The partner key (lbp_test_… or lbp_live_…), sent as Authorization: Bearer lbp_… and created in LettBestilt Partner under Partner key. It belongs to the whole partner, not to a restaurant, and only works against /api/v1/partner. Server to server only: a request with an Origin header is rejected with 403 FORBIDDEN, and the key must never be in a browser. A TEST key sees and creates only sandboxes; LIVE keys work once LettBestilt Partner is open. A suspended or archived partner, a revoked key and a restaurant key (lbs_…) here give 401 UNAUTHORIZED; an expired key gives 401 KEY_EXPIRED.
The restaurant’s own key (lbs_test_… or lbs_live_…) for calls about one restaurant in the partner API, sent together with the partner key. The key must have full access (scope *, otherwise 403 INSUFFICIENT_SCOPE), belong to a restaurant the partner owns and be in the same environment as the partner key. A restaurant that does not exist, belongs to another partner or does not match the key always gives the same 404 RESTAURANT_NOT_FOUND. Older keys without an environment in the prefix (lbs_<random>) are not accepted here; rotate them in LettBestilt Partner.
Path parameters
The restaurant’s slug. It must match the restaurant key in X-LettBestilt-Restaurant-Key.
The addon group’s id from GET …/addon-groups.
Request
Without isRequired the group becomes required exactly when the minimum is above 0.
Replaces the addons as a set (unique names). Send the ids from GET to keep the rows: a row with id is updated in place, a row without id is new (new id), and rows that are left out are deleted. New ids can make orders fail for up to six minutes, because the website shows the cached menu. Size prices (tierPrices) cannot be set through the API: a new addon in a group with sizes costs the same for all sizes (unless a deleted addon with the same name had prices), and a new size without addonTierKey gives the addons their base price. notice says so.
Optional: version from GET. 409 CHANGED when the row changed since then.

