API de FlavosoGuíasIntegración con caja
Integración con caja
Conectar una caja a Flavoso de principio a fin: conexión, sincronización de la carta, hacerse cargo de los pedidos y comunicar su estado, pausa, horarios de apertura y conciliación diaria.
El objetivo
Al terminar esta guía, la caja gestiona la carta y Flavoso es un canal de pedidos más: los pedidos online llegan a la caja como ticket, los estados vuelven al cliente y por la noche las ventas cuadran.
Caja
lleva artículos, precios, tickets y el cierre del día
Flavoso
página de pedidos, QR de mesa, app móvil
Caja → Flavoso
- PUT /menuSincronizar la carta
- POST /menu/availabilityComunicar platos agotados
- POST /orders/{id}/acknowledgeHacerse cargo del pedido, con número de ticket
- POST /orders/{id}/statusComunicar el estado: aceptado, listo
- POST /store/pausePausa, horarios de apertura, días especiales
Flavoso → Caja
- order.createdPedido nuevo, por webhook o consulta
- order.cancelledPedido cancelado
- menu.* · store.updatedCarta u horarios cambiados en el panel
- GET /reports/salesVentas del día para conciliar
Los permisos necesarios: orders:write, menu:write, store:write y reports:read; con reservas, además reservations:write.
1. Conectar
En tu caja hay un botón «Conectar con Flavoso». Inicia el flujo de OAuth; el propietario inicia sesión, elige su negocio y da su consentimiento. Tu caja guarda cifrados los tokens de cada conexión, junto con tenant.id y connection_id.
Después llama una vez a GET /me y guarda la zona horaria, la moneda, el país, livemode y el created_at de la conexión. Tu webhook recibe app.connected.
https.Primera conexión
Un negocio que conecta tu caja suele tener ya una carta, pedidos anteriores y eventos. Tres pasos antes de que la caja sincronice y recoja algo por primera vez, para que nada se cree por duplicado ni se registre a destiempo:
- 1
Adoptar la carta existente
Una sincronización solo reconoce los objetos que llevan un identificador de tu app; por eso, un primer
PUT /menucrea la carta existente por segunda vez. Léela antes conGET /menu, asocia los objetos a tus artículos y fija elexternal_idde cada objeto conPATCH. En el negocio de prueba lo hace «Asignar identificadores», en la pestaña Carta de la consola. Una simulación muestra los duplicados ensummary.*.created. - 2
Dejar fuera los pedidos anteriores
GET /orders?acknowledged=falseno tiene límite de tiempo. Consulta concreated_sincey elcreated_atde la conexión deGET /me, y hazte cargo de cada pedido que trates, incluso de uno cancelado que no registres. - 3
Eventos a partir de ahora
Sin
after,GET /eventsdevuelve los últimos 30 días, incluidos los eventos anteriores a la conexión. La primera vez, consulta conafter=latesty guardanext_afterpara esta conexión.
# 1. The connection itself: remember created_at, it bounds the order backlog
curl https://api.flavoso.com/v1/me -H "Authorization: Bearer $ACCESS_TOKEN"
# 2. Take over the menu that is already there: read it, map it to your articles and
# attach your article number to every object. Categories, option groups and options
# work the same way with their own PATCH route.
curl https://api.flavoso.com/v1/menu -H "Authorization: Bearer $ACCESS_TOKEN"
curl -X PATCH https://api.flavoso.com/v1/menu/products/PRODUCT_ID \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"external_id": "ART-1001"}'
# Then PUT /menu with "dry_run": true: summary.*.created only counts what is really new.
# 3. Only the orders since the connection, never the whole history
curl "https://api.flavoso.com/v1/orders?acknowledged=false&created_since=2026-10-04T17:40:00.000Z" \
-H "Authorization: Bearer $ACCESS_TOKEN"
# 4. Events from now on: store next_after for this connection
curl "https://api.flavoso.com/v1/events?after=latest" -H "Authorization: Bearer $ACCESS_TOKEN"3. Agotado
Cuando la caja marca algo como agotado, envías POST /menu/availability con hasta 500 entradas a la vez, por external_id o por identificador. available es siempre el estado de destino, nunca un interruptor: dos veces «agotado» sigue siendo agotado.
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
}
]
}'4. Recibir pedidos
Un pedido nuevo llega como order.created. Con pago en el local, de inmediato; con pago online, en cuanto se confirma el pago. Un pedido online sin pagar no lo ves nunca.
- Caja en la nube: por webhook, en pocos segundos.
- Caja en el local sin dirección pública: con GET /events, cada pocos segundos,
types=order.*,menu.*,store.*,app.*, con unafterpor conexión. Así no puede leerapp.disconnected, porque con la desconexión sus tokens dejan de ser válidos: reconoce una desconexión por un 401 seguido deinvalid_grantal renovar. - En ambos casos, como red de seguridad:
GET /orders?acknowledged=false&created_since=…con elcreated_atde la conexión, en cada arranque y periódicamente. Ahí aparecen los pedidos de los que aún no te has hecho cargo.
external_id y acknowledged_at aparecen siempre como null, aunque tu app ya se haya hecho cargo del pedido: el mismo sobre va a todos los destinatarios. Tu sistema decide por el identificador del pedido si ya está registrado, o lo consultas con GET /orders/{id}.// The queue worker behind your webhook endpoint (or behind pollEvents for a local POS)
export async function handleEvent(event) {
const conn = await connectionFor(event.connection_id); // your stored tokens for this restaurant
if (!conn) return; // not (or no longer) connected
// external_id and acknowledged_at are always null in events (one envelope for every
// recipient): your own records, keyed by the order id, say what is booked already.
switch (event.type) {
case "order.created": {
const order = event.data.object;
const receipt = await pos.book(order, { testOnly: !event.livemode }); // idempotent per order.id
await flavoso(conn, "POST", `/orders/${order.id}/acknowledge`, {
body: { external_id: receipt.number },
idempotencyKey: `ack-${order.id}`,
});
break;
}
case "order.cancelled":
await pos.voidReceiptForOrder(event.data.object.id);
break;
case "app.disconnected":
// Webhook only: GET /events never delivers it, a polling POS sees 401 and invalid_grant
await forgetConnection(event.connection_id); // no token of it works any more
break;
}
}5. Hacerse cargo con un número de ticket propio
Cuando el pedido está registrado en la caja, confirmas con POST /orders/{id}/acknowledge que te has hecho cargo de él, con tu número de ticket como external_id. Con ello, Flavoso fija acknowledged_at (la primera vez) y external_id, y el pedido sale de GET /orders?acknowledged=false.
- Idempotente: hacerse cargo dos veces no cambia nada. Un
external_iddistinto sustituye al anterior. - Si tu número de ticket ya está asociado a otro pedido de tu app, Flavoso responde con 409
external_id_taken. - Hazte cargo también de un pedido que no registres, por ejemplo uno que ya estaba cancelado, si hace falta sin
external_id: si no, se queda enGET /orders?acknowledged=false. - Hacerse cargo no es aceptar. Si la cocina acepta el pedido, lo comunicas por separado (paso siguiente). Muchas cajas hacen las dos cosas una detrás de otra.
curl -X POST https://api.flavoso.com/v1/orders/ORDER_ID/acknowledge \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ack-ORDER_ID" \
-d '{"external_id": "R-2026-000123"}'6. Aceptar y hacer avanzar
Cuando la caja acepta, llamas a POST /orders/{id}/accept. El pedido pasa al siguiente estado que usa el negocio, normalmente confirmed (si ha desactivado confirmed, preparing y ready, completed); un pedido que ya ha pasado de pending vuelve sin cambios, uno terminado o cancelado responde con 409. Después avanzas con POST /orders/{id}/status: preparing, ready, completed. El cliente recibe su mensaje, y la pantalla de cocina y el panel muestran el estado al momento, como con un clic en el panel.
Puedes saltar hacia delante, por ejemplo de confirmed directamente a ready. Si el negocio no usa un estado, Flavoso responde con 409 stage_not_used; entonces te lo saltas. Rechazar (reject) solo es posible mientras el pedido esté en pending; cancelar (cancel), desde cualquier estado abierto. El flujo en detalle está en Flujo de pedidos.
# The cashier accepted the order in the POS
curl -X POST https://api.flavoso.com/v1/orders/ORDER_ID/accept \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Idempotency-Key: accept-ORDER_ID"
# The kitchen printer printed the ticket / the food is ready / handed over
curl -X POST https://api.flavoso.com/v1/orders/ORDER_ID/status \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ready-ORDER_ID" \
-d '{"status": "ready"}'Líneas, opciones e impuestos
Cada línea lleva product_id. Con los identificadores de la sincronización encuentras tu artículo; GET /menu/products/{id} muestra además el external_id de tu app. Las opciones aparecen en la línea con su nombre, su suplemento, su id y el group_id y el group_name de su grupo, para que encuentres tu propio modificador (null en pedidos anteriores al 8 de octubre de 2026); unit_price ya las incluye.
tax.lines desglosa el pedido por tipo impositivo, con las líneas del pedido, los gastos de envío y la tarifa de servicio, con el mismo cálculo que el ticket y la exportación DATEV. La propina (amounts.tip) no cuenta como venta y no aparece en ninguna línea de impuestos. Una parte sin tipo guardado lleva rate_bps: null; consulta Líneas de impuestos.
payment.method indica si se paga online (online) o en el local (on_site), y payment.status, si ya está pagado. Un pedido online llega siempre pagado; un pedido con pago en el local sigue en open, porque Flavoso no registra los pagos en el local, y no comunica order.paid.
7. Pausa, horarios de apertura, días especiales
Cuando la cocina no da abasto, la caja pausa los pedidos online: POST /store/pause con minutes (de 1 a 720) o until (con zona horaria) y, opcionalmente, una note de hasta 140 caracteres; sin minutes ni until, la pausa dura 30 minutos. Termina siempre sola, como máximo a las doce horas; POST /store/resume la termina antes.
Si la caja lleva los horarios de apertura, los fija con PUT /store/hours: como máximo dos franjas por día de la semana, que no se solapen, y un día que no aparezca se considera cerrado. Los días especiales los fija con POST /store/closures. Los tiempos de preparación y los tipos de pedido los cambia PATCH /store, con las mismas comprobaciones que en el panel.
Si el hostelero cambia algo de esto en el panel, recibes store.updated con changed.
# Kitchen at capacity: no new online orders for 30 minutes
curl -X POST https://api.flavoso.com/v1/store/pause \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"minutes": 30, "note": "Kitchen at capacity"}'
# Open again before the time is up
curl -X POST https://api.flavoso.com/v1/store/resume -H "Authorization: Bearer $ACCESS_TOKEN"
# Opening hours from the POS: weekday 0 = Monday, at most two windows per weekday
# that do not overlap; a weekday that is missing is closed
curl -X PUT https://api.flavoso.com/v1/store/hours \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"opening_hours": [
{
"weekday": 0,
"open": "11:30",
"close": "14:30"
},
{
"weekday": 0,
"open": "17:30",
"close": "22:00"
},
{
"weekday": 5,
"open": "12:00",
"close": "23:00"
}
],
"delivery_hours": []
}'
# Closed on Christmas Eve, shorter on New Year's Eve
curl -X POST https://api.flavoso.com/v1/store/closures \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"date": "2026-12-24", "closed": true, "note": "Christmas Eve"}'
curl -X POST https://api.flavoso.com/v1/store/closures \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"date": "2026-12-31", "hours": [{"open": "11:30", "close": "16:00"}]}'
# Lead times and order types
curl -X PATCH https://api.flavoso.com/v1/store \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"lead_times": {"pickup_minutes": 25, "delivery_minutes": 50}}'8. Conciliar las ventas del día
Después de medianoche en la zona horaria del negocio, la caja obtiene el día anterior: GET /reports/sales?from=…&to=…&group_by=day. Bruto, neto, impuestos por tipo, propinas, tarifas, descuentos, por método de pago y tipo de pedido, con las mismas reglas que las cifras del panel. Si algo difiere de tu cierre para el canal Flavoso, muéstralo. Más en Informes.
# Yesterday, as one bucket per day, in the restaurant's local time
curl "https://api.flavoso.com/v1/reports/sales?from=2026-10-03&to=2026-10-03&group_by=day" \
-H "Authorization: Bearer $ACCESS_TOKEN"Casos de error y reintentos
| Caso | Qué hace la caja |
|---|---|
| La caja estaba apagada | Al arrancar, GET /events desde el after guardado para esta conexión, y GET /orders?acknowledged=false&created_since=…. Los eventos se conservan 30 días, y los webhooks se reintentan durante casi dos días. Si tu after ya no se conserva, sigue por el evento conservado más antiguo y la respuesta indica gap: true: entonces recupera además con GET /orders?updated_since=…. Una conexión nueva empieza con after=latest. |
| Sin respuesta a un POST | La misma solicitud otra vez con la misma Idempotency-Key. |
401 unauthorized | Renovar el token y repetir una vez. Si la renovación falla con invalid_grant: mostrar «Conexión con Flavoso interrumpida» y ofrecer «Volver a conectar». Con unauthorized_client, la app está bloqueada: conservar los tokens, mostrar «bloqueada temporalmente» y volver a intentarlo más tarde. |
409 invalid_transition | El pedido se ha cambiado en otro sitio, por ejemplo se ha cancelado en el panel. Volver a leerlo y ajustar el ticket. |
409 external_id_taken | El número de ticket ya está en uso. Revisar la numeración de la caja. |
429 rate_limited | Esperar Retry-After. El límite se aplica por negocio. |
app.disconnected | Borrar los tokens de esta conexión, no enviar nada más e informar al propietario en la caja. Una caja sin webhook se entera de la desconexión por un 401 seguido de invalid_grant al renovar. |
order.cancelled | Anular el ticket e informar a la cocina. |
Lista de comprobación
- Tokens guardados cifrados por conexión, renovación en un único sitio.
- En la primera conexión, carta existente adoptada antes del primer
PUT /menu. - Firma de cada webhook comprobada, duplicados reconocidos por el
id, oGET /eventscon unafterguardado por conexión. GET /orders?acknowledged=falseconcreated_sincecomo red de seguridad al arrancar y periódicamente; tu caja se hace cargo de cada pedido tratado, también de los cancelados.- Cada POST con
Idempotency-Key, derivada del identificador del pedido y del paso. livemode: falsenunca se registra en la caja real.stage_not_used,invalid_transition,app.disconnectedy una app bloqueada (unauthorized_client) tratados.- Probado de principio a fin con el negocio de prueba: pedido de prueba, hacerse cargo, aceptar, listo, cancelación en la consola, pausa, conciliación.
¿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.