Pushing Menus to Pepper
Introduction
The Pepper platform stores menu information to drive the App ordering journeys at all of a merchant's locations. Integrators can push menus and scheduling information into Pepper using the following endpoints.
| Endpoint | When to use |
|---|---|
| POST /menus | Create or update a single menu, and, optionally, change the location links and rules for using this menu, for all the locations |
| POST /locations/:locationId/menulinks | Create or update all the menu links and rules for a location |
A menu has a top-level id that is unique to the menu, not the location. The same menu definition (same id) can appear in the menus array of multiple POST /locations/:locationId/menulinks pushes. Pepper uses this id to detect that it is the same menu and share across locations.
If a location has its own variant of a menu, give it a distinct id.
Summary:
- A menu contains the entire menu used for ordering. Where different menus are used in the POS for different categories (e.g. Drinks vs. Food), it is expected that the integrator configures a single menu which separates these into categories.
- A menu may be linked to one or more locations. Making a change to that menu will affect all the linked locations.
- A location may have several menu links and rules configured. The menu that an App uses at the start of a customer order is computed based on menu rules timetable, scenario, zone and priority order.
Menu Links and Rules
Use the POST /locations/:locationId/menulinks endpoint to push all the menu links for a single location. It contains all menu links for that location and the rules for which menu is active when.
Example payload:
{
"menus": [
{
"menuId": "<integrator menu id>", // the menu id of the menu to use
"name": "Joiners Takeout",
"priorityOrder": 100,
"scenarios": ["TAKEOUT"],
"timetableControl": "ALWAYS_ACTIVE"
},
{
...
}
]
}
Each entry in menus ties a menu to the location, and the rule governing when it is active. You can add several entries for one menu.
| Field | Purpose |
|---|---|
menuId | References a menu that has been pushed (or will be pushed) via POST /menus. |
name | Human-readable label. |
priorityOrder | When multiple controls match, lowest value wins. |
scenarios | Limits the control to specific order scenarios (e.g. DELIVERY, DINE_IN). Omit to match all scenarios. Order scenarios: are: TAB, TABLE, PAYATPOS, PREORDER, ORDER_TO_TABLE, DINE_IN, TAKE_OUT, DELIVERY |
timetableControl | One of ALWAYS_ACTIVE, ALWAYS_INACTIVE, or USE_TIMETABLE. |
timetable | Required when timetableControl is USE_TIMETABLE. |
zones | Limits to named zones (e.g. "Garden", "Bar"). Omit to match all zones. |
You can also push menu links via the POST /menus endpoint locations field. This contains the same link and rule information as POST /locations/:locationId/menulinks, but the links are the other way around, namely: all the location links for a menu as opposed to all the menu links for a location.
Example payload:
{
"menu": { ... }, // the menu
"locations": [
{
"locationId": "<pepper location id>", // pepper location id
"name": "Joiners Takeout",
"priorityOrder": 100,
"scenarios": ["TAKEOUT"],
"timetableControl": "ALWAYS_ACTIVE"
},
{
...
}
]
}
Timetables
A timetable entry has an optional date range (startDate / endDate) and a timesByDay map:
{
"startDate": "2026-01-01T00:00:00",
"endDate": "2026-12-31T23:59:59",
"timesByDay": {
"MON": ["11:00 - 15:00", "17:00 - 22:00"],
"TUE": ["11:00 - 22:00"],
"SAT": ["10:00 - 23:00"],
"SUN": ["10:00 - 22:00"]
}
}
- Day keys:
MON,TUE,WED,THU,FRI,SAT,SUN - Time periods:
"HH:MM - HH:MM"(24-hour, zero-padded) - Omitting a day means the menu is not active that day
- Timezone: dates and times are interpreted as local time at the location, not UTC
Common patterns
Single always-on menu — push one menu and push a location menulinks with single menus entry ALWAYS_ACTIVE.
Separate delivery menu — push two menus and push a location menulinks with two menus entries: one with scenarios: ["DELIVERY"] pointing to the delivery menu, one with no scenarios for the default. Set priorityOrder so the more specific rule wins (lower number).
Seasonal or time-limited menu — use timetableControl: "USE_TIMETABLE" with startDate/endDate on the timetable entry, plus timesByDay for the active hours within that period.
Temporarily disable a menu — set timetableControl: "ALWAYS_INACTIVE" on its control without removing the menu definition.
Menu Schema Overview
Use the POST /menus endpoint to push menus to Pepper.
Example payload:
{
"menu": { ... }, // the menu
"locations": [ ... ] // see earlier
}
The principle menu fields are:
| Field | Purpose |
|---|---|
id | Integrator id of the menu. It should be unique for a merchant and stable for a particular menu - that means the id must not change when the menu is edited |
name | Human-readable label. |
currency | An ISO 4217 code (e.g. "GBP"). |
groups | One or more groups of items. A group represents a menu category. It contains links to items or to other groups so that nested categories are supported. |
items | Defines all the items that can be ordered. Items can be single items like a 'steak', or items groups of variants like a wine by the glass or bottle. |
modifiers | Defines all the modifiers (aka choices, options, condiments) of items and are referenced by items. |
taxes | Define taxes in the taxes array, then reference their ids from Item.taxIds. The inclusive flag on a tax distinguishes VAT-style (already in the price) from add-on taxes. Taxes can be scoped to specific order scenarios (dine-in, delivery, etc.) via applyForScenario. |
allergens | Declare integrator allergen codes as an array of { code, name } so Apps know what to display. This is for POS systems that use their own allergen identifiers as opposed to FSA codes |
All items, groups, and modifiers live in their own top-level arrays and reference each other by id. Nothing is embedded inside anything else.
Although groups can be nested into multiple levels, Pepper Apps currently only support one level of nesting only. A MenuGroup can contain items or sub-groups, but not both at the same time, and sub-groups cannot themselves have sub-groups.
Item
An item is a directly orderable thing with a price. Items live in the items block.
| Field | Purpose |
|---|---|
id | What is sent to a POS in an order basket. If the POS needs extra values other than a PLU, like a portion code for example, these should be encoded in the id. Typically the encoding would take the form of <plu>~<extra-id1>~<extra-id2>, but the format should be agreed with Pepper in advance. |
name | Label displayed in App menu. |
price | Prices are in monetary units (e.g. 2.50 means £2.50, not 2.5p). They are inclusive of any inclusive taxes such as UK VAT. The item prices should exclude modifier prices. |
modifierGroupIds | Ids of all the modifier groups (aka choices, options, condiments) of the item. The groups may be simple options eg. 'Extra Shot', 'Decaf', 'Extra Hot', or more complex options each with their own options like 'Caesar Salad Side', 'Tomato Salad Side', etc |
taxIds | Ids of taxes applicable to this item. |
allergens | Standard UK FSA allergen codes (e.g. "gluten", "milk"), or integrator allergen codes which must also be defined in the menu allergens block. |
Modifier Group
Modifier groups are defind in modifiers.
A modifier group is a list of options that may be ordered along with another item in the basket. For example: A capuccino may have a modifier group of milk choices 'Semi', 'Full fat', 'Oat', etc
There are two forms of modifier group:-
| Type | When to use |
|---|---|
ModifierOptionsGroup | A group of simple options fully defined inline (e.g. "With Ice", "Double Shot"). Each option has its own id, name, and price. |
ModifierItemsGroup | A group of options defined as items. The options can have their own modifiers, or rich imagery. Examples would be options of 'Salads Choices' each with 'Dressing' modifiers. Each entry is an ItemOption referencing an itemId from the items array. |