# Counter Order API — LLM Reference

> Single-file, self-contained reference for the Cheers Counter Order integration.
> Source: https://integration.cheersapp.io/docs/integrations/counter-order/overview
> OpenAPI spec: `counter-order-openapi` — https://integration.cheersapp.io/docs/api-reference/counter-order-openapi/cheers-api-documentation

## What this API does

Counter Order lets external POS and kitchen management systems receive, manage, and fulfill
orders that customers place through the Cheers app, web, or kiosk.

The integration is **callback-driven**: when an order becomes actionable, Cheers POSTs to your
callback URL with the order id. Your system fetches the order, optionally handles payment,
then drives the order through its lifecycle (approve → done → completed).

Your system can also enable or disable **desks** to control where customers may order —
useful for closing time or to pause an overloaded kitchen.

## Environments

| Environment | Base URL |
|---|---|
| Sandbox | `https://api.dev.votesess.com` |
| Production | `https://api.uniqpon.com` |

## Authentication

Every request requires an API key header:

```
X-API-Key: YOUR_API_KEY
```

Errors return `401 Unauthorized` (bad/missing key) or `403 Forbidden` (key lacks access to the
resource).

`GET /v1/integrations/me` returns the identity behind the key — organization, place,
`authorities`, and `relatedDesks` (the desks this device serves, with their `enabled` state).
Order handling requires the `ORDER` authority. Call it once at POS startup and cache the
result. Full reference: https://integration.cheersapp.io/llms/device-details.md

## Callbacks

Configure your callback URL in the Cheers CMS. When an order becomes actionable (status
`APPROVABLE`), Cheers sends:

```
POST https://your-system.com/your-callback-endpoint?orderId=ORDER_ID
```

The order id arrives as a **query parameter**; the body carries no order payload. Your system
should:

1. Respond `200 OK` promptly.
2. Use the `orderId` to fetch the full order via `GET /v1/integrations/counter-orders/{orderId}`.

If the callback fails, Cheers retries with exponential backoff. Callback status and manual
retries are available in the Cheers CMS.

## Key concepts

| Concept | Description |
|---|---|
| Callback | POST from Cheers to your URL when an order becomes actionable; carries `orderId` as a query param. |
| Order Status | Lifecycle state: `NEW`, `CANCELLED`, `APPROVABLE`, `APPROVED`, `REJECTED`, `DONE`, `LEFT`, `COMPLETED`. |
| Transaction Status | Payment state: `UNPAID`, `AUTHORIZED`, `CAPTURED`, `PAID_EXTERNALLY`, `RELEASED`, `FAILED`, `PAYMENT_NOT_REQUIRED`. |
| Payment Processor | `BARION`, `SIMPLE_PAY` (in-app), `EXTERNAL_MANUAL` (at counter), `EXTERNAL_AUTO`, `NONE`. |
| Desk | The counter or table the order belongs to. Type `COUNTER` or `TABLE`, plus an `enabled` flag. |
| Order Source | Where the order originated: `MOBILE`, `WEB`, `KIOSK`. |

## Endpoint summary

All paths are relative to the base URL. All require `X-API-Key`.

| Method | Path | Purpose |
|---|---|---|
| GET | `/v1/integrations/counter-orders/{orderId}` | Fetch full order details |
| PUT | `/v1/integrations/counter-orders/{orderId}/status` | Update order status |
| PUT | `/v1/integrations/counter-orders/{orderId}/finalize-external-payment` | Confirm external payment was collected |
| PUT | `/v1/integrations/counter-orders/{orderId}/cancel-external-payment` | Cancel a failed external payment |
| GET | `/v1/integrations/me/desks` | List your desks and their enabled state |
| PUT | `/v1/integrations/desks/status` | Enable or disable desks |

---

## Order lifecycle

State transitions:

```
[start] --> NEW                     (customer places order)
NEW        --> CANCELLED            (customer cancels)          [system]
NEW        --> APPROVABLE           (payment confirmed)         [system] -> callback fires
APPROVABLE --> APPROVED             (POS approves)              [your system]
APPROVABLE --> REJECTED             (POS rejects)               [your system]
APPROVED   --> DONE                 (order prepared)            [your system]
APPROVED   --> LEFT                 (customer left)             [your system]
DONE       --> COMPLETED            (customer picked up)        [your system]
DONE       --> LEFT                 (customer left)             [your system]
CANCELLED / REJECTED / LEFT / COMPLETED are terminal.
```

| Status | Description | Who sets it |
|---|---|---|
| `NEW` | Order placed, payment may still be processing. | System |
| `CANCELLED` | Customer cancelled. | System |
| `APPROVABLE` | Ready for the POS to act on. **Callback is sent at this point.** | System |
| `APPROVED` | POS accepted and will prepare the order. | Your system |
| `REJECTED` | POS rejected (e.g. out of stock). Optional `rejectionMessage`. | Your system |
| `DONE` | Prepared and ready for pickup. Customer is notified. | Your system |
| `LEFT` | Customer left without picking up. Settable from `APPROVED` **or** `DONE`. | Your system |
| `COMPLETED` | Customer received the order. | Your system |

### Transaction statuses

| Value | Description |
|---|---|
| `UNPAID` | Payment not processed yet. |
| `AUTHORIZED` | Authorized but not captured (in-app payments). |
| `CAPTURED` | Captured successfully (in-app payments). |
| `PAID_EXTERNALLY` | Collected at the counter (after `finalize-external-payment`). |
| `RELEASED` | Authorized payment released back (e.g. order rejected/cancelled). |
| `FAILED` | Payment failed. |
| `PAYMENT_NOT_REQUIRED` | No payment needed for this order. |

Typical progressions: in-app `UNPAID → AUTHORIZED → CAPTURED` (or `→ RELEASED`);
external `UNPAID → PAID_EXTERNALLY` (or `→ FAILED`).

---

## 1. Fetch order details

`GET /v1/integrations/counter-orders/{orderId}`
Operation id: `getCounterOrderByIdByIntegration`
Success: `200 OK`. Errors: `400`, `401`, `403`, `404`.

```bash
curl -X GET https://api.dev.votesess.com/v1/integrations/counter-orders/abc-123-def \
  -H "X-API-Key: YOUR_API_KEY"
```

### Response — `IntegrationCounterOrderResponse`

```json
{
  "id": "abc-123-def",
  "createdAt": "2026-03-15T14:30:00Z",
  "orderNumber": "A-42",
  "status": "APPROVABLE",
  "transactionStatus": "CAPTURED",
  "paymentProcessor": "BARION",
  "currencyCode": "HUF",
  "orderSource": "MOBILE",
  "tip": { "tip": 200, "externalPayment": 0 },
  "desk": { "id": "desk-001", "name": "Counter 1", "type": "COUNTER", "enabled": true },
  "invoicingType": "PLACE_RECEIPT",
  "items": [
    {
      "productId": "beer-500ml",
      "unitPrice": 800,
      "discount": 200,
      "externalPayment": 0,
      "vat": 27,
      "isTakeaway": false,
      "modifiers": [
        { "productId": "extra-lemon", "unitPrice": 100, "discount": 0, "externalPayment": 0, "vat": 27 }
      ]
    }
  ]
}
```

Required fields: `id`, `createdAt`, `orderNumber`, `status`, `transactionStatus`,
`paymentProcessor`, `currencyCode`, `tip`, `items`, `desk`, `invoicingType`, `orderSource`.

Optional fields: `approvedAt`, `rejectedAt`, `finishedAt`, `completedAt`, `leftAt`,
`userComment`, `rejectionMessage`, `deliveryOption`, `metadata`, `buyerData`,
`deliveryAddressData`. All timestamps are ISO 8601 date-time.

| Field | How to use it |
|---|---|
| `orderNumber` | Display to the customer / call out when ready. |
| `status` | Current lifecycle state. |
| `transactionStatus` | Payment state — tells you whether payment still needs handling. |
| `paymentProcessor` | If `EXTERNAL_MANUAL`, you must collect payment at the counter. |
| `items` | Products with prices, discounts, VAT, modifiers, takeaway flag. |
| `items[].externalPayment` | Per-item amount to collect externally (split payment). |
| `tip` | Tip amount and the externally-paid portion of it. |
| `deliveryOption` | Delivery fee, VAT, and type — present only for delivery orders. |
| `desk` | Which counter/table the order belongs to. |
| `userComment` | Free-text note from the customer. |
| `metadata` | Custom key-value pairs, if configured. |

---

## 2. Handle payment — `EXTERNAL_MANUAL` only

Branch on `paymentProcessor`:

- `BARION`, `SIMPLE_PAY`, `EXTERNAL_AUTO`, `NONE` → payment is handled by the Cheers system.
  Skip straight to status updates.
- `EXTERNAL_MANUAL` → collect payment at the counter, then report the outcome.

**On success:**

`PUT /v1/integrations/counter-orders/{orderId}/finalize-external-payment`
Operation id: `finalizeExternalCounterOrderPayment`
No request body. Returns the updated `IntegrationCounterOrderResponse`.

```bash
curl -X PUT https://api.dev.votesess.com/v1/integrations/counter-orders/abc-123-def/finalize-external-payment \
  -H "X-API-Key: YOUR_API_KEY"
```

**On failure:**

`PUT /v1/integrations/counter-orders/{orderId}/cancel-external-payment`
Operation id: `cancelExternalCounterOrderPayment`
No request body. Returns the updated `IntegrationCounterOrderResponse`.

```bash
curl -X PUT https://api.dev.votesess.com/v1/integrations/counter-orders/abc-123-def/cancel-external-payment \
  -H "X-API-Key: YOUR_API_KEY"
```

---

## 3. Update order status

`PUT /v1/integrations/counter-orders/{orderId}/status`
Operation id: `updateCounterOrderStatusByIntegration`
Success: `200 OK` with the updated order. Errors: `400`, `401`, `403`, `404`, `503`.

### Request body — `IntegrationUpdateCounterOrderStatusRequest`

Discriminated on `status`.

| Field | Type | Required | Notes |
|---|---|---|---|
| `status` | enum | yes | `APPROVED` \| `REJECTED` \| `LEFT` \| `DONE` \| `COMPLETED` |
| `rejectionMessage` | string | no | Only with `REJECTED`. Max 2000 chars. |

Approve — customer is notified the order was accepted:

```bash
curl -X PUT https://api.dev.votesess.com/v1/integrations/counter-orders/abc-123-def/status \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "APPROVED"}'
```

Reject — customer is notified, optional reason:

```bash
curl -X PUT .../status -H "X-API-Key: ..." -H "Content-Type: application/json" \
  -d '{"status": "REJECTED", "rejectionMessage": "Item is out of stock"}'
```

Mark done — order is prepared; customer is notified it is ready:

```bash
curl -X PUT .../status -d '{"status": "DONE"}'
```

Complete — customer picked up:

```bash
curl -X PUT .../status -d '{"status": "COMPLETED"}'
```

Left — customer abandoned the order. Valid from `APPROVED` or `DONE`, regardless of
preparation state:

```bash
curl -X PUT .../status -d '{"status": "LEFT"}'
```

---

## Order data model

```
Order
├── items: BasketItem[]
│   └── modifiers: ModifierItem[]
├── desk: Desk
├── tip: Tip
├── deliveryOption: DeliveryOption?   (0..1)
├── buyerData: BuyerData?
└── deliveryAddressData: DeliveryAddress?
```

### Basket item — `IntegrationBasketItemResponse`

| Field | Type | Required | Description |
|---|---|---|---|
| `productId` | string | yes | Product id in **your** system (the integrator's id). |
| `unitPrice` | number | yes | Unit price, excluding modifiers. |
| `discount` | number | yes | Discount Cheers applied to this product, excluding modifiers. |
| `externalPayment` | number | yes | Amount to collect outside Cheers, excluding modifiers. |
| `vat` | number | yes | VAT rate (e.g. 27). |
| `isTakeaway` | boolean | yes | Whether the item is marked takeaway. |
| `modifiers` | array | yes | Add-ons/extras (e.g. "extra lemon", "no ice"). |

**Product id mapping:** `productId` already matches the id in your external system — the same
id you supplied during product synchronization. No mapping table is required.

### Modifier — `IntegrationModifierItemResponse`

Same price structure as a basket item: `productId`, `unitPrice`, `discount`,
`externalPayment`, `vat` — all required.

### Desk — `IntegrationDeskResponse`

| Field | Type | Description |
|---|---|---|
| `id` | string | Desk id in the Cheers system. |
| `name` | string | Display name (e.g. "Counter 1"). |
| `type` | enum | `COUNTER` or `TABLE`. |
| `enabled` | boolean | Whether customers can currently order at this desk. |

### Tip — `IntegrationTipResponse`

| Field | Type | Description |
|---|---|---|
| `tip` | number | Total tip amount. |
| `externalPayment` | number | Portion of the tip collected externally. |

### Delivery option — `IntegrationOrderContextOrderDeliveryOptionResponse`

Present only when the order includes delivery.

| Field | Type | Description |
|---|---|---|
| `id` | string | Delivery option id in the Cheers system. |
| `deliveryOptionFee` | number | The delivery fee. |
| `externalPayment` | number | Portion of the fee collected externally. |
| `vat` | number | VAT rate for the fee. |
| `type` | enum | `CUSTOM`, `TAKEAWAY`, or `HOME_DELIVERY`. |

### Buyer data — `IntegrationOrderBuyerDataResponse`

Required: `id`, `name`, `countryCode` (ISO 3166-1 alpha-2), `zipCode`, `city`, `address`,
`isCompany`. Nullable: `companyName`, `taxNumber`.

### Delivery address — `IntegrationOrderDeliveryAddressDataResponse`

Required: `id`, `name`, `countryCode`, `zipCode`, `city`, `address`, `lat`, `lon`.
Optional: `comment`.

### Invoicing types

| Type | Description |
|---|---|
| `PLACE_RECEIPT` | The venue issues a receipt. |
| `PLACE_INVOICE` | The venue issues an invoice. |
| `CHEERS_INVOICE` | Cheers issues the invoice. |

Supported combinations: `PLACE_RECEIPT` alone; `PLACE_INVOICE` alone; `CHEERS_INVOICE` alone;
`PLACE_RECEIPT` + `PLACE_INVOICE`; `PLACE_RECEIPT` + `CHEERS_INVOICE`.

---

## Desk availability

Every counter order is placed at a desk. The `enabled` flag controls whether customers can
currently order there. Disabling a desk **temporarily suspends new orders** at it — customers
in the app, web, or kiosk cannot place orders there until it is re-enabled. Orders already
placed are unaffected; you still manage their lifecycle as usual.

Use cases: closing time (disable all desks), temporary suspension (kitchen overloaded, POS
offline), selective availability (a section under maintenance).

### List your desks

`GET /v1/integrations/me/desks`
Operation id: `getDesksForIntegration`
Returns all desks belonging to your integration API key, with their current `enabled` state.
Success: `200 OK` (array). Errors: `400`, `401`, `403`, `404`.

```bash
curl -X GET https://api.dev.votesess.com/v1/integrations/me/desks \
  -H "X-API-Key: YOUR_API_KEY"
```

```json
[
  { "id": "desk-001", "name": "Counter 1", "type": "COUNTER", "enabled": true },
  { "id": "desk-002", "name": "Table 5",   "type": "TABLE",   "enabled": false }
]
```

### Enable or disable desks

`PUT /v1/integrations/desks/status`
Operation id: `changeDeskStatusesByIntegration`
Sets the `enabled` state for one or more desks in a single request.
Success: `200 OK` — the updated desks, same shape as the list endpoint.

Request body — `IntegrationChangeDeskStatusesRequest`:

| Field | Type | Required | Description |
|---|---|---|---|
| `deskIds` | string[] | yes | Desk ids in the Cheers system. Unique, at least one. |
| `enabled` | boolean | yes | `true` to allow ordering, `false` to suspend it. |

```bash
# Suspend ordering
curl -X PUT https://api.dev.votesess.com/v1/integrations/desks/status \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"deskIds": ["desk-001", "desk-002"], "enabled": false}'

# Resume ordering
curl -X PUT https://api.dev.votesess.com/v1/integrations/desks/status \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"deskIds": ["desk-001", "desk-002"], "enabled": true}'
```

If your POS knows the venue's opening hours, automate this: disable all desks at closing time
and enable them at opening, so customers never place orders nobody will fulfill.

---

## Error handling

All errors use this envelope:

```json
{
  "errorModel": {
    "errorCode": "ERROR_CODE",
    "message": "Human-readable description",
    "descriptors": ["additional", "context"]
  }
}
```

| Status | Meaning |
|---|---|
| `400` | Bad request — malformed body, invalid status transition. |
| `401` | Missing or invalid `X-API-Key`. |
| `403` | The key lacks access to this order or desk. |
| `404` | Order or desk not found. |
| `503` | Service unavailable (status update endpoint only) — safe to retry with backoff. |

---

## End-to-end flow

1. Customer places an order in the app, web, or kiosk. Cheers processes in-app payment if applicable.
2. Order reaches `APPROVABLE`; Cheers POSTs `?orderId=...` to your callback. Reply `200 OK`.
3. `GET /counter-orders/{orderId}` for the full order.
4. If `paymentProcessor == EXTERNAL_MANUAL`: collect payment, then
   `PUT /finalize-external-payment` on success or `PUT /cancel-external-payment` on failure.
5. `PUT /status {"status": "APPROVED"}` (or `REJECTED` with a `rejectionMessage`).
6. Prepare the order, then `PUT /status {"status": "DONE"}` — the customer is notified.
7. `PUT /status {"status": "COMPLETED"}` on pickup, or `{"status": "LEFT"}` if the customer
   never came back.
