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:

  • Price information is not linked to an items availability — this is indicated by the Schedule Updates Status field.
  • The first time a menu cycle is published, we send through the price with the longest possible date range. Subsequent updates will often amend parts of this range. If the menu cycle availability is extended, a new date range covering the extension is sent through.
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:
FIXED_PRICE — Manually entered price from Fourth user (no calculation).
MARGIN — Percentage by gross profit.
MARKUP — By percentage from the cost price.

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.