API de FlavosoGuíasSincronizar la carta
Sincronizar la carta
PUT /menu sincroniza toda la carta en una sola llamada, con external_id como clave. Además, los agotados en una sola llamada y rutas individuales para cada objeto.
El modelo
Una carta tiene cuatro tipos de objetos, como listas planas con referencias:
| Objeto | Campos importantes |
|---|---|
Categoría (category) | name, position, tax_class |
Plato (product) | category_id, name, description, price, available, position, allergens, diets, tax_class, image_url, option_group_ids |
Grupo de opciones (option_group) | name, min_select, max_select, position, options |
Opción (option) | name, price_delta, available, position |
Cada objeto lleva id, external_id (el de tu app) y updated_at. GET /menu lo devuelve todo de una vez, también lo que está agotado en ese momento.
Sincronizar con PUT /menu
PUT /menu toma la carta tal como la conoce tu caja. La clave es siempre el external_id: un objeto con un identificador de tu app se actualiza; todo lo demás del cuerpo se crea. Los platos remiten a su categoría con category_external_id y a sus grupos de opciones con option_group_external_ids (en ese orden). Las opciones van dentro de su grupo.
PUT /menu la crea por segunda vez. Adóptala antes: lee GET /menu, asocia los objetos a tus artículos y fija el external_id de cada objeto con PATCH (PATCH /menu/categories/{id}, PATCH /menu/products/{id}, PATCH /menu/option-groups/{id}, PATCH /menu/options/{id}). En el negocio de prueba lo hace «Asignar identificadores», en la pestaña Carta de la consola. Una simulación muestra los duplicados en summary.*.created.{
"mode": "merge",
"dry_run": false,
"categories": [
{
"external_id": "CAT-PIZZA",
"name": "Pizza",
"position": 1,
"tax_class": "food"
},
{
"external_id": "CAT-DRINKS",
"name": "Getränke",
"position": 2,
"tax_class": "drink"
}
],
"products": [
{
"external_id": "ART-1001",
"category_external_id": "CAT-PIZZA",
"name": "Pizza Margherita",
"description": "Tomato, mozzarella, basil",
"price": 950,
"available": true,
"position": 1,
"allergens": [
"gluten",
"milk"
],
"diets": [
"vegetarian"
],
"option_group_external_ids": [
"GRP-EXTRAS"
]
},
{
"external_id": "ART-2001",
"category_external_id": "CAT-DRINKS",
"name": "Cola 0,33 l",
"price": 290,
"available": true,
"position": 1,
"image_url": "https://pos.example.com/images/cola.jpg"
}
],
"option_groups": [
{
"external_id": "GRP-EXTRAS",
"name": "Extras",
"min_select": 0,
"max_select": 3,
"options": [
{
"external_id": "OPT-CHEESE",
"name": "Extra cheese",
"price_delta": 150
},
{
"external_id": "OPT-SALAMI",
"name": "Spicy salami",
"price_delta": 200,
"available": false
}
]
}
]
}- Precios en céntimos, brutos:
priceen el plato,price_deltaen la opción. - Son obligatorios
mode, en cada objetoexternal_idyname, además depriceen los platos yoptionsen los grupos (puede estar vacía). Cada sincronización fijanameypricecon tus valores; una opción de tu app que ya no aparece en el cuerpo solo la borrareplace, conmergese queda. Todos los demás campos que omites se quedan como están: una caja que no conoce alérgenos ni el estado de agotado no sobrescribe lo que haya introducido el hostelero. Un campo que la sincronización no conoce da 400 conparam. - Una lista que falta en el cuerpo no forma parte de la sincronización:
replacesinproductsno borra ningún plato. Solo una lista vacía[]significa «ninguno». - Flavoso descarga por sí mismo la imagen de
image_url(https) y la guarda como una imagen subida desde el panel: PNG, JPEG o WebP, como máximo 1 MB. Si un plato ya lleva la imagen de esa misma dirección, Flavoso no la vuelve a descargar; por eso no detecta una imagen nueva en la misma dirección. En ese caso, añade un parámetro de versión, por ejemplo?v=2. Si el hostelero ha sustituido la imagen en el panel, la siguiente sincronización vuelve a poner la tuya. Por sincronización llegan hasta 25 imágenes nuevas, el resto en la siguiente (avisoimages_deferred). Un negocio de prueba guarda como máximo 50 imágenes descargadas; por encima de ese límite, las imágenes se omiten (avisoimages_capped). Restablecer en la consola las borra.nullquita la imagen. allergensadmite las 14 claves de abajo y sus nombres en los cinco idiomas (Sojapasa a sersoybeans),dietslas seis claves de abajo, también escritas de otra forma (Gluten-free). Todo lo demás queda como indicación propia del negocio y aparece tal cual en la página de pedidos: como máximo 20 alérgenos propios de 40 caracteres cada uno y 14 dietas propias de 60 cada una.tax_classes el único campo que solo admite los valores de abajo.
# menu.json holds the body from above. Run it with "dry_run": true first:
# nothing changes, the answer says what would. Then again with "dry_run": false.
curl -X PUT https://api.flavoso.com/v1/menu \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @menu.jsonmerge o replace
| mode | Lo que está en el cuerpo | Lo que falta |
|---|---|---|
merge | se crea o se cambia | se queda como está |
replace | se crea o se cambia | se borra, pero solo si lleva un external_id de TU app |
Una sincronización no cambia ni borra los objetos sin identificador de tu app (creados en el panel, por la importación de la carta o por otra app), en ninguno de los dos modos, con dos excepciones en replace: si borra una de tus categorías, los platos propios del negocio que hay en ella pasan a «Sin categoría», y si borra uno de tus grupos de opciones, las opciones propias del negocio en ese grupo se borran con él (aviso unmanaged_options_deleted). replace es lo correcto si la caja es la fuente completa de verdad sobre sus artículos; merge, si solo aporta una parte.
Simulación
Con "dry_run": true, Flavoso comprueba todo el cuerpo y responde con el mismo resumen, sin cambiar nada; las imágenes no se descargan. Ideal en la primera conexión y antes de un replace: ves lo que se borraría. Si la primera vez aparece en summary.*.created un plato que ya existe, todavía no lleva ningún identificador de tu app.
La respuesta
La respuesta cuenta por tipo lo que se ha creado, cambiado, dejado sin cambios y borrado, indica para cada external_id el identificador de Flavoso y reúne avisos en warnings. Guarda los identificadores: los eventos y los pedidos usan los de Flavoso.
{
"object": "menu_sync",
"mode": "merge",
"dry_run": false,
"summary": {
"categories": {
"created": 2,
"updated": 0,
"unchanged": 0,
"deleted": 0
},
"products": {
"created": 2,
"updated": 0,
"unchanged": 0,
"deleted": 0
},
"option_groups": {
"created": 1,
"updated": 0,
"unchanged": 0,
"deleted": 0
},
"options": {
"created": 2,
"updated": 0,
"unchanged": 0,
"deleted": 0
}
},
"warnings": [],
"ids": {
"categories": [
{
"external_id": "CAT-PIZZA",
"id": "5a1c2e3d-4b5f-4a6e-8d7c-9b0a1f2e3d4c"
},
{
"external_id": "CAT-DRINKS",
"id": "b1c2d3e4-f5a6-4b7c-8d9e-0f1a2b3c4d5e"
}
],
"products": [
{
"external_id": "ART-1001",
"id": "303b87c9-58a3-4644-9a8b-2841cc1f89d6"
},
{
"external_id": "ART-2001",
"id": "6a7b8c9d-0e1f-4a2b-9c3d-4e5f6a7b8c9d"
}
],
"option_groups": [
{
"external_id": "GRP-EXTRAS",
"id": "8e7d6c5b-4a39-4281-b0f9-e8d7c6b5a4f3"
}
],
"options": [
{
"external_id": "OPT-CHEESE",
"id": "1f2e3d4c-5b6a-4798-8a7b-6c5d4e3f2a1b"
},
{
"external_id": "OPT-SALAMI",
"id": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f"
}
]
}
}Cada aviso tiene code y message:
| code | Significado |
|---|---|
image_failed | Una imagen no se ha podido usar; el plato conserva su imagen. |
images_deferred | Más imágenes nuevas de las que descarga una sincronización. Vuelve a sincronizar enseguida. |
images_not_checked | Simulación: las imágenes no se han descargado. |
images_capped | Solo en el negocio de prueba: más imágenes de las que guarda (50); el resto no se ha descargado. |
unmanaged_kept | Objetos sin identificador de tu app que un replace ha dejado en su sitio. |
unmanaged_options_deleted | Opciones sin tu identificador que se han borrado junto con un grupo. |
own_option_group_kept | El grupo propio de un plato sigue asociado. |
option_group_released | El grupo de un plato borrado se ha convertido en un grupo compartido. |
Límites y transacción
Una sincronización puede contener como máximo 200 categorías, 2000 platos, 500 grupos de opciones y 5000 opciones; por encima de eso, Flavoso responde con 422 limit_exceeded. El cuerpo puede ocupar hasta 2 MB, contados en bytes; si es mayor, recibe 413 payload_too_large. Por conexión se permiten como máximo 10 sincronizaciones por minuto.
La sincronización se ejecuta en una transacción, todo o nada: si falla una parte, la carta se queda como estaba. Un error en una entrada llega como 400 invalid_request con param, por ejemplo products[3].price, y lo mismo una referencia a algo que no existe, por ejemplo products[3].category_external_id.
Valores válidos
| Nombre | Valores |
|---|---|
Grupo fiscal (tax_class) | fooddrinkalcohol |
Alérgenos (allergens) | glutencrustaceanseggsfishpeanutssoybeansmilknutscelerymustardsesamesulphiteslupinmolluscs |
Dietas (diets) | vegetarianvegangluten_freelactose_freehalalkosher |
Agotado en una sola llamada
POST /menu/availability fija la disponibilidad de hasta 500 platos y opciones a la vez, todo o nada. Cada entrada indica type (product u option), además de id o external_id, y available como estado de destino. Cada objeto que cambia con ello comunica su propio menu.product.updated o menu.option.updated.
curl -X POST https://api.flavoso.com/v1/menu/availability \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"type": "product",
"external_id": "ART-1001",
"available": false
},
{
"type": "option",
"external_id": "OPT-SALAMI",
"available": true
},
{
"type": "product",
"id": "6a7b8c9d-0e1f-4a2b-9c3d-4e5f6a7b8c9d",
"available": true
}
]
}'Objetos sueltos
Para cambios concretos, cada tipo tiene sus propias rutas: listar, crear, leer, cambiar, borrar. La lista completa está en la referencia.
GET /menu/productsfiltra porcategory_id(un identificador ononepara «Sin categoría»),available,external_idyupdated_since, con paginación;GET /menu/categories,GET /menu/option-groupsyGET /menu/options, solo porexternal_idyupdated_since.GET /menu/optionslista cada opción: un suplemento nuevo o una opción agotada no cambia ni su plato ni su grupo, así que consulta también esa lista. Las listas de la carta llegan siempre en orden ascendente porupdated_at.PATCH /menu/products/{id}cambia cualquier campo, también los grupos de opciones (option_group_ids, en ese orden), la imagen y elexternal_id. El grupo propio de un plato va siempre delante y no se puede desasociar, solo borrar; el grupo propio de otro plato no se puede asociar (400).POST /menu/option-groupscrea un grupo con sus opciones;POST /menu/option-groups/{id}/options, una opción en un grupo existente. Crear responde con 201.DELETE /menu/categories/{id}pasa los platos a «Sin categoría», como en el panel. Con?with_products=truese borran con ella.
# A new dish in an existing category
curl -X POST https://api.flavoso.com/v1/menu/products \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: create-ART-1002" \
-d '{"external_id": "ART-1002", "category_id": "5a1c2e3d-4b5f-4a6e-8d7c-9b0a1f2e3d4c", "name": "Pizza Funghi", "price": 1090}'
# New price, sold out, other option groups (in this order)
curl -X PATCH https://api.flavoso.com/v1/menu/products/PRODUCT_ID \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"price": 1150, "available": false, "option_group_ids": ["8e7d6c5b-4a39-4281-b0f9-e8d7c6b5a4f3"]}'
# The dishes of one category, only what is sold out
curl "https://api.flavoso.com/v1/menu/products?category_id=5a1c2e3d-4b5f-4a6e-8d7c-9b0a1f2e3d4c&available=false" \
-H "Authorization: Bearer $ACCESS_TOKEN"
# Delete a category together with its dishes (without with_products they move to "No category")
curl -X DELETE "https://api.flavoso.com/v1/menu/categories/CATEGORY_ID?with_products=true" \
-H "Authorization: Bearer $ACCESS_TOKEN"Eventos de la carta
Cada cambio en la carta comunica un evento, también los de tu propia app: menu.category.*, menu.product.*, menu.option_group.* y menu.option.*, cada uno con created, updated y deleted. updated indica en changed los campos cambiados y en previous sus valores anteriores; deleted muestra el objeto tal como era antes de borrarlo. Consulta Tipos de eventos.
Una sincronización con PUT /menu comunica, en lugar de muchos eventos sueltos, exactamente uno: menu.synced con mode, summary y el connection_id de la conexión que sincronizó; si es la tuya, puedes omitirlo. Lo que ha cambiado lo obtienes después con GET /menu/products?updated_since=… y GET /menu/options?updated_since=…. external_id aparece siempre como null en los eventos, porque el mismo sobre va a todos los destinatarios.
Quién tiene la última palabra
El hostelero puede seguir cambiándolo todo en el panel, también los platos con un identificador de tu app. Una sincronización sobrescribe los campos que envía. Lo que ha dado buen resultado: la caja es la fuente de sus artículos, nombres y precios; las descripciones, las imágenes y los alérgenos los mantiene el hostelero en Flavoso, y la caja entonces no envía esos campos. Con los eventos menu.* ves lo que se ha cambiado en el panel.
¿Algo no ha quedado claro? Escribe a support@flavoso.com e indica el identificador de X-Request-Id. Así podremos encontrar tu solicitud en el registro.