---
title: "POS order integration"
description: "Pull webshop, kiosk and QR orders into your own POS or kitchen system, acknowledge them and report kitchen progress."
---

> Documentation Index
> Fetch the complete documentation index at: https://developer.unipage.be/llms.txt
> Use this file to discover all available pages before exploring further.

# POS order integration

A third-party POS or kitchen system can take the restaurant's online orders
(webshop, app, QR table orders, kiosk and accepted delivery-platform orders)
from an order queue, mark them as received and report back when the kitchen
starts, finishes or runs late. This is the same queue the Unipage till
(Unipos PLUS) reads.

All calls use the restaurant's [Core API key](/core/authentication/) in the
`X-API-Key` header. Paths are relative to
`https://api.unipage.eu/{business id}/generic_polling`.

> **Caution**
>
> **One system pulls the orders.** The queue serves an order to whoever asks
> until someone acknowledges it, so two systems that pull for the same
> restaurant both receive (and print) every order. Ask Unipage to switch the
> restaurant to the polling integration, and make sure no Unipage till of the
> same restaurant group pulls webshop orders as well.

## How the queue works

1. **Step 1**

   **Poll.** `GET /orders` returns every order that is waiting for the POS.
2. **Step 2**

   **Acknowledge.** Once an order is stored on your side, `POST /orders/{id}`
   with `{"status": "SYNCED"}`. From then on the poll no longer returns it.
3. **Step 3**

   **Report progress (optional).** Tell Unipage when the kitchen starts or
   finishes, or when an order runs late.

- **Served until acknowledged.** Every poll returns every waiting order
  again, without a time window or a limit, until it is acknowledged. Do not
  rely on seeing an order only once: deduplicate on the order `id`.
- **An acknowledged order does not come back by itself.** Only Unipage
  support can put an order back in the queue (for example after a POS was
  replaced).
- **Group-wide.** For a restaurant group whose locations share one catalog,
  the poll returns the waiting orders of every location of the group. The
  `business_id` field in an order is not the location id: do not build on
  it.

## `GET /orders`

The orders waiting for the POS, as a plain JSON array (no `data` wrapper).
An order is in it while it:

- is placed and not finished: `status` is `PLACED`, `CHEF_STARTED` or
  `CHEF_COMPLETED` (an unpaid online payment, a declined or a cancelled order
  never appears),
- is a real order (top-ups and other payment-only orders are left out),
- has not been acknowledged yet.

Delivery-platform orders (Uber Eats, Deliveroo) only appear after the
restaurant has accepted them.

```sh
curl https://api.unipage.eu/x94d3/generic_polling/orders \
  -H "X-API-Key: $UNIPAGE_CORE_API_KEY"
```

### Polling rules

- **Poll every 10 to 30 seconds**, not faster. The poll has its own limit of
  120 calls per minute per restaurant and IP on top of the
  [key limits](/core/errors-and-limits/); a `429` costs you one cycle and the
  orders stay in the queue.
- **Acknowledge right after storing.** Until you post `SYNCED`, the order is
  in every response.
- **Never retry a `4xx` unchanged in a loop.** See
  [Errors and limits](/core/errors-and-limits/).

## `GET /orders/{id}`

One order, by its `id`, in the same shape as the poll, plus the `payment`
block. An order of another restaurant group answers `404`.

## The order fields

The fields an integration needs most:

| Field | Meaning |
|---|---|
| `id` | the order id (short hash id); use it for every call on the order |
| `status` | `PLACED`, `CHEF_STARTED` or `CHEF_COMPLETED` |
| `order_type`, `order_source` | how it is fulfilled (takeout, delivery, ...) and which channel placed it |
| `is_paid`, `payment_method` | whether it is paid online already, or still has to be paid at pickup or delivery |
| `order_number_for_company` | the short order number shown to the customer |
| `takeout_delivery_timestamp` | when the customer expects the order (Unix seconds) |
| `delay_time_in_minutes` | the delay reported so far |
| `total_price`, `delivery_cost`, `service_cost` | amounts including VAT |
| `coupon_reduction`, `coupon_string` | the coupon discount and its code; a code starting with `LOYALTY:` is a loyalty reward |
| `wallet_amount` | the part paid with the customer's wallet |
| `order_notes` | the customer's note on the order |
| `user_firstname`, `user_lastname`, `user_email`, `user_phone` | the customer |
| `delivery_*` | the delivery address (delivery orders) |
| `table_hashid` | the QR table the order was placed at, if any |
| `is_kiosk_order`, `is_digital_menu_order` | placed at a kiosk, or from a QR menu at the table |
| `display_id`, `delivery_type`, `dropoff_notes` | delivery-platform details (Uber Eats, Deliveroo) |
| `created_at`, `updated_at` | Unix seconds |
| `orderItems[]` | the lines, see below |

Each line in `orderItems` carries `item_id` and the full `item` (the product
as in the [catalog](#the-catalog)), `reference` (the product name),
`amount`, `item_price`, `total_price`, `vat_rate`, `notes` (the customer's
remark on the line), `ordered_for` (in a group order: the person the line is
for), `orderItemAddons` (the chosen options per option group) and
`discount_lines`.

- **Options.** An option of a quantity group has `type: "AMOUNT"` and an
  integer `value`; every other option has `type: "BOOLEAN"` and a boolean
  `value`.
- **Discounts are lines.** `discount_lines` (on lines and on options) carry
  the discount as Unipage calculated it (`source`, `amount`, `vat_rate` and
  the totals). Ring them as they are; do not recalculate them.

Ignore fields you do not know: fields are added over time.

## Acknowledge: `POST /orders/{id}`

```sh
curl -X POST https://api.unipage.eu/x94d3/generic_polling/orders/k2f9a \
  -H "X-API-Key: $UNIPAGE_CORE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "SYNCED"}'
```

| Body field | Meaning |
|---|---|
| `status` | `SYNCED` (stored, take it out of the queue) or `SYNCING` (you are still processing it). Any other value is a `422`. |

The answer is `{"success": true}`.

## Kitchen progress: `POST /orders/{id}/fulfilment`

Moves the order through the kitchen statuses the restaurant also sees in
Unipage:

```json
{ "order": { "status": "CHEF_STARTED" } }
```

Send `CHEF_STARTED` when the kitchen starts and `CHEF_COMPLETED` when the
order is ready. Only send these statuses: the value is stored as sent.

When the status becomes `CHEF_STARTED`, Unipage tells the customer the
kitchen has started (mail, and SMS where the restaurant has it), if the
restaurant has customer communication switched on. The answer is
`{"success": true}`.

## Running late: `POST /orders/{id}/delay`

```json
{ "minutes": 15 }
```

`minutes` is a whole number from 1 to 480 (otherwise `422`). Unipage stores
the delay on the order and mails the customer that the order is late. The
restaurant must have order delays enabled, otherwise the answer is `403`.

## The catalog

To map order lines to your own articles, read the products of the
restaurant. These paths are relative to the same base URL:

| Call | Returns |
|---|---|
| `GET /products` | the restaurant's products with category, prices, translations and tags. Optional `updated_after` (Unix seconds: only products changed since then) and `category_id`. |
| `GET /products/{id}` | one product with its option groups and options |
| `GET /products/info` | `{"products_last_updated_at_timestamp": ...}`: when a product last changed, a cheap check before you fetch |
| `GET /categories` | the categories, optional `updated_after` |

`updated_after` overlaps one second with the previous answer, so a product
changed in the same second as your last sync is never missed: expect to see
a product twice and upsert on its `id`.

Source: https://developer.unipage.be/core/pos-integration/index.mdx
