API di FlavosoGuideIntegrazione cassa
Integrazione cassa
Collegare una cassa a Flavoso dalla A alla Z: collegamento, sincronizzazione del menu, presa in carico degli ordini e aggiornamento dello stato, pausa, orari di apertura e riconciliazione giornaliera.
L'obiettivo
Alla fine di questa guida la cassa gestisce il menu e Flavoso è un canale di ordinazione in più: gli ordini online arrivano in cassa come scontrini, gli stati tornano al cliente e la sera il fatturato quadra.
Cassa
gestisce articoli, prezzi, scontrini e chiusura giornaliera
Flavoso
pagina ordini, QR al tavolo, app per il telefono
Cassa → Flavoso
- PUT /menuSincronizzare il menu
- POST /menu/availabilitySegnalare gli esauriti
- POST /orders/{id}/acknowledgeOrdine preso in carico, con numero di scontrino
- POST /orders/{id}/statusSegnalare lo stato: accettato, pronto
- POST /store/pausePausa, orari di apertura, giorni speciali
Flavoso → Cassa
- order.createdNuovo ordine, via webhook o recupero
- order.cancelledOrdine annullato
- menu.* · store.updatedMenu o orari cambiati nella dashboard
- GET /reports/salesFatturato giornaliero per la riconciliazione
I permessi necessari: orders:write, menu:write, store:write e reports:read; con le prenotazioni anche reservations:write.
1. Collegare
Nella tua cassa c'è un pulsante «Collega con Flavoso». Avvia il flusso OAuth; il titolare accede, sceglie il suo locale e dà il consenso. La tua cassa salva i token per collegamento, cifrati, insieme a tenant.id e connection_id.
Poi chiama una volta GET /me e memorizza fuso orario, valuta, paese, livemode e il created_at del collegamento. Il tuo webhook riceve app.connected.
https.Primo collegamento
Un locale che collega la tua cassa di solito ha già un menu, ordini precedenti ed eventi. Prima che la cassa sincronizzi e recuperi qualcosa per la prima volta, segui questi tre passi, perché nulla venga creato due volte o registrato in ritardo:
- 1
Abbinare il menu esistente
Una sincronizzazione riconosce solo gli oggetti che hanno un identificativo della tua app; per questo un primo
PUT /menuduplica il menu esistente. Leggilo prima conGET /menu, associa gli oggetti ai tuoi articoli e imposta l'external_iddi ogni oggetto conPATCH. Nel locale di prova lo fa «Assegna identificativi» nella scheda Menu della console. Una simulazione mostra i doppioni insummary.*.created. - 2
Lasciare fuori gli ordini precedenti
GET /orders?acknowledged=falsenon ha limiti di tempo. Interroga concreated_sincee ilcreated_atdel collegamento daGET /me, e prendi in carico ogni ordine che gestisci, anche uno annullato che non registri. - 3
Eventi da adesso
Senza
after,GET /eventsrestituisce gli ultimi 30 giorni, anche eventi precedenti al collegamento. La prima volta interroga conafter=lateste salvanext_afterper questo collegamento.
# 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. Esaurito
Quando la cassa segnala qualcosa come esaurito, invia POST /menu/availability con fino a 500 voci alla volta, tramite external_id o identificativo. available è sempre lo stato di destinazione, mai un interruttore: due volte «esaurito» resta esaurito.
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. Ricevere gli ordini
Un nuovo ordine arriva come order.created. Con il pagamento in loco subito, con il pagamento online non appena il pagamento è confermato; un ordine online non pagato non lo vedi mai.
- Cassa nel cloud: via webhook, in pochi secondi.
- Cassa nel locale senza indirizzo pubblico: con GET /events, ogni pochi secondi,
types=order.*,menu.*,store.*,app.*, con unafterper collegamento. Così non può leggereapp.disconnected, perché con lo scollegamento i suoi token smettono di valere: riconosce uno scollegamento da un 401 seguito dainvalid_grantnel rinnovo. - In entrambi i casi come rete di sicurezza:
GET /orders?acknowledged=false&created_since=…con ilcreated_atdel collegamento, a ogni avvio e regolarmente. Lì trovi tutto ciò che non è ancora stato preso in carico.
external_id e acknowledged_at sono sempre null, anche se la tua app ha già preso in carico l'ordine: una busta è identica per tutti i destinatari. Se un ordine è già registrato lo decide il tuo sistema in base all'identificativo dell'ordine, oppure lo rileggi 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. Prendere in carico con il proprio numero di scontrino
Quando l'ordine è registrato in cassa, lo segnali come preso in carico con POST /orders/{id}/acknowledge, con il tuo numero di scontrino come external_id. Flavoso imposta così acknowledged_at (la prima volta) ed external_id, e l'ordine esce da GET /orders?acknowledged=false.
- Idempotente: prendere in carico due volte non cambia nulla. Un
external_iddiverso sostituisce quello vecchio. - Se il tuo numero di scontrino è già associato a un altro ordine della tua app, Flavoso risponde 409
external_id_taken. - Prendi in carico anche un ordine che non registri, per esempio uno già annullato, se serve senza
external_id: altrimenti resta inGET /orders?acknowledged=false. - Prendere in carico non è accettare. Se la cucina accetta l'ordine lo segnali a parte (passo successivo). Molte casse fanno le due cose una subito dopo l'altra.
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. Accettare e far avanzare
Quando la cassa accetta, chiami POST /orders/{id}/accept. L'ordine passa allo stato successivo che il locale usa, di solito confirmed (se ha disattivato confirmed, preparing e ready, completed); un ordine che ha già superato pending torna invariato, uno completato o annullato risponde 409. Poi fai avanzare con POST /orders/{id}/status: preparing, ready, completed. Il cliente riceve il suo messaggio, il display di cucina e la dashboard mostrano subito lo stato, come con un clic nella dashboard.
In avanti puoi saltare, per esempio da confirmed direttamente a ready. Se il locale non usa uno stato, Flavoso risponde 409 stage_not_used; allora lo salti. Rifiutare (reject) è possibile solo finché l'ordine è pending; annullare (cancel) da ogni stato aperto. Il flusso nel dettaglio è in Flusso degli ordini.
# 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"}'Voci, opzioni e imposte
Ogni voce ha un product_id. Con gli identificativi della sincronizzazione trovi il tuo articolo; GET /menu/products/{id} mostra anche l'external_id della tua app. Le opzioni compaiono nella voce con nome, supplemento, il loro id e group_id e group_name del loro gruppo, così trovi il tuo modificatore (null per gli ordini precedenti all'8 ottobre 2026); unit_price le include già.
tax.lines suddivide l'ordine per aliquota, con voci, spese di consegna e costo del servizio, con lo stesso calcolo dello scontrino e dell'esportazione DATEV. La mancia (amounts.tip) non è fatturato e non compare in nessuna riga d'imposta. Una quota senza aliquota registrata ha rate_bps: null, vedi Righe d'imposta.
payment.method dice se si paga online (online) o in loco (on_site), payment.status se è già pagato. Un ordine online arriva sempre pagato; un ordine con pagamento in loco resta open, perché Flavoso non registra i pagamenti in loco, e non segnala order.paid.
7. Pausa, orari di apertura, giorni speciali
Quando la cucina è piena, la cassa mette in pausa la ricezione degli ordini: POST /store/pause con minutes (da 1 a 720) o until (con fuso orario), più una note facoltativa di al massimo 140 caratteri; senza minutes né until la pausa dura 30 minuti. Finisce sempre da sola, al massimo dopo dodici ore; POST /store/resume la termina prima.
Se è la cassa a gestire gli orari di apertura, li imposta con PUT /store/hours: al massimo due fasce per giorno della settimana, che non si sovrappongano, e un giorno che manca è chiuso. I giorni speciali li imposta con POST /store/closures. Tempi di preparazione e tipi di ordine si cambiano con PATCH /store, con gli stessi controlli della dashboard.
Se il ristoratore cambia qualcosa di tutto ciò nella dashboard, ricevi 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. Riconciliare il fatturato giornaliero
Dopo la mezzanotte nel fuso orario del locale, la cassa recupera il giorno precedente: GET /reports/sales?from=…&to=…&group_by=day. Lordo, netto, imposta per aliquota, mance, spese di consegna e di servizio, sconti, per metodo di pagamento e tipo di ordine, con le stesse regole dei numeri della dashboard. Se qualcosa non corrisponde alla tua chiusura per il canale Flavoso, mostralo. Di più in Report.
# 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"Casi di errore e nuovi tentativi
| Caso | Cosa fa la cassa |
|---|---|
| La cassa era spenta | All'avvio GET /events dall'after salvato per questo collegamento, e GET /orders?acknowledged=false&created_since=…. Gli eventi vengono conservati per 30 giorni, i webhook vengono ritentati per quasi due giorni. Se il tuo after non è più conservato, si riparte dall'evento conservato più vecchio e la risposta indica gap: true: allora recupera anche con GET /orders?updated_since=…. Un nuovo collegamento parte con after=latest. |
| Nessuna risposta a un POST | La stessa richiesta di nuovo con lo stesso Idempotency-Key. |
401 unauthorized | Rinnova il token, riprova una volta. Se il rinnovo fallisce con invalid_grant: mostra «Collegamento a Flavoso interrotto» e offri «Ricollega». Con unauthorized_client l'app è bloccata: conserva i token, mostra «bloccata temporaneamente» e riprova più tardi. |
409 invalid_transition | L'ordine è stato modificato altrove, per esempio annullato nella dashboard. Rileggilo e adegua lo scontrino. |
409 external_id_taken | Il numero di scontrino è già in uso. Controlla la numerazione della cassa. |
429 rate_limited | Attendi Retry-After. Il limite vale per locale. |
app.disconnected | Elimina i token di questo collegamento, non inviare più nulla, informa il titolare nella cassa. Una cassa senza webhook si accorge dello scollegamento da un 401 seguito da invalid_grant nel rinnovo. |
order.cancelled | Storna lo scontrino, informa la cucina. |
Lista di controllo
- Token salvati cifrati per collegamento, rinnovo in un solo punto.
- Al primo collegamento, menu esistente abbinato ai tuoi articoli prima del primo
PUT /menu. - Firma di ogni webhook verificata, duplicati riconosciuti dall'
id, oppureGET /eventscon unaftersalvato per collegamento. GET /orders?acknowledged=falseconcreated_sincecome rete di sicurezza all'avvio e regolarmente; ogni ordine gestito preso in carico, anche uno annullato.- Ogni POST con
Idempotency-Key, derivato da identificativo dell'ordine e passo. - Gli ordini con
livemode: falsenon vengono mai registrati nella cassa reale. stage_not_used,invalid_transition,app.disconnectede un'app bloccata (unauthorized_client) gestiti.- Provato fino in fondo con il locale di prova: ordine di prova, presa in carico, accettazione, pronto, annullamento nella console, pausa, riconciliazione.
Qualcosa non è chiaro? Scrivi a support@flavoso.com indicando l'identificativo di X-Request-Id. Così troviamo la tua richiesta nel registro.