> ## Documentation Index
> Fetch the complete documentation index at: https://docs.loyalty.xeniamoments.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Calculate loyalty basket

> Called by the OnlinePOS backend once per basket calculation, after the Nexi Engage lookup
returned a loyalty ID and before the amount is authorised.

The REKOM backend answers synchronously with one of three statuses, always HTTP `200`:

- `applied` — the full basket with loyalty discounts; the POS replaces its basket with it.
- `unchanged` — nothing applies; no lines returned; the POS keeps its basket.
- `member_not_found` — the loyalty ID is unknown to REKOM; the POS keeps its basket.

Any other HTTP status, a timeout, or a response that fails OnlinePOS's arithmetic validation
means the POS keeps the original basket and sets the order's loyalty status to `failed`;
the sale itself completes normally.

Target latency: p95 ≤ 150 ms, p99 ≤ 250 ms. Proposed POS timeout: 500 ms per attempt, one retry
only on connection failure before the request was sent, hard cap 3 s.

Field names and structures are directional (see the API description).




## OpenAPI

````yaml /api-reference/openapi.yaml post /v1/onlinepos/basket
openapi: 3.1.0
info:
  title: REKOM Loyalty Partner API (for OnlinePOS)
  version: 0.1.0-draft
  summary: >-
    Synchronous basket calculation and post-sale events between OnlinePOS and
    the REKOM backend.
  description: >
    Draft contract for the two endpoints the REKOM backend exposes to OnlinePOS.


    - `POST /v1/onlinepos/basket` — calculate the loyalty basket for a
    recognised member (synchronous, inside the payment motion).

    - `POST /v1/onlinepos/sale` — report the outcome of a calculation once the
    sale is finalised or abandoned.


    Money is in integer minor units with an ISO 4217 currency. All requests are
    signed with

    `OnlinePOS-Signature` (HMAC-SHA256) and carry an `Idempotency-Key` equal to
    the `calculationId`.

    Status: **draft v0.1, directional** (REKOM, 8 Oct 2026). Every field name,
    header, enum value and

    structure here shows which information is exchanged; none has been aligned
    with OnlinePOS. REKOM will

    adopt OnlinePOS's default names and structures for the order object and
    webhooks and does not require

    any of these names. Hosts are TBC.
  contact:
    name: REKOM
servers:
  - url: https://loyalty.xeniamoments.com
    description: Production (TBC)
  - url: https://loyalty-staging.xeniamoments.com
    description: Staging / test (TBC)
security:
  - OnlinePosSignature: []
tags:
  - name: Basket
    description: Synchronous loyalty basket calculation.
  - name: Sale
    description: Post-sale outcome per calculation.
paths:
  /v1/onlinepos/basket:
    post:
      tags:
        - Basket
      summary: Calculate loyalty basket
      description: >
        Called by the OnlinePOS backend once per basket calculation, after the
        Nexi Engage lookup

        returned a loyalty ID and before the amount is authorised.


        The REKOM backend answers synchronously with one of three statuses,
        always HTTP `200`:


        - `applied` — the full basket with loyalty discounts; the POS replaces
        its basket with it.

        - `unchanged` — nothing applies; no lines returned; the POS keeps its
        basket.

        - `member_not_found` — the loyalty ID is unknown to REKOM; the POS keeps
        its basket.


        Any other HTTP status, a timeout, or a response that fails OnlinePOS's
        arithmetic validation

        means the POS keeps the original basket and sets the order's loyalty
        status to `failed`;

        the sale itself completes normally.


        Target latency: p95 ≤ 150 ms, p99 ≤ 250 ms. Proposed POS timeout: 500 ms
        per attempt, one retry

        only on connection failure before the request was sent, hard cap 3 s.


        Field names and structures are directional (see the API description).
      operationId: calculateBasket
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - $ref: '#/components/parameters/RequestId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BasketRequest'
            examples:
              twoLinesOneNoDiscount:
                summary: Two lines, one flagged noDiscount
                value:
                  calculationId: 6f1d2c3e-8a4b-4c5d-9e0f-1a2b3c4d5e6f
                  loyaltyId: rk_8f2a1c9d4b7e
                  currency: DKK
                  requestedAt: '2026-10-08T21:14:03+02:00'
                  context:
                    venueId: '123'
                    cashRegisterId: '7'
                    baxId: '601553'
                    terminalId: '1001'
                    clerkNumber: 12
                    channel: pos
                  lines:
                    - lineId: '1'
                      productId: '4711'
                      productMasterId: M-4711
                      productGroupId: '12'
                      ean: '5700000000011'
                      name: Heidi's Lager 0,5 l
                      quantity: 2
                      unitPrice: 6500
                      originalLinePrice: 13000
                      noDiscount: false
                      noPercentageDiscount: false
                      existingLineDiscountType: none
                      existingLineDiscountAmount: 0
                      orderDiscountAllocation: []
                      orderLinePrice: 13000
                    - lineId: '2'
                      productId: '8120'
                      productGroupId: '40'
                      name: Cigarettes 20 pcs
                      quantity: 1
                      unitPrice: 6000
                      originalLinePrice: 6000
                      noDiscount: true
                      noPercentageDiscount: true
                      existingLineDiscountType: none
                      existingLineDiscountAmount: 0
                      orderDiscountAllocation: []
                      orderLinePrice: 6000
                  orderDiscounts: []
                  totals:
                    originalTotal: 19000
                    existingDiscountTotal: 0
                    orderDiscountTotal: 0
                    loyaltyDiscountTotal: 0
                    total: 19000
      responses:
        '200':
          description: >-
            Calculation result. The `status` field says whether the basket
            changed.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
            Server-Timing:
              $ref: '#/components/headers/ServerTiming'
            Idempotent-Replayed:
              $ref: '#/components/headers/IdempotentReplayed'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BasketResponse'
              examples:
                applied:
                  summary: applied — 20 % member discount on line 1, line 2 untouched
                  value:
                    calculationId: 6f1d2c3e-8a4b-4c5d-9e0f-1a2b3c4d5e6f
                    status: applied
                    memberLabel: REKOM Member
                    receiptFooterText: 'Member benefits today: -26,00 kr.'
                    lines:
                      - lineId: '1'
                        productId: '4711'
                        productMasterId: M-4711
                        productGroupId: '12'
                        ean: '5700000000011'
                        name: Heidi's Lager 0,5 l
                        quantity: 2
                        unitPrice: 6500
                        originalLinePrice: 13000
                        noDiscount: false
                        noPercentageDiscount: false
                        existingLineDiscountType: none
                        existingLineDiscountAmount: 0
                        orderDiscountAllocation: []
                        lineDiscountType: percent
                        lineDiscountPercent: 20
                        lineDiscountAmount: 2600
                        lineDiscountText: Members 20 %
                        showLineDiscountText: true
                        lineDiscountCode: loyalty
                        orderLinePrice: 10400
                      - lineId: '2'
                        productId: '8120'
                        productGroupId: '40'
                        name: Cigarettes 20 pcs
                        quantity: 1
                        unitPrice: 6000
                        originalLinePrice: 6000
                        noDiscount: true
                        noPercentageDiscount: true
                        existingLineDiscountType: none
                        existingLineDiscountAmount: 0
                        orderDiscountAllocation: []
                        lineDiscountType: none
                        lineDiscountAmount: 0
                        showLineDiscountText: false
                        orderLinePrice: 6000
                    orderDiscounts: []
                    totals:
                      originalTotal: 19000
                      existingDiscountTotal: 0
                      orderDiscountTotal: 0
                      loyaltyDiscountTotal: 2600
                      total: 16400
                appliedOrderDiscount:
                  summary: >-
                    applied — 10 % member order discount, split onto the
                    eligible line only
                  value:
                    calculationId: 6f1d2c3e-8a4b-4c5d-9e0f-1a2b3c4d5e6f
                    status: applied
                    memberLabel: REKOM Member
                    receiptFooterText: 'Members 10 %: -13,00 kr.'
                    lines:
                      - lineId: '1'
                        productId: '4711'
                        productMasterId: M-4711
                        productGroupId: '12'
                        ean: '5700000000011'
                        name: Heidi's Lager 0,5 l
                        quantity: 2
                        unitPrice: 6500
                        originalLinePrice: 13000
                        noDiscount: false
                        noPercentageDiscount: false
                        existingLineDiscountType: none
                        existingLineDiscountAmount: 0
                        orderDiscountAllocation:
                          - orderDiscountId: rk-od-1
                            orderDiscountAllocationAmount: 1300
                        lineDiscountType: none
                        lineDiscountAmount: 0
                        showLineDiscountText: false
                        orderLinePrice: 11700
                      - lineId: '2'
                        productId: '8120'
                        productGroupId: '40'
                        name: Cigarettes 20 pcs
                        quantity: 1
                        unitPrice: 6000
                        originalLinePrice: 6000
                        noDiscount: true
                        noPercentageDiscount: true
                        existingLineDiscountType: none
                        existingLineDiscountAmount: 0
                        orderDiscountAllocation: []
                        lineDiscountType: none
                        lineDiscountAmount: 0
                        showLineDiscountText: false
                        orderLinePrice: 6000
                    orderDiscounts:
                      - orderDiscountId: rk-od-1
                        type: percent
                        percent: 10
                        amount: 1300
                        text: Members 10 %
                        discountCode: loyalty
                    totals:
                      originalTotal: 19000
                      existingDiscountTotal: 0
                      orderDiscountTotal: 1300
                      loyaltyDiscountTotal: 1300
                      total: 17700
                unchanged:
                  summary: unchanged — member known, nothing applies
                  value:
                    calculationId: 6f1d2c3e-8a4b-4c5d-9e0f-1a2b3c4d5e6f
                    status: unchanged
                    reason: no_applicable_benefits
                    memberLabel: REKOM Member
                memberNotFound:
                  summary: member_not_found — loyalty ID unknown to REKOM
                  value:
                    calculationId: 6f1d2c3e-8a4b-4c5d-9e0f-1a2b3c4d5e6f
                    status: member_not_found
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/IdempotencyConflict'
        '422':
          description: >-
            The request is well-formed but cannot be processed. The POS keeps
            the original basket.
          headers:
            X-Request-Id:
              $ref: '#/components/headers/XRequestId'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                arithmeticMismatch:
                  summary: arithmetic_mismatch — totals do not add up
                  value:
                    error:
                      code: arithmetic_mismatch
                      message: >-
                        totals.total (19000) does not equal the sum of
                        orderLinePrice (18000).
                      details:
                        field: totals.total
                        expected: 18000
                        received: 19000
                      requestId: 0b7a7a4e-0c1b-4e2a-9f3d-5c6d7e8f9a0b
                venueNotConfigured:
                  summary: venue_not_configured — venue not mapped in this environment
                  value:
                    error:
                      code: venue_not_configured
                      message: >-
                        Venue 123 is not configured for loyalty in this
                        environment.
                      requestId: 0b7a7a4e-0c1b-4e2a-9f3d-5c6d7e8f9a0b
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/Unavailable'
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: >-
        The `calculationId`. Same key with the same body replays the stored
        response; same key with a different body is rejected with `409`.
      schema:
        type: string
        format: uuid
    RequestId:
      name: X-Request-Id
      in: header
      required: false
      description: Fresh UUID per HTTP attempt, echoed in the response for log correlation.
      schema:
        type: string
        format: uuid
  schemas:
    BasketRequest:
      type: object
      required:
        - calculationId
        - loyaltyId
        - currency
        - requestedAt
        - context
        - lines
        - orderDiscounts
        - totals
      properties:
        calculationId:
          allOf:
            - $ref: '#/components/schemas/Uuid'
          description: >-
            GUID generated by POS/mPOS for this calculation. Also sent as
            `Idempotency-Key` and stored on the transaction.
        supersedesCalculationId:
          allOf:
            - $ref: '#/components/schemas/Uuid'
          description: >-
            Set when the basket changed after an earlier calculation; REKOM
            releases anything reserved for that GUID.
        loyaltyId:
          $ref: '#/components/schemas/LoyaltyId'
        currency:
          $ref: '#/components/schemas/Currency'
        requestedAt:
          type: string
          format: date-time
          description: POS time of the request, RFC 3339 with offset.
          examples:
            - '2026-10-08T21:14:03+02:00'
        context:
          $ref: '#/components/schemas/Context'
        lines:
          type: array
          minItems: 1
          maxItems: 200
          items:
            $ref: '#/components/schemas/BasketLine'
        orderDiscounts:
          type: array
          description: Order-level discounts already applied by staff. Empty when none.
          items:
            $ref: '#/components/schemas/OrderDiscount'
        totals:
          $ref: '#/components/schemas/Totals'
    BasketResponse:
      type: object
      description: >
        Result of a calculation. `status` decides which fields are present:


        - `applied`: `lines` (kept lines echoed, untouched ones with
        `lineDiscountType: none`; removed lines omitted; split and added lines
        marked), `orderDiscounts` (staff ones echoed, loyalty ones added) and
        `totals`. The basket differs from the request in at least one way.

        - `unchanged`: `reason`; no `lines`, no `totals`.

        - `member_not_found`: nothing else.
      required:
        - calculationId
        - status
      properties:
        calculationId:
          $ref: '#/components/schemas/Uuid'
        status:
          type: string
          enum:
            - applied
            - unchanged
            - member_not_found
        reason:
          type: string
          enum:
            - no_applicable_benefits
            - all_lines_excluded
            - member_inactive
            - basket_too_small
          description: >-
            Present when `status` is `unchanged`. Informational; never shown to
            the guest. New values may be added.
        memberLabel:
          type: string
          maxLength: 40
          description: >-
            Optional label OnlinePOS may print on the receipt if it has a place
            for it.
          examples:
            - REKOM Member
        receiptFooterText:
          type: string
          maxLength: 200
          description: >-
            Optional free text for the bottom of the receipt describing
            order-level benefits. Informational; not in reporting.
          examples:
            - 'Member benefits today: -26,00 kr.'
        lines:
          type: array
          description: >-
            Present only when `status` is `applied`. The full basket after
            loyalty, including added and split lines and without removed ones.
          items:
            $ref: '#/components/schemas/BasketLineOut'
        orderDiscounts:
          type: array
          description: >-
            Present only when `status` is `applied`. Existing order discounts as
            kept or changed by REKOM, plus any REKOM added.
          items:
            $ref: '#/components/schemas/OrderDiscount'
        totals:
          $ref: '#/components/schemas/Totals'
    Error:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
            - requestId
          properties:
            code:
              type: string
              enum:
                - invalid_request
                - invalid_signature
                - stale_timestamp
                - idempotency_conflict
                - arithmetic_mismatch
                - unknown_currency
                - venue_not_configured
                - too_many_lines
                - rate_limited
                - internal_error
                - unavailable
            message:
              type: string
              description: Human-readable, for logs. Not shown to staff or guests.
            details:
              type: object
              additionalProperties: true
              description: Optional machine-readable detail (field, expected, received).
            requestId:
              type: string
              format: uuid
    Uuid:
      type: string
      format: uuid
      description: Lower-case UUID with hyphens (36 characters).
      examples:
        - 6f1d2c3e-8a4b-4c5d-9e0f-1a2b3c4d5e6f
    LoyaltyId:
      type: string
      minLength: 1
      maxLength: 64
      description: >
        Opaque REKOM member identifier as returned by Nexi Engage (`getasset` /
        Softpay). Contains no PII.

        Exact format pending Nexi confirmation; treat as an opaque string.
      examples:
        - rk_8f2a1c9d4b7e
    Currency:
      type: string
      pattern: ^[A-Z]{3}$
      description: ISO 4217 currency code.
      examples:
        - DKK
        - NOK
        - SEK
        - EUR
    Context:
      type: object
      description: >
        Where the basket is being sold. Field meanings mirror the OnlinePOS REST
        transaction API so reconciliation is a join.

        `baxId` and `cashRegisterId` are required so REKOM can attribute every
        order to a venue and a till (agreed after 7 Oct 2026).
      required:
        - venueId
        - baxId
        - cashRegisterId
        - channel
      properties:
        venueId:
          type: string
          description: OnlinePOS venue id (`venue` in the REST API).
          examples:
            - '123'
        cashRegisterId:
          type: string
          description: >-
            OnlinePOS terminal setup / till id (`cash_register_id`). One
            terminal setup corresponds to one BAX.
          examples:
            - '7'
        baxId:
          type: string
          description: >-
            BAX number of the terminal setup; equals the Nexi Engage `storeId`.
            Identifies the venue for REKOM. Required on both channels.
          examples:
            - '601553'
        terminalId:
          type: string
          description: >-
            Payment terminal identifier (TID) that performed the lookup and
            payment, when the POS knows it.
          examples:
            - '1001'
        clerkNumber:
          type: integer
          description: Clerk number of the staff member (`clerk_number`).
          examples:
            - 12
        channel:
          $ref: '#/components/schemas/Channel'
    BasketLine:
      type: object
      description: >
        A basket line as sent by OnlinePOS, i.e. OnlinePOS's own object as it
        has it. REKOM echoes identity and

        quantity fields verbatim on every line it keeps; the existing-discount
        fields are input that REKOM may

        change or clear when it swaps in a larger loyalty discount. REKOM may
        also remove a line, split it, or

        add new lines (see `BasketLineOut`).

        Identity on the request: `orderLinePrice = originalLinePrice −
        existingLineDiscountAmount − Σ orderDiscountAllocationAmount`.
      required:
        - lineId
        - productId
        - name
        - quantity
        - unitPrice
        - originalLinePrice
        - noDiscount
        - noPercentageDiscount
        - existingLineDiscountType
        - existingLineDiscountAmount
        - orderDiscountAllocation
        - orderLinePrice
      properties:
        lineId:
          type: string
          description: Unique within the basket.
          examples:
            - '1'
        productId:
          type: string
          description: Venue-level product id (`product_id`).
          examples:
            - '4711'
        productMasterId:
          type: string
          description: >-
            Cross-venue master product id (`product_master_id`), when the
            product has one.
          examples:
            - M-4711
        productGroupId:
          type: string
          description: Product group id.
          examples:
            - '12'
        ean:
          type: string
          description: EAN / barcode (`product_ean`), when known.
          examples:
            - '5700000000011'
        name:
          type: string
          description: Receipt text of the product. Informational; never changed by REKOM.
          examples:
            - Heidi's Lager 0,5 l
        quantity:
          type: number
          exclusiveMinimum: 0
          description: Units on the line. Decimals allowed for weight-based products.
          examples:
            - 2
        unitPrice:
          $ref: '#/components/schemas/Money'
        originalLinePrice:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: '`quantity × unitPrice`, before any discount.'
        parentLineId:
          type: string
          description: >-
            For option / modifier lines, the `lineId` of the parent product
            line.
        noDiscount:
          type: boolean
          description: >
            `true` when the product may not legally be discounted (e.g.
            cigarettes). Mirrors the product's

            `no_discount` setting in OnlinePOS. Mandatory: REKOM needs it to
            split order-level benefits onto

            lines correctly (agreed 3 Sep 2026). REKOM never discounts such a
            line.
        noPercentageDiscount:
          type: boolean
          description: >-
            Product may not receive percentage discounts. Mirrors the product's
            `no_percentage_discount` setting. Mandatory.
        existingLineDiscountType:
          allOf:
            - $ref: '#/components/schemas/DiscountType'
          description: >-
            Discount already on the line before the call (staff discount,
            product price discount). REKOM may keep it or swap it for a loyalty
            discount.
        existingLineDiscountAmount:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: >-
            Existing discount on the line, in minor units (0 when type is
            `none`). May be reduced or cleared by REKOM when a larger loyalty
            discount replaces it.
        existingLineDiscountPercent:
          $ref: '#/components/schemas/Percent'
        existingLineDiscountText:
          type: string
          maxLength: 40
        existingLineDiscountCampaignId:
          type: string
        orderDiscountAllocation:
          type: array
          items:
            $ref: '#/components/schemas/OrderDiscountAllocation'
        orderLinePrice:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: Current revenue value of the line (before loyalty).
    OrderDiscount:
      type: object
      description: >
        An order-level discount. In the request: discounts already on the
        basket, which REKOM may keep, change or

        remove per its loyalty rules. In an `applied` response REKOM may add
        entries of its own; it then also allocates the amount onto the

        eligible lines (`orderDiscountAllocation`), skipping `noDiscount` lines
        (agreed 3 Sep 2026).

        The sum of allocations per `orderDiscountId` must equal `amount`.
      required:
        - orderDiscountId
        - type
        - amount
      properties:
        orderDiscountId:
          type: string
          examples:
            - disc_1
            - rk-od-1
        type:
          type: string
          enum:
            - amount
            - percent
        amount:
          $ref: '#/components/schemas/Money'
        percent:
          $ref: '#/components/schemas/Percent'
        text:
          type: string
          maxLength: 40
          description: Shown at the bottom of the receipt.
        campaignId:
          type: string
          description: POS discount campaign id (staff order discounts only).
        discountCode:
          type: string
          description: >-
            The predefined catch-all loyalty discount code, set on order
            discounts REKOM adds (proposed value `loyalty`).
          examples:
            - loyalty
    Totals:
      type: object
      description: |
        Basket totals in minor units. Identities: `total = Σ orderLinePrice`,
        `loyaltyDiscountTotal = Σ lineDiscountAmount`, `total ≥ 0`.
      required:
        - originalTotal
        - existingDiscountTotal
        - orderDiscountTotal
        - loyaltyDiscountTotal
        - total
      properties:
        originalTotal:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: Σ originalLinePrice.
        existingDiscountTotal:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: Σ existingLineDiscountAmount.
        orderDiscountTotal:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: Σ over all lines of Σ orderDiscountAllocationAmount.
        loyaltyDiscountTotal:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: >-
            Σ lineDiscountAmount + Σ amount of the order discounts REKOM added.
            0 in the request.
        total:
          allOf:
            - $ref: '#/components/schemas/Money'
          description: Σ orderLinePrice — the amount the guest pays.
    BasketLineOut:
      allOf:
        - $ref: '#/components/schemas/BasketLine'
        - type: object
          description: >
            A line in an `applied` response, with REKOM's fields added.
            Untouched lines are returned with

            `lineDiscountType: none` and `lineDiscountAmount: 0`. Removed lines
            are omitted; split lines carry

            `splitFromLineId`; added lines carry `addedByLoyalty: true` (product
            must exist on the venue).

            Identity: `linePrice = quantity × (overrideUnitPrice ?? unitPrice)`;

            `orderLinePrice = linePrice − existingLineDiscountAmount −
            lineDiscountAmount − Σ orderDiscountAllocationAmount`.
          required:
            - lineDiscountType
            - lineDiscountAmount
            - showLineDiscountText
            - orderLinePrice
          properties:
            overrideUnitPrice:
              allOf:
                - $ref: '#/components/schemas/Money'
              description: >-
                New unit price when REKOM updates the line's price (V1 "update a
                line's price"). Absent when the price is unchanged.
            addedByLoyalty:
              type: boolean
              default: false
              description: >-
                `true` on lines REKOM added. The product must exist on the
                venue.
            splitFromLineId:
              type: string
              description: >-
                Set on lines produced by splitting an original line; the split
                lines' quantities add up to the original.
            lineDiscountType:
              $ref: '#/components/schemas/DiscountType'
            lineDiscountPercent:
              allOf:
                - $ref: '#/components/schemas/Percent'
              description: Required when `lineDiscountType` is `percent`.
            lineDiscountAmount:
              allOf:
                - $ref: '#/components/schemas/Money'
              description: >-
                Loyalty discount in minor units. For `percent`,
                `round_half_up(originalLinePrice × lineDiscountPercent / 100)`.
            lineDiscountText:
              type: string
              maxLength: 40
              description: Printed on the note line under the product.
              examples:
                - Members 20 %
                - Member price
            showLineDiscountText:
              type: boolean
              description: Whether the note line is printed.
            lineDiscountCode:
              type: string
              description: >
                Predefined, semantic code identifying the catch-all
                third-party-loyalty discount type in POS.

                Required when `lineDiscountType` is not `none`; absent
                otherwise. One value for the whole

                integration (proposed `loyalty`; exact string to be confirmed by
                OnlinePOS). Loyalty discounts

                carry no campaign id.
              examples:
                - loyalty
            orderLinePrice:
              allOf:
                - $ref: '#/components/schemas/Money'
              description: Final revenue value of the line.
    Channel:
      type: string
      enum:
        - pos
        - mpos
      description: '`pos` = Windows POS (BAXI/Viking), `mpos` = Android mPOS (Softpay).'
    Money:
      type: integer
      minimum: 0
      description: >-
        Amount in integer minor units of the basket `currency` (øre for DKK).
        Never negative, never fractional.
      examples:
        - 6500
    DiscountType:
      type: string
      enum:
        - none
        - amount
        - percent
    Percent:
      type: number
      minimum: 0
      maximum: 100
      multipleOf: 0.01
      description: Percentage with at most two decimals.
      examples:
        - 20
        - 12.5
    OrderDiscountAllocation:
      type: object
      description: >-
        Share of an existing staff order discount allocated to this line. Set by
        OnlinePOS; echoed verbatim by REKOM.
      required:
        - orderDiscountId
        - orderDiscountAllocationAmount
      properties:
        orderDiscountId:
          type: string
          description: References an entry in `orderDiscounts`.
          examples:
            - disc_1
        orderDiscountAllocationAmount:
          $ref: '#/components/schemas/Money'
  headers:
    XRequestId:
      description: Echo of the request's `X-Request-Id`, or a server-generated UUID.
      schema:
        type: string
        format: uuid
    ServerTiming:
      description: Server processing time, e.g. `app;dur=42`.
      schema:
        type: string
        examples:
          - app;dur=42
    IdempotentReplayed:
      description: >-
        Present and `true` when the response was served from the idempotency
        store.
      schema:
        type: boolean
    RetryAfter:
      description: Seconds to wait before retrying.
      schema:
        type: integer
        minimum: 1
  responses:
    BadRequest:
      description: Malformed JSON or schema violation.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: invalid_request
              message: lines[0].quantity must be a number.
              requestId: 0b7a7a4e-0c1b-4e2a-9f3d-5c6d7e8f9a0b
    Unauthorized:
      description: Signature missing, invalid, or timestamp outside the ±300 s window.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            invalidSignature:
              value:
                error:
                  code: invalid_signature
                  message: Signature does not verify.
                  requestId: 0b7a7a4e-0c1b-4e2a-9f3d-5c6d7e8f9a0b
            staleTimestamp:
              value:
                error:
                  code: stale_timestamp
                  message: Signature timestamp is 412 s from server time (max 300 s).
                  requestId: 0b7a7a4e-0c1b-4e2a-9f3d-5c6d7e8f9a0b
    IdempotencyConflict:
      description: The `Idempotency-Key` was already used with a different body.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: idempotency_conflict
              message: >-
                Idempotency-Key 6f1d2c3e-… was used with a different request
                body.
              requestId: 0b7a7a4e-0c1b-4e2a-9f3d-5c6d7e8f9a0b
    RateLimited:
      description: Too many requests.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: rate_limited
              message: Rate limit exceeded; retry after 2 s.
              requestId: 0b7a7a4e-0c1b-4e2a-9f3d-5c6d7e8f9a0b
    InternalError:
      description: Unexpected error on REKOM's side.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: internal_error
              message: Unexpected error.
              requestId: 0b7a7a4e-0c1b-4e2a-9f3d-5c6d7e8f9a0b
    Unavailable:
      description: Maintenance or overload.
      headers:
        X-Request-Id:
          $ref: '#/components/headers/XRequestId'
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: unavailable
              message: Service temporarily unavailable.
              requestId: 0b7a7a4e-0c1b-4e2a-9f3d-5c6d7e8f9a0b
  securitySchemes:
    OnlinePosSignature:
      type: apiKey
      in: header
      name: OnlinePOS-Signature
      description: >
        HMAC-SHA256 request signature: `t=<unix seconds>,v1=<hex>` where

        `v1 = HMAC_SHA256(secret, "<t>.<raw body>")`. Timestamp window ±300 s.

        One secret per environment. No scheduled rotation; when a secret is
        swapped, old and new stay

        active with an overlap so nothing is interrupted. See Configuration and
        security.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.