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.

A POS running on a device in the restaurant is usually a public client without a secret. If your POS runs in the cloud, use a confidential client and exchange the tokens on the server. A public client may return through loopback or a custom scheme; a confidential one connects real restaurants only through 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. 1

    Take over the existing menu

    A sync only recognises objects that carry an id of your app; a first PUT /menu therefore creates the existing menu a second time. Read it with GET /menu first, map the objects to your items and set the external_id per object with PATCH. In the test restaurant, “Assign ids” on the console's Menu tab does this. A dry run shows duplicates in summary.*.created.

  2. 2

    Leave out older orders

    GET /orders?acknowledged=false has no time limit. Ask with created_since and the connection's created_at from GET /me, and take over every order you handle, even a cancelled one you do not book.

  3. 3

    Events from now on

    Without after, GET /events returns the last 30 days, including events from before the connection. Ask with after=latest the first time and store next_after for 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 one after per connection. It cannot read app.disconnected this way, because its tokens end with the disconnection: it recognises a disconnection by a 401 and then invalid_grant on the refresh.
  • In both cases as a safety net: GET /orders?acknowledged=false&created_since=… with the connection's created_at, on every start and regularly. Whatever is not taken over is listed there.
In events, 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_id replaces 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 in GET /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

CaseWhat the POS does
The POS was offOn 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 POSTThe same request again with the same Idempotency-Key.
401 unauthorizedRefresh 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_transitionThe order was changed elsewhere, for example cancelled in the dashboard. Read it again and adjust the receipt.
409 external_id_takenThe receipt number is already taken. Check the POS numbering.
429 rate_limitedWait for Retry-After. The limit applies per restaurant.
app.disconnectedDelete 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.cancelledVoid 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 /menu runs.
  • Every webhook signature verified, duplicates detected by id, or GET /events with one stored after per connection.
  • GET /orders?acknowledged=false with created_since as 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: false never books into the real till.
  • stage_not_used, invalid_transition, app.disconnected and 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.