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.
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
Die vorhandene Karte übernehmen
Ein Abgleich erkennt nur Objekte wieder, die eine Kennung deiner App tragen; ein erster
PUT /menulegt die vorhandene Karte deshalb doppelt an. Lies sie vorher mitGET /menu, ordne die Objekte deinen Artikeln zu und setz je Objekt dieexternal_idmitPATCH. Im Test-Betrieb erledigt das „Kennungen vergeben“ im Reiter Speisekarte der Konsole. Ein Probelauf zeigt Doppelte insummary.*.created. - 2
Ältere Bestellungen ausklammern
GET /orders?acknowledged=falsehat keine zeitliche Grenze. Frag mitcreated_sinceund demcreated_atder Verbindung ausGET /me, und übernimm jede Bestellung, die du bearbeitest, auch eine stornierte, die du nicht buchst. - 3
Ereignisse ab jetzt
Ohne
afterliefertGET /eventsdie letzten 30 Tage, auch Ereignisse von vor der Verbindung. Frag beim ersten Mal mitafter=latestund speicherenext_afterfü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 einemafterje Verbindung.app.disconnectedkann sie so nicht lesen, denn mit der Trennung enden ihre Tokens: eine Trennung erkennt sie an 401 und danachinvalid_grantbeim Erneuern. - In beiden Fällen als Sicherheitsnetz:
GET /orders?acknowledged=false&created_since=…mit demcreated_atder Verbindung, bei jedem Start und regelmäßig. Was nicht übernommen ist, steht dort.
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_idersetzt 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 inGET /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
| Fall | Was die Kasse tut |
|---|---|
| Die Kasse war aus | Beim 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 POST | Dieselbe Anfrage mit demselben Idempotency-Key noch einmal. |
401 unauthorized | Token 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_transition | Die Bestellung wurde woanders geändert, etwa im Dashboard storniert. Neu lesen und den Beleg anpassen. |
409 external_id_taken | Die Belegnummer ist schon vergeben. Die Nummernvergabe der Kasse prüfen. |
429 rate_limited | Retry-After abwarten. Die Grenze gilt je Betrieb. |
app.disconnected | Tokens 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.cancelled | Den 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 /menuläuft. - Signatur jedes Webhooks geprüft, Doppelte an der
iderkannt, oderGET /eventsmit einem gespeichertenafterje Verbindung. GET /orders?acknowledged=falsemitcreated_sinceals Sicherheitsnetz beim Start und regelmäßig; jede bearbeitete Bestellung übernommen, auch eine stornierte.- Jeder POST mit
Idempotency-Key, abgeleitet aus Bestellkennung und Schritt. livemode: falsebucht nie in die echte Kasse.stage_not_used,invalid_transition,app.disconnectedund 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.