# POS Software Sync — LLM Reference

> Single-file, self-contained reference for the Cheers POS Software Sync integration.
> Source: https://integration.cheersapp.io/docs/integrations/pos-software-sync/overview
> No OpenAPI spec — in this integration Cheers is the HTTP client and the POS is the server,
> so the contract is defined by content requirements rather than by a Cheers-side spec.

## What this integration does

POS Software Sync imports a venue's **products** and **sales transactions** from a POS software
into Cheers. It makes the venue's real catalogue (prices, VAT rates, stock) available in the
Cheers app, and feeds the turnover reports the partner sees in the Cheers CMS.

## The direction is reversed compared to every other Cheers integration

- **Cheers is the client.** It polls the POS on a schedule (daily by default, configurable) and
  on demand when the partner triggers a manual sync from the Cheers CMS.
- **The POS is the server.** It only answers requests. There is **no webhook, no push, no
  callback URL** — nothing to send to Cheers.

In the other three integrations Cheers exposes the API and the POS is the client:

- Cheers Loyalty (POS API) — see `/llms/cheers-loyalty.md`
- Counter Order — see `/llms/counter-order.md`
- Hybrid RevoUnion — see `/llms/hybrid-revo-token.md`
- Device Details (shared) — see `/llms/device-details.md`

```
scheduled run (daily, configurable) OR manual sync from CMS:
    GET <partner product endpoint>
    -> full current product list
    match on product identifier
    update price/stock if the partner enabled those syncs
    products present in the previous run but absent now
        -> flagged in CMS as "disappeared from the POS"

    if the partner also exposes a transaction endpoint:
        GET <partner transaction endpoint>?createdAtFrom=<newest stored createdAt>
        -> closed transactions
        drop ids already stored
        store the rest raw
```

## The two APIs

| API | Required | Purpose |
|---|---|---|
| Product API | **Yes** | Full current product list. Drives product matching, price sync, stock sync. |
| Transaction API | No, strongly recommended | Closed payment transactions. Drives sales reporting. |

Without the Product API there is no integration. Without the Transaction API the integration
works but the partner gets no turnover reporting.

## Format is flexible, content is fixed

Cheers writes a **dedicated adapter per POS software**. Field names, language, wrapper objects,
pagination style and HTTP method are the POS vendor's choice — Cheers adapts. What Cheers cannot
do is invent data that was never sent. The required fields below are required because the sync
cannot function without them.

---

## Authentication

The POS defines the scheme; Cheers adapts. All of these are in production use:

| Scheme | Example |
|---|---|
| API key in a custom header | `X-API-KEY: YOUR_KEY` (any header name) |
| `Authorization` header, token | `Authorization: Bearer YOUR_TOKEN` (prefix optional) |
| `Authorization` header, Basic auth | `Authorization: Basic base64(user:password)` |
| Key in the request body | `{ "apiKey": "YOUR_KEY", ... }` |

**Static credentials only.** Cheers stores what the partner enters and sends it with every
request. There is no token-exchange or refresh step. If the POS issues short-lived tokens, it
must provide a long-lived key for the sync.

### Values the partner enters in the Cheers CMS

| Value | Required | Description |
|---|---|---|
| Base URL | If it differs per partner | Host and path prefix, e.g. `https://pos.example.com/api`. If all partners share one URL it can be hard-coded in the adapter. |
| Credentials | Yes | API key, token, or username + password. |
| Scoping identifier | If applicable | Business unit / restaurant / venue / price list id, when one credential serves several units. |

Cheers configures the sync **per venue**. If one key covers several venues, the POS must offer a
way to select one (path parameter, query parameter or body field).

Reject bad credentials with `401`.

---

## Product API (required)

One endpoint returning the venue's **complete, current product list**.

### Required and optional data

| Data | Required | Notes |
|---|---|---|
| Identifier | **Yes** | Stable, never reused. The Cheers product is linked to this value; if it changes the link is lost and the product appears as new and unmatched. |
| Name | **Yes** | Display and partner-side matching. |
| Gross price | **Yes** | Major currency unit — forints for HUF, no decimals. **Not** minor units (no fillér, no cents). |
| VAT rate | **Yes** | Percentage. Supported: integers `0`–`27`, plus `5.5`, `7.7`, `9.5`. Anything else falls back to `27`. |
| Takeaway VAT rate | No | If absent, no separate takeaway price is handled. |
| Stock information | No | Boolean or status value (`ACTIVE`, `available`, …). If absent, stock sync stays disabled. |
| Currency | No | Assumed HUF if absent. |
| Description, category, barcode, tags, unit of measure | No | Stored as supplementary data, visible to the partner in the CMS. |

**VAT must be a percentage, not an internal tax code.** A value like `A`, `TAX_1` or `3`
(meaning "the third VAT band") cannot be interpreted and falls back to 27%.

### Free choices

- **Response shape:** plain JSON array, or an object wrapping the array under `data`, `results`
  or `content`. All four are in live use.
- **Field names and language:** anything. One live integration returns Hungarian field names.
- **HTTP method:** typically `GET`; `POST` with a body is equally acceptable.
- **Pagination:** not required. If absent, Cheers requests the whole list in one call.

### Pagination rules (only if paginating)

1. **Signal whether more pages exist** — `last: true`, a `next` URL, or a total page count.
2. **Order deterministically** — the same query must return items in the same order across
   pages. Without a stable sort, products shift between pages while Cheers walks them and some
   are never seen.

### Accepted shapes

Plain array:

```json
[
  { "id": "SKU-1042", "name": "Draft Beer 0.5L", "price": 1890, "vat": 27 },
  { "id": "SKU-1043", "name": "Mineral Water 0.33L", "price": 690, "vat": 27, "takeawayVat": 5, "available": true }
]
```

Wrapped object (here with Hungarian field names — also accepted):

```json
{
  "data": [
    { "cikkszam": "SKU-1042", "megnevezes": "Csapolt sör 0,5L", "bruttoAr": 1890, "afaKulcs": 27, "keszleten": true }
  ]
}
```

Paginated:

```json
{
  "content": [
    { "id": "SKU-1042", "name": "Draft Beer 0.5L", "price": 1890, "vat": 27 }
  ],
  "page": 0,
  "size": 100,
  "totalElements": 254,
  "last": false
}
```

### Recommended schema (suggestion, not a specification)

For a new endpoint with no existing shape to preserve. Any other naming, nesting or language is
equally accepted — only the content requirements are binding. Do not restructure an existing
endpoint to match this.

```json
{
  "id": "SKU-1042",
  "name": "Draft Beer 0.5L",
  "price": 1890,
  "vat": 27,
  "takeawayVat": 5,
  "currency": "HUF",
  "available": true,
  "description": "Locally brewed lager",
  "category": "Beers",
  "barcode": "5998765432109",
  "unit": "pcs"
}
```

### Variants

Every variant, size or packaging needs **its own identifier and its own price**. In Cheers each
variant becomes a separate product.

```json
[
  { "id": "SKU-1042-05", "name": "Draft Beer 0.5L", "price": 1890, "vat": 27 },
  { "id": "SKU-1042-03", "name": "Draft Beer 0.3L", "price": 1390, "vat": 27 }
]
```

A single product carrying a list of sizes without individual identifiers cannot be imported —
there is no stable value to link each variant to.

### Modifiers

Modifiers (add-ons, options, extras) may come from a **separate endpoint** or be **embedded in
the product**; both work. They also become separate products in Cheers, so each requires an
**identifier, a name and a price**.

```json
{
  "id": "SKU-1042",
  "name": "Draft Beer 0.5L",
  "price": 1890,
  "vat": 27,
  "modifiers": [
    { "id": "MOD-01", "name": "Extra lemon", "price": 150, "vat": 27 },
    { "id": "MOD-02", "name": "No ice", "price": 0, "vat": 27 }
  ]
}
```

Modifier identifiers sent here are the exact `productId` values returned in Counter Order order
items — no mapping needed on the POS side.

### Processing rules

- **Always return the full current list, never a delta.** Every sync compares the complete
  response against the previous one; products absent from the current response are flagged in
  the CMS as "disappeared from the POS" rather than deleted. A changes-only response would flag
  the entire rest of the catalogue as missing.
- **Price and stock sync overwrite Cheers data.** If the partner enables them, the POS price and
  stock replace what is on the Cheers product. The price must therefore always be the real,
  current **gross selling price** — not a list price, net price or historical price.
- **Duplicate identifiers:** the first occurrence is kept, the rest discarded.

---

## Transaction API (optional, strongly recommended)

One endpoint returning **closed payment transactions**. Produces the partner's turnover reports.

### Required data — only two fields

| Data | Required | Notes |
|---|---|---|
| Transaction identifier | **Yes** | Deduplication key. Stored exactly once, ever. |
| Creation timestamp | **Yes** | ISO-8601 **with time zone offset**, e.g. `2026-07-28T14:32:05+02:00`. The start of the next fetch is derived from it. |
| Everything else | No | Stored **raw, exactly as sent**, and shown to the partner in the CMS. |

The transaction body is **freely extensible** — whatever the POS sends is preserved, and adding
fields later breaks nothing. Recommended content, because partners ask for it:

- Line items — product identifier, quantity, unit price, discount
- Payment parts — payment method and amount per part
- Total amount, service charge, tip, discount, currency
- Void / cancellation timestamp

Line-item product identifiers should match the Product API identifiers — that is what makes
per-product turnover reporting possible.

```json
[
  {
    "id": "TRX-2026-0001",
    "createdAt": "2026-07-28T14:32:05+02:00",
    "total": 4270,
    "currency": "HUF",
    "items": [
      { "productId": "SKU-1042", "quantity": 2, "unitPrice": 1890, "discount": 0 },
      { "productId": "MOD-01", "quantity": 1, "unitPrice": 150, "discount": 0 }
    ],
    "payments": [
      { "method": "CARD", "amount": 4000 },
      { "method": "CASH", "amount": 270 }
    ],
    "serviceCharge": 340,
    "tip": 0
  }
]
```

The Product API response-shape rules apply here too: plain array or wrapped object, any field
names, `GET` or `POST`, optional pagination.

### Time filtering

- A **start-time parameter** (e.g. `createdAtFrom`) is strongly recommended. Cheers sends the
  timestamp of the most recently stored transaction so only new ones are transferred.
- The filter may be **inclusive** (`>=`) — the repeated boundary transaction is filtered out by
  identifier on the Cheers side.
- **Without a time filter**, Cheers requests the entire set on every run: acceptable at low
  volume, unworkable at high volume.

### Deduplication and immutability

- Deduplication is **by identifier**; an already-stored identifier is skipped.
- **Stored transactions are never updated.** Cheers does not re-read or refresh a transaction it
  has already saved.
- Therefore a later modification only reaches Cheers **as a new transaction with a new
  identifier** — a void must be a separate record, not an edit of the original.

### Critical constraint: monotonic creation timestamps

The next fetch starts from the newest timestamp already stored. A transaction inserted later but
carrying an **earlier** creation timestamp — a backdated record, a delayed upload from an
offline terminal, a batch replay — falls behind the watermark and **is never fetched**.

If the POS can produce backdated records, it must either stamp them at insertion time or expose
an additional filter based on insertion order rather than business time.

---

## General requirements

### Transport

- **HTTPS with a valid certificate.** Plain HTTP, self-signed and expired certificates are
  rejected.
- **Reasonable response times even for large lists.** If the full catalogue cannot be served in
  sensible time, offer pagination.
- **Concurrent requests must be tolerated** — several venues may sync simultaneously, and a
  manual CMS sync can overlap a scheduled run.

### Behaviour

- **Idempotent and side-effect free.** Cheers only reads; a fetch must never create, modify,
  consume or lock anything.
- **Stable, permanent identifiers** for products, variants, modifiers and transactions. A reused
  or regenerated identifier silently breaks product matching or causes transactions to be
  dropped as duplicates.
- **Always the full current product list**, never a delta.

### HTTP status codes

| Status | When |
|---|---|
| `200` | Success — and only success. |
| `401` | Missing or invalid credentials. |
| `4xx` | Client-side problem (bad parameter, unknown venue identifier). |
| `5xx` | Server-side problem. |

**Never return an error inside a `200` response.** A `200 OK` with an error body is read as a
successful sync carrying whatever payload it describes — typically an empty or partial product
list, which flags every absent product as disappeared. On a non-`2xx` response Cheers marks the
sync run as failed, leaves existing data untouched, and shows the error message to the partner
in the CMS.

## Go-live checklist

- HTTPS endpoint with a valid certificate
- Authentication scheme documented, credentials handed to the partner
- Venue scoping available if one credential serves several venues
- Product endpoint returns the **full** catalogue with identifier, name, gross price and VAT
- Variants and modifiers carry their own identifier, name and price
- Pagination — if used — signals the last page and sorts deterministically
- Transaction endpoint returns an identifier and an ISO-8601 creation timestamp with offset
- Start-time filter supported on transactions
- Errors returned as `4xx` / `5xx`, never as `200`
