Flavoso APILeitfädenKassenanbindung

Kassenanbindung

Eine Kasse an Flavoso anbinden, von A bis Z: verbinden, Speisekarte abgleichen, Bestellungen übernehmen und zurückmelden, Pause, Öffnungszeiten und der Tagesabgleich.

Das Ziel

Nach dieser Anleitung führt die Kasse die Speisekarte, und Flavoso ist ein weiterer Bestellkanal: Online-Bestellungen landen als Beleg in der Kasse, Stände gehen zurück an den Gast, und abends stimmt der Umsatz.

Kasse

führt Artikel, Preise, Belege und Tagesabschluss

Flavoso

Bestellseite, Tisch-QR, Handy-App

Kasse → Flavoso

  • PUT /menuSpeisekarte abgleichen
  • POST /menu/availabilityAusverkauft melden
  • POST /orders/{id}/acknowledgeBestellung übernommen, mit Belegnummer
  • POST /orders/{id}/statusStand melden: angenommen, fertig
  • POST /store/pausePause, Öffnungszeiten, Sondertage

Flavoso → Kasse

  • order.createdNeue Bestellung, per Webhook oder Abruf
  • order.cancelledBestellung storniert
  • menu.* · store.updatedKarte oder Zeiten im Dashboard geändert
  • GET /reports/salesTagesumsatz zum Abgleich

Die Rechte dafür: orders:write, menu:write, store:write und reports:read; mit Reservierungen dazu reservations:write.

1. Verbinden

In deiner Kasse gibt es einen Knopf „Mit Flavoso verbinden“. Er startet den OAuth-Ablauf; der Inhaber meldet sich an, wählt seinen Betrieb und stimmt zu. Deine Kasse speichert die Tokens je Verbindung, verschlüsselt, zusammen mit tenant.id und connection_id.

Danach ruft sie einmal GET /me und merkt sich Zeitzone, Währung, Land, livemode und den created_at der Verbindung. Dein Webhook bekommt app.connected.

Eine Kasse, die im Laden auf einem Gerät läuft, ist meist ein öffentlicher Client ohne Secret. Läuft deine Kasse in der Cloud, nimm einen vertraulichen Client und tausche die Tokens auf dem Server. Ein öffentlicher Client darf über Loopback oder ein eigenes Schema zurückkehren; ein vertraulicher verbindet echte Betriebe nur über https.

Erstverbindung

Ein Betrieb, der deine Kasse verbindet, hat meist schon eine Speisekarte, ältere Bestellungen und Ereignisse. Drei Schritte, bevor die Kasse zum ersten Mal abgleicht und abholt, damit nichts doppelt angelegt oder nachgebucht wird:

  1. 1

    Die vorhandene Karte übernehmen

    Ein Abgleich erkennt nur Objekte wieder, die eine Kennung deiner App tragen; ein erster PUT /menu legt die vorhandene Karte deshalb doppelt an. Lies sie vorher mit GET /menu, ordne die Objekte deinen Artikeln zu und setz je Objekt die external_id mit PATCH. Im Test-Betrieb erledigt das „Kennungen vergeben“ im Reiter Speisekarte der Konsole. Ein Probelauf zeigt Doppelte in summary.*.created.

  2. 2

    Ältere Bestellungen ausklammern

    GET /orders?acknowledged=false hat keine zeitliche Grenze. Frag mit created_since und dem created_at der Verbindung aus GET /me, und übernimm jede Bestellung, die du bearbeitest, auch eine stornierte, die du nicht buchst.

  3. 3

    Ereignisse ab jetzt

    Ohne after liefert GET /events die letzten 30 Tage, auch Ereignisse von vor der Verbindung. Frag beim ersten Mal mit after=latest und speichere next_after für diese Verbindung.

# 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. Ausverkauft

Meldet die Kasse etwas als ausverkauft, schickst du POST /menu/availability mit bis zu 500 Einträgen auf einmal, über external_id oder Kennung. available ist immer der Zielzustand, nie ein Umschalter: zweimal „ausverkauft“ bleibt ausverkauft.

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. Bestellungen empfangen

Eine neue Bestellung kommt als order.created. Bei Zahlung vor Ort sofort, bei Online-Zahlung, sobald die Zahlung bestätigt ist; eine unbezahlte Online-Bestellung siehst du nie.

  • Kasse in der Cloud: per Webhook, in wenigen Sekunden.
  • Kasse im Laden ohne öffentliche Adresse: mit GET /events, alle paar Sekunden, types=order.*,menu.*,store.*,app.*, mit einem after je Verbindung. app.disconnected kann sie so nicht lesen, denn mit der Trennung enden ihre Tokens: eine Trennung erkennt sie an 401 und danach invalid_grant beim Erneuern.
  • In beiden Fällen als Sicherheitsnetz: GET /orders?acknowledged=false&created_since=… mit dem created_at der Verbindung, bei jedem Start und regelmäßig. Was nicht übernommen ist, steht dort.
In Ereignissen stehen external_id und acknowledged_at immer auf null, auch wenn deine App die Bestellung schon übernommen hat: ein Umschlag geht an alle Empfänger gleich. Ob eine Bestellung schon gebucht ist, entscheidet dein System anhand der Bestellkennung, oder du liest sie mit GET /orders/{id} nach.
// 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. Übernehmen mit eigener Belegnummer

Ist die Bestellung in der Kasse gebucht, meldest du sie mit POST /orders/{id}/acknowledge als übernommen, mit deiner Belegnummer als external_id. Damit setzt Flavoso acknowledged_at (beim ersten Mal) und external_id, und die Bestellung fällt aus GET /orders?acknowledged=false.

  • Idempotent: zweimal übernehmen ändert nichts. Eine andere external_id ersetzt die alte.
  • Hängt deine Belegnummer schon an einer anderen Bestellung deiner App, antwortet Flavoso mit 409 external_id_taken.
  • Übernimm auch eine Bestellung, die du nicht buchst, etwa eine, die schon storniert war, notfalls ohne external_id: sonst bleibt sie in GET /orders?acknowledged=false.
  • Übernehmen ist nicht annehmen. Ob die Küche die Bestellung annimmt, meldest du getrennt (nächster Schritt). Viele Kassen tun beides direkt nacheinander.
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. Annehmen und weiterschalten

Nimmt die Kasse an, rufst du POST /orders/{id}/accept. Das schaltet die Bestellung auf den nächsten Stand, den der Betrieb benutzt, meist confirmed (hat er confirmed, preparing und ready abgeschaltet, completed); eine Bestellung, die schon hinter pending steht, kommt unverändert zurück, eine fertige oder stornierte antwortet mit 409. Danach schaltest du mit POST /orders/{id}/status weiter: preparing, ready, completed. Der Gast bekommt seine Nachricht, der Küchenbildschirm und das Dashboard zeigen den Stand sofort, wie bei einem Klick im Dashboard.

Vorwärts darfst du springen, etwa von confirmed direkt auf ready. Benutzt der Betrieb einen Stand nicht, antwortet Flavoso mit 409 stage_not_used; dann überspringst du ihn. Ablehnen (reject) geht nur, solange die Bestellung pending ist; stornieren (cancel) aus jedem offenen Stand. Der Ablauf im Einzelnen steht unter Bestellablauf.

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

Positionen, Optionen und Steuer

Jede Position trägt product_id. Über die Kennungen aus dem Abgleich findest du deinen Artikel; GET /menu/products/{id} zeigt zusätzlich die external_id deiner App. Optionen stehen an der Position mit Name, Aufpreis, ihrer id und group_id und group_name ihrer Gruppe, damit du deinen eigenen Modifier findest (null bei Bestellungen von vor dem 8. Oktober 2026); unit_price enthält sie schon.

tax.lines teilt die Bestellung nach Steuersatz auf, mit Positionen, Liefergebühr und Servicegebühr, nach derselben Rechnung wie Bon und DATEV-Export. Das Trinkgeld (amounts.tip) ist kein Umsatz und steht in keiner Steuerzeile. Ein Anteil ohne hinterlegten Satz trägt rate_bps: null, siehe Steuerzeilen.

payment.method sagt, ob online (online) oder vor Ort (on_site) bezahlt wird, payment.status, ob es schon bezahlt ist. Eine Online-Bestellung kommt immer bezahlt; eine Bestellung mit Zahlung vor Ort bleibt open, weil Flavoso Zahlungen vor Ort nicht vermerkt, und meldet kein order.paid.

7. Pause, Öffnungszeiten, Sondertage

Ist die Küche voll, pausiert die Kasse die Bestellannahme: POST /store/pause mit minutes (1 bis 720) oder until (mit Zeitzone), dazu optional eine note mit bis zu 140 Zeichen; ohne minutes und until dauert die Pause 30 Minuten. Sie endet immer von selbst, nach höchstens zwölf Stunden; POST /store/resume beendet sie vorher.

Führt die Kasse die Öffnungszeiten, setzt sie sie mit PUT /store/hours: höchstens zwei Fenster je Wochentag, die sich nicht überschneiden, und ein Wochentag, der fehlt, ist geschlossen. Sondertage setzt sie mit POST /store/closures. Vorlaufzeiten und Bestellarten ändert PATCH /store, mit denselben Prüfungen wie im Dashboard.

Ändert der Wirt etwas davon im Dashboard, bekommst du store.updated mit 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. Den Tagesumsatz abgleichen

Nach Mitternacht in der Zeitzone des Betriebs holt die Kasse den Vortag: GET /reports/sales?from=…&to=…&group_by=day. Brutto, netto, Steuer je Satz, Trinkgeld, Gebühren, Rabatte, nach Zahlart und Bestellart, mit denselben Regeln wie die Zahlen im Dashboard. Weicht etwas von deinem Abschluss für den Kanal Flavoso ab, zeig es an. Mehr unter Berichte.

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

Fehlerfälle und Wiederholungen

FallWas die Kasse tut
Die Kasse war ausBeim Start GET /events ab dem after, das für diese Verbindung gespeichert ist, und GET /orders?acknowledged=false&created_since=…. Ereignisse werden 30 Tage aufbewahrt, Webhooks knapp zwei Tage lang wiederholt. Ist dein after nicht mehr aufbewahrt, geht es beim ältesten aufbewahrten Ereignis weiter, und die Antwort sagt gap: true: dann zusätzlich mit GET /orders?updated_since=… nachholen. Eine neue Verbindung beginnt mit after=latest.
Keine Antwort auf einen POSTDieselbe Anfrage mit demselben Idempotency-Key noch einmal.
401 unauthorizedToken erneuern, einmal wiederholen. Scheitert das Erneuern mit invalid_grant: „Verbindung zu Flavoso getrennt“ anzeigen und „Neu verbinden“ anbieten. Mit unauthorized_client ist die App gesperrt: Tokens behalten, „vorübergehend gesperrt“ anzeigen und später erneut versuchen.
409 invalid_transitionDie Bestellung wurde woanders geändert, etwa im Dashboard storniert. Neu lesen und den Beleg anpassen.
409 external_id_takenDie Belegnummer ist schon vergeben. Die Nummernvergabe der Kasse prüfen.
429 rate_limitedRetry-After abwarten. Die Grenze gilt je Betrieb.
app.disconnectedTokens dieser Verbindung löschen, nichts mehr senden, den Inhaber in der Kasse informieren. Eine Kasse ohne Webhook erfährt die Trennung an 401 und invalid_grant beim Erneuern.
order.cancelledDen Beleg stornieren, die Küche informieren.

Checkliste

  • Tokens je Verbindung verschlüsselt gespeichert, Refresh an genau einer Stelle.
  • Bei der Erstverbindung die vorhandene Karte übernommen, bevor der erste PUT /menu läuft.
  • Signatur jedes Webhooks geprüft, Doppelte an der id erkannt, oder GET /events mit einem gespeicherten after je Verbindung.
  • GET /orders?acknowledged=false mit created_since als Sicherheitsnetz beim Start und regelmäßig; jede bearbeitete Bestellung übernommen, auch eine stornierte.
  • Jeder POST mit Idempotency-Key, abgeleitet aus Bestellkennung und Schritt.
  • livemode: false bucht nie in die echte Kasse.
  • stage_not_used, invalid_transition, app.disconnected und eine gesperrte App (unauthorized_client) behandelt.
  • Mit dem Test-Betrieb durchgespielt: Testbestellung, übernehmen, annehmen, fertig, Storno in der Konsole, Pause, Abgleich.

Etwas unklar geblieben? Schreib an support@flavoso.com und nenne die Kennung aus X-Request-Id. Damit finden wir deine Anfrage im Protokoll.