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.

Una cassa che gira su un dispositivo nel locale è di solito un client pubblico senza secret. Se la tua cassa gira nel cloud, usa un client riservato e scambia i token sul server. Un client pubblico può tornare tramite loopback o uno schema proprio; uno riservato collega i locali reali solo tramite 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. 1

    Abbinare il menu esistente

    Una sincronizzazione riconosce solo gli oggetti che hanno un identificativo della tua app; per questo un primo PUT /menu duplica il menu esistente. Leggilo prima con GET /menu, associa gli oggetti ai tuoi articoli e imposta l'external_id di ogni oggetto con PATCH. Nel locale di prova lo fa «Assegna identificativi» nella scheda Menu della console. Una simulazione mostra i doppioni in summary.*.created.

  2. 2

    Lasciare fuori gli ordini precedenti

    GET /orders?acknowledged=false non ha limiti di tempo. Interroga con created_since e il created_at del collegamento da GET /me, e prendi in carico ogni ordine che gestisci, anche uno annullato che non registri.

  3. 3

    Eventi da adesso

    Senza after, GET /events restituisce gli ultimi 30 giorni, anche eventi precedenti al collegamento. La prima volta interroga con after=latest e salva next_after per 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 un after per collegamento. Così non può leggere app.disconnected, perché con lo scollegamento i suoi token smettono di valere: riconosce uno scollegamento da un 401 seguito da invalid_grant nel rinnovo.
  • In entrambi i casi come rete di sicurezza: GET /orders?acknowledged=false&created_since=… con il created_at del collegamento, a ogni avvio e regolarmente. Lì trovi tutto ciò che non è ancora stato preso in carico.
Negli eventi 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_id diverso 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 in GET /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

CasoCosa fa la cassa
La cassa era spentaAll'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 POSTLa stessa richiesta di nuovo con lo stesso Idempotency-Key.
401 unauthorizedRinnova 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_transitionL'ordine è stato modificato altrove, per esempio annullato nella dashboard. Rileggilo e adegua lo scontrino.
409 external_id_takenIl numero di scontrino è già in uso. Controlla la numerazione della cassa.
429 rate_limitedAttendi Retry-After. Il limite vale per locale.
app.disconnectedElimina 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.cancelledStorna 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, oppure GET /events con un after salvato per collegamento.
  • GET /orders?acknowledged=false con created_since come 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: false non vengono mai registrati nella cassa reale.
  • stage_not_used, invalid_transition, app.disconnected e 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.