Flavoso APIGuidesMenu sync
Menu sync
PUT /menu syncs the whole menu in one call, with external_id as the key. Plus sold-out updates in one call and single routes for every object.
The model
A menu has four kinds of objects, as flat lists with references:
| Object | Important fields |
|---|---|
Category (category) | name, position, tax_class |
Product (product) | category_id, name, description, price, available, position, allergens, diets, tax_class, image_url, option_group_ids |
Option group (option_group) | name, min_select, max_select, position, options |
Option (option) | name, price_delta, available, position |
Every object carries id, external_id (your app's) and updated_at. GET /menu returns everything at once, including what is currently sold out.
Syncing with PUT /menu
PUT /menu takes the menu as your POS knows it. The key is always the external_id: an object carrying an id of your app is updated, anything else in the body is created. Products point to their category with category_external_id and to their option groups with option_group_external_ids (in this order). Options sit inside their group.
PUT /menu creates it a second time. Take it over first: read GET /menu, map the objects to your items and set the external_id per object with PATCH (PATCH /menu/categories/{id}, PATCH /menu/products/{id}, PATCH /menu/option-groups/{id}, PATCH /menu/options/{id}). In the test restaurant, “Assign ids” on the console's Menu tab does this. A dry run shows duplicates in summary.*.created.{
"mode": "merge",
"dry_run": false,
"categories": [
{
"external_id": "CAT-PIZZA",
"name": "Pizza",
"position": 1,
"tax_class": "food"
},
{
"external_id": "CAT-DRINKS",
"name": "Getränke",
"position": 2,
"tax_class": "drink"
}
],
"products": [
{
"external_id": "ART-1001",
"category_external_id": "CAT-PIZZA",
"name": "Pizza Margherita",
"description": "Tomato, mozzarella, basil",
"price": 950,
"available": true,
"position": 1,
"allergens": [
"gluten",
"milk"
],
"diets": [
"vegetarian"
],
"option_group_external_ids": [
"GRP-EXTRAS"
]
},
{
"external_id": "ART-2001",
"category_external_id": "CAT-DRINKS",
"name": "Cola 0,33 l",
"price": 290,
"available": true,
"position": 1,
"image_url": "https://pos.example.com/images/cola.jpg"
}
],
"option_groups": [
{
"external_id": "GRP-EXTRAS",
"name": "Extras",
"min_select": 0,
"max_select": 3,
"options": [
{
"external_id": "OPT-CHEESE",
"name": "Extra cheese",
"price_delta": 150
},
{
"external_id": "OPT-SALAMI",
"name": "Spicy salami",
"price_delta": 200,
"available": false
}
]
}
]
}- Prices in cents, gross:
priceon the product,price_deltaon the option. - Required:
mode, and on every objectexternal_idandname, pluspriceon products andoptionson groups (it may be empty). Every sync setsnameandpriceto your values; an option of your app that no longer appears in the body is only deleted byreplace, withmergeit stays. All other fields you leave out stay as they are: a POS that does not track allergens or sold-out states does not overwrite what the owner entered. An unknown field gets 400 withparam. - A list missing from the body is not part of the sync:
replacewithoutproductsdeletes no products. Only an empty list[]means “none”. - Flavoso fetches
image_url(https) itself and stores it like an upload in the dashboard: PNG, JPEG or WebP, at most 1 MB. If a product already carries the image from exactly this address, Flavoso does not download it again; so it does not notice a new image at the same address; in that case, append a version parameter, for example?v=2. If the owner replaced the image in the dashboard, the next sync sets yours again. Each sync fetches up to 25 new images, the rest with the next one (warningimages_deferred). A test restaurant keeps at most 50 downloaded images; beyond that, images are skipped (warningimages_capped). Resetting in the console deletes them.nullremoves the image. allergenstakes the 14 keys below and their names in the five languages (Sojabecomessoybeans),dietsthe six keys below, also spelled differently (Gluten-free). Anything else stays a custom entry and appears verbatim on the ordering page: at most 20 custom allergens of 40 characters each and 14 custom diets of 60 each. Onlytax_classtakes nothing but the values below.
# menu.json holds the body from above. Run it with "dry_run": true first:
# nothing changes, the answer says what would. Then again with "dry_run": false.
curl -X PUT https://api.flavoso.com/v1/menu \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @menu.jsonmerge or replace
| mode | What is in the body | What is missing |
|---|---|---|
merge | is created or changed | stays as it is |
replace | is created or changed | is deleted, but only if it carries an external_id of YOUR app |
Objects without your app's id (created in the dashboard, by the menu import or by another app) are not changed or deleted by a sync, in neither mode, with two exceptions in replace: if it deletes one of your categories, the restaurant's own products in it move to “No category”, and if it deletes one of your option groups, the restaurant's own options in that group are deleted with it (warning unmanaged_options_deleted). replace is right when the POS is the single source of truth for its items; merge when it only delivers part of them.
Dry run
With "dry_run": true, Flavoso checks the whole body and answers with the same summary, without changing anything; images are not downloaded. Ideal for the first connection and before a replace: you see what would be deleted. If a product that exists already is counted in summary.*.created the first time, it does not carry an id of your app yet.
The response
The response counts per kind what was created, changed, left unchanged and deleted, names the Flavoso id for every external_id and collects hints in warnings. Store the ids: events and orders carry Flavoso's.
{
"object": "menu_sync",
"mode": "merge",
"dry_run": false,
"summary": {
"categories": {
"created": 2,
"updated": 0,
"unchanged": 0,
"deleted": 0
},
"products": {
"created": 2,
"updated": 0,
"unchanged": 0,
"deleted": 0
},
"option_groups": {
"created": 1,
"updated": 0,
"unchanged": 0,
"deleted": 0
},
"options": {
"created": 2,
"updated": 0,
"unchanged": 0,
"deleted": 0
}
},
"warnings": [],
"ids": {
"categories": [
{
"external_id": "CAT-PIZZA",
"id": "5a1c2e3d-4b5f-4a6e-8d7c-9b0a1f2e3d4c"
},
{
"external_id": "CAT-DRINKS",
"id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e"
}
],
"products": [
{
"external_id": "ART-1001",
"id": "303b87c9-58a3-4644-9a8b-2841cc1f89d6"
},
{
"external_id": "ART-2001",
"id": "6a7b8c9d-0e1f-4a2b-9c3d-4e5f6a7b8c9d"
}
],
"option_groups": [
{
"external_id": "GRP-EXTRAS",
"id": "8e7d6c5b-4a39-4281-b0f9-e8d7c6b5a4f3"
}
],
"options": [
{
"external_id": "OPT-CHEESE",
"id": "1f2e3d4c-5b6a-4798-8a7b-6c5d4e3f2a1b"
},
{
"external_id": "OPT-SALAMI",
"id": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f"
}
]
}
}Every warning has code and message:
| code | Meaning |
|---|---|
image_failed | An image could not be used; the product keeps its image. |
images_deferred | More new images than one sync fetches. Sync again right away. |
images_not_checked | Dry run: images were not downloaded. |
images_capped | Test restaurant only: more images than it keeps (50); the rest was not downloaded. |
unmanaged_kept | Objects without your app's id that a replace left in place. |
unmanaged_options_deleted | Options without your id that went with a deleted group. |
own_option_group_kept | A product's own group stays attached. |
option_group_released | The group of a deleted product became a shared group. |
Limits and transaction
A sync may contain at most 200 categories, 2000 products, 500 option groups and 5000 options; beyond that, Flavoso answers 422 limit_exceeded. The body may be up to 2 MB, counted in bytes; a larger one gets 413 payload_too_large. Each connection can run at most 10 syncs per minute.
The sync runs in one transaction, completely or not at all: if one part fails, the menu stays as it was. An error in one entry comes as 400 invalid_request with param, for example products[3].price, and so does a reference to nothing, for example products[3].category_external_id.
Valid values
| Name | Values |
|---|---|
Tax group (tax_class) | fooddrinkalcohol |
Allergens (allergens) | glutencrustaceanseggsfishpeanutssoybeansmilknutscelerymustardsesamesulphiteslupinmolluscs |
Diets (diets) | vegetarianvegangluten_freelactose_freehalalkosher |
Sold out in one call
POST /menu/availability sets the availability of up to 500 products and options at once, all or nothing. Every entry names type (product or option), plus id or external_id, and available as the target state. Every object that changes reports its own menu.product.updated or menu.option.updated.
curl -X POST https://api.flavoso.com/v1/menu/availability \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"type": "product",
"external_id": "ART-1001",
"available": false
},
{
"type": "option",
"external_id": "OPT-SALAMI",
"available": true
},
{
"type": "product",
"id": "6a7b8c9d-0e1f-4a2b-9c3d-4e5f6a7b8c9d",
"available": true
}
]
}'Single objects
For targeted changes, every kind has its own routes: list, create, read, change, delete. The complete list is in the reference.
GET /menu/productsfilters bycategory_id(an id ornonefor “No category”),available,external_idandupdated_since, with pagination;GET /menu/categories,GET /menu/option-groupsandGET /menu/optionsonly byexternal_idandupdated_since.GET /menu/optionslists every option: a new surcharge or a sold-out option changes neither its product nor its group, so poll it as well. The menu lists always come in ascending order ofupdated_at.PATCH /menu/products/{id}changes every field, including the option groups (option_group_ids, in this order), the image and theexternal_id. A product's own group always comes first there and cannot be detached, only deleted; another product's own group cannot be attached (400).POST /menu/option-groupscreates a group with its options;POST /menu/option-groups/{id}/optionsan option in an existing group. Creating answers 201.DELETE /menu/categories/{id}moves the products to “No category”, as in the dashboard. With?with_products=truethey are deleted as well.
# A new dish in an existing category
curl -X POST https://api.flavoso.com/v1/menu/products \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: create-ART-1002" \
-d '{"external_id": "ART-1002", "category_id": "5a1c2e3d-4b5f-4a6e-8d7c-9b0a1f2e3d4c", "name": "Pizza Funghi", "price": 1090}'
# New price, sold out, other option groups (in this order)
curl -X PATCH https://api.flavoso.com/v1/menu/products/PRODUCT_ID \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"price": 1150, "available": false, "option_group_ids": ["8e7d6c5b-4a39-4281-b0f9-e8d7c6b5a4f3"]}'
# The dishes of one category, only what is sold out
curl "https://api.flavoso.com/v1/menu/products?category_id=5a1c2e3d-4b5f-4a6e-8d7c-9b0a1f2e3d4c&available=false" \
-H "Authorization: Bearer $ACCESS_TOKEN"
# Delete a category together with its dishes (without with_products they move to "No category")
curl -X DELETE "https://api.flavoso.com/v1/menu/categories/CATEGORY_ID?with_products=true" \
-H "Authorization: Bearer $ACCESS_TOKEN"Menu events
Every change to the menu reports an event, including changes made by your own app: menu.category.*, menu.product.*, menu.option_group.* and menu.option.*, each created, updated and deleted. updated lists the changed fields in changed and their old values in previous, deleted shows the object as it was before the deletion. See Event types.
A sync with PUT /menu reports exactly one event instead of many single ones: menu.synced with mode, summary and the connection_id of the connection that synced; if it is your own, you can skip it. Fetch what changed afterwards with GET /menu/products?updated_since=… and GET /menu/options?updated_since=…. external_id is always null in events, because one envelope goes to every recipient alike.
Who has the last word
The owner can still change everything in the dashboard, including products carrying your app's id. A sync overwrites the fields it sends. A proven split: the POS is the source for its items, names and prices; the owner maintains descriptions, images and allergens in Flavoso, and the POS leaves these fields out. With the menu.* events you see what was changed in the dashboard.
Still unclear? Write to support@flavoso.com and include the id from X-Request-Id. It lets us find your request in the log.