Menu Cycles API: Webhook Guide
Overview
A webhook enables us to send you data as and when it changes, rather than you needing to poll for updates. For menu cycles, you can receive updates to the price of menu items as well as their availability in the set meal periods via an optional webhook.
You will need to provide an HTTPS endpoint that can receive calls from the webhook. The URL you provide must use a valid certificate from trusted authority. We will send the call as an HTTPS POST request with JSON in the request body. We will only send calls when a menu cycle update is published in Fourth.
To subscribe to the menu cycles webhook, or update your subscription, please contact Fourth Customer Support.
Authentication
To support authenticating our webhook calls, we can provide two mechanisms in the request header:
- An origin header, set to our DNS.
- Optionally, we can configure an OAuth 2.0 Bearer token to add in the header. This token is configured as part of the webhook registration.
Example header elements
Origin: api.fourth.com
Authorization: Bearer eyJhbGciOiJS…
Data included in the request
The data is sent as an aggregate of update information only, to reduce the amount of data you need to process. It is split by location, with a separate call for each location.
When a menu cycle is first published, you may receive data aggregated for longer date ranges. As updates to the menu cycle occur, you may find that the edits are for smaller data ranges, such as for a single day. You should consider the updates for shorter data ranges as having priority over any previously received longer date range.
Also note that:
- Sales items only include recipes and menus which can be sold individually and have PLU assigned. Buffet menu recipes are not included.
- The schedule data is a combined data set for meal periods across all active menu cycles for the location.
- The pricing data is a combined data set for tariffs across all active menu cycles for the location.
- Data is normally aggregated across date ranges where data such as the meal periods are the same. However, you will also receive pricing for items that are not tied to meal periods throughout the day.
- All dates and times provided are in the timezone of the specified location.
- All pricing is provided in the currency of the specified location.
How to process changes in the correct order
Each call includes two values: ParentChangeSequence and ChangeSequence. These make sure that you can apply changes in the correct order. You should not apply an update if the ParentChangeSequence does not match the last update you applied.
As updates are based on location, the above values are specific to each location. As such, you need to track the sequence numbers individually for each location so that you can process each call by location.
For example, for location 17, a series of updates would look like the following (examples are shortened):
First update
{ "ChangeSequence": 81, "ParentChangeSequence": 70, "LocationID": "17", ...
Second update
{ "ChangeSequence": 93, "ParentChangeSequence": 81, "LocationID": "17", ...
Third update
{ "ChangeSequence": 101, "ParentChangeSequence": 93, "LocationID": "17", ...
Example request body
{ "ChangeSequence": 73287, "ParentChangeSequence": 73285, "LocationID": "123", "LocationName": "Canteen 1", "CurrencyIsoCode": "GBP", "ScheduledItems": [{ "PLU": "123BDS43", "ItemID": "1-1234ab", "ItemName": "Brown Bread", "ItemType": "RECIPE", "PriceUpdates": [{ "DateFrom": "2019-03-11", "DateTo": "2019-03-16", "TariffName": "TariffOne", "PriceModel": "MARGIN", "TaxCode": "STD_20", "TaxRate": 20, "SellPrice": 1.50 }, { "DateFrom": "2019-03-11", "DateTo": "2019-03-16", "TariffName": "TariffTwo", "PriceModel": "MARGIN", "TaxCode": "STD_20", "TaxRate": 20, "SellPrice": 2.10 }, { "DateFrom": "2019-03-17", "DateTo": "2019-03-17", "TariffName": "TariffOne", "PriceModel": "MARGIN", "TaxCode": "STD_20", "TaxRate": 20, "SellPrice": 1.60 }, { "DateFrom": "2019-03-17", "DateTo": "2019-03-17", "TariffName": "TariffTwo", "PriceModel": "MARGIN", "TaxCode": "STD_20", "TaxRate": 20, "SellPrice": 2.25 }], "ScheduleUpdates": [{ "DateFrom": "2019-03-11", "DateTo": "2019-03-15", "Status": "AVAILABLE", "MealPeriodName": "Lunch", }, { "DateFrom": "2019-03-11", "DateTo": "2019-03-15", "Status": "AVAILABLE", "MealPeriodName": "Dinner", }, { "DateFrom": "2019-03-16", "DateTo": "2019-03-17", "Status": "AVAILABLE", "MealPeriodName": "Lunch", }] }, { "PLU": "123BBS23", "ItemID": "1-20122", "ItemName": "Baked beans", "ItemType": "RECIPE", "PriceUpdates": [{ "DateFrom": "2019-03-11", "DateTo": "2019-03-12", "TariffName": "TariffOne", "PriceModel": "MARGIN", "TaxCode": "STD_20", "TaxRate": 20, "SellPrice": 3.50 }, { "DateFrom": "2019-03-11", "DateTo": "2019-03-12", "TariffName": "TariffTwo", "PriceModel": "MARGIN", "TaxCode": "STD_20", "TaxRate": 20, "SellPrice": 5.10 }], "ScheduleUpdates": [{ "DateFrom": "2019-03-11", "DateTo": "2019-03-12", "Status": "AVAILABLE", "MealPeriodName": "Lunch", }] }, { "PLU": "123BBS53", "ItemID": "2-20123", "ItemName": "Lunch Special", "ItemType": "MENU", "PriceUpdates": [{ "DateFrom": "2019-03-17", "DateTo": "2019-03-17", "TariffName": "TariffTwo", "PriceModel": "MARGIN", "TaxCode": "STD_20", "TaxRate": 20, "SellPrice": 7.50 }], "ScheduleUpdates": [{ "DateFrom": "2019-03-17", "DateTo": "2019-03-17", "Status": "AVAILABLE", "MealPeriodName": "Lunch", }] }] }
Data Fields
Field | Description |
---|---|
ChangeSequence |
The current change number. Type: integer |
ParentChangeSequence |
The previous change number sent. You must ensure that changes are processed in order. Type: integer |
LocationID |
The unique Fourth ID for the location. You can get this ID using a GET /Locations request. Type: integer |
LocationName |
A name for the location; for example School Canteen 1. Type: string |
CurrencyIsoCode |
The ISO 4217 currency code for the location; for example GBP. Type: string |
ScheduledItems | Array of items available for sale. |
PLU |
The PLU number assigned to the item for sale. PLU numbers are not necessarily unique. Type: integer |
ItemID |
The unique Fourth ID for the item. Type: integer |
ItemName |
A descriptive name for the item; for example, Baked beans. Type: string |
ItemType |
Whether the item is a RECIPE or MENU. Type: string |
PriceUpdates |
Array of price updates. Note that:
|
PriceUpdates.DateFrom |
Beginning of the date range for which the tariff applies. Type: date in the format YYYY-MM-DD |
PriceUpdates.DateTo |
End of the date range for which the tariff applies. Type: date in the format YYYY-MM-DD |
PriceUpdates.TariffName |
Name for this tariff. Type: string |
PriceUpdates.PriceModel |
The price model name, one of: Type: string |
PriceUpdates.TaxCode |
Identifier for the tax rate used for the item. This may be useful for matching the tax rate configuration within a POS system. For example, STD_20. Type: string |
PriceUpdates.TaxRate |
The percentage of tax applied to the item. For example, 20 represents a 20% tax rate. Type: integer |
PriceUpdates.SellPrice |
The price the item should be sold at, in the location's currency. Type: integer |
ScheduleUpdates |
Array of any meal periods updates that apply to the item. This identifies when the item should be available for sale using the details provided (such as the sell price). |
ScheduleUpdates.DateFrom |
Start of the date range during which the item is available. Type: date in the format YYYY-MM-DD |
ScheduleUpdates.DateTo |
End of the date range during which the item is available. Type: date in the format YYYY-MM-DD |
ScheduleUpdates.Status |
The availability of the item during the date ranges. AVAILABLE — the item is available for sale. UNAVAILABLE — the item is not available for sale. You may receive this status if an item that was previously available for a date range becomes unavailable. You should only remove the availability of the item for the specified dates. Type: string |
ScheduleUpdates.MealPeriodName |
The time of day the item is available for sale; for example, breakfast, lunch or dinner. Retail items usually use "all day" as the meal period. Type: string |
Responding to the request
Send an HTTPS 200 OK response to the request.
We recommend you store the payload in durable storage before sending your response to Fourth. If for some reason the payload is lost after sending back a response, you will need to contact Fourth Customer Support to receive it manually.
Retry strategy used by Fourth
In case your system is not available (or the network connection not working) the webhook delivery will be retried using an exponential back-off algorithm.