API FlavosoGuidesIntégration caisse

Intégration caisse

Connecter une caisse à Flavoso de A à Z : connexion, synchronisation de la carte, prise en charge des commandes et retour de leur statut, pause, horaires d'ouverture et rapprochement quotidien.

L'objectif

À la fin de ce guide, la caisse gère la carte et Flavoso est un canal de commande de plus : les commandes en ligne arrivent dans la caisse sous forme de tickets, les statuts reviennent au client, et le soir, le chiffre d'affaires concorde.

Caisse

gère articles, prix, tickets et clôture du jour

Flavoso

page de commande, QR à table, application mobile

Caisse → Flavoso

  • PUT /menuSynchroniser la carte
  • POST /menu/availabilitySignaler les ruptures
  • POST /orders/{id}/acknowledgeCommande prise en charge, avec numéro de ticket
  • POST /orders/{id}/statusSignaler le statut : acceptée, prête
  • POST /store/pausePause, horaires d'ouverture, jours particuliers

Flavoso → Caisse

  • order.createdNouvelle commande, par webhook ou récupération
  • order.cancelledCommande annulée
  • menu.* · store.updatedCarte ou horaires modifiés dans le tableau de bord
  • GET /reports/salesChiffre d'affaires du jour pour le rapprochement

Les droits nécessaires : orders:write, menu:write, store:write et reports:read ; avec les réservations, aussi reservations:write.

1. Connecter

Votre caisse a un bouton « Connecter avec Flavoso ». Il lance le parcours OAuth ; le propriétaire s'identifie, choisit son établissement et donne son accord. Votre caisse enregistre les jetons par connexion, chiffrés, avec tenant.id et connection_id.

Elle appelle ensuite une fois GET /me et retient le fuseau horaire, la devise, le pays, livemode et le created_at de la connexion. Votre webhook reçoit app.connected.

Une caisse qui tourne sur un appareil dans l'établissement est en général un client public sans secret. Si votre caisse tourne dans le cloud, utilisez un client confidentiel et échangez les jetons sur le serveur. Un client public peut recevoir le retour via loopback ou un schéma personnalisé ; un client confidentiel ne se connecte à de vrais établissements que via https.

Première connexion

Un établissement qui connecte votre caisse a en général déjà une carte, des commandes plus anciennes et des événements. Trois étapes avant que la caisse ne synchronise et ne récupère quoi que ce soit pour la première fois, pour que rien ne soit créé en double ni enregistré après coup :

  1. 1

    Reprendre la carte existante

    Une synchronisation ne reconnaît que les objets qui portent un identifiant de votre application ; un premier PUT /menu crée donc la carte existante une seconde fois. Lisez-la d'abord avec GET /menu, faites correspondre les objets à vos articles et définissez l'external_id de chaque objet avec PATCH. Dans l'établissement de test, « Attribuer des identifiants » dans l'onglet Carte de la console s'en charge. Une simulation montre les doublons dans summary.*.created.

  2. 2

    Laisser de côté les commandes plus anciennes

    GET /orders?acknowledged=false n'a pas de limite dans le temps. Interrogez avec created_since et le created_at de la connexion tiré de GET /me, et prenez en charge chaque commande que vous traitez, y compris une commande annulée que vous n'enregistrez pas.

  3. 3

    Les événements à partir de maintenant

    Sans after, GET /events renvoie les 30 derniers jours, y compris des événements antérieurs à la connexion. La première fois, interrogez avec after=latest et enregistrez next_after pour cette connexion.

# 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. Plats épuisés

Quand la caisse signale un plat comme épuisé, vous envoyez POST /menu/availability avec jusqu'à 500 entrées à la fois, par external_id ou par identifiant. available est toujours l'état cible, jamais une bascule : deux fois « épuisé » reste épuisé.

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. Recevoir les commandes

Une nouvelle commande arrive sous forme de order.created. Avec paiement sur place, immédiatement ; avec paiement en ligne, une fois le paiement confirmé. Vous ne voyez jamais une commande en ligne non payée.

  • Caisse dans le cloud : par webhook, en quelques secondes.
  • Caisse dans l'établissement sans adresse publique : avec GET /events, toutes les quelques secondes, types=order.*,menu.*,store.*,app.*, avec un after par connexion. Elle ne peut pas lire app.disconnected de cette façon, car ses jetons prennent fin avec la déconnexion : elle reconnaît une déconnexion à un 401 suivi de invalid_grant au rafraîchissement.
  • Dans les deux cas, comme filet de sécurité : GET /orders?acknowledged=false&created_since=… avec le created_at de la connexion, à chaque démarrage et régulièrement. Ce qui n'est pas pris en charge y figure.
Dans les événements, external_id et acknowledged_at valent toujours null, même quand votre application a déjà pris la commande en charge : une même enveloppe part vers tous les destinataires. Votre système décide si une commande est déjà enregistrée à l'aide de son identifiant, ou vous la relisez avec 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. Prendre en charge avec votre numéro de ticket

Une fois la commande enregistrée en caisse, vous la marquez comme prise en charge avec POST /orders/{id}/acknowledge, avec votre numéro de ticket comme external_id. Flavoso définit alors acknowledged_at (la première fois) et external_id, et la commande sort de GET /orders?acknowledged=false.

  • Idempotent : prendre en charge deux fois ne change rien. Un autre external_id remplace l'ancien.
  • Si votre numéro de ticket appartient déjà à une autre commande de votre application, Flavoso répond 409 external_id_taken.
  • Prenez aussi en charge une commande que vous n'enregistrez pas, par exemple une commande déjà annulée, au besoin sans external_id : sinon, elle reste dans GET /orders?acknowledged=false.
  • Prendre en charge n'est pas accepter. Que la cuisine accepte la commande, vous le signalez séparément (étape suivante). Beaucoup de caisses font les deux l'un juste après l'autre.
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. Accepter et faire avancer

Quand la caisse accepte, appelez POST /orders/{id}/accept. La commande passe au statut suivant utilisé par l'établissement, en général confirmed (s'il a désactivé confirmed, preparing et ready, completed) ; une commande qui a déjà dépassé pending est renvoyée inchangée, une commande terminée ou annulée répond 409. Ensuite, vous faites avancer la commande avec POST /orders/{id}/status : preparing, ready, completed. Le client reçoit son message, l'écran de cuisine et le tableau de bord affichent le statut immédiatement, comme pour un clic dans le tableau de bord.

Vous pouvez sauter des statuts vers l'avant, par exemple de confirmed directement à ready. Si l'établissement n'utilise pas un statut, Flavoso répond 409 stage_not_used ; vous le sautez alors. Refuser (reject) n'est possible que tant que la commande est pending ; annuler (cancel), depuis tout statut ouvert. Le déroulement en détail figure sous Déroulement des commandes.

# 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"}'

Lignes, options et TVA

Chaque ligne porte product_id. Grâce aux identifiants de la synchronisation, vous retrouvez votre article ; GET /menu/products/{id} montre en plus l'external_id de votre application. Les options figurent sur la ligne avec leur nom, leur supplément, leur id ainsi que le group_id et le group_name de leur groupe, pour retrouver votre propre modificateur (null pour les commandes antérieures au 8 octobre 2026) ; unit_price les inclut déjà.

tax.lines ventile la commande par taux de TVA, avec les lignes de commande, les frais de livraison et les frais de service, selon le même calcul que le ticket et l'export DATEV. Le pourboire (amounts.tip) n'est pas du chiffre d'affaires et ne figure dans aucune ligne de TVA. Une part sans taux enregistré porte rate_bps: null, voir Lignes de TVA.

payment.method indique si le paiement se fait en ligne (online) ou sur place (on_site), payment.status s'il est déjà effectué. Une commande en ligne arrive toujours payée ; une commande avec paiement sur place reste open, car Flavoso n'enregistre pas les paiements sur place, et ne signale pas order.paid.

7. Pause, horaires d'ouverture, jours particuliers

Quand la cuisine est débordée, la caisse met en pause la prise de commandes : POST /store/pause avec minutes (1 à 720) ou until (avec fuseau horaire), plus une note facultative de 140 caractères au plus ; sans minutes ni until, la pause dure 30 minutes. Elle se termine toujours d'elle-même, au plus tard après douze heures ; POST /store/resume y met fin plus tôt.

Si la caisse gère les horaires d'ouverture, elle les définit avec PUT /store/hours : deux plages au plus par jour de la semaine, qui ne se chevauchent pas, et un jour absent est fermé. Elle définit les jours particuliers avec POST /store/closures. Les délais de préparation et les modes de commande se modifient avec PATCH /store, avec les mêmes contrôles que dans le tableau de bord.

Si le restaurateur modifie l'un de ces réglages dans le tableau de bord, vous recevez store.updated avec 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. Rapprocher le chiffre d'affaires du jour

Après minuit dans le fuseau horaire de l'établissement, la caisse récupère la veille : GET /reports/sales?from=…&to=…&group_by=day. TTC, HT, TVA par taux, pourboires, frais, remises, par mode de paiement et mode de commande, selon les mêmes règles que les chiffres du tableau de bord. Si quelque chose diffère de votre clôture pour le canal Flavoso, signalez-le. Plus de détails sous Rapports.

# 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"

Cas d'erreur et nouvelles tentatives

CasCe que fait la caisse
La caisse était éteinteAu démarrage, GET /events à partir du after enregistré pour cette connexion, et GET /orders?acknowledged=false&created_since=…. Les événements sont conservés 30 jours, les webhooks sont retentés pendant près de deux jours. Si votre after n'est plus conservé, la lecture reprend au plus ancien événement conservé et la réponse indique gap: true : rattrapez alors aussi avec GET /orders?updated_since=…. Une nouvelle connexion commence avec after=latest.
Pas de réponse à un POSTLa même requête une nouvelle fois avec la même Idempotency-Key.
401 unauthorizedRafraîchir le jeton, réessayer une fois. Si le rafraîchissement échoue avec invalid_grant : afficher « Connexion à Flavoso interrompue » et proposer « Reconnecter ». Avec unauthorized_client, l'application est bloquée : conserver les jetons, afficher « temporairement bloquée » et réessayer plus tard.
409 invalid_transitionLa commande a été modifiée ailleurs, par exemple annulée dans le tableau de bord. La relire et ajuster le ticket.
409 external_id_takenLe numéro de ticket est déjà pris. Vérifier la numérotation de la caisse.
429 rate_limitedAttendre Retry-After. La limite s'applique par établissement.
app.disconnectedSupprimer les jetons de cette connexion, ne plus rien envoyer, informer le propriétaire dans la caisse. Une caisse sans webhook apprend la déconnexion par un 401 suivi de invalid_grant au rafraîchissement.
order.cancelledAnnuler le ticket, prévenir la cuisine.

Liste de contrôle

  • Jetons enregistrés chiffrés par connexion, rafraîchis à un seul endroit.
  • Lors de la première connexion, carte existante reprise avant le premier PUT /menu.
  • Signature de chaque webhook vérifiée, doublons repérés par id, ou GET /events avec un after enregistré par connexion.
  • GET /orders?acknowledged=false avec created_since comme filet de sécurité au démarrage et régulièrement ; chaque commande traitée prise en charge, y compris une commande annulée.
  • Chaque POST avec une Idempotency-Key, dérivée de l'identifiant de commande et de l'étape.
  • livemode: false n'est jamais enregistré dans la vraie caisse.
  • stage_not_used, invalid_transition, app.disconnected et une application bloquée (unauthorized_client) traités.
  • Scénario complet joué avec l'établissement de test : commande test, prise en charge, acceptation, prête, annulation dans la console, pause, rapprochement.

Une question reste ouverte ? Écrivez à support@flavoso.com en indiquant l'identifiant de X-Request-Id. Il nous permet de retrouver votre requête dans le journal.