# Hybrid RevoUnion (Hybrid Revo Token) API — LLM Reference

> Single-file, self-contained reference for the Cheers Hybrid RevoUnion integration.
> Source: https://integration.cheersapp.io/docs/integrations/hybrid-revo-token/overview
> OpenAPI spec: `revo-hybrid-openapi` — https://integration.cheersapp.io/docs/api-reference/revo-hybrid-openapi/votesess-integration-api-hybrid-revo-token

## What this API does

Hybrid RevoUnion is a digital container-deposit system for events and venues. Reusable
containers (cups, plates, …) are issued to guests against a deposit fee, and their return is
tracked digitally through a unique transaction code.

"Hybrid" = physical container handling combined with digital token-based tracking: the guest
receives a physical cup, but issuance, expiration, and return are tracked digitally.

The POS flow is four steps: **fetch configuration → create transaction (issue) → preview →
validate (return)**.

## Environments

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

## Authentication

Every request requires an API key header:

```bash
curl -H "X-API-Key: YOUR_API_KEY" \
  https://api.dev.votesess.com/v1/integrations/revo-union-configurations
```

Failure returns `401 Unauthorized`:

```json
{
  "errorModel": {
    "errorCode": "UNAUTHORIZED",
    "message": "Invalid or missing API key",
    "descriptors": []
  }
}
```

If the authenticated device lacks permission for the requested resource, the API returns
`403 Forbidden`.

## Core concepts

| Concept | Description |
|---|---|
| RevoUnion Category | A reusable container type (e.g. "0.5L Cup", "Plate"). Has a name, description, and optional image. |
| Deposit (`depositGross`) | Amount the guest pays when receiving a container; refunded on return. Specified in gross. |
| Expiration Time (`expirationHours`) | Hours during which a token can be returned. After that it is invalid. |
| Transaction | One issuance event — one or more containers handed to a guest. Has a unique return code. |
| Transaction Code | A unique 40-character identifier given to the guest. Used to return containers and claim the refund. |

## Item statuses

Each issued container item is in exactly one state:

```
[issued] --> ACTIVE
ACTIVE --> RETURNED   (guest returns container; deposit refunded)
ACTIVE --> EXPIRED    (expiration window passes; deposit forfeited)
```

| Status | Description |
|---|---|
| `ACTIVE` | Issued and returnable within the expiration window. Deposit will be refunded on return. |
| `RETURNED` | Successfully returned. Deposit refunded. |
| `EXPIRED` | Expiration passed without return. Deposit forfeited. |

The system checks for expired items **every 5 minutes** and flips them to `EXPIRED`
automatically, even if the guest never attempts a return.

## Business rules

- **One configuration per organization** — exactly one Hybrid RevoUnion configuration each.
- **Versioning** — every configuration change creates a new version. Existing transactions stay
  valid under the configuration version they were created with.
- **Location-based access** — a device can only manage transactions for its assigned location.
- **Partial returns** — guests need not return all containers at once.
- **Cross-device returns** — containers can be returned at any authorized device at the same
  location, not just the issuing device.

## Endpoint summary

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

| Method | Path | Purpose |
|---|---|---|
| GET | `/v1/integrations/revo-union-configurations` | Fetch the product configuration (product mappings, deposits, expiration) |
| POST | `/v1/integrations/hybrid-revo-token-transactions` | Create a transaction — issue containers |
| GET | `/v1/integrations/hybrid-revo-token-transactions/{code}/preview` | See active items per category for a transaction |
| POST | `/v1/integrations/hybrid-revo-token-transactions/{code}/validate` | Return containers and release the deposit |
| GET | `/v1/integrations/me` | Device identity: organization, place, desks (used for receipt headers) |

---

## How Hybrid Revo fits with Cheers Loyalty

The two integrations are independent and compose in one POS transaction:

1. **Is Hybrid Revo enabled?** If yes, fetch the Revo configuration **once** and cache it — do
   not fetch before every transaction. If no, skip all Revo product checks; Cheers Loyalty
   handles everything automatically.
2. **User places order** — the basket is created.
3. **Does the user have Cheers?** (presents a QR code.) If yes, run the full Cheers Loyalty
   flow: scan QR → extract Transaction Key → `POST /preview` with basket items → discounts and
   credits applied → `POST /finalize`.
4. **After Loyalty (or without it):** if Hybrid Revo is enabled **and** the basket contains Revo
   products **with a remaining balance > 0**, start the Hybrid Revo flow.
5. **Hybrid Revo flow:** `POST /hybrid-revo-token-transactions` with product ids and quantities
   → a transaction code is generated → the deposit covers the remaining Revo product balance →
   print the code on the receipt or as a QR.

Key points:

- Revo product configuration should be **cached** and refreshed periodically.
- Cheers Loyalty is independent of Revo products — any user with a QR code goes through the
  Loyalty flow regardless of basket contents.
- If Hybrid Revo is not enabled, no basket inspection for Revo products is needed.
- The Revo flow only starts when the feature is enabled **and** Revo products with a remaining
  balance > 0 exist after Loyalty.

---

## 1. Fetch configuration

`GET /v1/integrations/revo-union-configurations`
Operation id: `getRevoUnionConfigurationByIntegration`
Success: `200 OK`. Errors: `400`, `401`, `403`, `404`.

Fetch before creating transactions so the POS knows which products trigger container issuance.

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

### Response — `IntegrationRevoUnionConfigurationResponse`

The response is a **flat list of products**, each carrying the category it maps to.

```json
{
  "revoTokenProducts": [
    {
      "id": "rtp_001",
      "action": "BORROW",
      "productId": "cup_deposit_borrow",
      "category": {
        "id": "rtc_001",
        "name": "0.5L Cup",
        "description": "Standard half-liter reusable cup",
        "currencyCode": "HUF"
      }
    }
  ],
  "hybridRevoTokenProducts": [
    {
      "id": "hrtp_001",
      "productId": "beer_500ml",
      "hybridCategory": {
        "id": "cat_001",
        "expirationHours": 48,
        "depositGross": 500,
        "currencyCode": "HUF",
        "revoTokenCategory": {
          "id": "rtc_001",
          "name": "0.5L Cup",
          "description": "Standard half-liter reusable cup",
          "currencyCode": "HUF"
        }
      }
    }
  ]
}
```

Top level:

| Field | Type | Required | Description |
|---|---|---|---|
| `revoTokenProducts` | array | yes | Products that borrow or return a **plain** RevoUnion token. |
| `hybridRevoTokenProducts` | array | yes | Products that issue a **hybrid container deposit**. This is the list Hybrid RevoUnion integrations use. |

`revoTokenProducts[]` — `IntegrationRevoTokenProductResponse`:

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Mapping id. |
| `action` | enum | yes | `BORROW` or `RETURN`. |
| `productId` | string | yes | Product id in **your** system. |
| `category` | object | yes | `IntegrationSimpleRevoTokenCategoryResponse` — `id`, `name`, `currencyCode` required; `description` optional. |

`hybridRevoTokenProducts[]` — `IntegrationHybridRevoTokenProductResponse`:

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Mapping id. |
| `productId` | string | yes | Product id in **your** system. Match basket lines against this. |
| `hybridCategory` | object | yes | The container category — see below. |

`hybridCategory` — `IntegrationSimpleHybridRevoTokenCategoryResponse` (**also used by the
preview and validate responses**):

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Send as `hybridRevoTokenCategoryId` when validating a return. |
| `expirationHours` | int32 | yes | Return window in hours. |
| `depositGross` | number | yes | Deposit per container; also the refund per container returned. |
| `currencyCode` | string | yes | Currency of the deposit. |
| `revoTokenCategory` | object | **no** | Optional display details: `id`, `name`, `currencyCode`, optional `description`. |

Use this response to build a `productId → category` map, know the deposit amounts to charge,
and cache the configuration.

**There is no `isEnabled` field.** Enablement is signalled by the response status: a `200` means
the configuration is active. If it is disabled, the endpoint returns `400` with
`HYBRID_REVO_TOKEN_CONFIGURATION_NOT_ENABLED` — treat that as "skip Hybrid RevoUnion issuance
and process the order normally".

---

## 2. Create transaction (issue containers)

`POST /v1/integrations/hybrid-revo-token-transactions`
Operation id: `createHybridRevoTokenTransactionByIntegration`
Success: `201 Created`. Errors: `400`, `401`, `403`, `404`.

The request body is a **JSON array** (not an object).

### Request — array of `IntegrationCreateHybridRevoTokenTransactionRequest`

| Field | Type | Required |
|---|---|---|
| `productId` | string | yes |
| `quantity` | int32 | yes |

```bash
curl -X POST https://api.dev.votesess.com/v1/integrations/hybrid-revo-token-transactions \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '[
    { "productId": "beer_500ml", "quantity": 2 },
    { "productId": "wine_300ml", "quantity": 1 }
  ]'
```

### Response — `IntegrationCreateHybridRevoTokenTransactionResponse`

```json
{
  "id": "txn_abc123",
  "createdAt": "2026-03-15T14:30:00Z",
  "code": "HRT1234567890ABCDEF1234567890ABCDEFGHIJ",
  "sequenceNumber": "2026/000123",
  "categorySummaries": [
    {
      "hybridRevoTokenCategory": {
        "id": "cat_001",
        "expirationHours": 48,
        "depositGross": 500,
        "currencyCode": "HUF",
        "revoTokenCategory": { "id": "rtc_001", "name": "0.5L Cup", "currencyCode": "HUF" }
      },
      "activeItemCount": 2,
      "expiresAt": "2026-03-17T14:30:00Z"
    },
    {
      "hybridRevoTokenCategory": {
        "id": "cat_002",
        "expirationHours": 48,
        "depositGross": 300,
        "currencyCode": "HUF",
        "revoTokenCategory": { "id": "rtc_002", "name": "0.3L Cup", "currencyCode": "HUF" }
      },
      "activeItemCount": 1,
      "expiresAt": "2026-03-17T14:30:00Z"
    }
  ]
}
```

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Transaction id. |
| `createdAt` | date-time | yes | |
| `code` | string | yes | The 40-char return code. **Print or display this for the guest** (receipt, QR). |
| `sequenceNumber` | string | yes | Human-readable reference for this transaction. Print it on the receipt. |
| `categorySummaries[].hybridRevoTokenCategory` | object | yes | The container category issued — same `IntegrationSimpleHybridRevoTokenCategoryResponse` shape as the configuration and preview responses. |
| `categorySummaries[].activeItemCount` | int32 | yes | How many issued. |
| `categorySummaries[].expiresAt` | date-time | yes | When this category's items expire. |

The organization name and place (business unit) name printed on the receipt come from
`GET /v1/integrations/me` (see below).

---

## 3. Preview transaction

`GET /v1/integrations/hybrid-revo-token-transactions/{code}/preview`
Operation id: `previewHybridRevoTokenTransactionByIntegration`
Path param: `code` — the transaction code.
Success: `200 OK` (array). Errors: `400`, `401`, `403`, `404`.

Shows how many active containers remain per category before processing a return.

```bash
curl -X GET https://api.dev.votesess.com/v1/integrations/hybrid-revo-token-transactions/HRT1234567890ABCDEF1234567890ABCDEFGHIJ/preview \
  -H "X-API-Key: YOUR_API_KEY"
```

### Response — array of `IntegrationPreviewHybridRevoTokenTransactionResponse`

```json
[
  {
    "category": {
      "id": "cat_001",
      "expirationHours": 48,
      "depositGross": 500,
      "currencyCode": "HUF",
      "revoTokenCategory": { "id": "rtc_001", "name": "0.5L Cup", "currencyCode": "HUF" }
    },
    "activeItemCount": 2
  },
  {
    "category": {
      "id": "cat_002",
      "expirationHours": 48,
      "depositGross": 300,
      "currencyCode": "HUF",
      "revoTokenCategory": { "id": "rtc_002", "name": "0.3L Cup", "currencyCode": "HUF" }
    },
    "activeItemCount": 1
  }
]
```

Use `activeItemCount` to show the operator how many containers are still returnable, and
`category.depositGross` to compute the refund. `category.id` is the value to send as
`hybridRevoTokenCategoryId` in validate.

---

## 4. Validate transaction (return containers)

`POST /v1/integrations/hybrid-revo-token-transactions/{code}/validate`
Operation id: `validateHybridRevoTokenTransactionByIntegration`
Path param: `code` — the transaction code.
Success: `200 OK` (array). Errors: `400`, `401`, `403`, `404`.

The request body is a **JSON array**.

### Request — array of `IntegrationValidateHybridRevoTokenTransactionRequest`

| Field | Type | Required | Constraints |
|---|---|---|---|
| `hybridRevoTokenCategoryId` | string | yes | The `category.id` from preview / configuration. |
| `quantity` | int32 | yes | Minimum 1; cannot exceed `activeItemCount` for that category. |

A category id may appear only **once** per request — duplicates return
`DUPLICATE_CATEGORY_ID_IN_HYBRID_REVO_TOKEN_TRANSACTION`.

```bash
curl -X POST https://api.dev.votesess.com/v1/integrations/hybrid-revo-token-transactions/HRT1234567890ABCDEF1234567890ABCDEFGHIJ/validate \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '[
    { "hybridRevoTokenCategoryId": "cat_001", "quantity": 2 }
  ]'
```

### Response — array of `IntegrationValidateHybridRevoTokenTransactionResponse`

```json
[
  {
    "category": {
      "id": "cat_001",
      "expirationHours": 48,
      "depositGross": 500,
      "currencyCode": "HUF",
      "revoTokenCategory": { "id": "rtc_001", "name": "0.5L Cup", "currencyCode": "HUF" }
    },
    "validatedCount": 2,
    "remainingItemCount": 0
  }
]
```

| Field | Description |
|---|---|
| `validatedCount` | Containers successfully returned in this request. |
| `remainingItemCount` | Active containers still left on this transaction. |

**Refund amount = `validatedCount * category.depositGross`.** In the example: 2 × 500 = 1000.

---

## 5. Device identity (`/v1/integrations/me`)

`GET /v1/integrations/me`
Operation id: `getExternalDeviceByIntegration`
Spec: `basic-details-openapi`.
Returns the authenticated external device with its organization, place, and assigned desks.
Success: `200 OK`. Errors: `400`, `401`, `403`, `404`.

Use it to source the **organization name** and **place (business unit) name** printed on the
Hybrid Revo receipt.

### Response — `IntegrationExternalDeviceResponse`

| Field | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Device id. |
| `name` | string | yes | Device name. |
| `authorities` | enum[] | yes | `ORDER`, `ORDER_REFUND`, `SCAN`, `STATISTIC`, `INTEGRATION`, `THIRD_PARTY_INTEGRATION`, `CREDIT`, `TICKET`, `KIOSK`, `REVO`. Issuing Hybrid RevoUnions requires `REVO`. |
| `organization` | object | yes | `id`, `name`. |
| `place` | object | yes | `id`, `name`, `formattedAddress`, `placeStatus`; optional `description`, `avatarMedia`. |
| `relatedDesks` | array | yes | `id`, `name`, `type` (`COUNTER` \| `TABLE`), `enabled`. |

`placeStatus` values: `DRAFT`, `ACTIVE`, `TEMPORARILY_CLOSED`, `PERMANENTLY_CLOSED`, `HIDDEN`.

Full reference for this endpoint — authorities table, `avatarMedia` media schema, error
handling: https://integration.cheersapp.io/llms/device-details.md

---

## Error handling

All errors use this envelope:

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

| Error code | HTTP | Description |
|---|---|---|
| `HYBRID_REVO_TOKEN_CONFIGURATION_ALREADY_EXISTS` | 400 | A configuration already exists for this organization. Only one is allowed. |
| `HYBRID_REVO_TOKEN_CONFIGURATION_NOT_ENABLED` | 400 | The configuration exists but is currently disabled. |
| `HYBRID_REVO_TOKEN_TRANSACTION_NOT_FOUND` | 404 | No transaction found for the given code. |
| `HYBRID_REVO_TOKEN_PRODUCT_NOT_IN_CURRENT_VERSION` | 400 | One or more product ids are not part of the current configuration version. |
| `HYBRID_REVO_TOKEN_TRANSACTION_NOT_ENOUGH_ACTIVE_ITEMS` | 400 | Requested return quantity exceeds available active items for that category. |
| `DEVICE_CANNOT_ACCESS_HYBRID_REVO_TOKEN_TRANSACTION` | 403 | The device is not authorized for this transaction (different location). |
| `HYBRID_REVO_TOKEN_EXTERNAL_PRODUCT_NOT_FOUND` | 404 | The product id does not exist in the system. |
| `HYBRID_REVO_TOKEN_TRANSACTION_EMPTY_ITEMS` | 400 | The transaction request contains no items. |
| `HYBRID_REVO_TOKEN_PRODUCT_NOT_IN_DEVICE_PLACE` | 403 | The product is not available at the device's assigned location. |
| `DUPLICATE_CATEGORY_ID_IN_HYBRID_REVO_TOKEN_TRANSACTION` | 400 | The same category id appears more than once in the validate request. |

### Recovery flows

**Configuration:** `GET /v1/integrations/revo-union-configurations` → on `200`, cache the
product lists and proceed with the Revo flow. On `400`
`HYBRID_REVO_TOKEN_CONFIGURATION_NOT_ENABLED` or on `404`, skip Revo issuance entirely and
process the order normally without container deposits. Enablement is signalled by the response
status, not by a field in the payload.

**Transaction not found:** on `HYBRID_REVO_TOKEN_TRANSACTION_NOT_FOUND`, prompt the operator to
verify the transaction code was entered correctly.

**Not enough active items:** on `HYBRID_REVO_TOKEN_TRANSACTION_NOT_ENOUGH_ACTIVE_ITEMS`, refresh
the preview — items may have expired since the last one — show the updated active counts, let
the operator adjust the return quantity, then retry validate.

**Location mismatch:** `DEVICE_CANNOT_ACCESS_HYBRID_REVO_TOKEN_TRANSACTION` and
`HYBRID_REVO_TOKEN_PRODUCT_NOT_IN_DEVICE_PLACE` both mean the device belongs to a different
location than the transaction or product. The guest must return containers at a device at the
correct location.

---

## Worked example — festival cup deposit

Setup: organizer configures the "Summer Festival" organization with two categories.

| Category | Deposit | Expiration |
|---|---|---|
| 0.5L Cup | 500 HUF | 48 hours |
| 0.3L Cup | 300 HUF | 48 hours |

Products are mapped: beer → 0.5L cup, wine → 0.3L cup.

**Day 1, afternoon — guest orders 2 beers and 1 wine.**

```
POST /v1/integrations/hybrid-revo-token-transactions
[ { "productId": "beer_500ml", "quantity": 2 },
  { "productId": "wine_300ml", "quantity": 1 } ]
```

The guest receives 2× 0.5L Cup + 1× 0.3L Cup. Deposit charged: **1,300 HUF** (2 × 500 + 1 × 300).
The transaction code is printed on the receipt.

**Day 2, morning — guest returns 2 large cups.** The operator scans the code and previews:

```
GET /v1/integrations/hybrid-revo-token-transactions/{code}/preview
→ [ { "category": { "id": "cat_001", ... "name": "0.5L Cup" }, "activeItemCount": 2 },
    { "category": { "id": "cat_002", ... "name": "0.3L Cup" }, "activeItemCount": 1 } ]
```

The operator validates the return of 2× 0.5L cups:

```
POST /v1/integrations/hybrid-revo-token-transactions/{code}/validate
[ { "hybridRevoTokenCategoryId": "cat_001", "quantity": 2 } ]
```

Result: the guest receives a **1,000 HUF** refund (2 × 500). The 0.3L cup stays `ACTIVE` until
its 48-hour window closes.

**Day 3, afternoon — 48h expiration reached.** The 0.3L cup was never returned; the system flips
it to `EXPIRED` and the 300 HUF deposit is forfeited.

Final state: 0.5L Cup #1 `RETURNED` (500 refunded), 0.5L Cup #2 `RETURNED` (500 refunded),
0.3L Cup #1 `EXPIRED` (300 forfeited).

---

## Brand assets

Logo assets, brandbook, and other design resources:
https://drive.google.com/drive/folders/1L2QT46AnEQkCOD5JDbj0MA83briALmAb?usp=sharing
