Menu field reference
Reference for every object in the Menu API. "Sync" columns describe what
you send to POST /v1/sync; "Read" notes describe how
the object comes back from GET /v1/menu.
Conventions
All money fields (priceMinor, priceAdjustment)
are integers in minor currency units (kopecks, cents) —
21500 means 215.00. All objects are matched and returned by
externalId (your identifier); items created manually in the
DuckHub app have externalId: null in read responses.
Category#
| Field | Type | Sync | Notes |
|---|---|---|---|
externalId | string | Required, ≤ 255 | Unique per venue; upsert key |
name | string | Required, ≤ 200 | |
sortOrder | integer | Optional | Default 0 on create; lists are sorted by it |
Ingredient#
| Field | Type | Sync | Notes |
|---|---|---|---|
externalId | string | Required, ≤ 255 | Unique per venue; upsert key |
name | string | Required, ≤ 200 | |
sortOrder | integer | Optional | Default 0 on create |
Ingredients serve two purposes: product composition (via
ingredientExternalIds on a product) and modifier options (via
modifierGroups[].options[].ingredientExternalId).
Product#
| Field | Type | Sync | Notes |
|---|---|---|---|
externalId | string | Required, ≤ 255 | Unique per venue; upsert key; re-syncing a cleaned-up product restores it |
name | string | Required, ≤ 200 | |
description | string | Optional, ≤ 1000 | |
priceMinor | integer | Required, ≥ 0 | Minor units |
categoryExternalId | string | Optional, ≤ 255 | Unknown id → saved without category (warning). Omitting it on update clears the category link |
ingredientExternalIds | string[] | Optional | Composition. Omit = keep as is; [] = clear; list = replace (unknown ids skipped with warning) |
modifierGroups | ModifierGroup[] | Optional | Omit = keep as is; [] = clear; list = replace all groups |
sortOrder | integer | Optional | Default 0 on create |
menuVisible | boolean | Optional | Default true on create; false hides from published menu; set by cleanup deactivation |
Read shape adds: ingredients[] as { externalId, name } objects and
fully expanded modifierGroups (see below).
ModifierGroup#
| Field | Type | Sync | Notes |
|---|---|---|---|
name | string | Required, ≤ 200 | |
type | string | Required | single_choice | multiple_choice | add_ingredients | remove_ingredients |
isRequired | boolean | Optional | Only single_choice groups can be required — there it defaults to true; for all other types the server forces false |
options | ModifierOption[] | Required | |
sortOrder | integer | Optional | Defaults to the group's position in the array |
Group types:
| Type | Guest behaviour |
|---|---|
single_choice | Pick exactly one option (e.g. size); can be required |
multiple_choice | Pick any number of options |
add_ingredients | Add extra ingredients (typically priced) |
remove_ingredients | Remove ingredients (options default to action: "remove") |
Syncing a product with modifierGroups replaces its groups wholesale —
groups are not merged or matched individually.
ModifierOption#
| Field | Type | Sync | Notes |
|---|---|---|---|
ingredientExternalId | string | Required, ≤ 255 | Must reference a synced ingredient; unknown → option skipped (warning) |
action | string | Optional | add | remove; default remove in remove_ingredients groups, else add |
priceAdjustment | integer | Optional | Minor units, may be negative; default 0 |
sortOrder | integer | Optional | Defaults to the option's position |
Read shape adds ingredientName (denormalised from the ingredient).
Read-only response fields#
| Field | Where | Meaning |
|---|---|---|
generatedAt | GET /v1/menu | Server timestamp of the response |
syncedAt | POST /v1/sync response | Server timestamp of the sync |
publicationId | POST /v1/publish response | Id of the created snapshot |