Flavoso APIGuidesPOS integration
POS integration
Connecting a POS to Flavoso from A to Z: connect, sync the menu, take over and report back orders, pause, opening hours and the daily reconciliation.
The goal
After this guide, the POS runs the menu and Flavoso is one more ordering channel: online orders land in the till as receipts, statuses go back to the guest, and the sales add up at night.
POS
keeps items, prices, receipts and the end-of-day report
Flavoso
ordering page, table QR, phone app
POS → Flavoso
- PUT /menuSync the menu
- POST /menu/availabilityReport sold-out items
- POST /orders/{id}/acknowledgeOrder taken over, with receipt number
- POST /orders/{id}/statusReport the status: accepted, ready
- POST /store/pausePause, opening hours, special days
Flavoso → POS
- order.createdNew order, by webhook or fetch
- order.cancelledOrder cancelled
- menu.* · store.updatedMenu or hours changed in the dashboard
- GET /reports/salesDaily sales for reconciliation
The scopes for it: orders:write, menu:write, store:write and reports:read; with reservations, reservations:write as well.
1. Connect
Your POS has a “Connect with Flavoso” button. It starts the OAuth flow; the owner signs in, picks their restaurant and agrees. Your POS stores the tokens per connection, encrypted, together with tenant.id and connection_id.
Then it calls GET /me once and remembers time zone, currency, country, livemode and the connection's created_at. Your webhook receives app.connected.
https.First connection
A restaurant that connects your POS usually has a menu, older orders and events already. Three steps before the POS syncs and fetches for the first time, so that nothing is created twice or booked late:
- 1
Take over the existing menu
A sync only recognises objects that carry an id of your app; a first
PUT /menutherefore creates the existing menu a second time. Read it withGET /menufirst, map the objects to your items and set theexternal_idper object withPATCH. In the test restaurant, “Assign ids” on the console's Menu tab does this. A dry run shows duplicates insummary.*.created. - 2
Leave out older orders
GET /orders?acknowledged=falsehas no time limit. Ask withcreated_sinceand the connection'screated_atfromGET /me, and take over every order you handle, even a cancelled one you do not book. - 3
Events from now on
Without
after,GET /eventsreturns the last 30 days, including events from before the connection. Ask withafter=latestthe first time and storenext_afterfor this connection.
# 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. Sold out
When the POS reports something as sold out, you send POST /menu/availability with up to 500 entries at once, by external_id or id. available is always the target state, never a toggle: “sold out” twice stays sold out.
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. Receive orders
A new order arrives as order.created. For payment on site immediately, for online payment once the payment is confirmed; you never see an unpaid online order.
- POS in the cloud: by webhook, within seconds.
- POS in the restaurant without a public address: with GET /events, every few seconds,
types=order.*,menu.*,store.*,app.*, with oneafterper connection. It cannot readapp.disconnectedthis way, because its tokens end with the disconnection: it recognises a disconnection by a 401 and theninvalid_granton the refresh. - In both cases as a safety net:
GET /orders?acknowledged=false&created_since=…with the connection'screated_at, on every start and regularly. Whatever is not taken over is listed there.
external_id and acknowledged_at are always null, even when your app has already taken the order over: one envelope goes to every recipient alike. Your system decides whether an order is already booked, using the order id, or you read it with 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. Take over with your own receipt number
Once the order is booked in the POS, you mark it as taken over with POST /orders/{id}/acknowledge, with your receipt number as external_id. Flavoso then sets acknowledged_at (the first time) and external_id, and the order drops out of GET /orders?acknowledged=false.
- Idempotent: taking over twice changes nothing. A different
external_idreplaces the old one. - If your receipt number already belongs to another order of your app, Flavoso answers 409
external_id_taken. - Also take over an order you do not book, for example one that was cancelled already, if need be without
external_id: otherwise it stays inGET /orders?acknowledged=false. - Taking over is not accepting. You report separately whether the kitchen accepts the order (next step). Many POS systems do both, one right after the other.
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. Accept and advance
When the POS accepts, call POST /orders/{id}/accept. It moves the order to the next status the restaurant uses, usually confirmed (if it switched off confirmed, preparing and ready, completed); an order already past pending comes back unchanged, a completed or cancelled one answers 409. After that you advance with POST /orders/{id}/status: preparing, ready, completed. The guest gets their message, the kitchen display and the dashboard show the status right away, just like a click in the dashboard.
You may jump forward, for example from confirmed straight to ready. If the restaurant does not use a status, Flavoso answers 409 stage_not_used; then you skip it. Rejecting (reject) only works while the order is pending; cancelling (cancel) from any open status. The flow in detail is under Order flow.
# 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"}'Items, options and tax
Every item carries product_id. With the ids from the sync you find your own item; GET /menu/products/{id} also shows your app's external_id. Options are listed on the item with name, surcharge, their id and the group_id and group_name of their group, so you find your own modifier (null for orders from before 8 October 2026); unit_price already includes them.
tax.lines splits the order by tax rate, with items, delivery fee and service fee, by the same calculation as the receipt and the DATEV export. The tip (amounts.tip) is not revenue and appears in no tax line. A share without a rate on file carries rate_bps: null, see Tax lines.
payment.method says whether payment is online (online) or on site (on_site), payment.status whether it is paid already. An online order always arrives paid; an order with payment on site stays open, because Flavoso does not record payments on site, and reports no order.paid.
7. Pause, opening hours, special days
When the kitchen is full, the POS pauses online ordering: POST /store/pause with minutes (1 to 720) or until (with a time zone), plus an optional note of up to 140 characters; without minutes and until the pause lasts 30 minutes. It always ends by itself, after twelve hours at most; POST /store/resume ends it earlier.
If the POS runs the opening hours, it sets them with PUT /store/hours: at most two windows per weekday that do not overlap, and a weekday that is missing is closed. It sets special days with POST /store/closures. Lead times and order types are changed with PATCH /store, with the same checks as in the dashboard.
If the owner changes any of this in the dashboard, you receive store.updated with 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. Reconcile the daily sales
After midnight in the restaurant's time zone, the POS fetches the previous day: GET /reports/sales?from=…&to=…&group_by=day. Gross, net, tax per rate, tips, fees, discounts, by payment method and order type, by the same rules as the figures in the dashboard. If something differs from your end-of-day report for the Flavoso channel, show it. More under Reports.
# 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"Failures and retries
| Case | What the POS does |
|---|---|
| The POS was off | On start, GET /events from the after stored for this connection, and GET /orders?acknowledged=false&created_since=…. Events are kept for 30 days, webhooks are retried for almost two days. If your after is no longer kept, it continues with the oldest kept event and the response says gap: true: then also catch up with GET /orders?updated_since=…. A new connection starts with after=latest. |
| No answer to a POST | The same request again with the same Idempotency-Key. |
401 unauthorized | Refresh the token, retry once. If the refresh fails with invalid_grant: show “Connection to Flavoso ended” and offer “Connect again”. With unauthorized_client the app is suspended: keep the tokens, show “temporarily suspended” and try again later. |
409 invalid_transition | The order was changed elsewhere, for example cancelled in the dashboard. Read it again and adjust the receipt. |
409 external_id_taken | The receipt number is already taken. Check the POS numbering. |
429 rate_limited | Wait for Retry-After. The limit applies per restaurant. |
app.disconnected | Delete the tokens of this connection, send nothing more, tell the owner in the POS. A POS without a webhook learns of the disconnection by a 401 and invalid_grant on the refresh. |
order.cancelled | Void the receipt, tell the kitchen. |
Checklist
- Tokens stored encrypted per connection, refreshed in exactly one place.
- On the first connection, the existing menu taken over before the first
PUT /menuruns. - Every webhook signature verified, duplicates detected by
id, orGET /eventswith one storedafterper connection. GET /orders?acknowledged=falsewithcreated_sinceas a safety net on start and regularly; every handled order taken over, a cancelled one included.- Every POST with an
Idempotency-Key, derived from order id and step. livemode: falsenever books into the real till.stage_not_used,invalid_transition,app.disconnectedand a suspended app (unauthorized_client) handled.- Run through with the test restaurant: test order, take over, accept, ready, cancellation in the console, pause, reconciliation.
Still unclear? Write to support@flavoso.com and include the id from X-Request-Id. It lets us find your request in the log.