Appearance
Menu Management
Overview
A menu is a named container that belongs to a single location and is shown on the ordering channels you select. Each menu holds display groups (which in turn hold items), an availability schedule, and a publish status (in the back office: Published or Archive). A location can have multiple menus for different purposes (for example a regular menu and a separate kiosk or takeaway menu).
Key Purpose: Build the menu structure for one location and control which ordering channels it appears on.
Purpose
The Menu Builder page lets you create, edit, duplicate, publish, archive, and delete menus for the currently selected location. Within each menu you organize content into display groups and control where the menu is visible.
Key Concepts
- Menu: A named collection that belongs to one location, contains display groups, has an availability schedule, a publish status, and a list of channels it is visible on.
- Display Group: The grouping that actually holds items inside a menu (menus reference display groups by
display_group_ids). Display groups are created and edited inside the Menu Builder, not on the menu's basic-information form. Menus do not reference categories directly. - Menu Status: The status field can hold
Draft,Published, orArchive. A newly created menu is automatically set toPublished. The Menu Builder only ever switches betweenPublishedandArchive—Draftis an API-only status (POST /back-office/menus/draft/{id}) with no control in the back office. - Channel Visibility: Each menu specifies which ordering channels it appears on (
visibility). The selectable channels come fromGET /back-office/visibility-options, which returns Kiosk and Online Ordering always, Table Qr Ordering once QR ordering has been set up for the location, and plus POS when the location has a first-party POS footprint (at least one non-retiredPosRegister, or at least one POS-type Device belonging to the location's merchant). In practice only test-flagged and demo/seeded first-party merchants reach that state today, because theupvendoPOS provider is staged'test_only' => trueinconfig/pos-providers.php:52. TheChannelOptionsenum also defines Uber Eats, Takeaway and Shopify, but those are never offered in the Menu Builder. - Availability: Controls when the menu is offered. The form offers Always Available (
always-available, the default) and Specific day and time (specific-day-time, a per-day schedule you configure). A third stored value,location-default, is written only by integration-created menus and behaves identically toalways-available. - Menu Name Uniqueness: The menu
namemust be unique among menus at the same location. - POS Name Uniqueness: The optional
pos_namemust be unique among menus at the same location. - Draft vs. published menu content: Item changes inside a display group are saved as a draft first. Ordering channels keep serving the previously published item list until you press Publish.
- Default menu (cross-reference): "Default menu" is not a field on the menu itself. A menu becomes a kiosk's default or extra menu through the kiosk device profile (
default_menu_id/extra_menu_ids), which is configured in the kiosk/device settings, not on this page.
Actions
Create Menu
Open the Menu Builder and add a new menu. You provide a name, optional POS name, optional description, an availability type (and schedule if specific-day-time), and the channels it should be visible on. The menu is created for the currently selected location and is automatically set to Published.
Edit Menu
Update an existing menu's name, POS name, description, availability, or visibility. Saving dispatches a MenuUpdated event (used to sync Uber Eats) and a ReloadMenu event so the menu refreshes across ordering channels.
Manage Display Groups
Inside a menu you add, edit, reorder, and remove display groups, and add an item to additional display groups ("Add to another group") or move it to a different one. An item may belong to several display groups in the same menu: "Add to another group" keeps the existing membership, "Move" removes it from the source group. Both options appear on the item's ⋮ menu when the menu has more than one display group. Reordering display groups dispatches a ReloadMenu event.
Save Draft / Publish / Discard Menu Content
Changing the items inside a display group marks the menu as having unsaved changes and reveals Discard plus a single primary button in the Menu Builder header, which reads Save while the change is unsaved and Publish once a draft exists:
- Save stores the change as a draft. The previously published item list is snapshotted, and no menu reload is sent — kiosk and online ordering keep serving the old items. The toast confirms this: "Changes saved. Click Publish to make them live."
- Publish publishes the current item list, clears the draft snapshot, and fires a
ReloadMenuevent so the ordering channels pick the change up. - Discard restores the display group to its last published item list.
Only Publish makes item changes visible to customers.
Duplicate Menu
Create a copy of an existing menu. All of the source menu's display groups are duplicated and linked to the new menu.
Publish / Archive Menu
Change a menu's status:
- Archive (offered while viewing Published menus) sets the status to
Archive. - Publish (offered while viewing the Archives) sets the status back to
Published.
Draft is an API-only status (POST /back-office/menus/draft/{id}) — the Menu Builder exposes only Archive and Publish, so no back-office action ever puts a menu into Draft.
Archiving a menu is blocked (HTTP 400) if the menu is currently used by a kiosk device profile that has any devices assigned to it.
Delete Menu
Permanently delete a menu. All display groups belonging to that menu are deleted as well.
Location
- Backoffice Route:
/menus/menu-builder(navigation: Menus group → Menu Builder) - Backend Controller:
app/Http/Controllers/Api/MenuController.php - Backend Service:
app/Services/BackOffice/MenuService.php - Vue Page:
src/pages/menus/menu-builder/index.vue→src/views/menus/MenuBuilder.vue - Menu Form:
src/views/menus/forms/MenuForm.vue
Fields
These are the fields on the menu form. Items, prices, and modifiers live on display groups and items, not on the menu itself.
Menu Name
| Property | Value |
|---|---|
| Field ID | name |
| Label | Menu Name |
| Type | Text |
| Required | Yes |
| Validation | required, string, unique among menus at the same location |
Description: Display name of the menu. A duplicate name at the same location is rejected with HTTP 400 and the message "Menu name must be unique", on both create and update.
Examples:
- "Main Menu"
- "Lunch Menu"
- "Kiosk Menu"
POS Name
| Property | Value |
|---|---|
| Field ID | pos_name |
| Label | POS Name |
| Type | Text |
| Required | No |
| Validation | nullable, string, unique per location |
Description: Optional name used on the POS. If provided, it must be unique among menus at the same location.
Description
| Property | Value |
|---|---|
| Field ID | description |
| Label | Description |
| Type | Textarea |
| Required | No |
| Validation | nullable, string |
Description: Optional description of the menu.
Location
| Property | Value |
|---|---|
| Field ID | location_id |
| Label | Location |
| Type | Single location (the selected location) |
| Required | Yes |
| Validation | required, must reference an existing location |
Description: The single location this menu belongs to. A menu is created for the currently selected location; it is not shared across multiple locations.
Availability Type
| Property | Value |
|---|---|
| Field ID | availability_type |
| Label | Availability |
| Type | Select |
| Required | Yes |
| Options | always-available, location-default, specific-day-time |
| Default | always-available |
Description: Controls when the menu is offered.
Options:
always-available: The menu is available at all times (treated as the location's business hours).location-default: The menu follows the location's business hours.specific-day-time: The menu is available only on the days and times you configure in theavailabilityschedule.
location-default is not selectable in the Menu Builder — the Availability section offers only Always Available and Specific day and time. It is written by menus that integrations create automatically (Shopify, Kassanet, Lightspeed K Series), and it behaves identically to always-available: both resolve the schedule to the location's business hours. Saving an integration-created menu from the form rewrites it to always-available.
Availability Schedule
| Property | Value |
|---|---|
| Field ID | availability |
| Label | Availability schedule |
| Type | Per-day schedule |
| Required | Validated only when availability_type = specific-day-time |
Description: Per-day availability, keyed by day name (Monday–Sunday). Each day has the shape:
json
{
"is_available": true,
"available_all_day": false,
"times": [
{ "from": "11:00", "to": "15:00" }
]
}is_available(boolean) — whether the menu is offered that day.available_all_day(boolean) — if true, the menu uses the location's business hours for that day and no time slots are required.times— array of{ from, to }time slots (formatHH:mm), required when the day is available and not all-day.
This schedule is only enforced when availability_type is specific-day-time. For always-available and location-default, the location's business hours are used.
Visibility (Channels)
| Property | Value |
|---|---|
| Field ID | visibility |
| Label | Visibility |
| Type | Multi-select |
| Required | Yes (at least one channel) |
| Options | Kiosk, Online Ordering, Table Qr Ordering (only once QR ordering is set up for the location), and POS (only when the location has a first-party POS register or device) |
Description: The ordering channels this menu appears on. The Visibility section renders one checkbox per option returned by GET /back-office/visibility-options (DynamicConstantOrchestrator::getChannelOptions), so the list is location-dependent, not fixed: Kiosk and Online Ordering always, Table Qr Ordering when the location's QR-ordering first-time setup is complete, and POS when the location has a first-party POS footprint — at least one non-retired PosRegister, or at least one POS-type Device under the location's merchant (DynamicConstantOrchestrator.php:54-58, :97-112). Because the upvendo provider is staged 'test_only' => true in config/pos-providers.php:52, only test-flagged and demo/seeded first-party merchants see the POS checkbox today. The ChannelOptions enum also defines Uber Eats, Takeaway and Shopify and the API would accept them, but they are never returned as options, so a merchant cannot tick them in the Menu Builder.
Business Logic:
- Changing visibility so that Kiosk is removed is blocked (HTTP 400) if the menu is currently used by a kiosk device profile that has any devices assigned to it.
Status (read-only on the form)
| Property | Value |
|---|---|
| Field ID | status |
| Label | Status |
| Type | Enum |
| Options | Draft, Published, Archive |
Description: The publish status of the menu. It is set by the server: new menus are created as Published, and the status is changed afterward via the Publish / Archive actions rather than edited directly on the form. Draft is only reachable through the API.
Unpublished changes (read-only)
| Property | Value |
|---|---|
| Field ID | has_unpublished_changes (returned by the show-menu response) |
| Type | Boolean |
Description: True when the menu has item changes that have been saved but not published. It is derived from the menu's stored has_draft flag, from each display group's published item snapshot, and from a hash of the current item lists. It drives the Save-vs-Publish button in the Menu Builder header.
Display Groups (managed in the builder)
| Property | Value |
|---|---|
| Field ID | display_group_ids |
| Label | Display groups |
| Type | Ordered list of display groups |
Description: The display groups that make up the menu, in display order. Display groups are added, edited, reordered, and removed inside the Menu Builder (not on the menu's basic-information form). The menu stores the ordered list of display group IDs.
Business Logic
Menu structure
Menu (belongs to one location)
│
▼
Display Groups (display_group_ids, ordered)
│
▼
Items
│
▼
ModifiersStatus lifecycle
Create menu ──▶ Published (automatic)
Archive
Published ─────────────▶ Archive
Published ◀───────────── Archive
Publish
Archiving is blocked (400) while the menu is used by a
kiosk device profile that has any devices assigned to it.
Draft is a valid stored status but has no back-office
control; only the API can set it.Relations
Depends On
- Locations: A menu belongs to a single location.
- Display Groups: A menu's content is organized into display groups.
Affects
- Selected channels (
visibility): Where the menu appears (Kiosk, Online Ordering, Table Qr Ordering once QR ordering is set up for the location, and POS for locations with a first-party POS footprint). - Kiosk device profiles: A menu can be referenced as a kiosk's default or extra menu (configured on the device profile, not here).
Related Features
Business Rules
- A menu belongs to exactly one location; it is not assigned to multiple locations.
- The menu
namemust be unique within the location; a duplicate is rejected with HTTP 400 and the message "Menu name must be unique". This is checked on both create and update. - The optional
pos_namemust be unique among menus at the same location; a duplicate fails validation with HTTP 422 (pos_name: "The pos name has already been taken."). - New menus are automatically set to
Publishedon creation. - A menu cannot be archived while it is used by a kiosk device profile that has any devices assigned to it; the system returns a 400 error to avoid breaking live kiosks. The check counts every device assigned to the profile — a device's online/offline status is not considered.
- Visibility cannot be changed to remove the Kiosk channel while the menu is used by a kiosk device profile that has any devices assigned to it (400 error).
- When a menu is deleted, all display groups belonging to that menu are deleted as well.
- Updating a menu dispatches a
MenuUpdatedevent (Uber Eats sync) and aReloadMenuevent so ordering channels reflect the change. Reordering display groups also dispatchesReloadMenu. - Saving item changes inside a display group does not dispatch
ReloadMenu— it stores a draft. Only publishing those changes firesReloadMenu. - The same item can belong to several display groups within one menu. "Add to another group" appends it without removing it from the group it is already in, and re-adding an item a group already holds is a no-op.
- Duplicating a menu copies all of its display groups into the new menu.
FAQs
- Can one menu be used at multiple locations? No. A menu belongs to a single location. To reuse a menu's structure at another location, duplicate it there or rebuild it for that location.
- How do I make a menu the default menu for a kiosk? That is set on the kiosk's device profile (
default_menu_id/ extra menus), not on the menu itself. The menu page only controls which channels the menu is visible on. - What happens when I duplicate a menu? A new menu is created and every display group from the original is copied and linked to the new menu, so editing the copy does not affect the original.
- Why can't I archive my menu? The menu is in use by a kiosk device profile that has devices assigned to it. Reassign those devices to another device profile, or point the profile at a different menu, first.
- Why can't I turn off the Kiosk channel for this menu? The menu is being used by a kiosk device profile that has devices assigned to it, so Kiosk visibility cannot be removed while that is the case.
- What is the difference between Draft and Archive? In the back office there is no Draft: the Menu Builder only moves menus between Published and Archive.
Draftexists as a status value in the API but no back-office action sets it, so use Archive for menus you no longer want served. - I changed my menu but the kiosk / online ordering still shows the old items. Item changes inside a display group are saved as a draft first. Open the menu and press Publish in the header — only publishing sends the change to the ordering channels. Discard puts the display group back to its last published item list.
- Can the same item appear in two display groups? Yes. Open the item's ⋮ menu and choose Add to another group; the item stays in its current group as well. Move relocates it instead.
- Why does saving my menu say the name must be unique? Another menu at the same location already uses that name. Menu names must be unique per location.
- Why can't I select Uber Eats / Takeaway / Shopify under Visibility? Those channels are never offered in the Menu Builder. The selectable channels are Kiosk, Online Ordering, Table Qr Ordering once QR ordering is set up for the location, and POS — but POS only appears once the location actually has a first-party POS register or device, which is not something a merchant can set up self-serve today.
- Where do I adjust a menu's time schedule / when a menu is available? On the menu itself, not in the location's opening hours. Edit the menu and open the Availability section ("When is this menu available?"). Choose Always Available, or Specific day and time to set the days and hours the menu is offered. This is different from Business Hours, which sets when the whole location is open.
- How do I run different menus at different times of day (e.g. a lunch menu until 16:00 and a standard menu the rest of the day)? Create two menus for the location. On each, open the Availability section and choose Specific day and time, then set the days and the Time Available for each — e.g. the lunch menu from 11:00 to 16:00 on each open day, and the standard menu covering the rest. Both can be shown on the same channels; each is only offered during its set hours. See the "Lunch menu with a specific day/time schedule" example below.
Troubleshooting
Problem: Menu not appearing on a channel
Causes:
- The channel is not selected in the menu's visibility.
- The channel is not enabled for the location (so it isn't an available visibility option).
- The menu is not Published.
Solutions:
- Edit the menu and add the channel under Visibility.
- Confirm the channel is enabled for the location.
- Publish the menu.
Problem: Cannot archive a menu ("Can't archive the menu because it is being used", HTTP 400)
Causes:
- The menu is used by a kiosk device profile that has devices assigned to it. Device status is irrelevant — the guard counts every device assigned to the profile.
Solutions:
- Reassign those devices to another device profile, or point the profile at a different menu, before archiving.
Problem: Cannot remove the Kiosk channel from visibility
Causes:
- The menu is in use by a kiosk device profile that has devices assigned to it.
Solutions:
- Reassign the kiosk(s) to another menu first, then update visibility.
Problem: "Menu name must be unique" (HTTP 400)
Causes:
- Another menu at the same location already carries that name.
Solutions:
- Rename the menu, or rename/archive the existing menu that holds the name. (A duplicate POS name is a different error: it fails validation with HTTP 422 and the message "The pos name has already been taken.")
Problem: Item changes are not showing on the ordering channels
Causes:
- The changes were saved as a draft and never published — saving item changes deliberately does not reload the menu on the channels.
Solutions:
- Open the menu in the Menu Builder and press Publish in the header. Use Discard if you want to roll back to the last published item list instead.
Problem: Menu not available at the expected time
Causes:
availability_typeisspecific-day-timeand the current day/time is outside the configured slots.- The day is marked not available, or its time slots are wrong.
Solutions:
- Review the per-day availability schedule.
- Enable the day and set correct
from/totimes, or switch the menu to Always Available.
Examples
Note: payloads below reflect the fields accepted by the create/update menu requests. Display group contents (items) are managed through separate display-group endpoints inside the Menu Builder.
Always-available menu
json
{
"location_id": "665f0c2a1b2c3d4e5f600001",
"name": "Main Menu",
"pos_name": "MAIN",
"description": "Our full menu",
"availability_type": "always-available",
"visibility": ["Kiosk", "Online Ordering"]
}Integration-created menu using location-default
This payload is produced by an integration that auto-creates menus (Shopify, Kassanet, Lightspeed K Series). A merchant cannot pick
location-defaultin the Menu Builder; it behaves the same asalways-available.
json
{
"location_id": "665f0c2a1b2c3d4e5f600001",
"name": "House Menu",
"description": "Available during opening hours",
"availability_type": "location-default",
"visibility": ["Kiosk", "Online Ordering", "Table Qr Ordering"]
}Lunch menu with a specific day/time schedule
json
{
"location_id": "665f0c2a1b2c3d4e5f600001",
"name": "Lunch Menu",
"description": "Weekday lunch service",
"availability_type": "specific-day-time",
"visibility": ["Kiosk", "Online Ordering"],
"availability": {
"Monday": { "is_available": true, "available_all_day": false, "times": [{ "from": "11:00", "to": "15:00" }] },
"Tuesday": { "is_available": true, "available_all_day": false, "times": [{ "from": "11:00", "to": "15:00" }] },
"Wednesday": { "is_available": true, "available_all_day": false, "times": [{ "from": "11:00", "to": "15:00" }] },
"Thursday": { "is_available": true, "available_all_day": false, "times": [{ "from": "11:00", "to": "15:00" }] },
"Friday": { "is_available": true, "available_all_day": false, "times": [{ "from": "11:00", "to": "15:00" }] },
"Saturday": { "is_available": false, "available_all_day": false, "times": [] },
"Sunday": { "is_available": false, "available_all_day": false, "times": [] }
}
}Weekend menu using all-day availability
json
{
"location_id": "665f0c2a1b2c3d4e5f600001",
"name": "Weekend Brunch",
"availability_type": "specific-day-time",
"visibility": ["Kiosk", "Table Qr Ordering"],
"availability": {
"Monday": { "is_available": false, "available_all_day": false, "times": [] },
"Tuesday": { "is_available": false, "available_all_day": false, "times": [] },
"Wednesday": { "is_available": false, "available_all_day": false, "times": [] },
"Thursday": { "is_available": false, "available_all_day": false, "times": [] },
"Friday": { "is_available": false, "available_all_day": false, "times": [] },
"Saturday": { "is_available": true, "available_all_day": true, "times": [] },
"Sunday": { "is_available": true, "available_all_day": true, "times": [] }
}
}