API referenceMenu items

Menu items

11 operations. Every schema and example on this page is generated from the platform contract.

List a store’s menu items.

Merchant read of the store’s menu items. Removed items (soft-deleted) are excluded; merchant-hidden items are included (flagged via is_available/checkout_enabled). The menu is the PHYSICAL items only: a download (PRODUCT-SPEC 0.11) is never in it. `fulfilment_kind=digital` lists the shop’s downloads instead, and is a 404 until downloads are switched on.

Parameters

store_idstring · uuidqueryrequired

Store whose menu to list (also authorises the merchant).

fulfilment_kindenumqueryoptional

Which items to list. OMIT for the menu: the physical items, exactly as before. `digital` lists the shop’s DOWNLOADS instead (PRODUCT-SPEC 0.11), each carrying `fulfilment_kind`, `digital_ready_asset_count` and `is_complete`; it is a 404 until downloads are switched on. A download is never in the menu list.

physicaldigital

Response, 200

itemsarray of objectrequired
43 child fields
idstringrequired
namestringrequired
descriptionstring, nullablerequired
categorystring, nullablerequired
emojistring, nullablerequired
image_urlstring, nullablerequired
product_mediaarray of objectrequired

Her photos and videos, in her order: at most 10 together, at most 3 of them videos. Since 21 Oct 2026 a video may come first; the cover is the entry `primary-image` and `image_url`, always a photo.

5 child fields
idstringrequired
typeenumrequired
imagevideo
urlstringrequired
thumbnail_urlstring, nullablerequired
sort_ordernumberrequired
media_auto_advancebooleanoptional

False when she holds her item's photos and videos still on her drop page; true (the default) when they move on by themselves.

contains_allergensarray of stringrequired
no_allergensbooleanrequired

FA078. True ONLY when a human ticked "no allergens in this item". An empty contains_allergens with no_allergens=false means NOBODY HAS SAID, it is not a statement that the item is allergen-free, and must never be rendered as one.

dietary_tagsarray of stringrequired
dietsarray of stringrequired

The diets the merchant declared, in the canonical spelling: any of "Vegetarian", "Vegan", "Gluten-free", "Dairy-free". A legacy "Dairy Free" reads as "Dairy-free".

satisfies_dietsarray of stringrequired

`diets` plus what they imply, Vegan also satisfies Vegetarian and Dairy-free. Derived on read, never stored, so what the merchant actually said stays distinguishable. This is what a diet filter reads. Since 12 Oct 2026 it leaves out Gluten-free for an item whose `may_contain_allergens` names Gluten, and Dairy-free (the seller's own tag, or through Vegan) for one that names Dairy: a filter never shows an item as free from something the seller says it may contain. Vegan and Vegetarian are never left out, and `diets` and `dietary_tags` still say what the seller tagged.

allergen_groupsarray of stringrequired

`contains_allergens` grouped to the UK fourteen, in the law's order ("tree nuts (almond)" and "Tree Nuts (Pistachio)" are both "Tree Nuts"). Allergens MARK a dish on a menu, never hide it.

may_contain_allergensarray of stringoptional

What the item MAY contain (traces, e.g. made beside nuts): the UK fourteen the seller ticked under "May contain", in the law's order and spelling ("Tree Nuts", "Peanuts"). Empty means she has said nothing may be present, never "none". Not an answer to what it contains (`contains_allergens` / `no_allergens` are). Absent on a payload from before 8 Oct 2026.

allergen_notestring, nullableoptional

The seller's "Anything else" about allergens (one outside the fourteen, say), in her own words, at most 200 characters. Null when she wrote nothing. Show it as written.

service_period_idsarray of stringrequired

This dish's OWN serving times (service period ids). Empty when it follows its section's serving times, see `service_periods_inherited`.

service_periods_inheritedbooleanrequired

True when the dish has no serving times of its own and follows its section (or, when the section has none either, is served all day).

ingredientsstring, nullablerequired
is_foodbooleanoptional

False for an item that is not food (clothes, flowers): it needs no ingredients and no allergen answer, and `ingredients` holds what it is made of. True or absent: food.

nutrition_summarystring, nullablerequired
is_popularbooleanrequired
is_availablebooleanrequired
checkout_enabledbooleanrequired
stock_tracking_enabledbooleanrequired
stock_quantitynumber, nullablerequired
stock_remainingnumber, nullablerequired
sold_outbooleanrequired
collect_eligiblebooleanrequired
available_nowbooleanrequired
collect_prep_minutesnumber, nullablerequired
price_pencenumberrequired
currencystringrequired
sort_ordernumberrequired
item_kindenumrequired

Whether this row is a dish or a SUPPLEMENT, an add-on line off the printed menu ("Flavoured Syrup 50p") that nobody orders on its own. A supplement must never set a section’s "from" price or be counted in its dish tally: Santi’s "Breakfast Hot Drinks from £0.50" was a syrup shot against a cheapest real drink of £3.00. A row from before the column existed reads as a dish, deliberately, defaulting the other way silently drops unclassified rows out of both.

dishsupplement
heat_levelnumber, nullablerequired

The printed menu’s chilli marker, 1-3. Null when the dish has no heat.

photo_creditobjectrequired

What this photograph owes the person who took it, or null when it owes nothing. Non-null for every attribution-required licence (CC BY, CC BY-SA), null for the merchant’s own picture and for CC0 / public domain. Rendering it is a CONDITION OF USE of the licence, not a courtesy, see src/lib/menu/photoCredit.ts.

6 child fields
authorstring, nullablerequired

The photographer, or null when the source names nobody.

licencestringrequired

The licence short name, e.g. "CC BY-SA 3.0".

licence_urlstring, nullablerequired

The licence deed, or null when it cannot be linked.

source_urlstring, nullablerequired

The file page the picture came from, so the credit is checkable.

librarystring, nullablerequired

e.g. "Wikimedia Commons".

filestring, nullablerequired

The work’s title, e.g. "File:Red Wine Glass.jpg".

updated_atstring, nullableoptional

When the row last changed, exactly as the database wrote it (microseconds kept). Send it back UNTOUCHED as `expected_updated_at` on an update to have it refused (409 ITEM_CHANGED) if the item changed since it was read. Round it through a date type and every save is refused.

allergens_sourcestring, nullableoptional

Who gave the allergen answer: "merchant" or "merchant_confirmed" when the seller said it herself; anything else (a setup script, an import, null for not recorded) is not her word, and an editor shows the question as unanswered.

fulfilment_kindenumoptional

`digital` on a listed download. Absent on a menu item, which is physical.

physicaldigital
digital_ready_asset_countintegeroptional

A listed download’s ready files. A download with none cannot be sold. Absent on a menu item.

is_completebooleanoptional

A listed download’s own answer: it has a name, a cover photo and one ready file. Absent on a menu item.

choicesobjectoptional

Her sizes and colours (`ItemChoices`), on an item that has them. Absent on an item that has none.

4 child fields
size_namestringoptional
colour_namestringoptional
sizesarray of objectoptional
1 child field
labelstringrequired
coloursarray of objectoptional
2 child fields
labelstringrequired
photo_urlstringoptional
curl -G "https://www.membber.com/api/v1/menu-items" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  --data-urlencode "store_id=6659c139-0000-4000-8000-d0c500000066"
Response, 200
{
  "items": [
    {
      "id": "00000d1b-0000-4000-8000-d0c500000000",
      "name": "Example name",
      "description": "Added at the front desk",
      "category": "<category>",
      "emoji": "<emoji>",
      "image_url": "https://example.com/image.jpg",
      "product_media": [
        {
          "id": "00000d1b-0000-4000-8000-d0c500000000",
          "type": "image",
          "url": "https://example.com/image.jpg",
          "thumbnail_url": "https://example.com/image.jpg",
          "sort_order": 1
        }
      ],
      "media_auto_advance": true,
      "contains_allergens": [
        "<contains_allergen>"
      ],
      "no_allergens": true,
      "dietary_tags": [
        "<dietary_tag>"
      ],
      "diets": [
        "<diet>"
      ],
      "satisfies_diets": [
        "<satisfies_diet>"
      ],
      "allergen_groups": [
        "<allergen_group>"
      ],
      "may_contain_allergens": [
        "<may_contain_allergen>"
      ],
      "allergen_note": "Added at the front desk",
      "service_period_ids": [
        "af7d3b4f-0000-4000-8000-d0c5000000af"
      ],
      "service_periods_inherited": true,
      "ingredients": "<ingredients>",
      "is_food": true,
      "nutrition_summary": "<nutrition_summary>",
      "is_popular": true,
      "is_available": true,
      "checkout_enabled": true,
      "stock_tracking_enabled": true,
      "stock_quantity": 1,
      "stock_remaining": 1,
      "sold_out": true,
      "collect_eligible": true,
      "available_now": true,
      "collect_prep_minutes": 1,
      "price_pence": 1500,
      "currency": "GBP",
      "sort_order": 1,
      "item_kind": "dish",
      "heat_level": 1,
      "photo_credit": {
        "author": "<author>",
        "licence": "<licence>",
        "licence_url": "https://example.com/image.jpg",
        "source_url": "https://example.com/image.jpg",
        "library": "<library>",
        "file": "<file>"
      },
      "updated_at": "<updated_at>",
      "allergens_source": "<allergens_source>",
      "fulfilment_kind": "physical",
      "digital_ready_asset_count": 1,
      "is_complete": true,
      "choices": {
        "size_name": "<size_name>",
        "colour_name": "<colour_name>",
        "sizes": [
          {
            "label": "<label>"
          }
        ],
        "colours": [
          {
            "label": "<label>",
            "photo_url": "https://example.com/image.jpg"
          }
        ]
      }
    }
  ]
}

Create a store menu item.

Merchant creates a menu item. The service validates name + price and normalises all fields. Broadcasts a menu_item_created realtime event so customer apps re-pull the menu. No HTTP idempotency key (identical to the legacy route), a create is not deduplicated.

Request body

store_idstring · uuidrequired

Store the item belongs to (also authorises the merchant).

namestringrequired

Item name (required; max 120 chars).

price_penceintegerrequired

Price in pence (required; 0..10,000,000).

descriptionstring, nullableoptional
categorystring, nullableoptional

Her menu section, as she names it (max 120 chars; sections match without regard to case). Omitted on a create, null or blank: no section, stored as "Menu" (her shop page and the Drops web read it as no section).

emojistring, nullableoptional
image_urlstring, nullableoptional
product_mediaarray of anyoptional

Her photos and videos, in her order: `{ id, type: "image" | "video", url, thumbnail_url, sort_order }`. The item keeps at most 10 together; a photo past that is not kept. Up to 3 videos an item: a write that brings a video the item does not already store is taken only when the item then holds 3 or fewer; otherwise the whole write is refused (409 ITEM_VIDEO_LIMIT, `details.videos` is how many it holds) and nothing is written. A list that sends every photo before every video keeps each video where the item already holds it among her photos, unless `media_order` is "as_sent". `image_url` is the cover, and the first photo.

media_orderenumoptional

Sent by a client that orders photos and videos freely (the Drops web since 21 Oct 2026): `product_media` is stored in exactly the order sent. Omitted (every client before it), a list of photos then videos keeps each video where the item already holds it.

as_sent
media_auto_advancebooleanoptional

False holds her item's photos and videos still on her drop page; true moves them on by themselves. Omit to keep what she chose.

currencystringoptional
contains_allergensarray of stringoptional
no_allergensbooleanoptional

FA078. Tick "no allergens in this item". OMIT to leave any existing declaration untouched; false retracts it. Ignored when contains_allergens is non-empty, listing an allergen always withdraws a standing "none" declaration.

may_contain_allergensarray of stringoptional

What the item MAY contain (traces): any of the UK fourteen ("Celery", "Gluten", "Crustaceans", "Eggs", "Fish", "Lupin", "Dairy", "Molluscs", "Mustard", "Tree Nuts", "Peanuts", "Sesame", "Soya", "Sulphites"; other spellings of one of them, such as "milk" or "tree_nuts", are stored as that one). A word that names none of them is a 422 ALLERGEN_UNKNOWN naming it. OMIT to leave what is stored untouched (a whole-row save that never sends it keeps it); an empty list clears it. Independent of `contains_allergens` and `no_allergens`: it neither answers nor changes them. A save that sends it and would leave "Gluten-free" beside Gluten here, or "Dairy-free" beside Dairy, is a 422 DIET_CONTRADICTS_MAY_CONTAIN naming both (keep one: send `dietary_tags` without the diet, or this list without the allergen). A save that does not send this field is never refused for it, whatever is stored. Nor is a save that leaves both the item's diets and this list exactly as they are stored (a whole row sent back to change a photo or a price). Vegan beside Dairy here is allowed. Taking the diet off to keep this list (the only change to `dietary_tags` being that diet removed) never makes the item's other diets the seller's own word if they were not already.

allergen_notestring, nullableoptional

The seller's "Anything else" about allergens, in her own words (kept on one line, trimmed). At most 200 characters: longer is a 422 ALLERGEN_NOTE_TOO_LONG, never cut. OMIT to leave it untouched; null or blank clears it.

dietary_tagsarray of stringoptional

The dish's diets, from a closed set: "Vegetarian", "Vegan", "Gluten-free", "Dairy-free" (any spelling that differs only in case, spaces or hyphens is accepted and stored canonically). An unknown tag is a 422 DIET_UNKNOWN naming it. A diet the dish's own `contains_allergens` makes false, vegan with Dairy/Eggs/Fish/Crustaceans/Molluscs, vegetarian with Fish/Crustaceans/Molluscs, dairy-free with Dairy, is a 422 DIET_CONTRADICTS_ALLERGEN naming the allergen. Gluten-free with gluten listed is allowed (Reg. 828/2014 permits gluten-free wheat starch).

ingredientsstring, nullableoptional
is_foodbooleanoptional

Whether the item is food. False: it needs a name, a price and a photo, and no ingredients or allergen answer. Omitted on a create: taken from the shop's kind (food unless the shop sells streetwear, flowers or something else). Omitted on an update: unchanged.

nutrition_summarystring, nullableoptional
is_popularbooleanoptional
is_availablebooleanoptional
checkout_enabledbooleanoptional
collect_eligiblebooleanoptional
available_nowbooleanoptional
collect_prep_minutesinteger, nullableoptional
sort_orderintegeroptional
stock_tracking_enabledbooleanoptional
stock_quantityinteger, nullableoptional
stock_remaininginteger, nullableoptional
fulfilment_kindenumoptional

What the item is. OMIT for an ordinary item: it is `physical`. `digital` makes a DOWNLOAD (PRODUCT-SPEC 0.11): it needs no ingredients or allergens, costs 0 only when the write says `free_for_email: true`, is never sold through the pay page, a drop or Click & Collect, and holds its files through /api/v1/drops/digital/assets. Chosen when the item is created and never changed: an update that names a different kind is a 409 FULFILMENT_KIND_FIXED. `digital` is a 404 until downloads are switched on.

physicaldigital
choicesobjectoptional

Her sizes and colours (sizes and colours, 15 Oct 2026). OMIT to keep what is stored (every write that never sends it, an installed app included, keeps them); null removes them. Refused on a download, and when a colour's photo_url is not one of this item's own photos (its cover or a product_media photo, as this write leaves them). A write that removes a photo also clears any colour that showed it.

4 child fields
size_namestringoptional

Her own word for sizes ("Flavour"). Absent: the word for size.

colour_namestringoptional

Her own word for colours ("Grind"). Absent: the word for colour.

sizesarray of objectoptional

Her sizes, in her order: `{ label }`. No price: a pair’s price is its drop line’s.

1 child field
labelstringrequired
coloursarray of objectoptional

Her colours, in her order: `{ label, photo_url? }`.

2 child fields
labelstringrequired
photo_urlstringoptional

The address of one of this item’s own photos, shown when the colour is chosen.

free_for_emailbooleanoptional

A DOWNLOAD only: true says this download is FREE in exchange for an email, and must be sent with `price_pence: 0` on every write that STORES a price for a free download: a create, a whole row, or a partial write that names the price (nothing is stored beyond the price). A partial write that leaves the price out needs neither. A write that would store 0 for a download without it is a 422 DOWNLOAD_PRICE_NEEDED, so no download is ever free by accident; true beside a price above 0 is a 422 FREE_DOWNLOAD_HAS_A_PRICE. Not read for an ordinary item.

Response, 200

itemobjectrequired

A menu_items row (write result, raw DB shape).

8 child fields
idstringrequired
store_idstringrequired
namestringrequired
price_pencenumber, nullableoptional
currencystring, nullableoptional
is_availableboolean, nullableoptional
checkout_enabledboolean, nullableoptional
is_foodbooleanoptional

False for an item that is not food. True or absent: food.

curl -X POST "https://www.membber.com/api/v1/menu-items" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "store_id": "6659c139-0000-4000-8000-d0c500000066",
    "name": "Example name",
    "price_pence": 1500
  }'
Response, 200
{
  "item": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "store_id": "6659c139-0000-4000-8000-d0c500000066",
    "name": "Example name",
    "price_pence": 1500,
    "currency": "GBP",
    "is_available": true,
    "checkout_enabled": true,
    "is_food": true
  }
}

Remove (soft-delete) a store menu item.

Merchant removes a menu item. Soft delete (order-history integrity): flips visibility AND stamps deleted_at so the list can tell a REMOVED item apart from a merchant-HIDDEN one. Broadcasts a menu_item_archived realtime event. Idempotent. store_id is supplied as a query parameter. An item in a scheduled drop, or in one open for orders, is not removed (409 ITEM_IN_DROP, naming the drop in `details.drop_name`); one only in a draft, or in a drop that has closed, may go.

Parameters

itemIdstringpathrequired
store_idstring · uuidqueryrequired

Store the item belongs to (also authorises the merchant).

Response, 200

archivedbooleanrequired
curl -X DELETE "https://www.membber.com/api/v1/menu-items/b9bf0b8e-0000-4000-8000-d0c5000000b9?store_id=6659c139-0000-4000-8000-d0c500000066" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678"
Response, 200
{
  "archived": true
}

Update a store menu item.

Merchant updates a menu item. Stock edits preserve in-flight reservations (set_menu_item_stock); a plain edit cannot silently resurrect a removed item (409 ITEM_ARCHIVED unless restore is intended). Broadcasts a menu_item_updated realtime event. Idempotent (converges). `partial: true` changes only the fields sent; `expected_updated_at` refuses a stale write (409 ITEM_CHANGED, `details.item` is the item now). A change to the usual price while a drop selling the item is open for orders is refused (409 ITEM_SELLING_NOW, naming the drop in `details.drop_name`); a drop not yet open takes the new price. With `partial: true`, a new `category` (her section; null puts the item in no section) moves the item to the END of that section (its sort_order goes after every other item unless `sort_order` is sent), so her sections keep their order.

Parameters

itemIdstringpathrequired

Request body

store_idstring · uuidrequired

Store the item belongs to (also authorises the merchant).

partialbooleanoptional

true: change ONLY the fields this request sends (an explicit null clears one), and read the rest from the item as stored. `name` and `price_pence` may then be omitted. The Business app sends it from build 72 for its held photo removal (image_url and product_media) and from build 77 for the save of its item editor (only the fields she changed), both with `expected_updated_at`. Omitted or false (its other saves, and the editor in every earlier build): the whole row is written, and an omitted field is cleared or defaulted, as before. Allergens travel together: send `contains_allergens` with `no_allergens` (ticking "no allergens" on its own sends an empty list). A signed-in person sending the same allergen list over a source that is not hers confirms it as her own.

expected_updated_atstringoptional

The `updated_at` this client read, passed back untouched. If the item changed since, nothing is written and the answer is 409 ITEM_CHANGED with `details.item`, the item as it is now (list shape). Omitted: no check, as before.

namestringoptional

Item name (max 120 chars). Required unless `partial` is true.

price_penceintegeroptional

Usual price in pence (0..10,000,000). Required unless `partial` is true.

descriptionstring, nullableoptional
categorystring, nullableoptional

Her menu section, as she names it (max 120 chars; sections match without regard to case). Omitted on a create, null or blank: no section, stored as "Menu" (her shop page and the Drops web read it as no section).

emojistring, nullableoptional
image_urlstring, nullableoptional
product_mediaarray of anyoptional

Her photos and videos, in her order: `{ id, type: "image" | "video", url, thumbnail_url, sort_order }`. The item keeps at most 10 together; a photo past that is not kept. Up to 3 videos an item: a write that brings a video the item does not already store is taken only when the item then holds 3 or fewer; otherwise the whole write is refused (409 ITEM_VIDEO_LIMIT, `details.videos` is how many it holds) and nothing is written. A list that sends every photo before every video keeps each video where the item already holds it among her photos, unless `media_order` is "as_sent". `image_url` is the cover, and the first photo.

media_orderenumoptional

Sent by a client that orders photos and videos freely (the Drops web since 21 Oct 2026): `product_media` is stored in exactly the order sent. Omitted (every client before it), a list of photos then videos keeps each video where the item already holds it.

as_sent
media_auto_advancebooleanoptional

False holds her item's photos and videos still on her drop page; true moves them on by themselves. Omit to keep what she chose.

currencystringoptional
contains_allergensarray of stringoptional
no_allergensbooleanoptional

FA078. Tick "no allergens in this item". OMIT to leave any existing declaration untouched; false retracts it. Ignored when contains_allergens is non-empty, listing an allergen always withdraws a standing "none" declaration.

may_contain_allergensarray of stringoptional

What the item MAY contain (traces): any of the UK fourteen ("Celery", "Gluten", "Crustaceans", "Eggs", "Fish", "Lupin", "Dairy", "Molluscs", "Mustard", "Tree Nuts", "Peanuts", "Sesame", "Soya", "Sulphites"; other spellings of one of them, such as "milk" or "tree_nuts", are stored as that one). A word that names none of them is a 422 ALLERGEN_UNKNOWN naming it. OMIT to leave what is stored untouched (a whole-row save that never sends it keeps it); an empty list clears it. Independent of `contains_allergens` and `no_allergens`: it neither answers nor changes them. A save that sends it and would leave "Gluten-free" beside Gluten here, or "Dairy-free" beside Dairy, is a 422 DIET_CONTRADICTS_MAY_CONTAIN naming both (keep one: send `dietary_tags` without the diet, or this list without the allergen). A save that does not send this field is never refused for it, whatever is stored. Nor is a save that leaves both the item's diets and this list exactly as they are stored (a whole row sent back to change a photo or a price). Vegan beside Dairy here is allowed. Taking the diet off to keep this list (the only change to `dietary_tags` being that diet removed) never makes the item's other diets the seller's own word if they were not already.

allergen_notestring, nullableoptional

The seller's "Anything else" about allergens, in her own words (kept on one line, trimmed). At most 200 characters: longer is a 422 ALLERGEN_NOTE_TOO_LONG, never cut. OMIT to leave it untouched; null or blank clears it.

dietary_tagsarray of stringoptional

The dish's diets, from a closed set: "Vegetarian", "Vegan", "Gluten-free", "Dairy-free" (any spelling that differs only in case, spaces or hyphens is accepted and stored canonically). An unknown tag is a 422 DIET_UNKNOWN naming it. A diet the dish's own `contains_allergens` makes false, vegan with Dairy/Eggs/Fish/Crustaceans/Molluscs, vegetarian with Fish/Crustaceans/Molluscs, dairy-free with Dairy, is a 422 DIET_CONTRADICTS_ALLERGEN naming the allergen. Gluten-free with gluten listed is allowed (Reg. 828/2014 permits gluten-free wheat starch).

ingredientsstring, nullableoptional
is_foodbooleanoptional

Whether the item is food. False: it needs a name, a price and a photo, and no ingredients or allergen answer. Omitted on a create: taken from the shop's kind (food unless the shop sells streetwear, flowers or something else). Omitted on an update: unchanged.

nutrition_summarystring, nullableoptional
is_popularbooleanoptional
is_availablebooleanoptional
checkout_enabledbooleanoptional
collect_eligiblebooleanoptional
available_nowbooleanoptional
collect_prep_minutesinteger, nullableoptional
sort_orderintegeroptional
stock_tracking_enabledbooleanoptional
stock_quantityinteger, nullableoptional
stock_remaininginteger, nullableoptional
fulfilment_kindenumoptional

What the item is. OMIT for an ordinary item: it is `physical`. `digital` makes a DOWNLOAD (PRODUCT-SPEC 0.11): it needs no ingredients or allergens, costs 0 only when the write says `free_for_email: true`, is never sold through the pay page, a drop or Click & Collect, and holds its files through /api/v1/drops/digital/assets. Chosen when the item is created and never changed: an update that names a different kind is a 409 FULFILMENT_KIND_FIXED. `digital` is a 404 until downloads are switched on.

physicaldigital
choicesobjectoptional

Her sizes and colours (sizes and colours, 15 Oct 2026). OMIT to keep what is stored (every write that never sends it, an installed app included, keeps them); null removes them. Refused on a download, and when a colour's photo_url is not one of this item's own photos (its cover or a product_media photo, as this write leaves them). A write that removes a photo also clears any colour that showed it.

4 child fields
size_namestringoptional

Her own word for sizes ("Flavour"). Absent: the word for size.

colour_namestringoptional

Her own word for colours ("Grind"). Absent: the word for colour.

sizesarray of objectoptional

Her sizes, in her order: `{ label }`. No price: a pair’s price is its drop line’s.

1 child field
labelstringrequired
coloursarray of objectoptional

Her colours, in her order: `{ label, photo_url? }`.

2 child fields
labelstringrequired
photo_urlstringoptional

The address of one of this item’s own photos, shown when the colour is chosen.

free_for_emailbooleanoptional

A DOWNLOAD only: true says this download is FREE in exchange for an email, and must be sent with `price_pence: 0` on every write that STORES a price for a free download: a create, a whole row, or a partial write that names the price (nothing is stored beyond the price). A partial write that leaves the price out needs neither. A write that would store 0 for a download without it is a 422 DOWNLOAD_PRICE_NEEDED, so no download is ever free by accident; true beside a price above 0 is a 422 FREE_DOWNLOAD_HAS_A_PRICE. Not read for an ordinary item.

Response, 200

itemobjectrequired

A menu_items row (write result, raw DB shape).

8 child fields
idstringrequired
store_idstringrequired
namestringrequired
price_pencenumber, nullableoptional
currencystring, nullableoptional
is_availableboolean, nullableoptional
checkout_enabledboolean, nullableoptional
is_foodbooleanoptional

False for an item that is not food. True or absent: food.

curl -X PATCH "https://www.membber.com/api/v1/menu-items/b9bf0b8e-0000-4000-8000-d0c5000000b9" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "store_id": "6659c139-0000-4000-8000-d0c500000066"
  }'
Response, 200
{
  "item": {
    "id": "00000d1b-0000-4000-8000-d0c500000000",
    "store_id": "6659c139-0000-4000-8000-d0c500000066",
    "name": "Example name",
    "price_pence": 1500,
    "currency": "GBP",
    "is_available": true,
    "checkout_enabled": true,
    "is_food": true
  }
}

Set which services one dish is served at.

Replaces one dish's own serving times in a single transaction (set_menu_item_service_periods RPC). A dish with its own serving times ignores its section's; null resets it to follow the section. Every id must be one of the store's service periods (422 SERVICE_PERIOD_NOT_FOUND otherwise); a removed dish is 404. Idempotent (the same payload converges). Writes one audit row and broadcasts a menu_item_updated realtime event so customer apps re-pull the menu.

Parameters

itemIdstringpathrequired

Request body

store_idstring · uuidrequired

Store the dish belongs to (also authorises the merchant).

service_period_idsarray of string · uuidrequired

Exactly the services this dish is served at (ids from GET /api/v1/bookings/service-periods), replacing any it has. null puts the dish back on its section's serving times. An EMPTY array is refused with 422 SERVICE_SET_EMPTY: choose at least one, or send null to use its section's times.

Response, 200

item_idstringrequired
service_period_idsarray of stringrequired

The dish's own serving times after the write, in the merchant's card order. Empty = follows its section.

follows_sectionbooleanrequired

True when the dish now follows its section's serving times.

curl -X PUT "https://www.membber.com/api/v1/menu-items/b9bf0b8e-0000-4000-8000-d0c5000000b9/service-periods" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "store_id": "6659c139-0000-4000-8000-d0c500000066",
    "service_period_ids": [
      "af7d3b4f-0000-4000-8000-d0c5000000af"
    ]
  }'
Response, 200
{
  "item_id": "7e22b9c7-0000-4000-8000-d0c50000007e",
  "service_period_ids": [
    "af7d3b4f-0000-4000-8000-d0c5000000af"
  ],
  "follows_section": true
}

Change diets or serving times on many dishes at once.

The Business app's select-mode Diet… and Serving times… actions. ONE transaction (bulk_set_menu_item_attributes RPC), all of the dishes change or none do, where the client used to PATCH them one by one and could leave half a menu changed. Idempotent: every field SETS a value rather than toggling one, so a retried request converges to the same menu. Writes one audit row and broadcasts a menu_item_updated realtime event.

Request body

store_idstring · uuidrequired

Store the dishes belong to (also authorises the merchant).

item_idsarray of string · uuidrequired

The dishes to change.

dietary_tagsarray of stringoptional

Set EXACTLY these diets on every dish (same closed set and same refusals as a single dish). Omit to leave diets untouched. A diet any selected dish's allergens contradict refuses the whole batch (422 DIET_CONTRADICTS_ALLERGEN, naming the dish).

service_period_idsarray of string · uuidoptional

Give every dish exactly these serving times of its own. Omit to leave serving times untouched.

follow_sectionbooleanoptional

true puts every dish back on its section's serving times. Cannot be combined with service_period_ids.

Response, 200

updatedintegerrequired

How many dishes the write changed.

itemsarray of objectrequired

The full menu re-read after the write, so the client lands the server's answer.

43 child fields
idstringrequired
namestringrequired
descriptionstring, nullablerequired
categorystring, nullablerequired
emojistring, nullablerequired
image_urlstring, nullablerequired
product_mediaarray of objectrequired

Her photos and videos, in her order: at most 10 together, at most 3 of them videos. Since 21 Oct 2026 a video may come first; the cover is the entry `primary-image` and `image_url`, always a photo.

5 child fields
idstringrequired
typeenumrequired
imagevideo
urlstringrequired
thumbnail_urlstring, nullablerequired
sort_ordernumberrequired
media_auto_advancebooleanoptional

False when she holds her item's photos and videos still on her drop page; true (the default) when they move on by themselves.

contains_allergensarray of stringrequired
no_allergensbooleanrequired

FA078. True ONLY when a human ticked "no allergens in this item". An empty contains_allergens with no_allergens=false means NOBODY HAS SAID, it is not a statement that the item is allergen-free, and must never be rendered as one.

dietary_tagsarray of stringrequired
dietsarray of stringrequired

The diets the merchant declared, in the canonical spelling: any of "Vegetarian", "Vegan", "Gluten-free", "Dairy-free". A legacy "Dairy Free" reads as "Dairy-free".

satisfies_dietsarray of stringrequired

`diets` plus what they imply, Vegan also satisfies Vegetarian and Dairy-free. Derived on read, never stored, so what the merchant actually said stays distinguishable. This is what a diet filter reads. Since 12 Oct 2026 it leaves out Gluten-free for an item whose `may_contain_allergens` names Gluten, and Dairy-free (the seller's own tag, or through Vegan) for one that names Dairy: a filter never shows an item as free from something the seller says it may contain. Vegan and Vegetarian are never left out, and `diets` and `dietary_tags` still say what the seller tagged.

allergen_groupsarray of stringrequired

`contains_allergens` grouped to the UK fourteen, in the law's order ("tree nuts (almond)" and "Tree Nuts (Pistachio)" are both "Tree Nuts"). Allergens MARK a dish on a menu, never hide it.

may_contain_allergensarray of stringoptional

What the item MAY contain (traces, e.g. made beside nuts): the UK fourteen the seller ticked under "May contain", in the law's order and spelling ("Tree Nuts", "Peanuts"). Empty means she has said nothing may be present, never "none". Not an answer to what it contains (`contains_allergens` / `no_allergens` are). Absent on a payload from before 8 Oct 2026.

allergen_notestring, nullableoptional

The seller's "Anything else" about allergens (one outside the fourteen, say), in her own words, at most 200 characters. Null when she wrote nothing. Show it as written.

service_period_idsarray of stringrequired

This dish's OWN serving times (service period ids). Empty when it follows its section's serving times, see `service_periods_inherited`.

service_periods_inheritedbooleanrequired

True when the dish has no serving times of its own and follows its section (or, when the section has none either, is served all day).

ingredientsstring, nullablerequired
is_foodbooleanoptional

False for an item that is not food (clothes, flowers): it needs no ingredients and no allergen answer, and `ingredients` holds what it is made of. True or absent: food.

nutrition_summarystring, nullablerequired
is_popularbooleanrequired
is_availablebooleanrequired
checkout_enabledbooleanrequired
stock_tracking_enabledbooleanrequired
stock_quantitynumber, nullablerequired
stock_remainingnumber, nullablerequired
sold_outbooleanrequired
collect_eligiblebooleanrequired
available_nowbooleanrequired
collect_prep_minutesnumber, nullablerequired
price_pencenumberrequired
currencystringrequired
sort_ordernumberrequired
item_kindenumrequired

Whether this row is a dish or a SUPPLEMENT, an add-on line off the printed menu ("Flavoured Syrup 50p") that nobody orders on its own. A supplement must never set a section’s "from" price or be counted in its dish tally: Santi’s "Breakfast Hot Drinks from £0.50" was a syrup shot against a cheapest real drink of £3.00. A row from before the column existed reads as a dish, deliberately, defaulting the other way silently drops unclassified rows out of both.

dishsupplement
heat_levelnumber, nullablerequired

The printed menu’s chilli marker, 1-3. Null when the dish has no heat.

photo_creditobjectrequired

What this photograph owes the person who took it, or null when it owes nothing. Non-null for every attribution-required licence (CC BY, CC BY-SA), null for the merchant’s own picture and for CC0 / public domain. Rendering it is a CONDITION OF USE of the licence, not a courtesy, see src/lib/menu/photoCredit.ts.

6 child fields
authorstring, nullablerequired

The photographer, or null when the source names nobody.

licencestringrequired

The licence short name, e.g. "CC BY-SA 3.0".

licence_urlstring, nullablerequired

The licence deed, or null when it cannot be linked.

source_urlstring, nullablerequired

The file page the picture came from, so the credit is checkable.

librarystring, nullablerequired

e.g. "Wikimedia Commons".

filestring, nullablerequired

The work’s title, e.g. "File:Red Wine Glass.jpg".

updated_atstring, nullableoptional

When the row last changed, exactly as the database wrote it (microseconds kept). Send it back UNTOUCHED as `expected_updated_at` on an update to have it refused (409 ITEM_CHANGED) if the item changed since it was read. Round it through a date type and every save is refused.

allergens_sourcestring, nullableoptional

Who gave the allergen answer: "merchant" or "merchant_confirmed" when the seller said it herself; anything else (a setup script, an import, null for not recorded) is not her word, and an editor shows the question as unanswered.

fulfilment_kindenumoptional

`digital` on a listed download. Absent on a menu item, which is physical.

physicaldigital
digital_ready_asset_countintegeroptional

A listed download’s ready files. A download with none cannot be sold. Absent on a menu item.

is_completebooleanoptional

A listed download’s own answer: it has a name, a cover photo and one ready file. Absent on a menu item.

choicesobjectoptional

Her sizes and colours (`ItemChoices`), on an item that has them. Absent on an item that has none.

4 child fields
size_namestringoptional
colour_namestringoptional
sizesarray of objectoptional
1 child field
labelstringrequired
coloursarray of objectoptional
2 child fields
labelstringrequired
photo_urlstringoptional
curl -X POST "https://www.membber.com/api/v1/menu-items/bulk" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "store_id": "6659c139-0000-4000-8000-d0c500000066",
    "item_ids": [
      "7e22b9c7-0000-4000-8000-d0c50000007e"
    ]
  }'
Response, 200
{
  "updated": -9007199254740991,
  "items": [
    {
      "id": "00000d1b-0000-4000-8000-d0c500000000",
      "name": "Example name",
      "description": "Added at the front desk",
      "category": "<category>",
      "emoji": "<emoji>",
      "image_url": "https://example.com/image.jpg",
      "product_media": [
        {
          "id": "00000d1b-0000-4000-8000-d0c500000000",
          "type": "image",
          "url": "https://example.com/image.jpg",
          "thumbnail_url": "https://example.com/image.jpg",
          "sort_order": 1
        }
      ],
      "media_auto_advance": true,
      "contains_allergens": [
        "<contains_allergen>"
      ],
      "no_allergens": true,
      "dietary_tags": [
        "<dietary_tag>"
      ],
      "diets": [
        "<diet>"
      ],
      "satisfies_diets": [
        "<satisfies_diet>"
      ],
      "allergen_groups": [
        "<allergen_group>"
      ],
      "may_contain_allergens": [
        "<may_contain_allergen>"
      ],
      "allergen_note": "Added at the front desk",
      "service_period_ids": [
        "af7d3b4f-0000-4000-8000-d0c5000000af"
      ],
      "service_periods_inherited": true,
      "ingredients": "<ingredients>",
      "is_food": true,
      "nutrition_summary": "<nutrition_summary>",
      "is_popular": true,
      "is_available": true,
      "checkout_enabled": true,
      "stock_tracking_enabled": true,
      "stock_quantity": 1,
      "stock_remaining": 1,
      "sold_out": true,
      "collect_eligible": true,
      "available_now": true,
      "collect_prep_minutes": 1,
      "price_pence": 1500,
      "currency": "GBP",
      "sort_order": 1,
      "item_kind": "dish",
      "heat_level": 1,
      "photo_credit": {
        "author": "<author>",
        "licence": "<licence>",
        "licence_url": "https://example.com/image.jpg",
        "source_url": "https://example.com/image.jpg",
        "library": "<library>",
        "file": "<file>"
      },
      "updated_at": "<updated_at>",
      "allergens_source": "<allergens_source>",
      "fulfilment_kind": "physical",
      "digital_ready_asset_count": 1,
      "is_complete": true,
      "choices": {
        "size_name": "<size_name>",
        "colour_name": "<colour_name>",
        "sizes": [
          {
            "label": "<label>"
          }
        ],
        "colours": [
          {
            "label": "<label>",
            "photo_url": "https://example.com/image.jpg"
          }
        ]
      }
    }
  ]
}

Reorder the menu’s sections.

Merchant sets the order their menu sections appear in. The customer menu derives section order from each section’s first appearance in the sort_order-sorted item list, so this renumbers every item’s sort_order in ONE atomic statement (menu_reorder_sections RPC), item order inside a section is preserved byte for byte; only whole sections move. Idempotent (same payload converges to the same numbering). Broadcasts a menu_item_updated realtime event so customer apps re-pull the menu.

Request body

store_idstring · uuidrequired

Store whose menu sections to reorder (also authorises the merchant).

sectionsarray of stringrequired

Every menu section (item category name) in the merchant’s chosen order, first to last. Matching is case- and whitespace-insensitive. Sections not named here (including uncategorised items) keep their current relative order after the named ones, on the menu, never lost.

Response, 200

itemsarray of objectrequired
43 child fields
idstringrequired
namestringrequired
descriptionstring, nullablerequired
categorystring, nullablerequired
emojistring, nullablerequired
image_urlstring, nullablerequired
product_mediaarray of objectrequired

Her photos and videos, in her order: at most 10 together, at most 3 of them videos. Since 21 Oct 2026 a video may come first; the cover is the entry `primary-image` and `image_url`, always a photo.

5 child fields
idstringrequired
typeenumrequired
imagevideo
urlstringrequired
thumbnail_urlstring, nullablerequired
sort_ordernumberrequired
media_auto_advancebooleanoptional

False when she holds her item's photos and videos still on her drop page; true (the default) when they move on by themselves.

contains_allergensarray of stringrequired
no_allergensbooleanrequired

FA078. True ONLY when a human ticked "no allergens in this item". An empty contains_allergens with no_allergens=false means NOBODY HAS SAID, it is not a statement that the item is allergen-free, and must never be rendered as one.

dietary_tagsarray of stringrequired
dietsarray of stringrequired

The diets the merchant declared, in the canonical spelling: any of "Vegetarian", "Vegan", "Gluten-free", "Dairy-free". A legacy "Dairy Free" reads as "Dairy-free".

satisfies_dietsarray of stringrequired

`diets` plus what they imply, Vegan also satisfies Vegetarian and Dairy-free. Derived on read, never stored, so what the merchant actually said stays distinguishable. This is what a diet filter reads. Since 12 Oct 2026 it leaves out Gluten-free for an item whose `may_contain_allergens` names Gluten, and Dairy-free (the seller's own tag, or through Vegan) for one that names Dairy: a filter never shows an item as free from something the seller says it may contain. Vegan and Vegetarian are never left out, and `diets` and `dietary_tags` still say what the seller tagged.

allergen_groupsarray of stringrequired

`contains_allergens` grouped to the UK fourteen, in the law's order ("tree nuts (almond)" and "Tree Nuts (Pistachio)" are both "Tree Nuts"). Allergens MARK a dish on a menu, never hide it.

may_contain_allergensarray of stringoptional

What the item MAY contain (traces, e.g. made beside nuts): the UK fourteen the seller ticked under "May contain", in the law's order and spelling ("Tree Nuts", "Peanuts"). Empty means she has said nothing may be present, never "none". Not an answer to what it contains (`contains_allergens` / `no_allergens` are). Absent on a payload from before 8 Oct 2026.

allergen_notestring, nullableoptional

The seller's "Anything else" about allergens (one outside the fourteen, say), in her own words, at most 200 characters. Null when she wrote nothing. Show it as written.

service_period_idsarray of stringrequired

This dish's OWN serving times (service period ids). Empty when it follows its section's serving times, see `service_periods_inherited`.

service_periods_inheritedbooleanrequired

True when the dish has no serving times of its own and follows its section (or, when the section has none either, is served all day).

ingredientsstring, nullablerequired
is_foodbooleanoptional

False for an item that is not food (clothes, flowers): it needs no ingredients and no allergen answer, and `ingredients` holds what it is made of. True or absent: food.

nutrition_summarystring, nullablerequired
is_popularbooleanrequired
is_availablebooleanrequired
checkout_enabledbooleanrequired
stock_tracking_enabledbooleanrequired
stock_quantitynumber, nullablerequired
stock_remainingnumber, nullablerequired
sold_outbooleanrequired
collect_eligiblebooleanrequired
available_nowbooleanrequired
collect_prep_minutesnumber, nullablerequired
price_pencenumberrequired
currencystringrequired
sort_ordernumberrequired
item_kindenumrequired

Whether this row is a dish or a SUPPLEMENT, an add-on line off the printed menu ("Flavoured Syrup 50p") that nobody orders on its own. A supplement must never set a section’s "from" price or be counted in its dish tally: Santi’s "Breakfast Hot Drinks from £0.50" was a syrup shot against a cheapest real drink of £3.00. A row from before the column existed reads as a dish, deliberately, defaulting the other way silently drops unclassified rows out of both.

dishsupplement
heat_levelnumber, nullablerequired

The printed menu’s chilli marker, 1-3. Null when the dish has no heat.

photo_creditobjectrequired

What this photograph owes the person who took it, or null when it owes nothing. Non-null for every attribution-required licence (CC BY, CC BY-SA), null for the merchant’s own picture and for CC0 / public domain. Rendering it is a CONDITION OF USE of the licence, not a courtesy, see src/lib/menu/photoCredit.ts.

6 child fields
authorstring, nullablerequired

The photographer, or null when the source names nobody.

licencestringrequired

The licence short name, e.g. "CC BY-SA 3.0".

licence_urlstring, nullablerequired

The licence deed, or null when it cannot be linked.

source_urlstring, nullablerequired

The file page the picture came from, so the credit is checkable.

librarystring, nullablerequired

e.g. "Wikimedia Commons".

filestring, nullablerequired

The work’s title, e.g. "File:Red Wine Glass.jpg".

updated_atstring, nullableoptional

When the row last changed, exactly as the database wrote it (microseconds kept). Send it back UNTOUCHED as `expected_updated_at` on an update to have it refused (409 ITEM_CHANGED) if the item changed since it was read. Round it through a date type and every save is refused.

allergens_sourcestring, nullableoptional

Who gave the allergen answer: "merchant" or "merchant_confirmed" when the seller said it herself; anything else (a setup script, an import, null for not recorded) is not her word, and an editor shows the question as unanswered.

fulfilment_kindenumoptional

`digital` on a listed download. Absent on a menu item, which is physical.

physicaldigital
digital_ready_asset_countintegeroptional

A listed download’s ready files. A download with none cannot be sold. Absent on a menu item.

is_completebooleanoptional

A listed download’s own answer: it has a name, a cover photo and one ready file. Absent on a menu item.

choicesobjectoptional

Her sizes and colours (`ItemChoices`), on an item that has them. Absent on an item that has none.

4 child fields
size_namestringoptional
colour_namestringoptional
sizesarray of objectoptional
1 child field
labelstringrequired
coloursarray of objectoptional
2 child fields
labelstringrequired
photo_urlstringoptional
curl -X PUT "https://www.membber.com/api/v1/menu-items/section-order" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "store_id": "6659c139-0000-4000-8000-d0c500000066",
    "sections": [
      "<section>"
    ]
  }'
Response, 200
{
  "items": [
    {
      "id": "00000d1b-0000-4000-8000-d0c500000000",
      "name": "Example name",
      "description": "Added at the front desk",
      "category": "<category>",
      "emoji": "<emoji>",
      "image_url": "https://example.com/image.jpg",
      "product_media": [
        {
          "id": "00000d1b-0000-4000-8000-d0c500000000",
          "type": "image",
          "url": "https://example.com/image.jpg",
          "thumbnail_url": "https://example.com/image.jpg",
          "sort_order": 1
        }
      ],
      "media_auto_advance": true,
      "contains_allergens": [
        "<contains_allergen>"
      ],
      "no_allergens": true,
      "dietary_tags": [
        "<dietary_tag>"
      ],
      "diets": [
        "<diet>"
      ],
      "satisfies_diets": [
        "<satisfies_diet>"
      ],
      "allergen_groups": [
        "<allergen_group>"
      ],
      "may_contain_allergens": [
        "<may_contain_allergen>"
      ],
      "allergen_note": "Added at the front desk",
      "service_period_ids": [
        "af7d3b4f-0000-4000-8000-d0c5000000af"
      ],
      "service_periods_inherited": true,
      "ingredients": "<ingredients>",
      "is_food": true,
      "nutrition_summary": "<nutrition_summary>",
      "is_popular": true,
      "is_available": true,
      "checkout_enabled": true,
      "stock_tracking_enabled": true,
      "stock_quantity": 1,
      "stock_remaining": 1,
      "sold_out": true,
      "collect_eligible": true,
      "available_now": true,
      "collect_prep_minutes": 1,
      "price_pence": 1500,
      "currency": "GBP",
      "sort_order": 1,
      "item_kind": "dish",
      "heat_level": 1,
      "photo_credit": {
        "author": "<author>",
        "licence": "<licence>",
        "licence_url": "https://example.com/image.jpg",
        "source_url": "https://example.com/image.jpg",
        "library": "<library>",
        "file": "<file>"
      },
      "updated_at": "<updated_at>",
      "allergens_source": "<allergens_source>",
      "fulfilment_kind": "physical",
      "digital_ready_asset_count": 1,
      "is_complete": true,
      "choices": {
        "size_name": "<size_name>",
        "colour_name": "<colour_name>",
        "sizes": [
          {
            "label": "<label>"
          }
        ],
        "colours": [
          {
            "label": "<label>",
            "photo_url": "https://example.com/image.jpg"
          }
        ]
      }
    }
  ]
}

Remove a menu section (its items stay).

Merchant removes one of her sections, a ready-made one included. No item is deleted: every item in it stays on her menu in no section (category "Menu"), in its order, at the end of the menu. The section’s serving-time links go with it. One transaction (menu_remove_section RPC) with its audit row. Idempotent: a resend answers `changed: 0`. store_id and section are query parameters. Broadcasts a menu_item_updated realtime event.

Parameters

store_idstring · uuidqueryrequired

Store whose menu it is (also authorises the merchant).

sectionstringqueryrequired

The section to remove (matched without regard to case or outer spaces).

Response, 200

itemsarray of objectrequired
43 child fields
idstringrequired
namestringrequired
descriptionstring, nullablerequired
categorystring, nullablerequired
emojistring, nullablerequired
image_urlstring, nullablerequired
product_mediaarray of objectrequired

Her photos and videos, in her order: at most 10 together, at most 3 of them videos. Since 21 Oct 2026 a video may come first; the cover is the entry `primary-image` and `image_url`, always a photo.

5 child fields
idstringrequired
typeenumrequired
imagevideo
urlstringrequired
thumbnail_urlstring, nullablerequired
sort_ordernumberrequired
media_auto_advancebooleanoptional

False when she holds her item's photos and videos still on her drop page; true (the default) when they move on by themselves.

contains_allergensarray of stringrequired
no_allergensbooleanrequired

FA078. True ONLY when a human ticked "no allergens in this item". An empty contains_allergens with no_allergens=false means NOBODY HAS SAID, it is not a statement that the item is allergen-free, and must never be rendered as one.

dietary_tagsarray of stringrequired
dietsarray of stringrequired

The diets the merchant declared, in the canonical spelling: any of "Vegetarian", "Vegan", "Gluten-free", "Dairy-free". A legacy "Dairy Free" reads as "Dairy-free".

satisfies_dietsarray of stringrequired

`diets` plus what they imply, Vegan also satisfies Vegetarian and Dairy-free. Derived on read, never stored, so what the merchant actually said stays distinguishable. This is what a diet filter reads. Since 12 Oct 2026 it leaves out Gluten-free for an item whose `may_contain_allergens` names Gluten, and Dairy-free (the seller's own tag, or through Vegan) for one that names Dairy: a filter never shows an item as free from something the seller says it may contain. Vegan and Vegetarian are never left out, and `diets` and `dietary_tags` still say what the seller tagged.

allergen_groupsarray of stringrequired

`contains_allergens` grouped to the UK fourteen, in the law's order ("tree nuts (almond)" and "Tree Nuts (Pistachio)" are both "Tree Nuts"). Allergens MARK a dish on a menu, never hide it.

may_contain_allergensarray of stringoptional

What the item MAY contain (traces, e.g. made beside nuts): the UK fourteen the seller ticked under "May contain", in the law's order and spelling ("Tree Nuts", "Peanuts"). Empty means she has said nothing may be present, never "none". Not an answer to what it contains (`contains_allergens` / `no_allergens` are). Absent on a payload from before 8 Oct 2026.

allergen_notestring, nullableoptional

The seller's "Anything else" about allergens (one outside the fourteen, say), in her own words, at most 200 characters. Null when she wrote nothing. Show it as written.

service_period_idsarray of stringrequired

This dish's OWN serving times (service period ids). Empty when it follows its section's serving times, see `service_periods_inherited`.

service_periods_inheritedbooleanrequired

True when the dish has no serving times of its own and follows its section (or, when the section has none either, is served all day).

ingredientsstring, nullablerequired
is_foodbooleanoptional

False for an item that is not food (clothes, flowers): it needs no ingredients and no allergen answer, and `ingredients` holds what it is made of. True or absent: food.

nutrition_summarystring, nullablerequired
is_popularbooleanrequired
is_availablebooleanrequired
checkout_enabledbooleanrequired
stock_tracking_enabledbooleanrequired
stock_quantitynumber, nullablerequired
stock_remainingnumber, nullablerequired
sold_outbooleanrequired
collect_eligiblebooleanrequired
available_nowbooleanrequired
collect_prep_minutesnumber, nullablerequired
price_pencenumberrequired
currencystringrequired
sort_ordernumberrequired
item_kindenumrequired

Whether this row is a dish or a SUPPLEMENT, an add-on line off the printed menu ("Flavoured Syrup 50p") that nobody orders on its own. A supplement must never set a section’s "from" price or be counted in its dish tally: Santi’s "Breakfast Hot Drinks from £0.50" was a syrup shot against a cheapest real drink of £3.00. A row from before the column existed reads as a dish, deliberately, defaulting the other way silently drops unclassified rows out of both.

dishsupplement
heat_levelnumber, nullablerequired

The printed menu’s chilli marker, 1-3. Null when the dish has no heat.

photo_creditobjectrequired

What this photograph owes the person who took it, or null when it owes nothing. Non-null for every attribution-required licence (CC BY, CC BY-SA), null for the merchant’s own picture and for CC0 / public domain. Rendering it is a CONDITION OF USE of the licence, not a courtesy, see src/lib/menu/photoCredit.ts.

6 child fields
authorstring, nullablerequired

The photographer, or null when the source names nobody.

licencestringrequired

The licence short name, e.g. "CC BY-SA 3.0".

licence_urlstring, nullablerequired

The licence deed, or null when it cannot be linked.

source_urlstring, nullablerequired

The file page the picture came from, so the credit is checkable.

librarystring, nullablerequired

e.g. "Wikimedia Commons".

filestring, nullablerequired

The work’s title, e.g. "File:Red Wine Glass.jpg".

updated_atstring, nullableoptional

When the row last changed, exactly as the database wrote it (microseconds kept). Send it back UNTOUCHED as `expected_updated_at` on an update to have it refused (409 ITEM_CHANGED) if the item changed since it was read. Round it through a date type and every save is refused.

allergens_sourcestring, nullableoptional

Who gave the allergen answer: "merchant" or "merchant_confirmed" when the seller said it herself; anything else (a setup script, an import, null for not recorded) is not her word, and an editor shows the question as unanswered.

fulfilment_kindenumoptional

`digital` on a listed download. Absent on a menu item, which is physical.

physicaldigital
digital_ready_asset_countintegeroptional

A listed download’s ready files. A download with none cannot be sold. Absent on a menu item.

is_completebooleanoptional

A listed download’s own answer: it has a name, a cover photo and one ready file. Absent on a menu item.

choicesobjectoptional

Her sizes and colours (`ItemChoices`), on an item that has them. Absent on an item that has none.

4 child fields
size_namestringoptional
colour_namestringoptional
sizesarray of objectoptional
1 child field
labelstringrequired
coloursarray of objectoptional
2 child fields
labelstringrequired
photo_urlstringoptional
changedintegerrequired

How many items the write moved or renamed (0 when it had already landed).

curl -X DELETE "https://www.membber.com/api/v1/menu-items/sections?store_id=6659c139-0000-4000-8000-d0c500000066&section=%3Csection%3E" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678"
Response, 200
{
  "items": [
    {
      "id": "00000d1b-0000-4000-8000-d0c500000000",
      "name": "Example name",
      "description": "Added at the front desk",
      "category": "<category>",
      "emoji": "<emoji>",
      "image_url": "https://example.com/image.jpg",
      "product_media": [
        {
          "id": "00000d1b-0000-4000-8000-d0c500000000",
          "type": "image",
          "url": "https://example.com/image.jpg",
          "thumbnail_url": "https://example.com/image.jpg",
          "sort_order": 1
        }
      ],
      "media_auto_advance": true,
      "contains_allergens": [
        "<contains_allergen>"
      ],
      "no_allergens": true,
      "dietary_tags": [
        "<dietary_tag>"
      ],
      "diets": [
        "<diet>"
      ],
      "satisfies_diets": [
        "<satisfies_diet>"
      ],
      "allergen_groups": [
        "<allergen_group>"
      ],
      "may_contain_allergens": [
        "<may_contain_allergen>"
      ],
      "allergen_note": "Added at the front desk",
      "service_period_ids": [
        "af7d3b4f-0000-4000-8000-d0c5000000af"
      ],
      "service_periods_inherited": true,
      "ingredients": "<ingredients>",
      "is_food": true,
      "nutrition_summary": "<nutrition_summary>",
      "is_popular": true,
      "is_available": true,
      "checkout_enabled": true,
      "stock_tracking_enabled": true,
      "stock_quantity": 1,
      "stock_remaining": 1,
      "sold_out": true,
      "collect_eligible": true,
      "available_now": true,
      "collect_prep_minutes": 1,
      "price_pence": 1500,
      "currency": "GBP",
      "sort_order": 1,
      "item_kind": "dish",
      "heat_level": 1,
      "photo_credit": {
        "author": "<author>",
        "licence": "<licence>",
        "licence_url": "https://example.com/image.jpg",
        "source_url": "https://example.com/image.jpg",
        "library": "<library>",
        "file": "<file>"
      },
      "updated_at": "<updated_at>",
      "allergens_source": "<allergens_source>",
      "fulfilment_kind": "physical",
      "digital_ready_asset_count": 1,
      "is_complete": true,
      "choices": {
        "size_name": "<size_name>",
        "colour_name": "<colour_name>",
        "sizes": [
          {
            "label": "<label>"
          }
        ],
        "colours": [
          {
            "label": "<label>",
            "photo_url": "https://example.com/image.jpg"
          }
        ]
      }
    }
  ],
  "changed": -9007199254740991
}

Rename a menu section.

Merchant renames one of her sections, a ready-made one included: every item in it takes the new name in ONE transaction (menu_rename_section RPC) with its audit row, and its serving-time links follow it. Nothing moves. A name another of her sections has is refused (409 SECTION_EXISTS: sections are never merged silently); a section she does not have is 404 SECTION_NOT_FOUND. Idempotent: a resend of a rename that landed answers `changed: 0`. Broadcasts a menu_item_updated realtime event.

Request body

store_idstring · uuidrequired

Store whose menu it is (also authorises the merchant).

fromstringrequired

The section as it is now (matched without regard to case or outer spaces).

tostringrequired

Its new name.

Response, 200

itemsarray of objectrequired
43 child fields
idstringrequired
namestringrequired
descriptionstring, nullablerequired
categorystring, nullablerequired
emojistring, nullablerequired
image_urlstring, nullablerequired
product_mediaarray of objectrequired

Her photos and videos, in her order: at most 10 together, at most 3 of them videos. Since 21 Oct 2026 a video may come first; the cover is the entry `primary-image` and `image_url`, always a photo.

5 child fields
idstringrequired
typeenumrequired
imagevideo
urlstringrequired
thumbnail_urlstring, nullablerequired
sort_ordernumberrequired
media_auto_advancebooleanoptional

False when she holds her item's photos and videos still on her drop page; true (the default) when they move on by themselves.

contains_allergensarray of stringrequired
no_allergensbooleanrequired

FA078. True ONLY when a human ticked "no allergens in this item". An empty contains_allergens with no_allergens=false means NOBODY HAS SAID, it is not a statement that the item is allergen-free, and must never be rendered as one.

dietary_tagsarray of stringrequired
dietsarray of stringrequired

The diets the merchant declared, in the canonical spelling: any of "Vegetarian", "Vegan", "Gluten-free", "Dairy-free". A legacy "Dairy Free" reads as "Dairy-free".

satisfies_dietsarray of stringrequired

`diets` plus what they imply, Vegan also satisfies Vegetarian and Dairy-free. Derived on read, never stored, so what the merchant actually said stays distinguishable. This is what a diet filter reads. Since 12 Oct 2026 it leaves out Gluten-free for an item whose `may_contain_allergens` names Gluten, and Dairy-free (the seller's own tag, or through Vegan) for one that names Dairy: a filter never shows an item as free from something the seller says it may contain. Vegan and Vegetarian are never left out, and `diets` and `dietary_tags` still say what the seller tagged.

allergen_groupsarray of stringrequired

`contains_allergens` grouped to the UK fourteen, in the law's order ("tree nuts (almond)" and "Tree Nuts (Pistachio)" are both "Tree Nuts"). Allergens MARK a dish on a menu, never hide it.

may_contain_allergensarray of stringoptional

What the item MAY contain (traces, e.g. made beside nuts): the UK fourteen the seller ticked under "May contain", in the law's order and spelling ("Tree Nuts", "Peanuts"). Empty means she has said nothing may be present, never "none". Not an answer to what it contains (`contains_allergens` / `no_allergens` are). Absent on a payload from before 8 Oct 2026.

allergen_notestring, nullableoptional

The seller's "Anything else" about allergens (one outside the fourteen, say), in her own words, at most 200 characters. Null when she wrote nothing. Show it as written.

service_period_idsarray of stringrequired

This dish's OWN serving times (service period ids). Empty when it follows its section's serving times, see `service_periods_inherited`.

service_periods_inheritedbooleanrequired

True when the dish has no serving times of its own and follows its section (or, when the section has none either, is served all day).

ingredientsstring, nullablerequired
is_foodbooleanoptional

False for an item that is not food (clothes, flowers): it needs no ingredients and no allergen answer, and `ingredients` holds what it is made of. True or absent: food.

nutrition_summarystring, nullablerequired
is_popularbooleanrequired
is_availablebooleanrequired
checkout_enabledbooleanrequired
stock_tracking_enabledbooleanrequired
stock_quantitynumber, nullablerequired
stock_remainingnumber, nullablerequired
sold_outbooleanrequired
collect_eligiblebooleanrequired
available_nowbooleanrequired
collect_prep_minutesnumber, nullablerequired
price_pencenumberrequired
currencystringrequired
sort_ordernumberrequired
item_kindenumrequired

Whether this row is a dish or a SUPPLEMENT, an add-on line off the printed menu ("Flavoured Syrup 50p") that nobody orders on its own. A supplement must never set a section’s "from" price or be counted in its dish tally: Santi’s "Breakfast Hot Drinks from £0.50" was a syrup shot against a cheapest real drink of £3.00. A row from before the column existed reads as a dish, deliberately, defaulting the other way silently drops unclassified rows out of both.

dishsupplement
heat_levelnumber, nullablerequired

The printed menu’s chilli marker, 1-3. Null when the dish has no heat.

photo_creditobjectrequired

What this photograph owes the person who took it, or null when it owes nothing. Non-null for every attribution-required licence (CC BY, CC BY-SA), null for the merchant’s own picture and for CC0 / public domain. Rendering it is a CONDITION OF USE of the licence, not a courtesy, see src/lib/menu/photoCredit.ts.

6 child fields
authorstring, nullablerequired

The photographer, or null when the source names nobody.

licencestringrequired

The licence short name, e.g. "CC BY-SA 3.0".

licence_urlstring, nullablerequired

The licence deed, or null when it cannot be linked.

source_urlstring, nullablerequired

The file page the picture came from, so the credit is checkable.

librarystring, nullablerequired

e.g. "Wikimedia Commons".

filestring, nullablerequired

The work’s title, e.g. "File:Red Wine Glass.jpg".

updated_atstring, nullableoptional

When the row last changed, exactly as the database wrote it (microseconds kept). Send it back UNTOUCHED as `expected_updated_at` on an update to have it refused (409 ITEM_CHANGED) if the item changed since it was read. Round it through a date type and every save is refused.

allergens_sourcestring, nullableoptional

Who gave the allergen answer: "merchant" or "merchant_confirmed" when the seller said it herself; anything else (a setup script, an import, null for not recorded) is not her word, and an editor shows the question as unanswered.

fulfilment_kindenumoptional

`digital` on a listed download. Absent on a menu item, which is physical.

physicaldigital
digital_ready_asset_countintegeroptional

A listed download’s ready files. A download with none cannot be sold. Absent on a menu item.

is_completebooleanoptional

A listed download’s own answer: it has a name, a cover photo and one ready file. Absent on a menu item.

choicesobjectoptional

Her sizes and colours (`ItemChoices`), on an item that has them. Absent on an item that has none.

4 child fields
size_namestringoptional
colour_namestringoptional
sizesarray of objectoptional
1 child field
labelstringrequired
coloursarray of objectoptional
2 child fields
labelstringrequired
photo_urlstringoptional
changedintegerrequired

How many items the write moved or renamed (0 when it had already landed).

curl -X PATCH "https://www.membber.com/api/v1/menu-items/sections" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "store_id": "6659c139-0000-4000-8000-d0c500000066",
    "from": "<from>",
    "to": "<to>"
  }'
Response, 200
{
  "items": [
    {
      "id": "00000d1b-0000-4000-8000-d0c500000000",
      "name": "Example name",
      "description": "Added at the front desk",
      "category": "<category>",
      "emoji": "<emoji>",
      "image_url": "https://example.com/image.jpg",
      "product_media": [
        {
          "id": "00000d1b-0000-4000-8000-d0c500000000",
          "type": "image",
          "url": "https://example.com/image.jpg",
          "thumbnail_url": "https://example.com/image.jpg",
          "sort_order": 1
        }
      ],
      "media_auto_advance": true,
      "contains_allergens": [
        "<contains_allergen>"
      ],
      "no_allergens": true,
      "dietary_tags": [
        "<dietary_tag>"
      ],
      "diets": [
        "<diet>"
      ],
      "satisfies_diets": [
        "<satisfies_diet>"
      ],
      "allergen_groups": [
        "<allergen_group>"
      ],
      "may_contain_allergens": [
        "<may_contain_allergen>"
      ],
      "allergen_note": "Added at the front desk",
      "service_period_ids": [
        "af7d3b4f-0000-4000-8000-d0c5000000af"
      ],
      "service_periods_inherited": true,
      "ingredients": "<ingredients>",
      "is_food": true,
      "nutrition_summary": "<nutrition_summary>",
      "is_popular": true,
      "is_available": true,
      "checkout_enabled": true,
      "stock_tracking_enabled": true,
      "stock_quantity": 1,
      "stock_remaining": 1,
      "sold_out": true,
      "collect_eligible": true,
      "available_now": true,
      "collect_prep_minutes": 1,
      "price_pence": 1500,
      "currency": "GBP",
      "sort_order": 1,
      "item_kind": "dish",
      "heat_level": 1,
      "photo_credit": {
        "author": "<author>",
        "licence": "<licence>",
        "licence_url": "https://example.com/image.jpg",
        "source_url": "https://example.com/image.jpg",
        "library": "<library>",
        "file": "<file>"
      },
      "updated_at": "<updated_at>",
      "allergens_source": "<allergens_source>",
      "fulfilment_kind": "physical",
      "digital_ready_asset_count": 1,
      "is_complete": true,
      "choices": {
        "size_name": "<size_name>",
        "colour_name": "<colour_name>",
        "sizes": [
          {
            "label": "<label>"
          }
        ],
        "colours": [
          {
            "label": "<label>",
            "photo_url": "https://example.com/image.jpg"
          }
        ]
      }
    }
  ],
  "changed": -9007199254740991
}

Put items into a menu section (a new one, or one she has).

Merchant puts the items she chose into one section. A section is the items’ category, so a new one exists once it holds an item. Each item goes to the END of the section in the order the items sat, so her sections keep their order and a new section closes the menu. All or none, one transaction (menu_move_items_to_section RPC) with its audit row. Items already in it stay where they are. Idempotent (a resend changes nothing). Broadcasts a menu_item_updated realtime event.

Request body

store_idstring · uuidrequired

Store whose menu it is (also authorises the merchant).

namestringrequired

The section the items go into: a new one, or one she has (matched without regard to case; it keeps her spelling).

item_idsarray of string · uuidrequired

The items that go into it. Every one must be a live item of hers, or nothing is written (404 MENU_ITEM_NOT_FOUND).

Response, 200

itemsarray of objectrequired
43 child fields
idstringrequired
namestringrequired
descriptionstring, nullablerequired
categorystring, nullablerequired
emojistring, nullablerequired
image_urlstring, nullablerequired
product_mediaarray of objectrequired

Her photos and videos, in her order: at most 10 together, at most 3 of them videos. Since 21 Oct 2026 a video may come first; the cover is the entry `primary-image` and `image_url`, always a photo.

5 child fields
idstringrequired
typeenumrequired
imagevideo
urlstringrequired
thumbnail_urlstring, nullablerequired
sort_ordernumberrequired
media_auto_advancebooleanoptional

False when she holds her item's photos and videos still on her drop page; true (the default) when they move on by themselves.

contains_allergensarray of stringrequired
no_allergensbooleanrequired

FA078. True ONLY when a human ticked "no allergens in this item". An empty contains_allergens with no_allergens=false means NOBODY HAS SAID, it is not a statement that the item is allergen-free, and must never be rendered as one.

dietary_tagsarray of stringrequired
dietsarray of stringrequired

The diets the merchant declared, in the canonical spelling: any of "Vegetarian", "Vegan", "Gluten-free", "Dairy-free". A legacy "Dairy Free" reads as "Dairy-free".

satisfies_dietsarray of stringrequired

`diets` plus what they imply, Vegan also satisfies Vegetarian and Dairy-free. Derived on read, never stored, so what the merchant actually said stays distinguishable. This is what a diet filter reads. Since 12 Oct 2026 it leaves out Gluten-free for an item whose `may_contain_allergens` names Gluten, and Dairy-free (the seller's own tag, or through Vegan) for one that names Dairy: a filter never shows an item as free from something the seller says it may contain. Vegan and Vegetarian are never left out, and `diets` and `dietary_tags` still say what the seller tagged.

allergen_groupsarray of stringrequired

`contains_allergens` grouped to the UK fourteen, in the law's order ("tree nuts (almond)" and "Tree Nuts (Pistachio)" are both "Tree Nuts"). Allergens MARK a dish on a menu, never hide it.

may_contain_allergensarray of stringoptional

What the item MAY contain (traces, e.g. made beside nuts): the UK fourteen the seller ticked under "May contain", in the law's order and spelling ("Tree Nuts", "Peanuts"). Empty means she has said nothing may be present, never "none". Not an answer to what it contains (`contains_allergens` / `no_allergens` are). Absent on a payload from before 8 Oct 2026.

allergen_notestring, nullableoptional

The seller's "Anything else" about allergens (one outside the fourteen, say), in her own words, at most 200 characters. Null when she wrote nothing. Show it as written.

service_period_idsarray of stringrequired

This dish's OWN serving times (service period ids). Empty when it follows its section's serving times, see `service_periods_inherited`.

service_periods_inheritedbooleanrequired

True when the dish has no serving times of its own and follows its section (or, when the section has none either, is served all day).

ingredientsstring, nullablerequired
is_foodbooleanoptional

False for an item that is not food (clothes, flowers): it needs no ingredients and no allergen answer, and `ingredients` holds what it is made of. True or absent: food.

nutrition_summarystring, nullablerequired
is_popularbooleanrequired
is_availablebooleanrequired
checkout_enabledbooleanrequired
stock_tracking_enabledbooleanrequired
stock_quantitynumber, nullablerequired
stock_remainingnumber, nullablerequired
sold_outbooleanrequired
collect_eligiblebooleanrequired
available_nowbooleanrequired
collect_prep_minutesnumber, nullablerequired
price_pencenumberrequired
currencystringrequired
sort_ordernumberrequired
item_kindenumrequired

Whether this row is a dish or a SUPPLEMENT, an add-on line off the printed menu ("Flavoured Syrup 50p") that nobody orders on its own. A supplement must never set a section’s "from" price or be counted in its dish tally: Santi’s "Breakfast Hot Drinks from £0.50" was a syrup shot against a cheapest real drink of £3.00. A row from before the column existed reads as a dish, deliberately, defaulting the other way silently drops unclassified rows out of both.

dishsupplement
heat_levelnumber, nullablerequired

The printed menu’s chilli marker, 1-3. Null when the dish has no heat.

photo_creditobjectrequired

What this photograph owes the person who took it, or null when it owes nothing. Non-null for every attribution-required licence (CC BY, CC BY-SA), null for the merchant’s own picture and for CC0 / public domain. Rendering it is a CONDITION OF USE of the licence, not a courtesy, see src/lib/menu/photoCredit.ts.

6 child fields
authorstring, nullablerequired

The photographer, or null when the source names nobody.

licencestringrequired

The licence short name, e.g. "CC BY-SA 3.0".

licence_urlstring, nullablerequired

The licence deed, or null when it cannot be linked.

source_urlstring, nullablerequired

The file page the picture came from, so the credit is checkable.

librarystring, nullablerequired

e.g. "Wikimedia Commons".

filestring, nullablerequired

The work’s title, e.g. "File:Red Wine Glass.jpg".

updated_atstring, nullableoptional

When the row last changed, exactly as the database wrote it (microseconds kept). Send it back UNTOUCHED as `expected_updated_at` on an update to have it refused (409 ITEM_CHANGED) if the item changed since it was read. Round it through a date type and every save is refused.

allergens_sourcestring, nullableoptional

Who gave the allergen answer: "merchant" or "merchant_confirmed" when the seller said it herself; anything else (a setup script, an import, null for not recorded) is not her word, and an editor shows the question as unanswered.

fulfilment_kindenumoptional

`digital` on a listed download. Absent on a menu item, which is physical.

physicaldigital
digital_ready_asset_countintegeroptional

A listed download’s ready files. A download with none cannot be sold. Absent on a menu item.

is_completebooleanoptional

A listed download’s own answer: it has a name, a cover photo and one ready file. Absent on a menu item.

choicesobjectoptional

Her sizes and colours (`ItemChoices`), on an item that has them. Absent on an item that has none.

4 child fields
size_namestringoptional
colour_namestringoptional
sizesarray of objectoptional
1 child field
labelstringrequired
coloursarray of objectoptional
2 child fields
labelstringrequired
photo_urlstringoptional
changedintegerrequired

How many items the write moved or renamed (0 when it had already landed).

curl -X POST "https://www.membber.com/api/v1/menu-items/sections" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "store_id": "6659c139-0000-4000-8000-d0c500000066",
    "name": "Example name",
    "item_ids": [
      "7e22b9c7-0000-4000-8000-d0c50000007e"
    ]
  }'
Response, 200
{
  "items": [
    {
      "id": "00000d1b-0000-4000-8000-d0c500000000",
      "name": "Example name",
      "description": "Added at the front desk",
      "category": "<category>",
      "emoji": "<emoji>",
      "image_url": "https://example.com/image.jpg",
      "product_media": [
        {
          "id": "00000d1b-0000-4000-8000-d0c500000000",
          "type": "image",
          "url": "https://example.com/image.jpg",
          "thumbnail_url": "https://example.com/image.jpg",
          "sort_order": 1
        }
      ],
      "media_auto_advance": true,
      "contains_allergens": [
        "<contains_allergen>"
      ],
      "no_allergens": true,
      "dietary_tags": [
        "<dietary_tag>"
      ],
      "diets": [
        "<diet>"
      ],
      "satisfies_diets": [
        "<satisfies_diet>"
      ],
      "allergen_groups": [
        "<allergen_group>"
      ],
      "may_contain_allergens": [
        "<may_contain_allergen>"
      ],
      "allergen_note": "Added at the front desk",
      "service_period_ids": [
        "af7d3b4f-0000-4000-8000-d0c5000000af"
      ],
      "service_periods_inherited": true,
      "ingredients": "<ingredients>",
      "is_food": true,
      "nutrition_summary": "<nutrition_summary>",
      "is_popular": true,
      "is_available": true,
      "checkout_enabled": true,
      "stock_tracking_enabled": true,
      "stock_quantity": 1,
      "stock_remaining": 1,
      "sold_out": true,
      "collect_eligible": true,
      "available_now": true,
      "collect_prep_minutes": 1,
      "price_pence": 1500,
      "currency": "GBP",
      "sort_order": 1,
      "item_kind": "dish",
      "heat_level": 1,
      "photo_credit": {
        "author": "<author>",
        "licence": "<licence>",
        "licence_url": "https://example.com/image.jpg",
        "source_url": "https://example.com/image.jpg",
        "library": "<library>",
        "file": "<file>"
      },
      "updated_at": "<updated_at>",
      "allergens_source": "<allergens_source>",
      "fulfilment_kind": "physical",
      "digital_ready_asset_count": 1,
      "is_complete": true,
      "choices": {
        "size_name": "<size_name>",
        "colour_name": "<colour_name>",
        "sizes": [
          {
            "label": "<label>"
          }
        ],
        "colours": [
          {
            "label": "<label>",
            "photo_url": "https://example.com/image.jpg"
          }
        ]
      }
    }
  ],
  "changed": -9007199254740991
}

Start uploading a video for a menu item.

The server chooses where the video goes (the public `store-videos` bucket, a name of its own that is never written over) and returns a one-use signed upload target, so the bytes go straight to storage and never ride through a request body here (a function body is capped at 4.5 MB). Then send the returned `url` in the item's `product_media` as `{ type: "video" }` through the item write. MP4 or QuickTime, up to 15 MiB; an item keeps at most 10 photos and videos together and takes one video (a second is 409 ITEM_VIDEO_LIMIT on the item write). `menu_item_id`, when sent, must be one of this shop's items (404 ITEM_NOT_FOUND otherwise). With the video space rule on, three more refusals, each with nothing uploaded and no target handed out: 409 VIDEO_SPACE_FULL when the shop has no room for it ("Your video space is full.", and "Your video space is full. We've asked for more." once the request for more has gone; `details.used_bytes`, `details.allowance_bytes`, `details.asked`); 429 RATE_LIMITED at five uploads started and not finished ("Too many videos started. Try again in a little while."); 503 DOWNSTREAM_UNAVAILABLE when the count cannot be read ("Try again in a moment.").

Request body

store_idstring · uuidoptional
menu_item_idstring · uuid, nullableoptional

The item the video is for, when it is already on her menu (its id goes in the file name). Omit for an item not saved yet.

content_typeenumrequired

MP4, or QuickTime (`video/quicktime`, a `.mov`). The file is stored as the kind it is: its name ends `.mp4` or `.mov` to match.

video/mp4video/quicktime
byte_sizeintegerrequired

The size the client is about to send, at most 15 MiB. Storage refuses anything bigger whatever is declared.

Response, 200

uploadobjectrequired
3 child fields
pathstringrequired
tokenstringrequired
signed_urlstringrequired

PUT the bytes here with the `Content-Type` the upload was started with. Never writes over another file.

urlstringrequired

The video's public address once its bytes are up: what goes in the item's `product_media`.

max_bytesintegerrequired
curl -X POST "https://www.membber.com/api/v1/menu-items/videos" \
  -H "Authorization: Bearer $MEMBBER_TOKEN" \
  -H "Idempotency-Key: 1f0e2d3c-4b5a-4678-9abc-def012345678" \
  -H "Content-Type: application/json" \
  -d '{
    "content_type": "video/mp4",
    "byte_size": 1
  }'
Response, 200
{
  "upload": {
    "path": "<path>",
    "token": "<token>",
    "signed_url": "https://example.com/image.jpg"
  },
  "url": "https://example.com/image.jpg",
  "max_bytes": -9007199254740991
}
WhatsApp
Book a Call
Start Free