openapi: 3.0.3
info:
  title: LoyaltyPlant In-Store Loyalty API
  description: >
    The API enables POS systems to interact with LoyaltyPlant's loyalty program:

    - Process loyalty for in-store orders, identifying customer by QR-code, ID
    or phone number.

    - Apply rewards (menu items, discounts, payments) to orders.

    - Notify LP about closed orders to credit loyalty points.

    - Handle order refunds.
  version: '1.7'
  contact:
    name: LoyaltyPlant
externalDocs:
  description: See integration guides for a full walkthrough of the interation.
  url: /in-store/
servers:
  - url: https://release.loyaltyplant.com/CloudValidator
    description: Production server
tags:
  - name: One-Time Salt
    description: Obtaining one-time salt for request signing
  - name: Process Transaction
    description: Processing QR codes and phone-based customer identification
  - name: Order Lifecycle
    description: Order closure and refund notifications
  - name: Configuration
    description: Per-outlet POS/kiosk loyalty configuration fetch
paths:
  /one-time-salt/1.7:
    post:
      tags:
        - One-Time Salt
      operationId: getOneTimeSalt
      summary: Get one-time salt
      description: >
        Returns a one-time salt and its ID used to compute the
        `AuthorizationToken` header

        for subsequent API calls. Each salt can only be used once.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OneTimeSaltRequest'
            example:
              establishmentId: 5259
      responses:
        '200':
          description: Salt generated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OneTimeSaltResponse'
              example:
                saltId: 12345
                salt: abcdefghijklmnop
  /qr-code/1.7:
    post:
      tags:
        - Process Transaction
      operationId: processQrCode
      summary: Process loyalty transaction
      description: >
        Processes a LoyaltyPlant QR code scanned from the customer's mobile app,

        identifies/creates a customer by phone number (in-store user
        acquisition),

        or looks up an existing customer by `publicClientId`.


        Returns reward items, discounts, and payments that should be applied to
        the order.

        Response includes customer tier name and points balance (v1.7+).


        **QR code flow:** Send `qrCode` field with the scanned string.

        **Phone number flow (v1.6+):** Send `phoneNumber` instead of `qrCode`,

        along with optional `subscribedToPushNotifications` and `clientName`.

        **Public client ID flow (v1.7+):** Send `publicClientId` instead of
        `qrCode`/`phoneNumber`

        for direct client lookup by existing public ID.


        **Identification priority:** if more than one of `qrCode`,
        `phoneNumber`, `publicClientId`

        is sent, they are resolved in priority order `qrCode` → `phoneNumber` →
        `publicClientId`.

        If none of the three is sent, the request is rejected with message code
        503

        (QR code invalid).


        **Customer tier and balance (v1.7+):** `customer.customerTier` and

        `customer.customerPointsBalance` are best-effort enrichment. If the
        tier/balance lookup

        is unavailable, both fields are returned as `null` and the request is
        still accepted

        (`accepted: true`) — treat their absence as "unknown", not as a failure.


        Note: QR codes are valid for 5 minutes and can only be used once.
      parameters:
        - $ref: '#/components/parameters/SaltId'
        - $ref: '#/components/parameters/AuthorizationToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QrCodeRequest_v1_7'
            examples:
              qrCodeFlow:
                summary: QR code flow
                value:
                  qrCode: '1234567878901234567890'
                  orderId: '1234567'
                  total: 1234.3
                  posInfo:
                    establishmentId: 1234
                    terminalId: '1234'
                    terminalCurrentTimestamp: 1466337067000
                    terminalTimeZone: GMT-6
                  order:
                    menuItems:
                      - itemId: '1234'
                        title: Something
                        price: 10.2
                        quantity: 2
              publicClientIdFlow:
                summary: Client lookup by public ID (v1.7+)
                value:
                  publicClientId: 41234567
                  orderId: 68acf80c0d0a7e6d2fd19946
                  total: 5
                  posInfo:
                    establishmentId: 5259
                    terminalId: 650326fe75bec93f9e01f316
                    terminalCurrentTimestamp: 1756166153871
                    terminalTimeZone: GMT-05
                  order:
                    menuItems:
                      - itemId: '11804'
                        title: Blueberry Lavender Lemonade
                        quantity: 1
                        price: 5
              phoneNumberFlow:
                summary: In-store user acquisition (phone number)
                value:
                  phoneNumber: '+15551234567'
                  subscribedToPushNotifications: true
                  clientName: Jane
                  orderId: 68acf80c0d0a7e6d2fd19946
                  total: 5
                  posInfo:
                    establishmentId: 5259
                    terminalId: 650326fe75bec93f9e01f316
                    terminalCurrentTimestamp: 1756166153871
                    terminalTimeZone: GMT-05
      responses:
        '200':
          description: QR code/phone/publicClientId processed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QrCodeResponse_v1_7'
              examples:
                withRewards:
                  summary: Response with rewards and tier
                  value:
                    accepted: true
                    menuItems:
                      - itemId: '1234'
                        title: Something
                        price: 10.2
                        quantity: 2
                    discounts: []
                    messages:
                      - type: info
                        code: 201
                        text: 'Welcome guest by name: John'
                    customer:
                      loyaltyPlantId: 1234
                      customerName: John
                      customerEmail: johndoe@web.com
                      customerTier: Gold
                      customerPointsBalance: 450
                    payment:
                      amount: 2.34
                      transactionId: 1234
                      type: PAY_WITH_POINTS
                    hasEverRewardedPoints: true
                    maxPointsPercent: 10
                rejected:
                  summary: Business rejection (HTTP 200, accepted=false)
                  description: >
                    The identification was not accepted (here: the value is not
                    a

                    valid LoyaltyPlant QR code — code 503). Other rejection
                    reasons

                    use their own codes (e.g. 504 already scanned, 505 already

                    validated). The reason is reported as an `error`-type entry
                    in

                    `messages[]`; there is no top-level error field.

                    `menuItems`/`discounts` are omitted.
                  value:
                    accepted: false
                    messages:
                      - type: error
                        code: 503
                        text: QR code invalid
                tierEnrichmentUnavailable:
                  summary: Accepted, tier/balance enrichment unavailable
                  description: >
                    The request was accepted, but the tier/balance lookup was
                    unavailable,

                    so `customerTier` and `customerPointsBalance` are `null`.
                    This is not an

                    error — treat the missing values as "unknown".
                  value:
                    accepted: true
                    menuItems: []
                    discounts: []
                    messages:
                      - type: info
                        code: 201
                        text: 'Welcome guest by name: John'
                    customer:
                      loyaltyPlantId: 1234
                      customerName: John
                      customerEmail: johndoe@web.com
                      customerTier: null
                      customerPointsBalance: null
                    hasEverRewardedPoints: true
                    maxPointsPercent: 10
        '400':
          $ref: '#/components/responses/ValidationError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
  /order-closed/1.7:
    post:
      tags:
        - Order Lifecycle
      operationId: orderClosed
      summary: Complete order
      description: >
        Same as v1.6. Notifies LoyaltyPlant that the order has been paid and
        closed.
      parameters:
        - $ref: '#/components/parameters/SaltId'
        - $ref: '#/components/parameters/AuthorizationToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OrderClosedRequest'
            examples:
              standard:
                summary: Standard order closure
                value:
                  establishmentId: 1234
                  orderId: '1234'
                  orderTotal: 12.35
                  order:
                    menuItems:
                      - itemId: '1234'
                        title: Something
                        price: 10.2
                        quantity: 2
                        modifiers:
                          - itemId: '1234'
                            title: Modifier title
                            quantity: 3
                            price: 22.1
                        discounts:
                          - discountId: '1234'
                            type: percent
                            value: 50.4
                    discounts:
                      - discountId: '1234'
                        type: amount
                        value: 50.4
                  payments:
                    - type: '1'
                      amount: 2.34
                      transactionId: '1234'
                  waiter:
                    posId: '2134'
                    name: John Doe
              inStoreAcquisition:
                summary: In-store user acquisition order closure
                value:
                  phoneNumber: '+15551234567'
                  subscribedToPushNotifications: true
                  clientName: Jane
                  establishmentId: 5259
                  orderId: 68acf80c0d0a7e6d2fd19946
                  orderTotal: 5
                  order:
                    menuItems:
                      - itemId: '11804'
                        title: Blueberry Lavender Lemonade
                        quantity: 1
                        price: 5
                        modifiers:
                          - itemId: '4580'
                            title: Blueberry Bubbles
                            price: 0
                            quantity: 1
                  payments: []
                  waiter:
                    posId: 650326fe75bec93f9e01f316
                    name: BITE
      responses:
        '200':
          description: Order closed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OrderClosedResponse'
              example:
                accepted: true
                pointsReward: 10
                pointsSpent: 20
                errorCode: 0
                errorText: ''
        '400':
          $ref: '#/components/responses/ValidationError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
  /refund/1.7:
    post:
      tags:
        - Order Lifecycle
      operationId: refundOrder
      summary: Refund order
      description: |
        Same as v1.6. Notifies LoyaltyPlant that the order has been refunded.
      parameters:
        - $ref: '#/components/parameters/SaltId'
        - $ref: '#/components/parameters/AuthorizationToken'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RefundRequest'
            example:
              establishmentId: 5259
              orderId: 68acf80c0d0a7e6d2fd19946
      responses:
        '200':
          description: Refund processed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RefundResponse'
              example:
                accepted: true
                errorCode: 0
                errorText: ''
        '400':
          $ref: '#/components/responses/ValidationError'
        '403':
          $ref: '#/components/responses/ForbiddenError'
  /v1/pos/{pos}:
    post:
      tags:
        - Configuration
      operationId: getPosConfig
      summary: Fetch credential
      description: >
        Returns the loyalty configuration for one sales outlet, selected by the
        `identifier`

        query parameter (or `midIdentifier` when present) and routed by the
        `{pos}` path segment.


        The request has **no body** and **no auth handshake** (no
        `one-time-salt` /

        `AuthorizationToken`), and always responds with HTTP 200 —

        success or failure is conveyed by the `accepted` field of the response
        envelope.


        **`accepted: true`** — the configuration is returned in `config`. The
        outlet is now marked

        *applied* (fetch-once); subsequent fetches for the same identifier
        return `accepted: false`.


        **`accepted: false`** (with `config: null`) when any of:

        - the configuration was already fetched for this identifier
        (fetch-once), or

        - no configuration is bound to the identifier, or

        - the `{pos}` segment is not a registered handler.


        Persist the configuration you receive as the source of truth for the
        outlet;

        `accepted: false` is **not** an error to retry.


        **`version`** — optional plugin/integration version string; currently
        accepted but not

        persisted by the service.


        Note: the service also accepts the trailing-slash form `POST
        /v1/pos/{pos}/`.
      parameters:
        - name: pos
          in: path
          required: true
          description: >
            POS-type segment that selects the configuration handler:

            - `generic` — the default; use it unless your POS needs custom
            configuration.

            - `clover` — Clover.

            - `iiko` — Syrve/iiko.

            - `rkeeper` — r_keeper/Restera.


            LoyaltyPlant assigns the segment during onboarding. The currently
            deployed service

            registers `clover`, `iiko`, and `rkeeper`; confirm `generic` is
            enabled for your

            integration rather than assuming it.
          schema:
            type: string
            enum:
              - generic
              - clover
              - iiko
              - rkeeper
        - name: identifier
          in: query
          required: true
          description: >
            The outlet's lookup key for the configuration. Must be a **stable,
            globally-unique** value

            that already lives in the POS — assigned once, at provisioning, and
            **never overwritten**

            afterwards (do not reuse a mutable field such as a display name or a
            value the POS

            regenerates on update/reinstall). The outlet is resolved by this
            value on every fetch, so if

            it changes the outlet can no longer be matched.
          schema:
            type: string
        - name: midIdentifier
          in: query
          required: false
          description: >
            Alternative outlet-lookup identifier (a merchant/MID-level code)
            added for the

            **rkeeper** integration, used in place of the standard `identifier`.
            When present, it

            overrides `identifier` as the lookup key for this request; when
            omitted, `identifier`

            is used.
          schema:
            type: string
        - name: version
          in: query
          required: false
          description: >-
            Optional plugin/integration version string. Accepted but not
            persisted.
          schema:
            type: string
        - name: fullRestaurantCode
          in: query
          required: false
          description: >
            Alternative outlet-lookup identifier added for the **rkeeper**
            integration, used in

            place of the standard `identifier`. Present on the **deployed**
            service (v0.1.3) but not

            in the current source checkout; documented here to match the
            deployed contract.
          schema:
            type: string
      responses:
        '200':
          description: >
            Always returned. Inspect `accepted` to distinguish a delivered
            configuration from a

            fetch-once / not-bound / unknown-`{pos}` rejection.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PosConfigResponse'
              examples:
                genericAccepted:
                  summary: Configuration delivered (generic — the default)
                  description: >
                    The `generic` POS type returns just the core fields every
                    integration needs.

                    Only fields with a value are present.
                  value:
                    accepted: true
                    config:
                      apiUrl: https://release.loyaltyplant.com/CloudValidator
                      apiVersion: '1.6'
                      establishmentId: 1234
                      password: p7Qm0Zr8Kx2bN5tV9wYcAg==
                      discountId: LOYALTY
                      flow: REGULAR
                cloverAccepted:
                  summary: Configuration delivered (clover)
                  value:
                    accepted: true
                    config:
                      apiUrl: https://release.loyaltyplant.com/CloudValidator
                      apiVersion: '1.6'
                      establishmentId: 1234
                      password: p7Qm0Zr8Kx2bN5tV9wYcAg==
                      discountId: LOYALTY
                      flow: REGULAR
                      minCashPayment: 0
                      maxPwpPercent: 50
                      timeout: 60
                      firing: false
                syrveAccepted:
                  summary: Configuration delivered (Syrve — segment `iiko`)
                  description: >
                    Syrve returns the core fields plus Syrve-specific extras
                    such as `giftCode`,

                    `language`, and reward thresholds. Only fields with a value
                    are present.
                  value:
                    accepted: true
                    config:
                      apiUrl: https://release.loyaltyplant.com/CloudValidator
                      apiVersion: '1.7'
                      establishmentId: 5259
                      password: p7Qm0Zr8Kx2bN5tV9wYcAg==
                      discountId: LOYALTY
                      flow: PATCH_ORDER
                      minCashPayment: 1
                      maxPwpPercent: 50
                      timeout: 60
                      giftCode:
                        - GIFT-001
                        - GIFT-002
                      giftMinSum: 100
                      giftPrice: 0.01
                      language: EN
                      payForReqModifiers: true
                      restrictedPaymentIds:
                        - '7'
                        - '9'
                      softRegistration: false
                resteraAccepted:
                  summary: Configuration delivered (Restera — segment `rkeeper`)
                  description: >-
                    Restera returns the core fields plus `bonusDiscountId`
                    (distinct from the reward `discountId`).
                  value:
                    accepted: true
                    config:
                      apiUrl: https://release.loyaltyplant.com/CloudValidator
                      apiVersion: '1.6'
                      establishmentId: 1234
                      password: p7Qm0Zr8Kx2bN5tV9wYcAg==
                      discountId: LOYALTY
                      flow: REGULAR
                      minCashPayment: 0
                      maxPwpPercent: 100
                      timeout: 60
                      bonusDiscountId: BONUS
                notAccepted:
                  summary: Not accepted (already fetched / not bound / unknown pos)
                  description: >
                    Returned when the configuration was already fetched for this
                    identifier

                    (fetch-once), when no configuration is bound to the
                    identifier, or when the

                    `{pos}` segment is unknown. On the wire the response is

                    `{ "accepted": false, "config": null }`.
                  value:
                    accepted: false
                    config: null
      servers:
        - url: https://release.loyaltyplant.com/cloudvalidator-config-service
          description: Production server.
  /v1/config/{establishmentId}:
    get:
      tags:
        - Configuration
      operationId: getOutletConfig
      summary: Fetch loyalty configuration
      description: >
        Returns the complete per-outlet loyalty configuration — connection,
        program rules, and the

        kiosk-facing presentation and legal settings. Used identically by every
        device type: a

        cashier POS reads the connection and program fields and ignores the
        presentation fields; a

        self-service kiosk uses all of them. Only fields with a value are
        present (null fields omitted).


        Unlike `POST /v1/pos/{pos}`, this endpoint is **not** fetch-once — it is
        safe to call on every

        device startup. Compare the returned `revision` (a monotonic
        epoch-second stamp) with your

        cached copy and re-apply the configuration when `revision` increases;
        this is how admin-panel

        changes reach devices without reprovisioning. `accepted: false` here
        means the outlet is not

        bound / loyalty is not configured (guest mode) — never a fetch-once
        rejection.


        **Authentication.** Protected by the same `AuthorizationToken` scheme as
        the In-Store Loyalty

        API (SHA-256 of the outlet's decrypted `apiPrivateKey` + a
        one-time-salt). The `apiPrivateKey`

        is obtained once from the `password` field of `POST /v1/pos/{pos}`; the
        configuration payload

        itself never contains the key.


        **Caching.** The response carries an `ETag` equal to the `revision`;
        send it back as

        `If-None-Match` to get `304 Not Modified` when nothing changed. The
        `revision` in the body is

        the authoritative change signal; the `ETag` is a bandwidth optimization.
      security:
        - AuthorizationToken: []
      parameters:
        - name: establishmentId
          in: path
          required: true
          description: >-
            The LoyaltyPlant establishment (sales outlet) ID, obtained from
            `POST /v1/pos/{pos}`.
          schema:
            type: integer
            format: int32
        - name: identifier
          in: query
          required: false
          description: >-
            Optional outlet identifier, accepted as a fallback lookup key for a
            device that has not yet cached its `establishmentId`.
          schema:
            type: string
        - name: If-None-Match
          in: header
          required: false
          description: >-
            An `ETag` (a `revision`) from a previous response. When it matches
            the current `revision`, the service returns `304 Not Modified` with
            no body.
          schema:
            type: string
      responses:
        '200':
          description: >
            The configuration is returned in `config`. `accepted` is `false`
            (with `config: null`) when

            the outlet is not bound or loyalty is not configured (guest mode) —
            never because of fetch-once.
          headers:
            ETag:
              description: >-
                The current `revision`. Echo it as `If-None-Match` on the next
                poll.
              schema:
                type: string
            Cache-Control:
              description: >-
                Always `no-cache` — caches must revalidate against the
                `revision` / `ETag`.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OutletConfigResponse'
              examples:
                accepted:
                  summary: Configuration delivered
                  value:
                    accepted: true
                    config:
                      revision: 1748186400
                      apiUrl: https://release.loyaltyplant.com/CloudValidator
                      loyaltyApiVersion: '1.7'
                      establishmentId: 5259
                      currency: GEL
                      currencySymbol: ₾
                      timezone: Asia/Tbilisi
                      locales:
                        enabled:
                          - en
                          - ka
                        default: en
                        fallback: en
                      loyaltyEnabled: true
                      loyalty:
                        loyaltyPointAccrualRules:
                          pointsPerCurrencyUnit: 0.04
                          rounding: floor
                        pointsIconUrl: https://cdn.example.com/assets/points-icon.svg
                        phoneNumberFlow:
                          enabled: true
                          flow: two_step
                        loyaltyCardFlow:
                          enabled: true
                        rewardRedemption:
                          enabled: true
                        rewardBanner:
                          imageUrl: https://cdn.example.com/assets/rewards-banner.png
                        marketingConsent:
                          channels:
                            - sms
                            - whatsapp
                            - email
                            - push
                          screen:
                            headlineByLocale:
                              en: >-
                                Would you like to get exclusive deals to your
                                phone number?
                              ka: გსურთ მიიღოთ ექსკლუზიური შეთავაზებები?
                            bodyByLocale:
                              en: >-
                                We'll send personalized offers based on your
                                activity. See {privacy_policy}. Opt out anytime.
                              ka: >-
                                გამოგიგზავნით პერსონალურ შეთავაზებებს. იხ.
                                {privacy_policy}.
                            documentRefs:
                              - privacy_policy
                        phoneFormDisclaimer:
                          templateByLocale:
                            en: >-
                              By providing your phone number, you agree to
                              {privacy_policy}, {terms_conditions},
                              {loyalty_terms} and {marketing_consent}
                            ka: >-
                              ნომრის მითითებით თქვენ ეთანხმებით:
                              {privacy_policy}, {terms_conditions},
                              {loyalty_terms}, {marketing_consent}
                          documentRefs:
                            - privacy_policy
                            - terms_conditions
                            - loyalty_terms
                            - marketing_consent
                        appInstallBanner:
                          url: https://brand.app.link/get
                          imageUrl: https://cdn.example.com/assets/app-banner.png
                          promoTextByLocale:
                            en: >-
                              Get the app — earn and spend points right from
                              your phone
                            ka: ჩამოტვირთეთ აპლიკაცია — დააგროვეთ ქულები
                          showWhen:
                            justRegisteredWithApp: true
                            justRegisteredWithoutApp: true
                            alreadyRegistered: true
                            skipped: false
                      documents:
                        - kind: privacy_policy
                          version: '2026-04-15'
                          titleByLocale:
                            en: Privacy Policy
                            ka: კონფიდენციალურობის პოლიტიკა
                          pdfUrlByLocale:
                            en: https://cdn.example.com/docs/pp/en/2026-04-15.pdf
                            ka: https://cdn.example.com/docs/pp/ka/2026-04-15.pdf
                          requiresAcceptance: true
                        - kind: terms_conditions
                          version: '2026-04-15'
                          titleByLocale:
                            en: Terms & Conditions
                            ka: წესები და პირობები
                          pdfUrlByLocale:
                            en: https://cdn.example.com/docs/tc/en/2026-04-15.pdf
                            ka: https://cdn.example.com/docs/tc/ka/2026-04-15.pdf
                          requiresAcceptance: true
                        - kind: loyalty_terms
                          version: '2026-04-15'
                          titleByLocale:
                            en: Loyalty Program Terms
                            ka: ლოიალურობის წესები
                          pdfUrlByLocale:
                            en: https://cdn.example.com/docs/lt/en/2026-04-15.pdf
                            ka: https://cdn.example.com/docs/lt/ka/2026-04-15.pdf
                          requiresAcceptance: true
                        - kind: marketing_consent
                          version: '2026-04-15'
                          titleByLocale:
                            en: Marketing Consent
                            ka: მარკეტინგული თანხმობა
                          pdfUrlByLocale:
                            en: https://cdn.example.com/docs/mc/en/2026-04-15.pdf
                            ka: https://cdn.example.com/docs/mc/ka/2026-04-15.pdf
                          requiresAcceptance: false
                      phone:
                        defaultCountry: GE
                        allowedCountries:
                          - GE
                          - AM
                          - TR
                        masksByCountry:
                          GE: +995 XXX XXX XXX
                          AM: +374 XX XXX XXX
                          TR: +90 XXX XXX XX XX
                notBound:
                  summary: Outlet not bound / guest mode
                  description: >-
                    `accepted: false` — the outlet is not bound or loyalty is
                    not configured. On the wire the response is `{ "accepted":
                    false, "config": null }`.
                  value:
                    accepted: false
                    config: null
        '304':
          description: >
            Not Modified — the `If-None-Match` value you sent equals the current
            `revision`. No body is

            returned; keep using your cached configuration.
        '401':
          description: Missing or invalid `AuthorizationToken`.
      servers:
        - url: https://release.loyaltyplant.com/cloudvalidator-config-service
          description: Production server.
components:
  parameters:
    SaltId:
      name: SaltId
      in: header
      required: true
      description: Salt ID received from the `one-time-salt` endpoint.
      schema:
        type: integer
    AuthorizationToken:
      name: AuthorizationToken
      in: header
      required: true
      description: |
        SHA256 hash of `apiPrivateKey + salt`. Used in API v1.4+.
        ```java
        DigestUtils.sha256Hex(apiPrivateKey + salt)
        ```
      schema:
        type: string
    SignatureLegacy:
      name: Signature
      in: header
      required: true
      description: |
        **Legacy (v1.0–1.3).** MD5 hash of `requestBody + apiPrivateKey + salt`.
        ```java
        DigestUtils.md5Hex(requestBody + apiPrivateKey + salt)
        ```
      schema:
        type: string
  headers:
    SignatureResponse:
      description: >
        **Legacy (v1.0–1.3).** MD5 hash of `responseBody + apiPrivateKey +
        salt`.

        Used to verify response integrity.
      schema:
        type: string
  responses:
    ValidationError:
      description: Validation error — field-path-to-error mapping
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationError'
    ForbiddenError:
      description: Authentication failed
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ForbiddenError'
  schemas:
    OneTimeSaltRequest:
      type: object
      required:
        - establishmentId
      properties:
        establishmentId:
          type: integer
          minimum: 1
          description: >-
            LoyaltyPlant establishment ID. Should be configurable on the POS
            side.
    OneTimeSaltResponse:
      type: object
      required:
        - saltId
        - salt
      properties:
        saltId:
          type: integer
          format: int64
          description: Unique salt identifier to pass in subsequent request headers.
        salt:
          type: string
          description: One-time salt value for computing the authorization token/signature.
    PosInfo:
      type: object
      required:
        - establishmentId
        - terminalId
        - terminalCurrentTimestamp
        - terminalTimeZone
      properties:
        establishmentId:
          type: integer
          description: LoyaltyPlant establishment ID.
        terminalId:
          type: string
          description: POS terminal identifier.
        terminalCurrentTimestamp:
          type: integer
          format: int64
          description: Current timestamp of POS terminal in UTC (milliseconds).
        terminalTimeZone:
          type: string
          description: Terminal timezone in format `GMT<offset>`, e.g. `GMT-6` or `GMT+1`.
    MenuItem:
      type: object
      required:
        - itemId
        - quantity
      properties:
        itemId:
          type: string
          description: Item identifier in POS.
        title:
          type: string
          description: Menu item title. UTF-8.
        price:
          type: number
          format: decimal
          description: Item price.
        quantity:
          type: number
          format: decimal
          description: Quantity. Fractional values are allowed (weighted items).
        modifiers:
          type: array
          items:
            $ref: '#/components/schemas/Modifier'
          description: List of item modifiers.
        discounts:
          type: array
          items:
            $ref: '#/components/schemas/Discount'
          description: Discounts applied to this item.
    Modifier:
      type: object
      required:
        - itemId
        - quantity
      properties:
        itemId:
          type: string
          description: Modifier item identifier in POS.
        title:
          type: string
          description: Modifier title. UTF-8.
        price:
          type: number
          format: decimal
          description: Modifier price.
        quantity:
          type: number
          format: decimal
          description: Modifier quantity. Fractional values are allowed (weighted items).
    Discount:
      type: object
      required:
        - discountId
        - type
        - value
      description: >-
        Discount (v1.5+). Uses `type` + `value` fields. Types: `percent`,
        `amount`, `fixedPrice`.
      properties:
        discountId:
          type: string
          description: Discount identifier in POS.
        type:
          type: string
          enum:
            - percent
            - amount
            - fixedPrice
          description: Discount type.
        value:
          type: number
          format: decimal
          description: >-
            Discount value (percentage for `percent`, monetary amount for
            `amount`/`fixedPrice`).
    MenuItem_Legacy:
      type: object
      required:
        - itemId
        - quantity
      properties:
        itemId:
          type: string
          description: Item identifier in POS.
        title:
          type: string
          description: Menu item title (optional in some versions).
        price:
          type: number
          format: decimal
          description: Item price (added in v1.4).
        quantity:
          type: number
          format: decimal
          description: Quantity. Fractional values are allowed (weighted items).
        modifiers:
          type: array
          items:
            type: string
          description: List of modifier POS identifiers (string array in v1.0–1.4).
        discounts:
          type: array
          items:
            $ref: '#/components/schemas/Discount_Legacy'
          description: Discounts applied to this item.
    Discount_Legacy:
      type: object
      required:
        - discountId
        - type
      description: >-
        Discount (v1.0–1.4). Uses `percent`/`amount` fields. Types: `percent`,
        `amount`.
      properties:
        discountId:
          type: string
          description: Discount identifier in POS.
        type:
          type: string
          enum:
            - percent
            - amount
          description: Discount type.
        percent:
          type: number
          format: decimal
          description: Discount percentage. Required if `type` is `percent`.
        amount:
          type: number
          format: decimal
          description: Discount amount. Required if `type` is `amount`.
    Order:
      type: object
      description: Order contents with menu items and order-level discounts (v1.5+).
      properties:
        menuItems:
          type: array
          items:
            $ref: '#/components/schemas/MenuItem'
          description: List of menu items in the order.
        discounts:
          type: array
          items:
            $ref: '#/components/schemas/Discount'
          description: Order-level discounts.
    Order_Legacy:
      type: object
      description: Order contents (v1.0–1.4).
      properties:
        menuItems:
          type: array
          items:
            $ref: '#/components/schemas/MenuItem_Legacy'
          description: List of menu items in the order.
        discounts:
          type: array
          items:
            $ref: '#/components/schemas/Discount_Legacy'
          description: Order-level discounts.
    Payment:
      type: object
      required:
        - type
        - amount
        - transactionId
      properties:
        type:
          type: string
          description: Payment type in POS.
        amount:
          type: number
          format: decimal
          description: Payment amount.
        transactionId:
          type: string
          description: |
            External transaction identifier in POS. For LoyaltyPlant payments,
            this must match the `transactionId` from the qr-code response.
    Waiter:
      type: object
      required:
        - posId
        - name
      properties:
        posId:
          type: string
          description: Waiter ID in POS.
        name:
          type: string
          description: Waiter name in POS.
    Message:
      type: object
      required:
        - type
        - code
        - text
      properties:
        type:
          type: string
          enum:
            - error
            - warn
            - info
          description: Message type.
        code:
          type: integer
          description: |
            Message code:
            - 200: Generic information
            - 201: Bonus card accepted
            - 202: Reward added
            - 203: (reserved)
            - 500: Generic error
            - 501: QR code for another partner
            - 502: QR code for another profile
            - 503: QR code invalid
            - 504: QR code already scanned
            - 505: QR code already validated
            - 506: Order already closed
            - 507: Discount already exists in order
            - 508: Reward was not added to order
            - 509: Phone number wrong format
            - 510: Phone/publicClientId flow not available
            - 600: Generic warning
        text:
          type: string
          description: Human-readable message text for the cashier.
    Customer:
      type: object
      required:
        - loyaltyPlantId
      properties:
        loyaltyPlantId:
          type: integer
          description: Internal LoyaltyPlant unique customer ID.
        customerName:
          type: string
          nullable: true
          description: Customer's first name from mobile app or social login.
        customerEmail:
          type: string
          nullable: true
          description: Customer email from mobile app or social login (v1.5+).
    Customer_Legacy:
      type: object
      required:
        - loyaltyPlantId
      description: >-
        Customer information for v1.0–1.4 (no email, matches the monolith
        contract).
      properties:
        loyaltyPlantId:
          type: integer
          description: Internal LoyaltyPlant unique customer ID.
        customerName:
          type: string
          nullable: true
          description: Customer's first name from mobile app or social login.
    LoyaltyPayment:
      type: object
      required:
        - amount
        - transactionId
        - type
      description: LoyaltyPlant payment to be applied to the order.
      properties:
        amount:
          type: number
          format: decimal
          description: Payment amount (mobile wallet balance or points value).
        transactionId:
          type: integer
          format: int64
          description: >-
            Transaction ID in LoyaltyPlant system. Must be included in
            `order-closed` payments.
        type:
          type: string
          enum:
            - PAY_WITH_POINTS
            - MOBILE_WALLET
          description: |
            Payment type:
            - `PAY_WITH_POINTS` — pay with loyalty points
            - `MOBILE_WALLET` — pay with mobile wallet

            May be extended in future versions.
    Customer_v1_7:
      type: object
      required:
        - loyaltyPlantId
      description: Customer information with tier and balance enrichment (v1.7+).
      properties:
        loyaltyPlantId:
          type: integer
          description: Internal LoyaltyPlant unique customer ID.
        customerName:
          type: string
          nullable: true
          description: Customer's first name from mobile app or social login.
        customerEmail:
          type: string
          nullable: true
          description: Customer email from mobile app or social login.
        customerTier:
          type: string
          nullable: true
          description: >
            Customer's current loyalty tier name (e.g., 'Gold', 'Silver').

            Best-effort enrichment: `null` if the tier lookup is unavailable.
            The request is

            still accepted when this is `null`.
        customerPointsBalance:
          type: integer
          nullable: true
          description: >
            Customer's current loyalty points balance.

            Best-effort enrichment: `null` if the balance lookup is unavailable.
            The request is

            still accepted when this is `null`.
    QrCodeRequest_v1_7:
      type: object
      required:
        - orderId
        - posInfo
      description: |
        QR code request for v1.7. Supports three identification methods:
        - `qrCode` — scan from mobile app
        - `phoneNumber` — in-store user acquisition (v1.6+)
        - `publicClientId` — direct client lookup by existing public ID (v1.7+)
      properties:
        qrCode:
          type: string
          description: >
            QR code string from LoyaltyPlant mobile app. Valid for 5 minutes,
            single use.

            Required if `phoneNumber` and `publicClientId` are not provided.
        phoneNumber:
          type: string
          description: |
            Guest phone number in international format, e.g. `+13479436134`.
            Used for in-store user acquisition.
        subscribedToPushNotifications:
          type: boolean
          default: false
          description: |
            If `true`, the client will receive push notifications.
            Only applicable with `phoneNumber` or `publicClientId`.
        clientName:
          type: string
          description: >
            Client name for account creation. Only applicable with
            `phoneNumber`.
        publicClientId:
          type: integer
          nullable: true
          description: >
            Existing customer public ID. If provided, used for direct client
            lookup

            instead of QR code or phone number. The client must belong to the
            same partner.
        orderId:
          type: string
          description: Order identifier in POS.
        posInfo:
          $ref: '#/components/schemas/PosInfo'
        total:
          type: number
          format: decimal
          description: Current order total.
        order:
          $ref: '#/components/schemas/Order'
    QrCodeRequest:
      type: object
      required:
        - orderId
        - posInfo
      properties:
        qrCode:
          type: string
          description: >
            QR code string from LoyaltyPlant mobile app. Valid for 5 minutes,
            single use.

            Required if `phoneNumber` is not provided.
        phoneNumber:
          type: string
          description: >
            Guest phone number in international format, e.g. `+13479436134`.

            Required if `qrCode` is not provided. Used for in-store user
            acquisition (v1.6+).
        subscribedToPushNotifications:
          type: boolean
          default: false
          description: >
            Only applicable with `phoneNumber`. If `true`, the client will
            receive push notifications.

            If not sent, defaults to `false` and the client is unsubscribed.
        clientName:
          type: string
          description: >
            Client name for account creation. If not provided, defaults to
            "Anonymous".

            Only applicable with `phoneNumber`.
        orderId:
          type: string
          description: Order identifier in POS.
        posInfo:
          $ref: '#/components/schemas/PosInfo'
        total:
          type: number
          format: decimal
          description: Current order total.
        order:
          $ref: '#/components/schemas/Order'
    QrCodeRequest_v1_5:
      type: object
      required:
        - qrCode
        - orderId
        - posInfo
      properties:
        qrCode:
          type: string
          description: >-
            QR code string from LoyaltyPlant mobile app. A short code can also
            be used.
        orderId:
          type: string
          description: Order identifier in POS.
        posInfo:
          $ref: '#/components/schemas/PosInfo'
        total:
          type: number
          format: decimal
          description: Current order total.
        order:
          $ref: '#/components/schemas/Order'
    QrCodeRequest_v1_3:
      type: object
      required:
        - qrCode
        - orderId
        - posInfo
      description: >-
        QR code request for v1.0–1.4 (no `total`, legacy discount format, string
        modifiers).
      properties:
        qrCode:
          type: string
          description: QR code string from LoyaltyPlant mobile app.
        orderId:
          type: string
          description: Order identifier in POS.
        posInfo:
          $ref: '#/components/schemas/PosInfo'
        order:
          $ref: '#/components/schemas/Order_Legacy'
    QrCodeRequest_v1_0:
      type: object
      required:
        - qrCode
        - orderId
        - posInfo
      properties:
        qrCode:
          type: string
        orderId:
          type: string
        posInfo:
          $ref: '#/components/schemas/PosInfo'
        order:
          $ref: '#/components/schemas/Order_Legacy'
    QrCodeResponse:
      type: object
      required:
        - accepted
      properties:
        accepted:
          type: boolean
          description: Whether the QR code / phone number was successfully processed.
        menuItems:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/MenuItem'
          description: >-
            Menu items (rewards) to apply to the order. `null` when the request
            is rejected.
        discounts:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/Discount'
          description: Order-level discounts to apply. `null` when the request is rejected.
        messages:
          type: array
          items:
            $ref: '#/components/schemas/Message'
          description: Messages to display to the cashier.
        customer:
          $ref: '#/components/schemas/Customer'
        payment:
          type: object
          nullable: true
          allOf:
            - $ref: '#/components/schemas/LoyaltyPayment'
          description: LoyaltyPlant payment to apply to the order.
        hasEverRewardedPoints:
          type: boolean
          description: '`false` if the customer has never been rewarded points.'
        maxPointsPercent:
          type: number
          format: decimal
          nullable: true
          description: Maximum percentage of order total payable with points.
    QrCodeResponse_v1_7:
      type: object
      required:
        - accepted
      description: QR code response for v1.7 with customer tier and balance enrichment.
      properties:
        accepted:
          type: boolean
          description: >-
            Whether the QR code / phone number / publicClientId was successfully
            processed.
        menuItems:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/MenuItem'
          description: >-
            Menu items (rewards) to apply to the order. `null` when the request
            is rejected.
        discounts:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/Discount'
          description: Order-level discounts to apply. `null` when the request is rejected.
        messages:
          type: array
          items:
            $ref: '#/components/schemas/Message'
          description: Messages to display to the cashier.
        customer:
          $ref: '#/components/schemas/Customer_v1_7'
        payment:
          type: object
          nullable: true
          allOf:
            - $ref: '#/components/schemas/LoyaltyPayment'
          description: LoyaltyPlant payment to apply to the order.
        hasEverRewardedPoints:
          type: boolean
          description: '`false` if the customer has never been rewarded points.'
        maxPointsPercent:
          type: number
          format: decimal
          nullable: true
          description: Maximum percentage of order total payable with points.
    QrCodeResponse_v1_3:
      type: object
      required:
        - accepted
      description: >-
        QR code response for v1.3–1.4 (legacy discount format, message codes,
        maxPointsPercent).
      properties:
        accepted:
          type: boolean
        menuItems:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/MenuItem_Legacy'
        discounts:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/Discount_Legacy'
        messages:
          type: array
          items:
            $ref: '#/components/schemas/Message'
        customer:
          $ref: '#/components/schemas/Customer_Legacy'
        payment:
          $ref: '#/components/schemas/LoyaltyPayment'
        hasEverRewardedPoints:
          type: boolean
        maxPointsPercent:
          type: number
          format: decimal
          nullable: true
    QrCodeResponse_v1_2:
      type: object
      required:
        - accepted
      description: QR code response v1.2 (no message codes, no maxPointsPercent).
      properties:
        accepted:
          type: boolean
        menuItems:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/MenuItem_Legacy'
        discounts:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/Discount_Legacy'
        messages:
          type: array
          items:
            type: object
            required:
              - type
              - text
            properties:
              type:
                type: string
                enum:
                  - error
                  - warn
                  - info
              text:
                type: string
        customer:
          $ref: '#/components/schemas/Customer_Legacy'
        payment:
          $ref: '#/components/schemas/LoyaltyPayment'
        hasEverRewardedPoints:
          type: boolean
    QrCodeResponse_v1_1:
      type: object
      required:
        - accepted
      description: QR code response v1.1 (no hasEverRewardedPoints).
      properties:
        accepted:
          type: boolean
        menuItems:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/MenuItem_Legacy'
        discounts:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/Discount_Legacy'
        messages:
          type: array
          items:
            type: object
            required:
              - type
              - text
            properties:
              type:
                type: string
                enum:
                  - error
                  - warn
                  - info
              text:
                type: string
        customer:
          $ref: '#/components/schemas/Customer_Legacy'
        payment:
          $ref: '#/components/schemas/LoyaltyPayment'
    QrCodeResponse_v1_0:
      type: object
      description: >-
        QR code response v1.0 (no `accepted`, messages are plain strings,
        customer has loyaltyPlantId and customerName only).
      properties:
        menuItems:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/MenuItem_Legacy'
        discounts:
          type: array
          nullable: true
          items:
            $ref: '#/components/schemas/Discount_Legacy'
        messages:
          type: array
          items:
            type: string
          description: Plain text messages for the cashier.
        customer:
          type: object
          properties:
            loyaltyPlantId:
              type: integer
            customerName:
              type: string
              description: Customer's first name from mobile app or social login.
        payment:
          $ref: '#/components/schemas/LoyaltyPayment'
    OrderClosedRequest:
      type: object
      required:
        - establishmentId
        - orderId
        - orderTotal
        - order
        - payments
      properties:
        phoneNumber:
          type: string
          description: >-
            Guest phone number (v1.6, in-store acquisition flow). Not present in
            the monolith order-closed contract.
        subscribedToPushNotifications:
          type: boolean
          description: >-
            Push notification subscription flag (v1.6, in-store acquisition
            flow). Not present in the monolith order-closed contract.
        clientName:
          type: string
          description: >-
            Client name (v1.6, in-store acquisition flow). Not present in the
            monolith order-closed contract.
        establishmentId:
          type: integer
          description: LoyaltyPlant establishment ID.
        orderId:
          type: string
          description: Order identifier in POS.
        orderTotal:
          type: number
          format: decimal
          description: Final order total.
        pointsReward:
          type: integer
          description: >
            Override points reward amount. **Warning: usually should NOT be
            specified.**

            Only use to force a specific number of bonus points instead of the
            calculated amount.
        order:
          $ref: '#/components/schemas/Order'
        payments:
          type: array
          items:
            $ref: '#/components/schemas/Payment'
          description: List of payments applied to the order.
        waiter:
          $ref: '#/components/schemas/Waiter'
    OrderClosedRequest_v1_5:
      type: object
      required:
        - establishmentId
        - orderId
        - orderTotal
        - order
        - payments
      properties:
        establishmentId:
          type: integer
        orderId:
          type: string
        orderTotal:
          type: number
          format: decimal
        pointsReward:
          type: integer
        order:
          $ref: '#/components/schemas/Order'
        payments:
          type: array
          items:
            $ref: '#/components/schemas/Payment'
        waiter:
          $ref: '#/components/schemas/Waiter'
    OrderClosedRequest_v1_3:
      type: object
      required:
        - establishmentId
        - orderId
        - orderTotal
        - order
        - payments
      description: >-
        Order-closed request v1.3–1.4 (legacy discounts, string modifiers,
        waiter optional).
      properties:
        establishmentId:
          type: integer
        orderId:
          type: string
        orderTotal:
          type: number
          format: decimal
        pointsReward:
          type: integer
        order:
          $ref: '#/components/schemas/Order_Legacy'
        payments:
          type: array
          items:
            $ref: '#/components/schemas/Payment'
        waiter:
          $ref: '#/components/schemas/Waiter'
    OrderClosedRequest_v1_0:
      type: object
      required:
        - establishmentId
        - orderId
        - orderTotal
        - order
        - payments
      description: Order-closed request v1.0–1.2.
      properties:
        establishmentId:
          type: integer
        orderId:
          type: string
        orderTotal:
          type: number
          format: decimal
        pointsReward:
          type: integer
        order:
          $ref: '#/components/schemas/Order_Legacy'
        payments:
          type: array
          items:
            $ref: '#/components/schemas/Payment'
        waiter:
          $ref: '#/components/schemas/Waiter'
    OrderClosedResponse:
      type: object
      required:
        - accepted
      properties:
        accepted:
          type: boolean
          description: Whether the request was successful.
        pointsReward:
          type: integer
          description: Number of bonus points credited to the customer.
        pointsSpent:
          type: integer
          description: Number of bonus points deducted from the customer.
        errorCode:
          type: integer
          description: Error code. Zero or absent if successful.
        errorText:
          type: string
          description: Error message. Empty or absent if successful.
    OrderClosedResponse_v1_0:
      type: object
      required:
        - accepted
      description: Order-closed response v1.0 (no pointsReward/pointsSpent).
      properties:
        accepted:
          type: boolean
        errorCode:
          type: integer
        errorText:
          type: string
    RefundRequest:
      type: object
      required:
        - establishmentId
        - orderId
      properties:
        establishmentId:
          type: integer
          description: LoyaltyPlant establishment ID.
        orderId:
          type: string
          description: Order identifier in POS.
    RefundResponse:
      type: object
      required:
        - accepted
      properties:
        accepted:
          type: boolean
          description: Whether the refund was successful.
        errorCode:
          type: integer
          description: Error code. Zero or absent if successful.
        errorText:
          type: string
          description: Error message. Empty or absent if successful.
    ValidationError:
      type: object
      description: >
        Validation error response (v1.4+). Keys are JSON Pointer paths to
        invalid fields,

        values describe the validation failure.
      additionalProperties:
        type: string
      example:
        /order/discounts/0/type: >-
          instance value ("perent") not found in enum (possible values:
          ["percent","amount","fixedPrice"])
        /order/menuItems/0/itemId: >-
          instance type (integer) does not match any allowed primitive type
          (allowed: ["string"])
    ForbiddenError:
      type: object
      required:
        - message
      properties:
        message:
          type: string
          description: Error message.
      example:
        message: 'Authentication failed: illegal auth token'
    PosConfigResponse:
      type: object
      required:
        - accepted
        - config
      description: >-
        Two-field response envelope. `config` is populated only when `accepted`
        is `true`.
      properties:
        accepted:
          type: boolean
          description: >
            `true` — the configuration is in `config`. `false` — the
            configuration was already

            fetched for this identifier, the outlet is not bound, or the `{pos}`
            segment is unknown.
        config:
          type: object
          nullable: true
          description: >
            The outlet configuration. Its exact shape depends on the `{pos}`
            segment

            (generic / clover / Syrve / Restera). `null` whenever `accepted` is
            `false`.

            Only fields with a value are present (null fields are omitted).
          anyOf:
            - $ref: '#/components/schemas/GenericPOSConfiguration'
            - $ref: '#/components/schemas/CloverPosConfiguration'
            - $ref: '#/components/schemas/SyrvePosConfiguration'
            - $ref: '#/components/schemas/ResteraPosConfiguration'
            - type: object
              nullable: true
    GenericPOSConfiguration:
      type: object
      description: >
        Core configuration fields returned for **every** POS type, and the
        complete configuration

        for the `generic` POS type. The custom POS types (`clover`, Syrve,
        Restera) return these

        fields plus their own extras. All fields are optional — a field is
        omitted from the

        response when it has no value.
      properties:
        apiUrl:
          type: string
          description: >
            The in-store API base URL this outlet's loyalty operations call (the
            `{baseUrl}` for

            `one-time-salt`, `qr-code`, `order-closed`, `refund`). Defaults to

            `https://release.loyaltyplant.com/CloudValidator` when not set
            per-outlet.
        apiVersion:
          type: string
          description: >-
            The in-store API protocol version this outlet uses, e.g. `1.6` or
            `1.7`. Append it to each operation path (e.g. `/qr-code/1.6`).
            Defaults to `1.6` when not set per-outlet.
        flow:
          type: string
          enum:
            - REGULAR
            - PATCH_ORDER
          description: >
            The QR-scan timing workflow for this outlet. Honor the value you
            receive; do not

            hard-code one.

            - `REGULAR` (default) — loyalty codes may be scanned at any time
            before the order is
              closed. A redeemed reward is returned as a new, zero-priced menu line for you to add
              to the order.
            - `PATCH_ORDER` — assemble the full order with reward items already
            on it as ordinary
              paid lines, then scan immediately before closing; the validation matches the reward
              to a line already in the order and applies the discount that zeroes it. Scanning
              early is not expected.
        establishmentId:
          type: integer
          format: int32
          description: The LoyaltyPlant establishment (sales outlet) ID.
        password:
          type: string
          description: >
            The outlet's in-store API key — the `apiPrivateKey` used to compute
            the

            `AuthorizationToken`. Returned **encrypted** (DESede / Triple DES,
            CBC, PKCS5,

            Base64-encoded); decrypt it before use.
        discountId:
          type: string
          description: >
            The 100%-discount code used for reward items. Echo this exact value
            back on the

            zero-priced reward lines you send in `order-closed`. Configured per
            outlet — not

            always the literal string `LoyaltyPlant`.
    CloverPosConfiguration:
      description: >-
        Configuration returned for the `clover` POS type (core fields + Clover
        extras).
      allOf:
        - $ref: '#/components/schemas/GenericPOSConfiguration'
        - type: object
          properties:
            minCashPayment:
              type: number
              format: decimal
              description: >-
                Minimum cash amount that must remain on the order (constrains
                how much may be paid with points).
            maxPwpPercent:
              type: integer
              format: int32
              description: >-
                Maximum percentage of the order that may be paid with points
                (pay-with-points cap).
            timeout:
              type: integer
              format: int32
              description: >-
                Request timeout (seconds) the integration should apply to
                in-store API calls (admin default 60).
            firing:
              type: boolean
              description: Clover-specific order-firing toggle.
    SyrvePosConfiguration:
      description: >-
        Configuration returned for the **Syrve** POS type (segment `iiko`) —
        core fields + Syrve extras.
      allOf:
        - $ref: '#/components/schemas/GenericPOSConfiguration'
        - type: object
          properties:
            minCashPayment:
              type: number
              format: decimal
              description: >-
                Minimum cash amount that must remain on the order (constrains
                how much may be paid with points).
            giftCode:
              type: array
              items:
                type: string
              description: Syrve codes that identify reward items in the POS catalog.
            giftMinSum:
              type: number
              format: decimal
              description: Syrve reward threshold (minimum sum).
            maxPwpPercent:
              type: integer
              format: int32
              description: >-
                Maximum percentage of the order that may be paid with points
                (pay-with-points cap).
            timeout:
              type: integer
              format: int32
              description: >-
                Request timeout (seconds) the integration should apply to
                in-store API calls (admin default 60).
            giftPrice:
              type: number
              format: decimal
              description: Nominal price applied to reward lines.
            language:
              type: string
              enum:
                - EN
                - RU
              description: UI language for the Syrve integration.
            payForReqModifiers:
              type: boolean
              description: Whether required modifiers are charged.
            restrictedPaymentIds:
              type: array
              items:
                type: string
              description: Syrve payment-type IDs excluded from loyalty.
            softRegistration:
              type: boolean
              description: >
                Whether the phone number-based flow is enabled (legacy Syrve
                boolean). The richer,

                device-agnostic equivalent is `loyalty.phoneNumberFlow` on the
                `getOutletConfig`

                operation, which additionally conveys the consent `flow`. This
                boolean is retained

                unchanged for back-compatibility.
    ResteraPosConfiguration:
      description: >-
        Configuration returned for the **Restera** POS type (segment `rkeeper`)
        — core fields + Restera extras.
      allOf:
        - $ref: '#/components/schemas/GenericPOSConfiguration'
        - type: object
          properties:
            minCashPayment:
              type: number
              format: decimal
              description: >-
                Minimum cash amount that must remain on the order (constrains
                how much may be paid with points).
            maxPwpPercent:
              type: integer
              format: int32
              description: >-
                Maximum percentage of the order that may be paid with points
                (pay-with-points cap).
            timeout:
              type: integer
              format: int32
              description: >-
                Request timeout (seconds) the integration should apply to
                in-store API calls (admin default 60).
            bonusDiscountId:
              type: string
              description: >-
                Restera discount code used for the points/bonus discount,
                distinct from the reward `discountId`.
    OutletConfigResponse:
      type: object
      required:
        - accepted
        - config
      description: >
        Response envelope for `getOutletConfig`. Same two-field shape as
        `PosConfigResponse`. `config`

        is populated only when `accepted` is `true`.
      properties:
        accepted:
          type: boolean
          description: >-
            `true` — the configuration is in `config`. `false` — the outlet is
            not bound or loyalty is not configured (guest mode), and `config` is
            `null`.
        config:
          description: >-
            The outlet configuration (Section below), or `null` when `accepted`
            is `false`. Only fields with a value are present (null fields
            omitted).
          anyOf:
            - $ref: '#/components/schemas/OutletConfig'
            - type: object
              nullable: true
    OutletConfig:
      type: object
      description: >
        The full per-outlet loyalty configuration returned by `getOutletConfig`,
        used identically by

        POS and kiosk. All fields are optional — a field is omitted when it has
        no value.
      properties:
        revision:
          type: integer
          format: int64
          description: >-
            Monotonic epoch-second stamp of the last configuration change.
            Re-apply the config when this increases; also returned as the
            `ETag`.
        apiUrl:
          type: string
          description: >-
            The in-store API base URL this outlet calls at runtime (the
            `{baseUrl}` for `one-time-salt`, `qr-code`, `order-closed`,
            `refund`).
        loyaltyApiVersion:
          type: string
          description: >
            The in-store API protocol version this outlet uses, e.g. `1.7`.
            Append it to each operation

            path (`/qr-code/{loyaltyApiVersion}`). Because it is delivered here
            (not in the fetch-once

            credential), migration is a config change: ship app support for the
            new version, then update

            this value — devices switch on their next poll, no reprovisioning.
        establishmentId:
          type: integer
          format: int32
          description: >-
            The LoyaltyPlant establishment (sales outlet) ID, sent on every
            runtime call.
        currency:
          type: string
          description: >-
            ISO 4217 currency code, used to display the accrual rule and
            balances.
        currencySymbol:
          type: string
          description: Currency symbol (e.g. `₾`).
        timezone:
          type: string
          description: >-
            IANA timezone (e.g. `Asia/Tbilisi`), for timestamps and acceptance
            audit logs.
        locales:
          $ref: '#/components/schemas/Locales'
        loyaltyEnabled:
          type: boolean
          description: >
            Master switch. When `false`, the device runs as a plain POS/kiosk —
            no points, loyalty-card

            or reward scanning, and no registration — and the `loyalty` block is
            ignored.
        loyalty:
          $ref: '#/components/schemas/Loyalty'
        documents:
          type: array
          items:
            $ref: '#/components/schemas/LegalDocument'
          description: Legal documents shown during registration / consent.
        phone:
          $ref: '#/components/schemas/PhoneConfig'
    Locales:
      type: object
      description: Languages available on this device.
      properties:
        enabled:
          type: array
          items:
            type: string
          description: ISO 639-1 codes of the languages available on this device.
        default:
          type: string
          description: The default language.
        fallback:
          type: string
          description: The language to fall back to when a translation is missing.
    Loyalty:
      type: object
      description: >-
        Loyalty-program settings. Ignored entirely when `loyaltyEnabled` is
        `false`.
      properties:
        loyaltyPointAccrualRules:
          $ref: '#/components/schemas/LoyaltyPointAccrualRules'
        pointsIconUrl:
          type: string
          description: >-
            Image URL for the points icon shown next to balances/accrual. Not
            localized; omitted to use the device default.
        phoneNumberFlow:
          $ref: '#/components/schemas/PhoneNumberFlow'
        loyaltyCardFlow:
          $ref: '#/components/schemas/LoyaltyCardFlow'
        rewardRedemption:
          $ref: '#/components/schemas/RewardRedemption'
        rewardBanner:
          $ref: '#/components/schemas/RewardBanner'
        marketingConsent:
          $ref: '#/components/schemas/MarketingConsent'
        phoneFormDisclaimer:
          $ref: '#/components/schemas/PhoneFormDisclaimer'
        appInstallBanner:
          $ref: '#/components/schemas/AppInstallBanner'
    LoyaltyPointAccrualRules:
      type: object
      description: How purchases convert to points.
      properties:
        pointsPerCurrencyUnit:
          type: number
          description: >-
            Points earned per 1 unit of the outlet currency (`0.04` = 1 point
            per 25 of the outlet currency; `2` = 2 points per unit).
        rounding:
          type: string
          enum:
            - floor
            - round
            - ceil
          description: >-
            How to round fractional points — down (`floor`, the default),
            arithmetic (`round`), or up (`ceil`).
    PhoneNumberFlow:
      type: object
      description: Phone-number registration / identification at this device.
      properties:
        enabled:
          type: boolean
          description: >-
            Whether the phone-number flow is offered. When `false`, the
            phone-entry screen is not shown (the customer can still scan a
            loyalty card if `loyaltyCardFlow` is enabled).
        flow:
          type: string
          enum:
            - single_step
            - two_step
          description: >
            How marketing consent is collected (ignored when `enabled` is
            `false`):

            - `single_step` — phone number and marketing consent on one screen,
            confirmed with a single
              tap; the runtime `qr-code` request carries `subscribedToPushNotifications` reflecting that tap.
            - `two_step` — phone number first, then a separate marketing-consent
            screen;
              `subscribedToPushNotifications` reflects the answer on the second screen (cleaner GDPR posture).
    LoyaltyCardFlow:
      type: object
      description: Identification by scanning a loyalty QR card or entering its short code.
      properties:
        enabled:
          type: boolean
          description: >-
            Whether loyalty-card scan / short-code entry is offered (vs.
            phone-only identification). Defaults to enabled.
    RewardRedemption:
      type: object
      description: Redeeming a reward / gift by scanning its QR onto the order.
      properties:
        enabled:
          type: boolean
          description: >-
            Whether reward/gift QR scanning and application to the order is
            offered. Defaults to enabled.
    RewardBanner:
      type: object
      description: >-
        Promotional banner advertising the rewards/gifts available at this
        outlet.
      properties:
        imageUrl:
          type: string
          description: Image URL. Not localized; omitted when no banner is configured.
    MarketingConsent:
      type: object
      description: Marketing-consent channels and the consent-screen copy.
      properties:
        channels:
          type: array
          items:
            type: string
            enum:
              - sms
              - whatsapp
              - email
              - push
          description: >-
            Channels the brand offers. Empty or absent ⇒ marketing consent is
            not requested (and the `two_step` consent screen is not shown).
        screen:
          $ref: '#/components/schemas/ConsentScreen'
    ConsentScreen:
      type: object
      description: >-
        Copy for the dedicated marketing-consent screen (shown on the second
        step of the `two_step` flow).
      properties:
        headlineByLocale:
          type: object
          additionalProperties:
            type: string
          description: Headline question per ISO 639-1 locale.
        bodyByLocale:
          type: object
          additionalProperties:
            type: string
          description: >-
            Body text per locale. May contain `{document_kind}` placeholders the
            device replaces with links.
        documentRefs:
          type: array
          items:
            type: string
          description: '`kind`s of the documents referenced by the body placeholders.'
    PhoneFormDisclaimer:
      type: object
      description: Fine-print disclaimer under the phone-entry form (shown in both flows).
      properties:
        templateByLocale:
          type: object
          additionalProperties:
            type: string
          description: >-
            Disclaimer template per locale. The `{privacy_policy}`,
            `{terms_conditions}`, `{loyalty_terms}`, `{marketing_consent}`
            placeholders are replaced with links to the documents.
        documentRefs:
          type: array
          items:
            type: string
          description: '`kind`s of the documents linked from the template.'
    AppInstallBanner:
      type: object
      description: '"Get the app" banner shown on the final screen.'
      properties:
        url:
          type: string
          description: Universal / dynamic install link, rendered as a QR code.
        imageUrl:
          type: string
          description: Banner image URL. Not localized.
        promoTextByLocale:
          type: object
          additionalProperties:
            type: string
          description: >-
            Promo text next to the QR, per locale. Omitted ⇒ banner shown
            without text.
        showWhen:
          $ref: '#/components/schemas/ShowWhen'
    ShowWhen:
      type: object
      description: Which of the four end-states show the app-install banner.
      properties:
        justRegisteredWithApp:
          type: boolean
          description: Customer just registered and already has the app installed.
        justRegisteredWithoutApp:
          type: boolean
          description: >-
            Customer just registered and the app is not installed / not
            detected.
        alreadyRegistered:
          type: boolean
          description: The entered number was already in the loyalty base.
        skipped:
          type: boolean
          description: Customer skipped or declined registration (guest mode).
    LegalDocument:
      type: object
      description: A versioned legal document.
      properties:
        kind:
          type: string
          enum:
            - privacy_policy
            - terms_conditions
            - loyalty_terms
            - marketing_consent
          description: Document kind. Natural key, unique within `documents`.
        version:
          type: string
          description: >-
            Document version, recorded in the consent audit trail when the
            customer accepts.
        titleByLocale:
          type: object
          additionalProperties:
            type: string
          description: Modal / button title per locale.
        pdfUrlByLocale:
          type: object
          additionalProperties:
            type: string
          description: PDF URL per locale. Immutable — a new version is a new URL.
        requiresAcceptance:
          type: boolean
          description: >-
            Whether an explicit "I accept" tap is required (otherwise
            informational only).
    PhoneConfig:
      type: object
      description: Phone-number country and input settings.
      properties:
        defaultCountry:
          type: string
          description: >-
            ISO 3166-1 alpha-2 default country, for the dialing code and
            validation.
        allowedCountries:
          type: array
          items:
            type: string
          description: >-
            Optional extra ISO 3166-1 alpha-2 countries accepted (e.g. for
            tourists). Empty/absent ⇒ only `defaultCountry`.
        masksByCountry:
          type: object
          additionalProperties:
            type: string
          description: >-
            Optional per-country input mask (key = ISO 3166-1 alpha-2, `X` = a
            digit, other characters are literals). Falls back to libphonenumber
            when a country is absent.
  securitySchemes:
    AuthorizationToken:
      type: apiKey
      in: header
      name: AuthorizationToken
      description: >
        SHA-256 of the outlet's decrypted `apiPrivateKey` (from the `password`
        field of

        `POST /v1/pos/{pos}`) combined with a one-time-salt, exactly as for the
        In-Store Loyalty API.

        Required on `GET /v1/config/{establishmentId}`; the configuration
        payload never contains the key.
x-tagGroups:
  - name: In-Store Loyalty API
    tags:
      - One-Time Salt
      - Process Transaction
      - Order Lifecycle
  - name: Configuration API
    tags:
      - Configuration
