Menu
Read the whole menu, sync it in one call with PUT /menu, report sold-out items and create, change and delete categories, products, option groups and options one by one. More under Menu sync.
Endpoints
- get
/menuRetrieve the full menu - put
/menuSync the whole menu from your system - post
/menu/availabilityMark many products and options sold out or available - get
/menu/categoriesList categories - post
/menu/categoriesCreate a category - get
/menu/categories/{id}Retrieve a category - patch
/menu/categories/{id}Update a category - delete
/menu/categories/{id}Delete a category - get
/menu/productsList products - post
/menu/productsCreate a product - get
/menu/products/{id}Retrieve a product - patch
/menu/products/{id}Update a product - delete
/menu/products/{id}Delete a product - get
/menu/option-groupsList option groups - post
/menu/option-groupsCreate an option group - get
/menu/option-groups/{id}Retrieve an option group - patch
/menu/option-groups/{id}Update an option group - delete
/menu/option-groups/{id}Delete an option group - post
/menu/option-groups/{id}/optionsAdd an option to an option group - get
/menu/optionsList options - get
/menu/options/{id}Retrieve an option - patch
/menu/options/{id}Update an option - delete
/menu/options/{id}Delete an option
Retrieve the full menu
get/menu
- Scope:
menu:read- Plan:
- every plan
Categories, products and option groups as flat lists with references, including everything that is currently sold out. Each object carries your external_id (or null) and updated_at. For large menus that change often, poll the lists with updated_since instead: GET /menu/products and GET /menu/options (an option's surcharge or sold-out state does not change its product's updated_at).
Response200
OK
objectstringrequiredAlways
menu.Values:
"menu"currencystringrequiredCurrency of all amounts (ISO 4217).
categoriesCategory[]requiredAll categories by position.
Child fields (8)
objectstringrequiredAlways
category.Values:
"category"idstring (uuid)requiredThe category id.
external_idstringrequirednullableYour app's external_id for this category; null if it has none. Other apps never see yours. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
namestringrequiredName as the guest sees it.
max 80 chars
positionintegerrequiredSort order on the menu, ascending.
tax_classstringrequirednullableTax group for the products in it that have none of their own. null:
food.Values:
"food""drink""alcohol"created_atstring (date-time)requiredWhen the category was created. ISO 8601 in UTC (
Z).updated_atstring (date-time)requiredLast change of one of its fields. ISO 8601 in UTC (
Z).
productsProduct[]requiredAll products by position.
Child fields (17)
objectstringrequiredAlways
product.Values:
"product"idstring (uuid)requiredThe product id.
external_idstringrequirednullableYour app's external_id for this product; null if it has none. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
category_idstring (uuid)requirednullableIts category; null: no category.
namestringrequiredName as the guest sees it.
max 200 chars
descriptionstringrequirednullableDescription as the guest sees it.
max 2000 chars
priceintegerrequiredPrice including VAT, without options. Integer amount in the smallest currency unit (cents).
availablebooleanrequiredfalse = sold out: guests cannot order it until it is available again.
positionintegerrequiredSort order within its category, ascending.
allergensstring[]requiredThe 14 EU allergens as keys:
gluten,crustaceans,eggs,fish,peanuts,soybeans,milk,nuts,celery,mustard,sesame,sulphites,lupin,molluscs. Common names in German, English, French, Spanish and Italian are mapped to the key (Soja→soybeans). Anything else is kept as the restaurant's own label and shown verbatim (at most 20 such labels, 40 characters each).max 34 items
dietsstring[]requiredDiet marks:
vegetarian,vegan,gluten_free,lactose_free,halal,kosher(Gluten-freeis mapped togluten_free). Anything else is kept as the restaurant's own label, e.g. "gluten-free on request" (at most 14 such labels, 60 characters each). Never mixed with allergens.image_urlstring (uri)requirednullableThe product photo as Flavoso serves it; null: none.
tax_classstringrequirednullableThe product's own tax group; null: its category's, else
food.Values:
"food""drink""alcohol"tax_ratesTaxRatesrequiredThe resolved VAT rates in basis points.
Child fields (2)
dine_in_bpsintegerrequirednullableRate when eaten in (table orders).
takeaway_bpsintegerrequirednullableRate for pickup and delivery.
option_group_idsstring (uuid)[]requiredIts option groups in the order the guest sees them: groups that belong to this product only first, then shared groups from the option library.
created_atstring (date-time)requiredWhen the product was created. ISO 8601 in UTC (
Z).updated_atstring (date-time)requiredLast change of one of its fields, including its option groups. ISO 8601 in UTC (
Z).
option_groupsOptionGroup[]requiredAll option groups by position, each with its options.
Child fields (10)
objectstringrequiredAlways
option_group.Values:
"option_group"idstring (uuid)requiredThe option group id.
external_idstringrequirednullableYour app's external_id for this group; null if it has none. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
namestringrequiredName as the guest sees it, e.g. Size, Extras.
max 120 chars
min_selectintegerrequiredHow many options the guest must pick at least (0 = optional).
0 to 20
max_selectintegerrequiredHow many options the guest may pick at most.
1 to 20
positionintegerrequiredSort order among groups.
product_idstring (uuid)requirednullableThe product this group belongs to only; null for a shared group.
optionsOption[]requiredIts options in order, including sold-out ones.
Child fields (9)
objectstringrequiredAlways
option.Values:
"option"idstring (uuid)requiredThe option id.
external_idstringrequirednullableYour app's external_id for this option; null if it has none. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
group_idstring (uuid)requiredThe option group it belongs to.
namestringrequiredName as the guest sees it.
max 120 chars
price_deltaintegerrequiredSurcharge, may be negative. Integer amount in the smallest currency unit (cents).
availablebooleanrequiredfalse = sold out on every product that uses the group.
positionintegerrequiredSort order within the group.
updated_atstring (date-time)requiredLast change of one of its fields. ISO 8601 in UTC (
Z).
updated_atstring (date-time)requiredLast change of one of its own fields (not of its options). ISO 8601 in UTC (
Z).
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl "https://api.flavoso.com/v1/menu" \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"object": "menu",
"currency": "EUR",
"categories": [
{
"object": "category",
"id": "8f6b2c1e-4d3a-4b5f-9c7d-2e1a0b9c8d7e",
"external_id": "CAT-PIZZA",
"name": "Pizza",
"position": 2,
"tax_class": "food",
"created_at": "2026-10-06T09:12:00.000Z",
"updated_at": "2026-10-06T11:40:31.204Z"
}
],
"products": [
{
"object": "product",
"id": "3c9a7e5d-1b2f-4a6c-8d0e-7f5a3b1c9e2d",
"external_id": "PLU-1001",
"category_id": "8f6b2c1e-4d3a-4b5f-9c7d-2e1a0b9c8d7e",
"name": "Pizza Margherita",
"description": "Tomato, mozzarella, basil",
"price": 890,
"available": true,
"position": 0,
"allergens": [
"gluten",
"milk"
],
"diets": [
"vegetarian"
],
"image_url": "https://api.flavoso.com/storage/v1/object/public/tenant-assets/products/6f1e2d3c-4b5a-4c69-8d7e-0f1a2b3c4d5e/api-4f3a9c0d5b1e7a2c8d6f0e1b3a5c7d9e.jpg",
"tax_class": "food",
"tax_rates": {
"dine_in_bps": 700,
"takeaway_bps": 700
},
"option_group_ids": [
"5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170"
],
"created_at": "2026-10-06T09:12:00.000Z",
"updated_at": "2026-10-06T11:40:31.204Z"
}
],
"option_groups": [
{
"object": "option_group",
"id": "5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170",
"external_id": "GRP-EXTRAS",
"name": "Extras",
"min_select": 0,
"max_select": 3,
"position": 0,
"product_id": null,
"options": [
{
"object": "option",
"id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"external_id": "MOD-CHEESE",
"group_id": "5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170",
"name": "Extra cheese",
"price_delta": 150,
"available": true,
"position": 0,
"updated_at": "2026-10-06T11:40:31.204Z"
},
{
"object": "option",
"id": "2b3c4d5e-6f70-4182-93a4-b5c6d7e8f901",
"external_id": "MOD-SALAMI",
"group_id": "5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170",
"name": "Salami",
"price_delta": 200,
"available": false,
"position": 1,
"updated_at": "2026-10-06T11:40:31.204Z"
}
],
"updated_at": "2026-10-06T11:40:31.204Z"
}
]
}Sync the whole menu from your system
put/menu
- Scope:
menu:write- Plan:
- every plan
Creates, updates and (with mode: "replace") deletes categories, products, option groups and options so that Flavoso matches your system. The key is your external_id; objects without your id are not deleted or updated, except as a consequence of deleting yours in replace mode: the restaurant's products in one of your deleted categories move to no category, and the restaurant's options in one of your deleted option groups are deleted with it (warning unmanaged_options_deleted). Everything happens in one transaction: either the whole sync applies or nothing does.
Fields you leave out stay as they are, so a system that does not track allergens or sold-out states does not overwrite what the restaurant set. Use dry_run: true to see the outcome first.
Images: new image URLs are downloaded during the sync, at most 25 per request; the rest follow with the next sync (warning images_deferred). A URL that was used before is not downloaded again. A test restaurant keeps at most 50 downloaded images; beyond that, images are skipped (warning images_capped). Resetting the test restaurant or its menu deletes them.
A sync that changes something sends ONE webhook event menu.synced instead of one event per object; it names your connection in connection_id.
At most 10 syncs per minute per connection. Wrong references (an unknown category_external_id or option group) answer 400 with param, e.g. products[3].category_external_id; too many objects answer 422 limit_exceeded; a body larger than 2 MB (counted in bytes) answers 413 payload_too_large.
Bodyapplication/json
modestringrequiredmergecreates and updates;replacealso deletes your objects that are missing.Values:
"merge""replace"dry_runbooleantrue: validate and report what would happen, change nothing. Images are not downloaded.
default false
categoriesMenuSyncCategory[]Your categories.
max 200 items
Child fields (4)
external_idstringrequiredYour id; the key of the sync.
1 to 100 chars
namestringrequiredName as the guest sees it.
1 to 80 chars
positionintegerSort order. Missing: unchanged (new categories go last).
0 to 100000
tax_classstringnullableTax group:
food,drinkoralcohol. Flavoso stores the group, not a percentage: the restaurant sets the rate per group for its country, andtax_ratesshows the result. null means: use the category's group, elsefood. Missing: unchanged.Values:
"food""drink""alcohol"
productsMenuSyncProduct[]Your products.
max 2000 items
Child fields (12)
external_idstringrequiredYour id (article number, PLU); the key of the sync.
1 to 100 chars
category_external_idstringnullableYour id of its category, from
categoriesor synced before; null: no category.1 to 100 chars
namestringrequiredName as the guest sees it.
1 to 200 chars
descriptionstringnullableDescription; null or empty removes it.
max 2000 chars
priceintegerrequiredPrice including VAT in cents.
0 to 1000000
availablebooleanfalse = sold out. Missing: unchanged (new: true).
positionintegerSort order. Missing: unchanged (new products go last).
0 to 100000
allergensstring[]The 14 EU allergens as keys:
gluten,crustaceans,eggs,fish,peanuts,soybeans,milk,nuts,celery,mustard,sesame,sulphites,lupin,molluscs. Common names in German, English, French, Spanish and Italian are mapped to the key (Soja→soybeans). Anything else is kept as the restaurant's own label and shown verbatim (at most 20 such labels, 40 characters each).max 34 items
dietsstring[]Diet marks:
vegetarian,vegan,gluten_free,lactose_free,halal,kosher(Gluten-freeis mapped togluten_free). Anything else is kept as the restaurant's own label, e.g. "gluten-free on request" (at most 14 such labels, 60 characters each). Never mixed with allergens.tax_classstringnullableTax group:
food,drinkoralcohol. Flavoso stores the group, not a percentage: the restaurant sets the rate per group for its country, andtax_ratesshows the result. null means: use the category's group, elsefood.Values:
"food""drink""alcohol"image_urlstring (uri)nullableAn https URL of a PNG, JPEG or WebP image of at most 1 MB. Flavoso downloads it (public hosts only), checks the bytes and stores its own copy; the product then shows Flavoso's URL. The same URL again does not download again; to replace an image under the same URL, add a version parameter (
?v=2). null removes the image.max 1000 chars
option_group_external_idsstring[]Your ids of its option groups, in order. Replaces the product's attachments to groups carrying your ids; attachments to groups the restaurant created itself stay, after yours. Missing: unchanged.
max 50 items
option_groupsMenuSyncOptionGroup[]Your option groups with their options.
max 500 items
Child fields (6)
external_idstringrequiredYour id; the key of the sync.
1 to 100 chars
namestringrequiredName as the guest sees it.
1 to 120 chars
min_selectintegerMissing: unchanged (new: 0).
0 to 20
max_selectintegerMissing: unchanged (new: 1).
1 to 20
positionintegerMissing: unchanged (new groups go last).
0 to 100000
optionsMenuSyncOption[]requiredThe group's options carrying your ids. Options the restaurant added without your id stay.
Child fields (5)
external_idstringrequiredYour id; unique across all options of the sync. An existing option listed under another group moves there.
1 to 100 chars
namestringrequiredName as the guest sees it.
1 to 120 chars
price_deltaintegerSurcharge in cents. Missing: unchanged (new: 0).
-100000 to 100000
availablebooleanMissing: unchanged (new: true), so a sync never undoes a sold-out mark you do not track.
positionintegerSort order. Missing: unchanged (new options go last).
0 to 100000
Response200
OK
objectstringrequiredAlways
menu_sync.Values:
"menu_sync"modestringrequiredThe mode you sent.
Values:
"merge""replace"dry_runbooleanrequiredtrue: nothing was changed.
summaryMenuSyncSummaryrequiredWhat was created, updated, left unchanged and deleted.
Child fields (4)
categoriesMenuSyncCountsrequiredCategories.
Child fields (4)
createdintegerrequiredCreated by this sync.
updatedintegerrequiredChanged by this sync.
unchangedintegerrequiredListed, but already as in the body.
deletedintegerrequiredDeleted by this sync (
replaceonly).
productsMenuSyncCountsrequiredProducts.
Child fields (4)
createdintegerrequiredCreated by this sync.
updatedintegerrequiredChanged by this sync.
unchangedintegerrequiredListed, but already as in the body.
deletedintegerrequiredDeleted by this sync (
replaceonly).
option_groupsMenuSyncCountsrequiredOption groups.
Child fields (4)
createdintegerrequiredCreated by this sync.
updatedintegerrequiredChanged by this sync.
unchangedintegerrequiredListed, but already as in the body.
deletedintegerrequiredDeleted by this sync (
replaceonly).
optionsMenuSyncCountsrequiredOptions.
Child fields (4)
createdintegerrequiredCreated by this sync.
updatedintegerrequiredChanged by this sync.
unchangedintegerrequiredListed, but already as in the body.
deletedintegerrequiredDeleted by this sync (
replaceonly).
warningsMenuSyncWarning[]requiredEmpty when all went as asked. Comes before
ids, so it survives a truncated log.Child fields (3)
codestringrequiredimage_failed: an image could not be used (the product keeps its image).images_deferred: more new images than one sync fetches; sync again.images_not_checked: dry run.images_capped: only in a test restaurant, which keeps at most 50 images downloaded fromimage_url; the rest was not downloaded. Resetting the test restaurant or its menu in the developer console deletes them.unmanaged_kept: objects without your id that areplacekept.unmanaged_options_deleted: options without your id that went with a deleted group (inreplacemode also the restaurant's own options in one of your groups).own_option_group_kept: a group that belongs to a product only stays attached.option_group_released: a group of a deleted product became a shared group.Values:
"image_failed""images_deferred""images_not_checked""images_capped""unmanaged_kept""unmanaged_options_deleted""own_option_group_kept""option_group_released"messagestringrequiredExplanation for developers; may change.
paramstringThe field it concerns, e.g.
products[3].image_url, when there is one.
idsobjectrequiredFlavoso's id for each of your ids, to store on your side (webhooks and orders carry Flavoso's ids).
Child fields (4)
categoriesMenuSyncIdPair[]requiredIn the order of the body.
Child fields (2)
external_idstringrequiredYour id.
idstring (uuid)requirednullableFlavoso's id; null in a dry run for objects that would be created.
productsMenuSyncIdPair[]requiredIn the order of the body.
Child fields (2)
external_idstringrequiredYour id.
idstring (uuid)requirednullableFlavoso's id; null in a dry run for objects that would be created.
option_groupsMenuSyncIdPair[]requiredIn the order of the body.
Child fields (2)
external_idstringrequiredYour id.
idstring (uuid)requirednullableFlavoso's id; null in a dry run for objects that would be created.
optionsMenuSyncIdPair[]requiredIn the order of the body.
Child fields (2)
external_idstringrequiredYour id.
idstring (uuid)requirednullableFlavoso's id; null in a dry run for objects that would be created.
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.409invalid_transition(the status of the object does not allow this),stage_not_used(the restaurant does not use this status),external_id_taken(your app already uses thisexternal_idfor another object of the same kind in this restaurant),slot_unavailable(the time or the table is taken),idempotency_in_progress(a request with this Idempotency-Key is still running) orconflict(the same data was changed elsewhere at the same moment, three times in a row; retry unchanged afterRetry-Afterseconds).413payload_too_large: the body is larger than the operation takes, counted in bytes: 64 KB, 2 MB forPUT /menu. Split the request.422idempotency_key_reused(the key was used with a different request) orlimit_exceeded(too many entries in one request, such as products inPUT /menu, or a limit of the restaurant is reached, such as 60 special days).429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl -X PUT "https://api.flavoso.com/v1/menu" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mode": "replace",
"categories": [
{
"external_id": "CAT-PIZZA",
"name": "Pizza",
"position": 0,
"tax_class": "food"
}
],
"option_groups": [
{
"external_id": "GRP-EXTRAS",
"name": "Extras",
"min_select": 0,
"max_select": 3,
"options": [
{
"external_id": "MOD-CHEESE",
"name": "Extra cheese",
"price_delta": 150
},
{
"external_id": "MOD-SALAMI",
"name": "Salami",
"price_delta": 200
}
]
}
],
"products": [
{
"external_id": "PLU-1001",
"category_external_id": "CAT-PIZZA",
"name": "Pizza Margherita",
"description": "Tomato, mozzarella, basil",
"price": 890,
"allergens": [
"gluten",
"milk"
],
"diets": [
"vegetarian"
],
"image_url": "https://example.com/pos/images/plu-1001.jpg?v=3",
"option_group_external_ids": [
"GRP-EXTRAS"
]
}
]
}'{
"mode": "replace",
"categories": [
{
"external_id": "CAT-PIZZA",
"name": "Pizza",
"position": 0,
"tax_class": "food"
}
],
"option_groups": [
{
"external_id": "GRP-EXTRAS",
"name": "Extras",
"min_select": 0,
"max_select": 3,
"options": [
{
"external_id": "MOD-CHEESE",
"name": "Extra cheese",
"price_delta": 150
},
{
"external_id": "MOD-SALAMI",
"name": "Salami",
"price_delta": 200
}
]
}
],
"products": [
{
"external_id": "PLU-1001",
"category_external_id": "CAT-PIZZA",
"name": "Pizza Margherita",
"description": "Tomato, mozzarella, basil",
"price": 890,
"allergens": [
"gluten",
"milk"
],
"diets": [
"vegetarian"
],
"image_url": "https://example.com/pos/images/plu-1001.jpg?v=3",
"option_group_external_ids": [
"GRP-EXTRAS"
]
}
]
}{
"object": "menu_sync",
"mode": "replace",
"dry_run": false,
"summary": {
"categories": {
"created": 0,
"updated": 1,
"unchanged": 7,
"deleted": 0
},
"products": {
"created": 2,
"updated": 5,
"unchanged": 113,
"deleted": 1
},
"option_groups": {
"created": 0,
"updated": 0,
"unchanged": 12,
"deleted": 0
},
"options": {
"created": 1,
"updated": 0,
"unchanged": 64,
"deleted": 0
}
},
"warnings": [
{
"code": "unmanaged_kept",
"message": "Kept 3 products without your external_id (created in Flavoso or by another app). A sync never deletes those."
}
],
"ids": {
"categories": [
{
"external_id": "CAT-PIZZA",
"id": "8f6b2c1e-4d3a-4b5f-9c7d-2e1a0b9c8d7e"
}
],
"products": [
{
"external_id": "PLU-1001",
"id": "3c9a7e5d-1b2f-4a6c-8d0e-7f5a3b1c9e2d"
}
],
"option_groups": [
{
"external_id": "GRP-EXTRAS",
"id": "5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170"
}
],
"options": [
{
"external_id": "MOD-CHEESE",
"id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d"
},
{
"external_id": "MOD-SALAMI",
"id": "2b3c4d5e-6f70-4182-93a4-b5c6d7e8f901"
}
]
}
}Mark many products and options sold out or available
post/menu/availability
- Scope:
menu:write- Plan:
- every plan
Up to 500 target states in one call, by id or your external_id. All or nothing. Each object that changes sends its own menu.product.updated or menu.option.updated.
Parameters
| Name | In | Description |
|---|---|---|
Idempotency-Keystring | header | Any unique string up to 255 printable characters. Retrying a POST with the same key returns the first response (header Idempotent-Replayed: true) instead of acting twice; the same key with a different body is 422 idempotency_key_reused. Kept for 24 hours. Answers that ask for a retry (5xx, 409 conflict) are not kept. While the first request runs, a retry gets 409 idempotency_in_progress; a first request that never finished frees the key after 2 minutes.max 255 chars |
Bodyapplication/json
itemsMenuAvailabilityItem[]requiredEach object at most once.
max 500 items · min 1 items
Child fields (4)
typestringrequiredWhat it is.
Values:
"product""option"idstring (uuid)Flavoso's id.
external_idstringYour id.
1 to 100 chars
availablebooleanrequiredfalse = sold out, true = available again.
Response200
OK
objectstringrequiredAlways
menu_availability.Values:
"menu_availability"updatedintegerrequiredItems whose state changed.
unchangedintegerrequiredItems that already had this state.
itemsobject[]requiredIn the order of the request.
Child fields (5)
typestringrequiredWhat it is.
Values:
"product""option"idstring (uuid)requiredFlavoso's id.
external_idstringrequirednullableYour id, if the object has one.
availablebooleanrequiredThe state now.
changedbooleanrequiredWhether this request changed it.
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.404not_found: no such object in this restaurant (objects of other restaurants are never found).409invalid_transition(the status of the object does not allow this),stage_not_used(the restaurant does not use this status),external_id_taken(your app already uses thisexternal_idfor another object of the same kind in this restaurant),slot_unavailable(the time or the table is taken),idempotency_in_progress(a request with this Idempotency-Key is still running) orconflict(the same data was changed elsewhere at the same moment, three times in a row; retry unchanged afterRetry-Afterseconds).413payload_too_large: the body is larger than the operation takes, counted in bytes: 64 KB, 2 MB forPUT /menu. Split the request.422idempotency_key_reused(the key was used with a different request) orlimit_exceeded(too many entries in one request, such as products inPUT /menu, or a limit of the restaurant is reached, such as 60 special days).429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl -X POST "https://api.flavoso.com/v1/menu/availability" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"type": "product",
"external_id": "PLU-1001",
"available": false
},
{
"type": "option",
"id": "2b3c4d5e-6f70-4182-93a4-b5c6d7e8f901",
"available": true
}
]
}'{
"items": [
{
"type": "product",
"external_id": "PLU-1001",
"available": false
},
{
"type": "option",
"id": "2b3c4d5e-6f70-4182-93a4-b5c6d7e8f901",
"available": true
}
]
}{
"object": "menu_availability",
"updated": 1,
"unchanged": 1,
"items": [
{
"type": "product",
"id": "3c9a7e5d-1b2f-4a6c-8d0e-7f5a3b1c9e2d",
"external_id": "PLU-1001",
"available": false,
"changed": true
},
{
"type": "option",
"id": "2b3c4d5e-6f70-4182-93a4-b5c6d7e8f901",
"external_id": "MOD-SALAMI",
"available": true,
"changed": false
}
]
}List categories
get/menu/categories
- Scope:
menu:read- Plan:
- every plan
Sorted by updated_at ascending. GET /menu returns them by position.
Parameters
| Name | In | Description |
|---|---|---|
limitinteger | query | Page size, 1 to 100. Default 25.1 to 100 · default 25 |
cursorstring | query | next_cursor of the previous page. Opaque. Send the same filters again with it; the cursor only remembers where the last page ended. |
updated_sincestring (date-time) | query | Only objects changed after this instant (exclusive; ISO 8601 with time zone, write + as %2B in a URL). Lists are always sorted by updated_at ascending, and an object changed while you page through moves to the end and shows up again. Poll with an overlap: pass the updated_at of the last object you processed minus a minute and deduplicate by id + updated_at, because a change committed at the same moment as your previous poll can otherwise be missed. |
external_idstring | query | Only the object that carries this external_id of your app (zero or one result).max 100 chars |
Response200
OK
objectstringrequiredAlways
list.Values:
"list"dataCategory[]requiredThis page, oldest change first.
Child fields (8)
objectstringrequiredAlways
category.Values:
"category"idstring (uuid)requiredThe category id.
external_idstringrequirednullableYour app's external_id for this category; null if it has none. Other apps never see yours. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
namestringrequiredName as the guest sees it.
max 80 chars
positionintegerrequiredSort order on the menu, ascending.
tax_classstringrequirednullableTax group for the products in it that have none of their own. null:
food.Values:
"food""drink""alcohol"created_atstring (date-time)requiredWhen the category was created. ISO 8601 in UTC (
Z).updated_atstring (date-time)requiredLast change of one of its fields. ISO 8601 in UTC (
Z).
has_morebooleanrequiredWhether another page follows.
next_cursorstringrequirednullablePass as
cursorfor the next page; null on the last page.
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl "https://api.flavoso.com/v1/menu/categories" \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"object": "list",
"data": [
{
"object": "category",
"id": "8f6b2c1e-4d3a-4b5f-9c7d-2e1a0b9c8d7e",
"external_id": "CAT-PIZZA",
"name": "Pizza",
"position": 2,
"tax_class": "food",
"created_at": "2026-10-06T09:12:00.000Z",
"updated_at": "2026-10-06T11:40:31.204Z"
}
],
"has_more": false,
"next_cursor": null
}Create a category
post/menu/categories
- Scope:
menu:write- Plan:
- every plan
Without position, the category goes last, as in the dashboard. With external_id, 409 external_id_taken if another object of the same type in this restaurant already carries it for your app.
Parameters
| Name | In | Description |
|---|---|---|
Idempotency-Keystring | header | Any unique string up to 255 printable characters. Retrying a POST with the same key returns the first response (header Idempotent-Replayed: true) instead of acting twice; the same key with a different body is 422 idempotency_key_reused. Kept for 24 hours. Answers that ask for a retry (5xx, 409 conflict) are not kept. While the first request runs, a retry gets 409 idempotency_in_progress; a first request that never finished frees the key after 2 minutes.max 255 chars |
Bodyapplication/json
namestringrequiredName as the guest sees it.
1 to 80 chars
positionintegerSort order. Default: after the last category.
0 to 100000
tax_classstringnullableTax group:
food,drinkoralcohol. Flavoso stores the group, not a percentage: the restaurant sets the rate per group for its country, andtax_ratesshows the result. null means: use the category's group, elsefood.Values:
"food""drink""alcohol"external_idstringYour own id for this category; 409
external_id_takenif another category of this restaurant already has it.1 to 100 chars
Response201
OK
objectstringrequiredAlways
category.Values:
"category"idstring (uuid)requiredThe category id.
external_idstringrequirednullableYour app's external_id for this category; null if it has none. Other apps never see yours. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
namestringrequiredName as the guest sees it.
max 80 chars
positionintegerrequiredSort order on the menu, ascending.
tax_classstringrequirednullableTax group for the products in it that have none of their own. null:
food.Values:
"food""drink""alcohol"created_atstring (date-time)requiredWhen the category was created. ISO 8601 in UTC (
Z).updated_atstring (date-time)requiredLast change of one of its fields. ISO 8601 in UTC (
Z).
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.404not_found: no such object in this restaurant (objects of other restaurants are never found).409invalid_transition(the status of the object does not allow this),stage_not_used(the restaurant does not use this status),external_id_taken(your app already uses thisexternal_idfor another object of the same kind in this restaurant),slot_unavailable(the time or the table is taken),idempotency_in_progress(a request with this Idempotency-Key is still running) orconflict(the same data was changed elsewhere at the same moment, three times in a row; retry unchanged afterRetry-Afterseconds).413payload_too_large: the body is larger than the operation takes, counted in bytes: 64 KB, 2 MB forPUT /menu. Split the request.422idempotency_key_reused(the key was used with a different request) orlimit_exceeded(too many entries in one request, such as products inPUT /menu, or a limit of the restaurant is reached, such as 60 special days).429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl -X POST "https://api.flavoso.com/v1/menu/categories" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"name": "Pizza",
"external_id": "CAT-PIZZA",
"tax_class": "food"
}'{
"name": "Pizza",
"external_id": "CAT-PIZZA",
"tax_class": "food"
}{
"object": "category",
"id": "8f6b2c1e-4d3a-4b5f-9c7d-2e1a0b9c8d7e",
"external_id": "CAT-PIZZA",
"name": "Pizza",
"position": 2,
"tax_class": "food",
"created_at": "2026-10-06T09:12:00.000Z",
"updated_at": "2026-10-06T11:40:31.204Z"
}Retrieve a category
get/menu/categories/{id}
- Scope:
menu:read- Plan:
- every plan
Parameters
| Name | In | Description |
|---|---|---|
idstring (uuid)required | path | The category id (UUID). |
Response200
OK
objectstringrequiredAlways
category.Values:
"category"idstring (uuid)requiredThe category id.
external_idstringrequirednullableYour app's external_id for this category; null if it has none. Other apps never see yours. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
namestringrequiredName as the guest sees it.
max 80 chars
positionintegerrequiredSort order on the menu, ascending.
tax_classstringrequirednullableTax group for the products in it that have none of their own. null:
food.Values:
"food""drink""alcohol"created_atstring (date-time)requiredWhen the category was created. ISO 8601 in UTC (
Z).updated_atstring (date-time)requiredLast change of one of its fields. ISO 8601 in UTC (
Z).
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.404not_found: no such object in this restaurant (objects of other restaurants are never found).429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl "https://api.flavoso.com/v1/menu/categories/CATEGORY_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"object": "category",
"id": "8f6b2c1e-4d3a-4b5f-9c7d-2e1a0b9c8d7e",
"external_id": "CAT-PIZZA",
"name": "Pizza",
"position": 2,
"tax_class": "food",
"created_at": "2026-10-06T09:12:00.000Z",
"updated_at": "2026-10-06T11:40:31.204Z"
}Update a category
patch/menu/categories/{id}
- Scope:
menu:write- Plan:
- every plan
Only the fields you send change. With external_id, 409 external_id_taken if another object of the same type in this restaurant already carries it for your app.
Parameters
| Name | In | Description |
|---|---|---|
idstring (uuid)required | path | The category id (UUID). |
Bodyapplication/json
namestringNew name.
1 to 80 chars
positionintegerNew sort order.
0 to 100000
tax_classstringnullableTax group:
food,drinkoralcohol. Flavoso stores the group, not a percentage: the restaurant sets the rate per group for its country, andtax_ratesshows the result. null means: use the category's group, elsefood.Values:
"food""drink""alcohol"external_idstringnullableSet or change your id; null removes it.
1 to 100 chars
Response200
OK
objectstringrequiredAlways
category.Values:
"category"idstring (uuid)requiredThe category id.
external_idstringrequirednullableYour app's external_id for this category; null if it has none. Other apps never see yours. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
namestringrequiredName as the guest sees it.
max 80 chars
positionintegerrequiredSort order on the menu, ascending.
tax_classstringrequirednullableTax group for the products in it that have none of their own. null:
food.Values:
"food""drink""alcohol"created_atstring (date-time)requiredWhen the category was created. ISO 8601 in UTC (
Z).updated_atstring (date-time)requiredLast change of one of its fields. ISO 8601 in UTC (
Z).
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.404not_found: no such object in this restaurant (objects of other restaurants are never found).409invalid_transition(the status of the object does not allow this),stage_not_used(the restaurant does not use this status),external_id_taken(your app already uses thisexternal_idfor another object of the same kind in this restaurant),slot_unavailable(the time or the table is taken),idempotency_in_progress(a request with this Idempotency-Key is still running) orconflict(the same data was changed elsewhere at the same moment, three times in a row; retry unchanged afterRetry-Afterseconds).413payload_too_large: the body is larger than the operation takes, counted in bytes: 64 KB, 2 MB forPUT /menu. Split the request.422idempotency_key_reused(the key was used with a different request) orlimit_exceeded(too many entries in one request, such as products inPUT /menu, or a limit of the restaurant is reached, such as 60 special days).429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl -X PATCH "https://api.flavoso.com/v1/menu/categories/CATEGORY_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Pizza & Pinsa"
}'{
"name": "Pizza & Pinsa"
}{
"object": "category",
"id": "8f6b2c1e-4d3a-4b5f-9c7d-2e1a0b9c8d7e",
"external_id": "CAT-PIZZA",
"name": "Pizza & Pinsa",
"position": 2,
"tax_class": "food",
"created_at": "2026-10-06T09:12:00.000Z",
"updated_at": "2026-10-06T11:40:31.204Z"
}Delete a category
delete/menu/categories/{id}
- Scope:
menu:write- Plan:
- every plan
As in the dashboard: its products move to no category, or with with_products=true they are deleted with it (all of them, also those without your external_id). Orders keep their items.
Parameters
| Name | In | Description |
|---|---|---|
idstring (uuid)required | path | The category id (UUID). |
with_productsboolean | query | true: delete the category's products too. Default false.default false |
Response200
OK
objectstringrequiredAlways
category.Values:
"category"idstring (uuid)requiredThe deleted category.
deletedbooleanrequiredAlways true.
Values:
trueproducts_deletedintegerrequiredProducts deleted with it (
with_products=true).products_uncategorizedintegerrequiredProducts that are now without a category.
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.404not_found: no such object in this restaurant (objects of other restaurants are never found).409invalid_transition(the status of the object does not allow this),stage_not_used(the restaurant does not use this status),external_id_taken(your app already uses thisexternal_idfor another object of the same kind in this restaurant),slot_unavailable(the time or the table is taken),idempotency_in_progress(a request with this Idempotency-Key is still running) orconflict(the same data was changed elsewhere at the same moment, three times in a row; retry unchanged afterRetry-Afterseconds).429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl -X DELETE "https://api.flavoso.com/v1/menu/categories/CATEGORY_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"object": "category",
"id": "8f6b2c1e-4d3a-4b5f-9c7d-2e1a0b9c8d7e",
"deleted": true,
"products_deleted": 0,
"products_uncategorized": 4
}List products
get/menu/products
- Scope:
menu:read- Plan:
- every plan
Sorted by updated_at ascending, which is the way to poll: remember the updated_at of the last product you processed and pass it, minus a minute of overlap, as the next updated_since, deduplicating by id + updated_at. A product counts as changed when one of its fields or the list of its option groups change; a changed option does not change the product, poll GET /menu/options for those.
Parameters
| Name | In | Description |
|---|---|---|
limitinteger | query | Page size, 1 to 100. Default 25.1 to 100 · default 25 |
cursorstring | query | next_cursor of the previous page. Opaque. Send the same filters again with it; the cursor only remembers where the last page ended. |
updated_sincestring (date-time) | query | Only objects changed after this instant (exclusive; ISO 8601 with time zone, write + as %2B in a URL). Lists are always sorted by updated_at ascending, and an object changed while you page through moves to the end and shows up again. Poll with an overlap: pass the updated_at of the last object you processed minus a minute and deduplicate by id + updated_at, because a change committed at the same moment as your previous poll can otherwise be missed. |
external_idstring | query | Only the object that carries this external_id of your app (zero or one result).max 100 chars |
category_idstring | query | Only products in this category; none for products without a category. |
availableboolean | query | false: only sold-out products; true: only available ones. |
Response200
OK
objectstringrequiredAlways
list.Values:
"list"dataProduct[]requiredThis page, oldest change first.
Child fields (17)
objectstringrequiredAlways
product.Values:
"product"idstring (uuid)requiredThe product id.
external_idstringrequirednullableYour app's external_id for this product; null if it has none. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
category_idstring (uuid)requirednullableIts category; null: no category.
namestringrequiredName as the guest sees it.
max 200 chars
descriptionstringrequirednullableDescription as the guest sees it.
max 2000 chars
priceintegerrequiredPrice including VAT, without options. Integer amount in the smallest currency unit (cents).
availablebooleanrequiredfalse = sold out: guests cannot order it until it is available again.
positionintegerrequiredSort order within its category, ascending.
allergensstring[]requiredThe 14 EU allergens as keys:
gluten,crustaceans,eggs,fish,peanuts,soybeans,milk,nuts,celery,mustard,sesame,sulphites,lupin,molluscs. Common names in German, English, French, Spanish and Italian are mapped to the key (Soja→soybeans). Anything else is kept as the restaurant's own label and shown verbatim (at most 20 such labels, 40 characters each).max 34 items
dietsstring[]requiredDiet marks:
vegetarian,vegan,gluten_free,lactose_free,halal,kosher(Gluten-freeis mapped togluten_free). Anything else is kept as the restaurant's own label, e.g. "gluten-free on request" (at most 14 such labels, 60 characters each). Never mixed with allergens.image_urlstring (uri)requirednullableThe product photo as Flavoso serves it; null: none.
tax_classstringrequirednullableThe product's own tax group; null: its category's, else
food.Values:
"food""drink""alcohol"tax_ratesTaxRatesrequiredThe resolved VAT rates in basis points.
Child fields (2)
dine_in_bpsintegerrequirednullableRate when eaten in (table orders).
takeaway_bpsintegerrequirednullableRate for pickup and delivery.
option_group_idsstring (uuid)[]requiredIts option groups in the order the guest sees them: groups that belong to this product only first, then shared groups from the option library.
created_atstring (date-time)requiredWhen the product was created. ISO 8601 in UTC (
Z).updated_atstring (date-time)requiredLast change of one of its fields, including its option groups. ISO 8601 in UTC (
Z).
has_morebooleanrequiredWhether another page follows.
next_cursorstringrequirednullablePass as
cursorfor the next page; null on the last page.
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl "https://api.flavoso.com/v1/menu/products" \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"object": "list",
"data": [
{
"object": "product",
"id": "3c9a7e5d-1b2f-4a6c-8d0e-7f5a3b1c9e2d",
"external_id": "PLU-1001",
"category_id": "8f6b2c1e-4d3a-4b5f-9c7d-2e1a0b9c8d7e",
"name": "Pizza Margherita",
"description": "Tomato, mozzarella, basil",
"price": 890,
"available": true,
"position": 0,
"allergens": [
"gluten",
"milk"
],
"diets": [
"vegetarian"
],
"image_url": "https://api.flavoso.com/storage/v1/object/public/tenant-assets/products/6f1e2d3c-4b5a-4c69-8d7e-0f1a2b3c4d5e/api-4f3a9c0d5b1e7a2c8d6f0e1b3a5c7d9e.jpg",
"tax_class": "food",
"tax_rates": {
"dine_in_bps": 700,
"takeaway_bps": 700
},
"option_group_ids": [
"5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170"
],
"created_at": "2026-10-06T09:12:00.000Z",
"updated_at": "2026-10-06T11:40:31.204Z"
}
],
"has_more": false,
"next_cursor": null
}Create a product
post/menu/products
- Scope:
menu:write- Plan:
- every plan
Without position, the product goes last. image_url is downloaded before the product is created; if that fails, nothing is created (400 with param: "image_url"). With external_id, 409 external_id_taken if another object of the same type in this restaurant already carries it for your app.
Parameters
| Name | In | Description |
|---|---|---|
Idempotency-Keystring | header | Any unique string up to 255 printable characters. Retrying a POST with the same key returns the first response (header Idempotent-Replayed: true) instead of acting twice; the same key with a different body is 422 idempotency_key_reused. Kept for 24 hours. Answers that ask for a retry (5xx, 409 conflict) are not kept. While the first request runs, a retry gets 409 idempotency_in_progress; a first request that never finished frees the key after 2 minutes.max 255 chars |
Bodyapplication/json
namestringrequiredName as the guest sees it.
1 to 200 chars
priceintegerrequiredPrice including VAT in the smallest currency unit (cents).
0 to 1000000
category_idstring (uuid)nullableIts category; null or missing: no category.
descriptionstringnullableDescription; empty means none.
max 2000 chars
availablebooleanfalse = sold out. Default true.
positionintegerSort order. Default: after the last product.
0 to 100000
allergensstring[]The 14 EU allergens as keys:
gluten,crustaceans,eggs,fish,peanuts,soybeans,milk,nuts,celery,mustard,sesame,sulphites,lupin,molluscs. Common names in German, English, French, Spanish and Italian are mapped to the key (Soja→soybeans). Anything else is kept as the restaurant's own label and shown verbatim (at most 20 such labels, 40 characters each).max 34 items
dietsstring[]Diet marks:
vegetarian,vegan,gluten_free,lactose_free,halal,kosher(Gluten-freeis mapped togluten_free). Anything else is kept as the restaurant's own label, e.g. "gluten-free on request" (at most 14 such labels, 60 characters each). Never mixed with allergens.tax_classstringnullableTax group:
food,drinkoralcohol. Flavoso stores the group, not a percentage: the restaurant sets the rate per group for its country, andtax_ratesshows the result. null means: use the category's group, elsefood.Values:
"food""drink""alcohol"image_urlstring (uri)nullableAn https URL of a PNG, JPEG or WebP image of at most 1 MB. Flavoso downloads it (public hosts only), checks the bytes and stores its own copy; the product then shows Flavoso's URL. The same URL again does not download again; to replace an image under the same URL, add a version parameter (
?v=2). null removes the image.max 1000 chars
option_group_idsstring (uuid)[]Shared option groups to attach, in order.
max 50 items
external_idstringYour own id for this product; 409
external_id_takenif another product of this restaurant already has it.1 to 100 chars
Response201
OK
objectstringrequiredAlways
product.Values:
"product"idstring (uuid)requiredThe product id.
external_idstringrequirednullableYour app's external_id for this product; null if it has none. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
category_idstring (uuid)requirednullableIts category; null: no category.
namestringrequiredName as the guest sees it.
max 200 chars
descriptionstringrequirednullableDescription as the guest sees it.
max 2000 chars
priceintegerrequiredPrice including VAT, without options. Integer amount in the smallest currency unit (cents).
availablebooleanrequiredfalse = sold out: guests cannot order it until it is available again.
positionintegerrequiredSort order within its category, ascending.
allergensstring[]requiredThe 14 EU allergens as keys:
gluten,crustaceans,eggs,fish,peanuts,soybeans,milk,nuts,celery,mustard,sesame,sulphites,lupin,molluscs. Common names in German, English, French, Spanish and Italian are mapped to the key (Soja→soybeans). Anything else is kept as the restaurant's own label and shown verbatim (at most 20 such labels, 40 characters each).max 34 items
dietsstring[]requiredDiet marks:
vegetarian,vegan,gluten_free,lactose_free,halal,kosher(Gluten-freeis mapped togluten_free). Anything else is kept as the restaurant's own label, e.g. "gluten-free on request" (at most 14 such labels, 60 characters each). Never mixed with allergens.image_urlstring (uri)requirednullableThe product photo as Flavoso serves it; null: none.
tax_classstringrequirednullableThe product's own tax group; null: its category's, else
food.Values:
"food""drink""alcohol"tax_ratesTaxRatesrequiredThe resolved VAT rates in basis points.
Child fields (2)
dine_in_bpsintegerrequirednullableRate when eaten in (table orders).
takeaway_bpsintegerrequirednullableRate for pickup and delivery.
option_group_idsstring (uuid)[]requiredIts option groups in the order the guest sees them: groups that belong to this product only first, then shared groups from the option library.
created_atstring (date-time)requiredWhen the product was created. ISO 8601 in UTC (
Z).updated_atstring (date-time)requiredLast change of one of its fields, including its option groups. ISO 8601 in UTC (
Z).
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.404not_found: no such object in this restaurant (objects of other restaurants are never found).409invalid_transition(the status of the object does not allow this),stage_not_used(the restaurant does not use this status),external_id_taken(your app already uses thisexternal_idfor another object of the same kind in this restaurant),slot_unavailable(the time or the table is taken),idempotency_in_progress(a request with this Idempotency-Key is still running) orconflict(the same data was changed elsewhere at the same moment, three times in a row; retry unchanged afterRetry-Afterseconds).413payload_too_large: the body is larger than the operation takes, counted in bytes: 64 KB, 2 MB forPUT /menu. Split the request.422idempotency_key_reused(the key was used with a different request) orlimit_exceeded(too many entries in one request, such as products inPUT /menu, or a limit of the restaurant is reached, such as 60 special days).429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl -X POST "https://api.flavoso.com/v1/menu/products" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"name": "Pizza Margherita",
"price": 890,
"category_id": "8f6b2c1e-4d3a-4b5f-9c7d-2e1a0b9c8d7e",
"description": "Tomato, mozzarella, basil",
"allergens": [
"gluten",
"milk"
],
"diets": [
"vegetarian"
],
"tax_class": "food",
"option_group_ids": [
"5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170"
],
"external_id": "PLU-1001"
}'{
"name": "Pizza Margherita",
"price": 890,
"category_id": "8f6b2c1e-4d3a-4b5f-9c7d-2e1a0b9c8d7e",
"description": "Tomato, mozzarella, basil",
"allergens": [
"gluten",
"milk"
],
"diets": [
"vegetarian"
],
"tax_class": "food",
"option_group_ids": [
"5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170"
],
"external_id": "PLU-1001"
}{
"object": "product",
"id": "3c9a7e5d-1b2f-4a6c-8d0e-7f5a3b1c9e2d",
"external_id": "PLU-1001",
"category_id": "8f6b2c1e-4d3a-4b5f-9c7d-2e1a0b9c8d7e",
"name": "Pizza Margherita",
"description": "Tomato, mozzarella, basil",
"price": 890,
"available": true,
"position": 0,
"allergens": [
"gluten",
"milk"
],
"diets": [
"vegetarian"
],
"image_url": "https://api.flavoso.com/storage/v1/object/public/tenant-assets/products/6f1e2d3c-4b5a-4c69-8d7e-0f1a2b3c4d5e/api-4f3a9c0d5b1e7a2c8d6f0e1b3a5c7d9e.jpg",
"tax_class": "food",
"tax_rates": {
"dine_in_bps": 700,
"takeaway_bps": 700
},
"option_group_ids": [
"5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170"
],
"created_at": "2026-10-06T09:12:00.000Z",
"updated_at": "2026-10-06T11:40:31.204Z"
}Retrieve a product
get/menu/products/{id}
- Scope:
menu:read- Plan:
- every plan
Parameters
| Name | In | Description |
|---|---|---|
idstring (uuid)required | path | The product id (UUID). |
Response200
OK
objectstringrequiredAlways
product.Values:
"product"idstring (uuid)requiredThe product id.
external_idstringrequirednullableYour app's external_id for this product; null if it has none. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
category_idstring (uuid)requirednullableIts category; null: no category.
namestringrequiredName as the guest sees it.
max 200 chars
descriptionstringrequirednullableDescription as the guest sees it.
max 2000 chars
priceintegerrequiredPrice including VAT, without options. Integer amount in the smallest currency unit (cents).
availablebooleanrequiredfalse = sold out: guests cannot order it until it is available again.
positionintegerrequiredSort order within its category, ascending.
allergensstring[]requiredThe 14 EU allergens as keys:
gluten,crustaceans,eggs,fish,peanuts,soybeans,milk,nuts,celery,mustard,sesame,sulphites,lupin,molluscs. Common names in German, English, French, Spanish and Italian are mapped to the key (Soja→soybeans). Anything else is kept as the restaurant's own label and shown verbatim (at most 20 such labels, 40 characters each).max 34 items
dietsstring[]requiredDiet marks:
vegetarian,vegan,gluten_free,lactose_free,halal,kosher(Gluten-freeis mapped togluten_free). Anything else is kept as the restaurant's own label, e.g. "gluten-free on request" (at most 14 such labels, 60 characters each). Never mixed with allergens.image_urlstring (uri)requirednullableThe product photo as Flavoso serves it; null: none.
tax_classstringrequirednullableThe product's own tax group; null: its category's, else
food.Values:
"food""drink""alcohol"tax_ratesTaxRatesrequiredThe resolved VAT rates in basis points.
Child fields (2)
dine_in_bpsintegerrequirednullableRate when eaten in (table orders).
takeaway_bpsintegerrequirednullableRate for pickup and delivery.
option_group_idsstring (uuid)[]requiredIts option groups in the order the guest sees them: groups that belong to this product only first, then shared groups from the option library.
created_atstring (date-time)requiredWhen the product was created. ISO 8601 in UTC (
Z).updated_atstring (date-time)requiredLast change of one of its fields, including its option groups. ISO 8601 in UTC (
Z).
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.404not_found: no such object in this restaurant (objects of other restaurants are never found).429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl "https://api.flavoso.com/v1/menu/products/PRODUCT_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"object": "product",
"id": "3c9a7e5d-1b2f-4a6c-8d0e-7f5a3b1c9e2d",
"external_id": "PLU-1001",
"category_id": "8f6b2c1e-4d3a-4b5f-9c7d-2e1a0b9c8d7e",
"name": "Pizza Margherita",
"description": "Tomato, mozzarella, basil",
"price": 890,
"available": true,
"position": 0,
"allergens": [
"gluten",
"milk"
],
"diets": [
"vegetarian"
],
"image_url": "https://api.flavoso.com/storage/v1/object/public/tenant-assets/products/6f1e2d3c-4b5a-4c69-8d7e-0f1a2b3c4d5e/api-4f3a9c0d5b1e7a2c8d6f0e1b3a5c7d9e.jpg",
"tax_class": "food",
"tax_rates": {
"dine_in_bps": 700,
"takeaway_bps": 700
},
"option_group_ids": [
"5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170"
],
"created_at": "2026-10-06T09:12:00.000Z",
"updated_at": "2026-10-06T11:40:31.204Z"
}Update a product
patch/menu/products/{id}
- Scope:
menu:write- Plan:
- every plan
Any field: sold out or back (available is a target state, no toggling), price, name, description, category, position, allergens, diets, tax group, image, option groups (the complete list, in order) and your external_id. With external_id, 409 external_id_taken if another object of the same type in this restaurant already carries it for your app.
Parameters
| Name | In | Description |
|---|---|---|
idstring (uuid)required | path | The product id (UUID). |
Bodyapplication/json
namestringNew name.
1 to 200 chars
priceintegerNew price in cents.
0 to 1000000
category_idstring (uuid)nullableMove to this category; null: no category.
descriptionstringnullableNew description; null or empty removes it.
max 2000 chars
availablebooleanfalse = sold out, true = available again.
positionintegerNew sort order.
0 to 100000
allergensstring[]Replaces the list. The 14 EU allergens as keys:
gluten,crustaceans,eggs,fish,peanuts,soybeans,milk,nuts,celery,mustard,sesame,sulphites,lupin,molluscs. Common names in German, English, French, Spanish and Italian are mapped to the key (Soja→soybeans). Anything else is kept as the restaurant's own label and shown verbatim (at most 20 such labels, 40 characters each).max 34 items
dietsstring[]Replaces the list. Diet marks:
vegetarian,vegan,gluten_free,lactose_free,halal,kosher(Gluten-freeis mapped togluten_free). Anything else is kept as the restaurant's own label, e.g. "gluten-free on request" (at most 14 such labels, 60 characters each). Never mixed with allergens.tax_classstringnullableTax group:
food,drinkoralcohol. Flavoso stores the group, not a percentage: the restaurant sets the rate per group for its country, andtax_ratesshows the result. null means: use the category's group, elsefood.Values:
"food""drink""alcohol"image_urlstring (uri)nullableAn https URL of a PNG, JPEG or WebP image of at most 1 MB. Flavoso downloads it (public hosts only), checks the bytes and stores its own copy; the product then shows Flavoso's URL. The same URL again does not download again; to replace an image under the same URL, add a version parameter (
?v=2). null removes the image.max 1000 chars
option_group_idsstring (uuid)[]The complete list of option groups, in order; replaces the current one. A group that belongs to another product only cannot be attached. A group that belongs to this product only (
product_idset, e.g. from the menu import) cannot be detached, only deleted, and such groups always come first.max 50 items
external_idstringnullableSet or change your id; null removes it. Setting it on a product created in Flavoso lets your syncs take it over.
1 to 100 chars
Response200
OK
objectstringrequiredAlways
product.Values:
"product"idstring (uuid)requiredThe product id.
external_idstringrequirednullableYour app's external_id for this product; null if it has none. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
category_idstring (uuid)requirednullableIts category; null: no category.
namestringrequiredName as the guest sees it.
max 200 chars
descriptionstringrequirednullableDescription as the guest sees it.
max 2000 chars
priceintegerrequiredPrice including VAT, without options. Integer amount in the smallest currency unit (cents).
availablebooleanrequiredfalse = sold out: guests cannot order it until it is available again.
positionintegerrequiredSort order within its category, ascending.
allergensstring[]requiredThe 14 EU allergens as keys:
gluten,crustaceans,eggs,fish,peanuts,soybeans,milk,nuts,celery,mustard,sesame,sulphites,lupin,molluscs. Common names in German, English, French, Spanish and Italian are mapped to the key (Soja→soybeans). Anything else is kept as the restaurant's own label and shown verbatim (at most 20 such labels, 40 characters each).max 34 items
dietsstring[]requiredDiet marks:
vegetarian,vegan,gluten_free,lactose_free,halal,kosher(Gluten-freeis mapped togluten_free). Anything else is kept as the restaurant's own label, e.g. "gluten-free on request" (at most 14 such labels, 60 characters each). Never mixed with allergens.image_urlstring (uri)requirednullableThe product photo as Flavoso serves it; null: none.
tax_classstringrequirednullableThe product's own tax group; null: its category's, else
food.Values:
"food""drink""alcohol"tax_ratesTaxRatesrequiredThe resolved VAT rates in basis points.
Child fields (2)
dine_in_bpsintegerrequirednullableRate when eaten in (table orders).
takeaway_bpsintegerrequirednullableRate for pickup and delivery.
option_group_idsstring (uuid)[]requiredIts option groups in the order the guest sees them: groups that belong to this product only first, then shared groups from the option library.
created_atstring (date-time)requiredWhen the product was created. ISO 8601 in UTC (
Z).updated_atstring (date-time)requiredLast change of one of its fields, including its option groups. ISO 8601 in UTC (
Z).
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.404not_found: no such object in this restaurant (objects of other restaurants are never found).409invalid_transition(the status of the object does not allow this),stage_not_used(the restaurant does not use this status),external_id_taken(your app already uses thisexternal_idfor another object of the same kind in this restaurant),slot_unavailable(the time or the table is taken),idempotency_in_progress(a request with this Idempotency-Key is still running) orconflict(the same data was changed elsewhere at the same moment, three times in a row; retry unchanged afterRetry-Afterseconds).413payload_too_large: the body is larger than the operation takes, counted in bytes: 64 KB, 2 MB forPUT /menu. Split the request.422idempotency_key_reused(the key was used with a different request) orlimit_exceeded(too many entries in one request, such as products inPUT /menu, or a limit of the restaurant is reached, such as 60 special days).429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl -X PATCH "https://api.flavoso.com/v1/menu/products/PRODUCT_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"available": false
}'{
"available": false
}{
"object": "product",
"id": "3c9a7e5d-1b2f-4a6c-8d0e-7f5a3b1c9e2d",
"external_id": "PLU-1001",
"category_id": "8f6b2c1e-4d3a-4b5f-9c7d-2e1a0b9c8d7e",
"name": "Pizza Margherita",
"description": "Tomato, mozzarella, basil",
"price": 890,
"available": false,
"position": 0,
"allergens": [
"gluten",
"milk"
],
"diets": [
"vegetarian"
],
"image_url": "https://api.flavoso.com/storage/v1/object/public/tenant-assets/products/6f1e2d3c-4b5a-4c69-8d7e-0f1a2b3c4d5e/api-4f3a9c0d5b1e7a2c8d6f0e1b3a5c7d9e.jpg",
"tax_class": "food",
"tax_rates": {
"dine_in_bps": 700,
"takeaway_bps": 700
},
"option_group_ids": [
"5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170"
],
"created_at": "2026-10-06T09:12:00.000Z",
"updated_at": "2026-10-06T11:40:31.204Z"
}Delete a product
delete/menu/products/{id}
- Scope:
menu:write- Plan:
- every plan
Deleted for good, as in the dashboard. Orders keep their items with the name at the time of ordering; groups that belonged to this product only are deleted with it. A stamp card that gives this product away is switched off.
Parameters
| Name | In | Description |
|---|---|---|
idstring (uuid)required | path | The product id (UUID). |
Response200
OK
objectstringrequiredThe type of the deleted object.
Values:
"product""option_group""option"idstring (uuid)requiredThe deleted object.
deletedbooleanrequiredAlways true.
Values:
true
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.404not_found: no such object in this restaurant (objects of other restaurants are never found).409invalid_transition(the status of the object does not allow this),stage_not_used(the restaurant does not use this status),external_id_taken(your app already uses thisexternal_idfor another object of the same kind in this restaurant),slot_unavailable(the time or the table is taken),idempotency_in_progress(a request with this Idempotency-Key is still running) orconflict(the same data was changed elsewhere at the same moment, three times in a row; retry unchanged afterRetry-Afterseconds).429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl -X DELETE "https://api.flavoso.com/v1/menu/products/PRODUCT_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"object": "product",
"id": "3c9a7e5d-1b2f-4a6c-8d0e-7f5a3b1c9e2d",
"deleted": true
}List option groups
get/menu/option-groups
- Scope:
menu:read- Plan:
- every plan
Each with its options. Sorted by updated_at ascending.
Parameters
| Name | In | Description |
|---|---|---|
limitinteger | query | Page size, 1 to 100. Default 25.1 to 100 · default 25 |
cursorstring | query | next_cursor of the previous page. Opaque. Send the same filters again with it; the cursor only remembers where the last page ended. |
updated_sincestring (date-time) | query | Only objects changed after this instant (exclusive; ISO 8601 with time zone, write + as %2B in a URL). Lists are always sorted by updated_at ascending, and an object changed while you page through moves to the end and shows up again. Poll with an overlap: pass the updated_at of the last object you processed minus a minute and deduplicate by id + updated_at, because a change committed at the same moment as your previous poll can otherwise be missed. |
external_idstring | query | Only the object that carries this external_id of your app (zero or one result).max 100 chars |
Response200
OK
objectstringrequiredAlways
list.Values:
"list"dataOptionGroup[]requiredThis page, oldest change first.
Child fields (10)
objectstringrequiredAlways
option_group.Values:
"option_group"idstring (uuid)requiredThe option group id.
external_idstringrequirednullableYour app's external_id for this group; null if it has none. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
namestringrequiredName as the guest sees it, e.g. Size, Extras.
max 120 chars
min_selectintegerrequiredHow many options the guest must pick at least (0 = optional).
0 to 20
max_selectintegerrequiredHow many options the guest may pick at most.
1 to 20
positionintegerrequiredSort order among groups.
product_idstring (uuid)requirednullableThe product this group belongs to only; null for a shared group.
optionsOption[]requiredIts options in order, including sold-out ones.
Child fields (9)
objectstringrequiredAlways
option.Values:
"option"idstring (uuid)requiredThe option id.
external_idstringrequirednullableYour app's external_id for this option; null if it has none. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
group_idstring (uuid)requiredThe option group it belongs to.
namestringrequiredName as the guest sees it.
max 120 chars
price_deltaintegerrequiredSurcharge, may be negative. Integer amount in the smallest currency unit (cents).
availablebooleanrequiredfalse = sold out on every product that uses the group.
positionintegerrequiredSort order within the group.
updated_atstring (date-time)requiredLast change of one of its fields. ISO 8601 in UTC (
Z).
updated_atstring (date-time)requiredLast change of one of its own fields (not of its options). ISO 8601 in UTC (
Z).
has_morebooleanrequiredWhether another page follows.
next_cursorstringrequirednullablePass as
cursorfor the next page; null on the last page.
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl "https://api.flavoso.com/v1/menu/option-groups" \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"object": "list",
"data": [
{
"object": "option_group",
"id": "5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170",
"external_id": "GRP-EXTRAS",
"name": "Extras",
"min_select": 0,
"max_select": 3,
"position": 0,
"product_id": null,
"options": [
{
"object": "option",
"id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"external_id": "MOD-CHEESE",
"group_id": "5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170",
"name": "Extra cheese",
"price_delta": 150,
"available": true,
"position": 0,
"updated_at": "2026-10-06T11:40:31.204Z"
},
{
"object": "option",
"id": "2b3c4d5e-6f70-4182-93a4-b5c6d7e8f901",
"external_id": "MOD-SALAMI",
"group_id": "5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170",
"name": "Salami",
"price_delta": 200,
"available": false,
"position": 1,
"updated_at": "2026-10-06T11:40:31.204Z"
}
],
"updated_at": "2026-10-06T11:40:31.204Z"
}
],
"has_more": false,
"next_cursor": null
}Create an option group
post/menu/option-groups
- Scope:
menu:write- Plan:
- every plan
Creates a shared group in the option library, optionally with its options. Attach it to products with option_group_ids. With external_id, 409 external_id_taken if another object of the same type in this restaurant already carries it for your app.
Parameters
| Name | In | Description |
|---|---|---|
Idempotency-Keystring | header | Any unique string up to 255 printable characters. Retrying a POST with the same key returns the first response (header Idempotent-Replayed: true) instead of acting twice; the same key with a different body is 422 idempotency_key_reused. Kept for 24 hours. Answers that ask for a retry (5xx, 409 conflict) are not kept. While the first request runs, a retry gets 409 idempotency_in_progress; a first request that never finished frees the key after 2 minutes.max 255 chars |
Bodyapplication/json
namestringrequiredName as the guest sees it.
1 to 120 chars
min_selectintegerDefault 0; must not exceed max_select.
0 to 20
max_selectintegerDefault 1 (or min_select if higher).
1 to 20
positionintegerSort order. Default: after the last group.
0 to 100000
external_idstringYour own id for this group.
1 to 100 chars
optionsOptionInput[]Options to create with the group, in order (at most 100).
max 100 items
Child fields (5)
namestringrequiredName as the guest sees it.
1 to 120 chars
price_deltaintegerSurcharge in cents, may be negative. Default 0.
-100000 to 100000
availablebooleanfalse = sold out. Default true.
positionintegerSort order. Default: after the last option of the group.
0 to 100000
external_idstringYour own id for this option.
1 to 100 chars
Response201
OK
objectstringrequiredAlways
option_group.Values:
"option_group"idstring (uuid)requiredThe option group id.
external_idstringrequirednullableYour app's external_id for this group; null if it has none. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
namestringrequiredName as the guest sees it, e.g. Size, Extras.
max 120 chars
min_selectintegerrequiredHow many options the guest must pick at least (0 = optional).
0 to 20
max_selectintegerrequiredHow many options the guest may pick at most.
1 to 20
positionintegerrequiredSort order among groups.
product_idstring (uuid)requirednullableThe product this group belongs to only; null for a shared group.
optionsOption[]requiredIts options in order, including sold-out ones.
Child fields (9)
objectstringrequiredAlways
option.Values:
"option"idstring (uuid)requiredThe option id.
external_idstringrequirednullableYour app's external_id for this option; null if it has none. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
group_idstring (uuid)requiredThe option group it belongs to.
namestringrequiredName as the guest sees it.
max 120 chars
price_deltaintegerrequiredSurcharge, may be negative. Integer amount in the smallest currency unit (cents).
availablebooleanrequiredfalse = sold out on every product that uses the group.
positionintegerrequiredSort order within the group.
updated_atstring (date-time)requiredLast change of one of its fields. ISO 8601 in UTC (
Z).
updated_atstring (date-time)requiredLast change of one of its own fields (not of its options). ISO 8601 in UTC (
Z).
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.404not_found: no such object in this restaurant (objects of other restaurants are never found).409invalid_transition(the status of the object does not allow this),stage_not_used(the restaurant does not use this status),external_id_taken(your app already uses thisexternal_idfor another object of the same kind in this restaurant),slot_unavailable(the time or the table is taken),idempotency_in_progress(a request with this Idempotency-Key is still running) orconflict(the same data was changed elsewhere at the same moment, three times in a row; retry unchanged afterRetry-Afterseconds).413payload_too_large: the body is larger than the operation takes, counted in bytes: 64 KB, 2 MB forPUT /menu. Split the request.422idempotency_key_reused(the key was used with a different request) orlimit_exceeded(too many entries in one request, such as products inPUT /menu, or a limit of the restaurant is reached, such as 60 special days).429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl -X POST "https://api.flavoso.com/v1/menu/option-groups" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"name": "Extras",
"min_select": 0,
"max_select": 3,
"external_id": "GRP-EXTRAS",
"options": [
{
"name": "Extra cheese",
"price_delta": 150,
"external_id": "MOD-CHEESE"
},
{
"name": "Salami",
"price_delta": 200,
"external_id": "MOD-SALAMI"
}
]
}'{
"name": "Extras",
"min_select": 0,
"max_select": 3,
"external_id": "GRP-EXTRAS",
"options": [
{
"name": "Extra cheese",
"price_delta": 150,
"external_id": "MOD-CHEESE"
},
{
"name": "Salami",
"price_delta": 200,
"external_id": "MOD-SALAMI"
}
]
}{
"object": "option_group",
"id": "5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170",
"external_id": "GRP-EXTRAS",
"name": "Extras",
"min_select": 0,
"max_select": 3,
"position": 0,
"product_id": null,
"options": [
{
"object": "option",
"id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"external_id": "MOD-CHEESE",
"group_id": "5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170",
"name": "Extra cheese",
"price_delta": 150,
"available": true,
"position": 0,
"updated_at": "2026-10-06T11:40:31.204Z"
},
{
"object": "option",
"id": "2b3c4d5e-6f70-4182-93a4-b5c6d7e8f901",
"external_id": "MOD-SALAMI",
"group_id": "5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170",
"name": "Salami",
"price_delta": 200,
"available": false,
"position": 1,
"updated_at": "2026-10-06T11:40:31.204Z"
}
],
"updated_at": "2026-10-06T11:40:31.204Z"
}Retrieve an option group
get/menu/option-groups/{id}
- Scope:
menu:read- Plan:
- every plan
Parameters
| Name | In | Description |
|---|---|---|
idstring (uuid)required | path | The option group id (UUID). |
Response200
OK
objectstringrequiredAlways
option_group.Values:
"option_group"idstring (uuid)requiredThe option group id.
external_idstringrequirednullableYour app's external_id for this group; null if it has none. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
namestringrequiredName as the guest sees it, e.g. Size, Extras.
max 120 chars
min_selectintegerrequiredHow many options the guest must pick at least (0 = optional).
0 to 20
max_selectintegerrequiredHow many options the guest may pick at most.
1 to 20
positionintegerrequiredSort order among groups.
product_idstring (uuid)requirednullableThe product this group belongs to only; null for a shared group.
optionsOption[]requiredIts options in order, including sold-out ones.
Child fields (9)
objectstringrequiredAlways
option.Values:
"option"idstring (uuid)requiredThe option id.
external_idstringrequirednullableYour app's external_id for this option; null if it has none. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
group_idstring (uuid)requiredThe option group it belongs to.
namestringrequiredName as the guest sees it.
max 120 chars
price_deltaintegerrequiredSurcharge, may be negative. Integer amount in the smallest currency unit (cents).
availablebooleanrequiredfalse = sold out on every product that uses the group.
positionintegerrequiredSort order within the group.
updated_atstring (date-time)requiredLast change of one of its fields. ISO 8601 in UTC (
Z).
updated_atstring (date-time)requiredLast change of one of its own fields (not of its options). ISO 8601 in UTC (
Z).
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.404not_found: no such object in this restaurant (objects of other restaurants are never found).429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl "https://api.flavoso.com/v1/menu/option-groups/OPTION_GROUP_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"object": "option_group",
"id": "5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170",
"external_id": "GRP-EXTRAS",
"name": "Extras",
"min_select": 0,
"max_select": 3,
"position": 0,
"product_id": null,
"options": [
{
"object": "option",
"id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"external_id": "MOD-CHEESE",
"group_id": "5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170",
"name": "Extra cheese",
"price_delta": 150,
"available": true,
"position": 0,
"updated_at": "2026-10-06T11:40:31.204Z"
},
{
"object": "option",
"id": "2b3c4d5e-6f70-4182-93a4-b5c6d7e8f901",
"external_id": "MOD-SALAMI",
"group_id": "5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170",
"name": "Salami",
"price_delta": 200,
"available": false,
"position": 1,
"updated_at": "2026-10-06T11:40:31.204Z"
}
],
"updated_at": "2026-10-06T11:40:31.204Z"
}Update an option group
patch/menu/option-groups/{id}
- Scope:
menu:write- Plan:
- every plan
Name, selection limits, position and your external_id. With external_id, 409 external_id_taken if another object of the same type in this restaurant already carries it for your app.
Parameters
| Name | In | Description |
|---|---|---|
idstring (uuid)required | path | The option group id (UUID). |
Bodyapplication/json
namestringNew name.
1 to 120 chars
min_selectintegerNew minimum; must not exceed the (new) maximum.
0 to 20
max_selectintegerNew maximum.
1 to 20
positionintegerNew sort order.
0 to 100000
external_idstringnullableSet or change your id; null removes it.
1 to 100 chars
Response200
OK
objectstringrequiredAlways
option_group.Values:
"option_group"idstring (uuid)requiredThe option group id.
external_idstringrequirednullableYour app's external_id for this group; null if it has none. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
namestringrequiredName as the guest sees it, e.g. Size, Extras.
max 120 chars
min_selectintegerrequiredHow many options the guest must pick at least (0 = optional).
0 to 20
max_selectintegerrequiredHow many options the guest may pick at most.
1 to 20
positionintegerrequiredSort order among groups.
product_idstring (uuid)requirednullableThe product this group belongs to only; null for a shared group.
optionsOption[]requiredIts options in order, including sold-out ones.
Child fields (9)
objectstringrequiredAlways
option.Values:
"option"idstring (uuid)requiredThe option id.
external_idstringrequirednullableYour app's external_id for this option; null if it has none. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
group_idstring (uuid)requiredThe option group it belongs to.
namestringrequiredName as the guest sees it.
max 120 chars
price_deltaintegerrequiredSurcharge, may be negative. Integer amount in the smallest currency unit (cents).
availablebooleanrequiredfalse = sold out on every product that uses the group.
positionintegerrequiredSort order within the group.
updated_atstring (date-time)requiredLast change of one of its fields. ISO 8601 in UTC (
Z).
updated_atstring (date-time)requiredLast change of one of its own fields (not of its options). ISO 8601 in UTC (
Z).
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.404not_found: no such object in this restaurant (objects of other restaurants are never found).409invalid_transition(the status of the object does not allow this),stage_not_used(the restaurant does not use this status),external_id_taken(your app already uses thisexternal_idfor another object of the same kind in this restaurant),slot_unavailable(the time or the table is taken),idempotency_in_progress(a request with this Idempotency-Key is still running) orconflict(the same data was changed elsewhere at the same moment, three times in a row; retry unchanged afterRetry-Afterseconds).413payload_too_large: the body is larger than the operation takes, counted in bytes: 64 KB, 2 MB forPUT /menu. Split the request.422idempotency_key_reused(the key was used with a different request) orlimit_exceeded(too many entries in one request, such as products inPUT /menu, or a limit of the restaurant is reached, such as 60 special days).429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl -X PATCH "https://api.flavoso.com/v1/menu/option-groups/OPTION_GROUP_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"max_select": 2
}'{
"max_select": 2
}{
"object": "option_group",
"id": "5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170",
"external_id": "GRP-EXTRAS",
"name": "Extras",
"min_select": 0,
"max_select": 2,
"position": 0,
"product_id": null,
"options": [
{
"object": "option",
"id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"external_id": "MOD-CHEESE",
"group_id": "5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170",
"name": "Extra cheese",
"price_delta": 150,
"available": true,
"position": 0,
"updated_at": "2026-10-06T11:40:31.204Z"
},
{
"object": "option",
"id": "2b3c4d5e-6f70-4182-93a4-b5c6d7e8f901",
"external_id": "MOD-SALAMI",
"group_id": "5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170",
"name": "Salami",
"price_delta": 200,
"available": false,
"position": 1,
"updated_at": "2026-10-06T11:40:31.204Z"
}
],
"updated_at": "2026-10-06T11:40:31.204Z"
}Delete an option group
delete/menu/option-groups/{id}
- Scope:
menu:write- Plan:
- every plan
As in the dashboard: its options and its attachments to products go with it.
Parameters
| Name | In | Description |
|---|---|---|
idstring (uuid)required | path | The option group id (UUID). |
Response200
OK
objectstringrequiredThe type of the deleted object.
Values:
"product""option_group""option"idstring (uuid)requiredThe deleted object.
deletedbooleanrequiredAlways true.
Values:
true
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.404not_found: no such object in this restaurant (objects of other restaurants are never found).409invalid_transition(the status of the object does not allow this),stage_not_used(the restaurant does not use this status),external_id_taken(your app already uses thisexternal_idfor another object of the same kind in this restaurant),slot_unavailable(the time or the table is taken),idempotency_in_progress(a request with this Idempotency-Key is still running) orconflict(the same data was changed elsewhere at the same moment, three times in a row; retry unchanged afterRetry-Afterseconds).429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl -X DELETE "https://api.flavoso.com/v1/menu/option-groups/OPTION_GROUP_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"object": "option_group",
"id": "5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170",
"deleted": true
}Add an option to an option group
post/menu/option-groups/{id}/options
- Scope:
menu:write- Plan:
- every plan
Without position, the option goes last in the group. With external_id, 409 external_id_taken if another object of the same type in this restaurant already carries it for your app.
Parameters
| Name | In | Description |
|---|---|---|
idstring (uuid)required | path | The option group id (UUID). |
Idempotency-Keystring | header | Any unique string up to 255 printable characters. Retrying a POST with the same key returns the first response (header Idempotent-Replayed: true) instead of acting twice; the same key with a different body is 422 idempotency_key_reused. Kept for 24 hours. Answers that ask for a retry (5xx, 409 conflict) are not kept. While the first request runs, a retry gets 409 idempotency_in_progress; a first request that never finished frees the key after 2 minutes.max 255 chars |
Bodyapplication/json
namestringrequiredName as the guest sees it.
1 to 120 chars
price_deltaintegerSurcharge in cents, may be negative. Default 0.
-100000 to 100000
availablebooleanfalse = sold out. Default true.
positionintegerSort order. Default: after the last option of the group.
0 to 100000
external_idstringYour own id for this option.
1 to 100 chars
Response201
OK
objectstringrequiredAlways
option.Values:
"option"idstring (uuid)requiredThe option id.
external_idstringrequirednullableYour app's external_id for this option; null if it has none. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
group_idstring (uuid)requiredThe option group it belongs to.
namestringrequiredName as the guest sees it.
max 120 chars
price_deltaintegerrequiredSurcharge, may be negative. Integer amount in the smallest currency unit (cents).
availablebooleanrequiredfalse = sold out on every product that uses the group.
positionintegerrequiredSort order within the group.
updated_atstring (date-time)requiredLast change of one of its fields. ISO 8601 in UTC (
Z).
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.404not_found: no such object in this restaurant (objects of other restaurants are never found).409invalid_transition(the status of the object does not allow this),stage_not_used(the restaurant does not use this status),external_id_taken(your app already uses thisexternal_idfor another object of the same kind in this restaurant),slot_unavailable(the time or the table is taken),idempotency_in_progress(a request with this Idempotency-Key is still running) orconflict(the same data was changed elsewhere at the same moment, three times in a row; retry unchanged afterRetry-Afterseconds).413payload_too_large: the body is larger than the operation takes, counted in bytes: 64 KB, 2 MB forPUT /menu. Split the request.422idempotency_key_reused(the key was used with a different request) orlimit_exceeded(too many entries in one request, such as products inPUT /menu, or a limit of the restaurant is reached, such as 60 special days).429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl -X POST "https://api.flavoso.com/v1/menu/option-groups/OPTION_GROUP_ID/options" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"name": "Extra cheese",
"price_delta": 150,
"external_id": "MOD-CHEESE"
}'{
"name": "Extra cheese",
"price_delta": 150,
"external_id": "MOD-CHEESE"
}{
"object": "option",
"id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"external_id": "MOD-CHEESE",
"group_id": "5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170",
"name": "Extra cheese",
"price_delta": 150,
"available": true,
"position": 0,
"updated_at": "2026-10-06T11:40:31.204Z"
}List options
get/menu/options
- Scope:
menu:read- Plan:
- every plan
Every option of every group, sorted by updated_at ascending, with the same paging and polling rules as the other menu lists. The way to follow surcharges and sold-out states of options: a changed option changes neither its product nor its group. Options are created in their group (POST /menu/option-groups/{id}/options).
Parameters
| Name | In | Description |
|---|---|---|
limitinteger | query | Page size, 1 to 100. Default 25.1 to 100 · default 25 |
cursorstring | query | next_cursor of the previous page. Opaque. Send the same filters again with it; the cursor only remembers where the last page ended. |
updated_sincestring (date-time) | query | Only objects changed after this instant (exclusive; ISO 8601 with time zone, write + as %2B in a URL). Lists are always sorted by updated_at ascending, and an object changed while you page through moves to the end and shows up again. Poll with an overlap: pass the updated_at of the last object you processed minus a minute and deduplicate by id + updated_at, because a change committed at the same moment as your previous poll can otherwise be missed. |
external_idstring | query | Only the object that carries this external_id of your app (zero or one result).max 100 chars |
Response200
OK
objectstringrequiredAlways
list.Values:
"list"dataOption[]requiredThis page, oldest change first.
Child fields (9)
objectstringrequiredAlways
option.Values:
"option"idstring (uuid)requiredThe option id.
external_idstringrequirednullableYour app's external_id for this option; null if it has none. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
group_idstring (uuid)requiredThe option group it belongs to.
namestringrequiredName as the guest sees it.
max 120 chars
price_deltaintegerrequiredSurcharge, may be negative. Integer amount in the smallest currency unit (cents).
availablebooleanrequiredfalse = sold out on every product that uses the group.
positionintegerrequiredSort order within the group.
updated_atstring (date-time)requiredLast change of one of its fields. ISO 8601 in UTC (
Z).
has_morebooleanrequiredWhether another page follows.
next_cursorstringrequirednullablePass as
cursorfor the next page; null on the last page.
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl "https://api.flavoso.com/v1/menu/options" \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"object": "list",
"data": [
{
"object": "option",
"id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"external_id": "MOD-CHEESE",
"group_id": "5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170",
"name": "Extra cheese",
"price_delta": 150,
"available": true,
"position": 0,
"updated_at": "2026-10-06T11:40:31.204Z"
}
],
"has_more": false,
"next_cursor": null
}Retrieve an option
get/menu/options/{id}
- Scope:
menu:read- Plan:
- every plan
Parameters
| Name | In | Description |
|---|---|---|
idstring (uuid)required | path | The option id (UUID). |
Response200
OK
objectstringrequiredAlways
option.Values:
"option"idstring (uuid)requiredThe option id.
external_idstringrequirednullableYour app's external_id for this option; null if it has none. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
group_idstring (uuid)requiredThe option group it belongs to.
namestringrequiredName as the guest sees it.
max 120 chars
price_deltaintegerrequiredSurcharge, may be negative. Integer amount in the smallest currency unit (cents).
availablebooleanrequiredfalse = sold out on every product that uses the group.
positionintegerrequiredSort order within the group.
updated_atstring (date-time)requiredLast change of one of its fields. ISO 8601 in UTC (
Z).
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.404not_found: no such object in this restaurant (objects of other restaurants are never found).429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl "https://api.flavoso.com/v1/menu/options/OPTION_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"object": "option",
"id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"external_id": "MOD-CHEESE",
"group_id": "5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170",
"name": "Extra cheese",
"price_delta": 150,
"available": true,
"position": 0,
"updated_at": "2026-10-06T11:40:31.204Z"
}Update an option
patch/menu/options/{id}
- Scope:
menu:write- Plan:
- every plan
Sold out or back (a target state, no toggling), surcharge, name, position, group (group_id moves it) and your external_id. A sold-out option disappears from every product that uses its group. With external_id, 409 external_id_taken if another object of the same type in this restaurant already carries it for your app.
Parameters
| Name | In | Description |
|---|---|---|
idstring (uuid)required | path | The option id (UUID). |
Bodyapplication/json
namestringNew name.
1 to 120 chars
price_deltaintegerNew surcharge in cents, may be negative.
-100000 to 100000
availablebooleanfalse = sold out, true = available again. A target state, no toggling.
positionintegerNew sort order.
0 to 100000
group_idstring (uuid)Move the option into this option group.
external_idstringnullableSet or change your id; null removes it.
1 to 100 chars
Response200
OK
objectstringrequiredAlways
option.Values:
"option"idstring (uuid)requiredThe option id.
external_idstringrequirednullableYour app's external_id for this option; null if it has none. Always null in webhook events and in
GET /events: the envelope is built once and sent the same to every receiver, but this value belongs to a single app. Retrieve the object to see yours.1 to 100 chars
group_idstring (uuid)requiredThe option group it belongs to.
namestringrequiredName as the guest sees it.
max 120 chars
price_deltaintegerrequiredSurcharge, may be negative. Integer amount in the smallest currency unit (cents).
availablebooleanrequiredfalse = sold out on every product that uses the group.
positionintegerrequiredSort order within the group.
updated_atstring (date-time)requiredLast change of one of its fields. ISO 8601 in UTC (
Z).
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.404not_found: no such object in this restaurant (objects of other restaurants are never found).409invalid_transition(the status of the object does not allow this),stage_not_used(the restaurant does not use this status),external_id_taken(your app already uses thisexternal_idfor another object of the same kind in this restaurant),slot_unavailable(the time or the table is taken),idempotency_in_progress(a request with this Idempotency-Key is still running) orconflict(the same data was changed elsewhere at the same moment, three times in a row; retry unchanged afterRetry-Afterseconds).413payload_too_large: the body is larger than the operation takes, counted in bytes: 64 KB, 2 MB forPUT /menu. Split the request.422idempotency_key_reused(the key was used with a different request) orlimit_exceeded(too many entries in one request, such as products inPUT /menu, or a limit of the restaurant is reached, such as 60 special days).429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl -X PATCH "https://api.flavoso.com/v1/menu/options/OPTION_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"available": false
}'{
"available": false
}{
"object": "option",
"id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"external_id": "MOD-CHEESE",
"group_id": "5e4d3c2b-1a09-4f8e-b7d6-c5b4a3928170",
"name": "Extra cheese",
"price_delta": 150,
"available": false,
"position": 0,
"updated_at": "2026-10-06T11:40:31.204Z"
}Delete an option
delete/menu/options/{id}
- Scope:
menu:write- Plan:
- every plan
Parameters
| Name | In | Description |
|---|---|---|
idstring (uuid)required | path | The option id (UUID). |
Response200
OK
objectstringrequiredThe type of the deleted object.
Values:
"product""option_group""option"idstring (uuid)requiredThe deleted object.
deletedbooleanrequiredAlways true.
Values:
true
Errors
400invalid_request: a parameter or the body is wrong, or it contains a query parameter or field the operation does not know.paramnames the field.401unauthorized: missing, malformed, expired or revoked access token, or the app was disconnected or suspended.403insufficient_scope,plan_required(withparamwhen an order type is not in the plan) oraccount_inactive.404not_found: no such object in this restaurant (objects of other restaurants are never found).409invalid_transition(the status of the object does not allow this),stage_not_used(the restaurant does not use this status),external_id_taken(your app already uses thisexternal_idfor another object of the same kind in this restaurant),slot_unavailable(the time or the table is taken),idempotency_in_progress(a request with this Idempotency-Key is still running) orconflict(the same data was changed elsewhere at the same moment, three times in a row; retry unchanged afterRetry-Afterseconds).429rate_limited: more than 120 requests per minute on this connection. WaitRetry-Afterseconds.500server_error. Safe to retry with the same Idempotency-Key.
curl -X DELETE "https://api.flavoso.com/v1/menu/options/OPTION_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN"{
"object": "option",
"id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"deleted": true
}Still unclear? Write to support@flavoso.com and include the id from X-Request-Id. It lets us find your request in the log.