openapi: 3.0.3
info:
  title: Digital Ordering API
  version: 2.0.0
  description: >
    Contract for the **Digital Ordering API** — the API that connects

    a restaurant chain's POS / ordering back end to LoyaltyPlant's mobile-app

    ordering.


    **YOU (the vendor / POS integration) implement and host the five `/rest/...`
    endpoints below.** LoyaltyPlant is the HTTP

    **client**: it calls your endpoints to fetch your menu, run a healthcheck,

    push orders, and poll order state. The only call that travels the other way

    is **one webhook** that you send to LoyaltyPlant when an order's status

    changes (modeled as the `orderStatusWebhook` callback on `POST
    /rest/order`).

    You never call LoyaltyPlant to create an order; LoyaltyPlant drives the flow

    by calling you.
externalDocs:
  description: See integration guides for a full walkthrough of the interation.
  url: /digital-ordering/
servers:
  - url: '{vendorBaseUrl}'
    description: |
      The base URL of the service YOU host, provided to LoyaltyPlant during
      onboarding. LoyaltyPlant appends `/rest/...` to it (e.g.
      {vendorBaseUrl}/rest/order). Must be reachable from LoyaltyPlant over TLS.
    variables:
      vendorBaseUrl:
        default: https://pos.your-domain.example
        description: Public HTTPS base URL of your Digital Ordering integration service.
tags:
  - name: Health
    description: Availability probe LoyaltyPlant runs before sending orders.
  - name: Menu
    description: Menu retrieval and lightweight revision polling.
  - name: Orders
    description: Order creation, order-state retrieval, and the order-status webhook.
security:
  - AuthorizationToken: []
paths:
  /rest/healthcheck:
    get:
      summary: Health check
      operationId: getHealthcheck
      tags:
        - Health
      description: >
        LoyaltyPlant calls this before creating an order to confirm your

        integration and its downstream services are up. Return a flat JSON
        object

        mapping each service name **you** choose to its status (`UP` / `DOWN`).

        If any value is `DOWN`, LoyaltyPlant will not place an order at this

        outlet. No request body is sent.


        Respond within ~5 s connect / 60 s read — LoyaltyPlant's client
        timeouts.
      x-lp-client-timeouts:
        connectSeconds: 5
        readSeconds: 60
      parameters:
        - $ref: '#/components/parameters/SalesOutletIdHeader'
        - $ref: '#/components/parameters/AuthorizationTokenHeader'
      responses:
        '200':
          description: |
            Per-service health map. A bare JSON object whose keys are
            service names you define and whose values are `UP` or `DOWN`.
            Body is `application/json; charset=UTF-8`.
          headers:
            AuthorizationToken:
              $ref: '#/components/headers/ResponseAuthorizationToken'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthcheckResponse'
              examples:
                allUp:
                  $ref: '#/components/examples/HealthcheckAllUpExample'
                oneDown:
                  $ref: '#/components/examples/HealthcheckDownExample'
  /rest/menu:
    get:
      summary: Get full menu
      operationId: getMenu
      tags:
        - Menu
      description: >
        Returns the full menu for the outlet identified by `SalesOutletId`:

        categories, reusable modifier groups, the modifier pool, and items, plus

        a `revision` (epoch timestamp). LoyaltyPlant imports this and maps your

        ids to its own catalog. LoyaltyPlant typically calls `GET

        /rest/menu/revision` first and only calls this when the revision changed

        (see that operation). No request body.


        The top-level object is wrapped under a `menu` key and also carries a

        `requestId`.


        Respond within ~30 s connect / 180 s read — LoyaltyPlant's client
        timeouts.
      x-lp-client-timeouts:
        connectSeconds: 30
        readSeconds: 180
      parameters:
        - $ref: '#/components/parameters/SalesOutletIdHeader'
        - $ref: '#/components/parameters/AuthorizationTokenHeader'
      responses:
        '200':
          description: Full menu for the outlet.
          headers:
            AuthorizationToken:
              $ref: '#/components/headers/ResponseAuthorizationToken'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetMenuResponseV2'
              examples:
                menu:
                  $ref: '#/components/examples/MenuResponseExample'
  /rest/menu/revision:
    get:
      summary: Get menu revision
      operationId: getMenuRevision
      tags:
        - Menu
      description: >
        Returns just the current menu `revision` (an epoch timestamp), so

        LoyaltyPlant can cheaply decide whether to re-pull the full menu. It
        must

        equal the `revision` inside `GET /rest/menu`. LoyaltyPlant skips the
        full

        menu when the revision is unchanged, and re-pulls when it changed, when
        it

        has no cached value, or when the returned revision is lower than its

        cache. No request body.


        Fail-open: if this pre-check fails for any reason, LoyaltyPlant falls
        back

        to a full `GET /rest/menu`.


        Respond within ~30 s connect / 180 s read — LoyaltyPlant's client
        timeouts.
      x-lp-client-timeouts:
        connectSeconds: 30
        readSeconds: 180
      parameters:
        - $ref: '#/components/parameters/SalesOutletIdHeader'
        - $ref: '#/components/parameters/AuthorizationTokenHeader'
      responses:
        '200':
          description: Current menu revision.
          headers:
            AuthorizationToken:
              $ref: '#/components/headers/ResponseAuthorizationToken'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MenuRevisionResponse'
              examples:
                revision:
                  $ref: '#/components/examples/MenuRevisionExample'
  /rest/order:
    post:
      summary: Create order
      operationId: createOrder
      tags:
        - Orders
      description: >
        LoyaltyPlant calls this to place a new order at your POS. The request is

        wrapped under an `order` key. Return the POS order id (`posId`) and the

        customer-facing `queueNumber`.


        After the order is created, **you** notify LoyaltyPlant of every

        subsequent status change by POSTing the order-status webhook — see the

        `orderStatusWebhook` callback on this operation.


        Reward items arrive in `presents[]`: the POS must charge **0** for them.

        Amounts are decimal strings/numbers; `pan` is masked when present.


        Respond within ~5 s connect / 60 s read — LoyaltyPlant's client
        timeouts.
      x-lp-client-timeouts:
        connectSeconds: 5
        readSeconds: 60
      parameters:
        - $ref: '#/components/parameters/SalesOutletIdHeader'
        - $ref: '#/components/parameters/AuthorizationTokenHeader'
        - $ref: '#/components/parameters/LpWebhookUrlHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateOrderRequest'
            examples:
              pickup:
                $ref: '#/components/examples/CreateOrderPickupExample'
              delivery:
                $ref: '#/components/examples/CreateOrderDeliveryExample'
              eatIn:
                $ref: '#/components/examples/CreateOrderEatInExample'
      responses:
        '200':
          description: >
            Either the order was **accepted** — returns `posId` (+
            `queueNumber`)

            — or the POS **rejected** it: return `state` with `status: FAILED`,
            a

            `reason`, and an optional structured `posErrorType`. A rejection

            carries no `posId`; LoyaltyPlant then fails the order and records
            the

            reason. Body is `application/json; charset=UTF-8`.
          headers:
            AuthorizationToken:
              $ref: '#/components/headers/ResponseAuthorizationToken'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateOrderResponse'
              examples:
                created:
                  $ref: '#/components/examples/CreateOrderResponseExample'
                rejected:
                  $ref: '#/components/examples/CreateOrderRejectedExample'
      callbacks:
        orderStatusWebhook:
          $ref: '#/components/callbacks/OrderStatusWebhook'
  /rest/orders:
    post:
      summary: Get orders by POS id
      operationId: getOrders
      tags:
        - Orders
      description: >
        Returns the current state of one or more orders, looked up by the POS
        ids

        you returned from `createOrder`. It is a **read**, expressed as `POST`

        because the id list travels in the request body (wrapped under

        `orderPosIds`). LoyaltyPlant uses it to poll / reconcile order lifecycle

        when a webhook was missed.


        Each returned order carries its `state.status` (one of the nine

        `OrderStatus` values); `state.reason` is required when the status is

        `CANCELED` or `FAILED`, and an optional structured `state.posErrorType`

        may accompany it (see the `PosErrorType` enum).


        Respond within ~5 s connect / 60 s read — LoyaltyPlant's client
        timeouts.
      x-lp-client-timeouts:
        connectSeconds: 5
        readSeconds: 60
      parameters:
        - $ref: '#/components/parameters/SalesOutletIdHeader'
        - $ref: '#/components/parameters/AuthorizationTokenHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GetOrdersRequest'
            examples:
              byIds:
                $ref: '#/components/examples/GetOrdersRequestExample'
      responses:
        '200':
          description: Current state of the requested orders.
          headers:
            AuthorizationToken:
              $ref: '#/components/headers/ResponseAuthorizationToken'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GetOrdersResponse'
              examples:
                orders:
                  $ref: '#/components/examples/GetOrdersResponseExample'
                canceled:
                  $ref: '#/components/examples/GetOrdersCanceledExample'
components:
  securitySchemes:
    AuthorizationToken:
      type: apiKey
      in: header
      name: AuthorizationToken
      description: >
        SHA-256 hex of the shared outlet API key (`SHA256-hex(apiKey)`,
        lowercase

        hex). Sent by LoyaltyPlant on every `/menu`, `/menu/revision`,

        `/healthcheck`, `/order` and `/orders` request — validate it against the

        key you hold for the `SalesOutletId` — and sent by you on the
        order-status

        webhook (the same hashed token, reverse direction).
  parameters:
    SalesOutletIdHeader:
      name: SalesOutletId
      in: header
      required: true
      schema:
        type: integer
      description: |
        Numeric id of the establishment this call targets. This is
        **LoyaltyPlant's** outlet id, not yours: LoyaltyPlant assigns it and
        gives it to you during onboarding. Resolve it to the correct
        venue/menu/configuration on your side. Sent on every LoyaltyPlant→vendor
        call and on the status webhook.
    AuthorizationTokenHeader:
      name: AuthorizationToken
      in: header
      required: true
      schema:
        type: string
      description: >
        Authentication token. For all Digital Ordering API calls this is the

        **lowercase SHA-256 hex** of the shared API key for the outlet

        (`SHA256-hex(apiKey)`), compared case-insensitively. On

        LoyaltyPlant→vendor requests, validate it against the key you hold for
        the

        `SalesOutletId`; on the order-status webhook you send the same hashed

        token. Example: "a3f5...e1" (64 lowercase hex chars).
    LpWebhookUrlHeader:
      name: X-LP-Webhook-Url
      in: header
      required: true
      schema:
        type: string
        format: uri
      description: |
        LoyaltyPlant's base URL for the order-status webhook callback. Sent by
        LoyaltyPlant on every `createOrder` call. The callback is posted to
        `{X-LP-Webhook-Url}/digitalordering/pos/v2/integrated` whenever an
        order's status changes.
  headers:
    ResponseAuthorizationToken:
      required: true
      schema:
        type: string
      description: >
        **Mandatory response header on every LoyaltyPlant→vendor response —

        `/order`, `/orders`, `/menu`, `/menu/revision` and `/healthcheck`.** Set
        it

        to the lowercase SHA-256 hex of the *response* API key

        (`SHA256-hex(apiKeyResponse)`) — the response key. LoyaltyPlant verifies

        this and **discards the entire response (treating the operation as
        failed)

        if it is missing or does not match.**
  schemas:
    HealthcheckResponse:
      type: object
      description: >
        Free-form health map. A bare JSON object whose keys are service names
        you

        define (e.g. "POS", "DB", "KITCHEN") and whose values are `UP` or
        `DOWN`.

        There is no fixed key set. If any value is `DOWN`, LoyaltyPlant will not

        place orders at this outlet.
      additionalProperties:
        type: string
        enum:
          - UP
          - DOWN
        description: >
          Service status:

          - UP: the named service is operational.

          - DOWN: the named service is unavailable; LoyaltyPlant will not order
          here.
    GetMenuResponseV2:
      type: object
      description: |
        Top-level menu response. The whole catalog is nested under `menu`;
        `requestId` is a server-generated UUID for correlation.
      required:
        - menu
      properties:
        requestId:
          type: string
          format: uuid
          description: Server-generated correlation id (UUID).
        menu:
          $ref: '#/components/schemas/MenuAggregatorV2'
    MenuAggregatorV2:
      type: object
      description: |
        The v2 menu payload. Categories, reusable modifier-group prototypes, the
        single modifier pool, items, and the menu revision. The four top-level
        arrays (`categories`, `modifierGroups`, `modifiers`, `items`) default to
        empty collections and are serialized as `[]` when nothing is present —
        they are not omitted from the wire.
      properties:
        categories:
          type: array
          description: All categories (root and nested). Max two levels of nesting.
          items:
            $ref: '#/components/schemas/MenuCategoryV2'
        modifierGroups:
          type: array
          description: |
            Reusable modifier-group **prototypes**. Items reference these by
            `refId` from their embedded groups' `basedOn` field.
          items:
            $ref: '#/components/schemas/ModifierGroupPrototypeV2'
        modifiers:
          type: array
          description: >
            The single pool of modifiers. Groups and items reference entries
            here

            by `refId`.
          items:
            $ref: '#/components/schemas/ModifierV2'
        items:
          type: array
          description: Sellable menu items.
          items:
            $ref: '#/components/schemas/MenuItemV2'
        revision:
          type: integer
          format: int64
          description: |
            Menu revision (epoch timestamp). Identical to the value returned by
            `GET /rest/menu/revision`. LoyaltyPlant uses it to decide whether to
            re-import.
    BaseEntityV2:
      type: object
      description: >
        Common identity fields shared by categories, items, modifiers and
        modifier

        groups.
      required:
        - refId
        - id
        - code
        - title
      properties:
        refId:
          type: string
          description: >
            **Response-scoped reference key.** MUST be unique within a single

            `/rest/menu` response; parents reference children by `refId` (e.g.
            an

            item's category reference, an embedded group's `basedOn`). Defaults
            to

            a UUID. May be reused across separate responses.
        id:
          type: string
          description: |
            Global product id in the POS. The same product carries the same `id`
            across outlets. Required.
        code:
          type: string
          description: >
            Outlet-local product code. Required. May equal `id` when the POS has
            a

            single code per product.
        title:
          $ref: '#/components/schemas/MultilingualText'
        sortOrder:
          type: integer
          description: Display order hint (lower first). Optional.
    MultilingualText:
      type: object
      description: |
        Multilingual string keyed by ISO 639-1 language code (e.g. "en", "es",
        "fr"). At least one language is expected for required titles.
      additionalProperties:
        type: string
      example:
        en: Cheeseburger
        es: Hamburguesa con queso
    MenuCategoryV2:
      allOf:
        - $ref: '#/components/schemas/BaseEntityV2'
        - type: object
          description: >
            A menu category. Categories nest at most **two levels** (no

            sub-subcategories). A root category contains **either**
            subcategories

            **or** items — not both.
          properties:
            description:
              $ref: '#/components/schemas/MultilingualText'
            root:
              type: boolean
              description: |
                Whether this is a root (top-level) category. Always serialized.
            isPopup:
              type: boolean
              description: >
                UI hint for non-root categories. When `true`, LoyaltyPlant
                renders

                the category as a popup. **Applied on every sync for non-root

                categories** — return the desired state each time; `false` (or

                omitting the field) reverts to an ordinary category. Root

                categories ignore it (LoyaltyPlant logs a warning). Optional.
            rootSortOrder:
              type: integer
              description: |
                Priority among root categories, numbered from 1 descending (1 =
                highest priority). Optional.
            subCategories:
              type: array
              description: |
                Child categories, each referencing the child by `refId`. Present
                only on root categories that group subcategories.
              items:
                $ref: '#/components/schemas/SubCategoryRefV2'
    SubCategoryRefV2:
      type: object
      description: A child-category reference within a parent category.
      required:
        - subCategory
      properties:
        subCategory:
          type: string
          description: >
            `refId` of the child `MenuCategoryV2` (serialized as an id
            reference,

            not an inline object).
        sortOrder:
          type: integer
          description: Display order of this child within the parent. Optional.
    PricedEntityV2:
      allOf:
        - $ref: '#/components/schemas/BaseEntityV2'
        - type: object
          description: |
            Identity fields plus pricing — the base for items and modifiers.
          properties:
            description:
              $ref: '#/components/schemas/MultilingualText'
            pricesByOrderingType:
              type: object
              description: >
                Price keyed by ordering type. Keys are `OrderingType` enum
                names:

                `PICKUP`, `EAT_IN`, `DELIVERY` (and the less common `CATERING`,

                `DRIVE_THROUGH`). At least one entry is expected. A product may
                be

                priced for only some ordering types.
              additionalProperties:
                $ref: '#/components/schemas/Price'
            costPrice:
              type: number
              description: >
                Cost (not sale) price of the product. Optional; code-backed but

                absent from the public menu doc — supply only if your POS
                exposes

                it.
    Price:
      type: object
      description: A single price with optional tax percentages.
      required:
        - value
      properties:
        value:
          type: number
          description: Monetary price value (decimal).
        taxes:
          type: array
          description: |
            Tax percentages applied to this price. Optional; an empty array is
            common.
          items:
            type: number
    MenuItemV2:
      allOf:
        - $ref: '#/components/schemas/PricedEntityV2'
        - type: object
          description: A sellable menu item.
          properties:
            categories:
              type: array
              description: >
                Categories this item belongs to, each referencing the category
                by

                `refId`.
              items:
                $ref: '#/components/schemas/MenuItemInCategoryV2'
            modifierGroups:
              type: array
              description: >
                Modifier groups embedded in this item. Each may be derived from
                a

                prototype (`basedOn`) and may override the prototype's

                `min`/`max`/`free`.
              items:
                $ref: '#/components/schemas/ModifierGroupEmbeddedV2'
    MenuItemInCategoryV2:
      type: object
      description: An item-to-category link.
      properties:
        category:
          type: string
          description: '`refId` of the `MenuCategoryV2` (serialized as an id reference).'
        sortOrder:
          type: integer
          description: Display order of the item within the category. Optional.
    ModifierV2:
      allOf:
        - $ref: '#/components/schemas/PricedEntityV2'
        - type: object
          description: |
            A single modifier in the shared modifier pool (`menu.modifiers`).
            Referenced by groups via `refId`.
    ModifierGroupV2:
      allOf:
        - $ref: '#/components/schemas/BaseEntityV2'
        - type: object
          description: >
            Base of both the reusable prototype group and the item-embedded
            group.

            `min`/`max`/`free` are **scalar integers**.
          properties:
            description:
              $ref: '#/components/schemas/MultilingualText'
            modifiers:
              type: array
              description: Modifiers in this group, each referencing a modifier by `refId`.
              items:
                $ref: '#/components/schemas/ModifierInGroupV2'
            min:
              type: integer
              description: Minimum number of modifiers the customer must select.
            max:
              type: integer
              description: Maximum number of modifiers the customer may select.
            free:
              type: integer
              description: >-
                Number of modifiers included free (above which they are
                charged).
    ModifierGroupPrototypeV2:
      allOf:
        - $ref: '#/components/schemas/ModifierGroupV2'
        - type: object
          description: >
            A standalone, reusable modifier group listed in
            `menu.modifierGroups`.

            Items embed it by referencing its `refId` via `basedOn`.
    ModifierGroupEmbeddedV2:
      allOf:
        - $ref: '#/components/schemas/ModifierGroupV2'
        - type: object
          description: |
            A modifier group embedded inside an item. It may be based on a
            prototype and may override the prototype's `min`/`max`/`free`.
          properties:
            basedOn:
              type: string
              description: >
                `refId` of the `ModifierGroupPrototypeV2` this embedded group is

                derived from (serialized as an id reference). Present only when
                the

                embedded group is based on a prototype.
    ModifierInGroupV2:
      type: object
      description: A modifier's membership in a group.
      properties:
        modifier:
          type: string
          description: '`refId` of the `ModifierV2` (serialized as an id reference).'
        selected:
          type: boolean
          description: Whether this modifier is pre-selected by default.
        sortOrder:
          type: integer
          description: Display order within the group. Optional.
    MenuRevisionResponse:
      type: object
      description: The current menu revision only.
      required:
        - revision
      properties:
        revision:
          type: integer
          format: int64
          description: >
            Menu revision (epoch timestamp). Equal to `menu.revision` in `GET

            /rest/menu`. LoyaltyPlant compares it to its cache to decide whether
            to

            re-import.
    CreateOrderRequest:
      type: object
      description: |
        New-order envelope. The order is wrapped under `order`.
      required:
        - order
      properties:
        order:
          $ref: '#/components/schemas/Order'
    Order:
      type: object
      description: >
        A Digital Ordering order. Used both as the `createOrder` request payload

        (inside `CreateOrderRequest.order`) and as the read model returned by

        `getOrders` (on reads, `state`, `queueNumber`, `id` and `items` are the

        meaningful fields). `null` fields are omitted from the wire.


        **Required-ness is direction-dependent and business-driven**, so it is

        *not* expressed as schema `required` here; treat the expected set for
        each

        direction as required input regardless. On a

        **`createOrder` request** LoyaltyPlant always sends: `type`,
        `deliveryAt`,

        `client` (with `name`/`email`/`phoneNumber`), `payment` (with

        `charge.tax`/`subtotal`/`total`), `additionalInfo`, `items`; and
        `address`

        (with `coordinates` + `addressString1`) when `type = DELIVERY`. On a

        **`getOrders` response** only `id`, `state` and the line items are

        meaningful.
      properties:
        id:
          type: string
          description: >
            Direction-dependent. On a **`createOrder` request** this is

            **LoyaltyPlant's** order id (string). On a **`getOrders` response**

            (and on the status webhook) this is the **`posId`** you returned
            from

            `createOrder` — echo it back unchanged, because LoyaltyPlant keys
            the

            poll match on it.
        client:
          $ref: '#/components/schemas/Client'
        deliveryAt:
          $ref: '#/components/schemas/DeliveryAt'
        type:
          $ref: '#/components/schemas/OrderingType'
        address:
          allOf:
            - $ref: '#/components/schemas/Address'
          description: |
            Delivery address. Required (with `coordinates` and `addressString1`)
            when `type = DELIVERY`; omitted otherwise.
        comment:
          type: string
          description: Free-text customer comment for the order.
        queueNumber:
          type: string
          description: |
            Customer-facing queue / pickup number. Set by the POS and echoed on
            reads and webhooks; not sent by LoyaltyPlant on create.
        payment:
          $ref: '#/components/schemas/PaymentInfo'
        state:
          allOf:
            - $ref: '#/components/schemas/OrderState'
          description: |
            Order state. Present on `getOrders` responses (and pushed via the
            webhook); not sent on `createOrder`.
        additionalInfo:
          $ref: '#/components/schemas/AdditionalInfo'
        presents:
          type: array
          description: |
            Reward (free) items granted by the loyalty program. **The POS must
            charge 0 for these** — they are not paid by the customer.
          items:
            $ref: '#/components/schemas/OrderItem'
        items:
          type: array
          description: Paid order line items. Each `id` maps to a menu item `id`.
          items:
            $ref: '#/components/schemas/OrderItem'
    Client:
      type: object
      description: The ordering customer. `name`, `email` and `phoneNumber` are required.
      required:
        - name
        - email
        - phoneNumber
      properties:
        name:
          type: string
          description: Customer first name.
        lastName:
          type: string
          description: Customer last name. Optional.
        email:
          type: string
          description: Customer email.
        phoneNumber:
          type: string
          description: Customer phone number (E.164 recommended).
    DeliveryAt:
      type: object
      description: When the order is wanted.
      required:
        - type
      properties:
        type:
          $ref: '#/components/schemas/DeliveryAtType'
        datetime:
          type: string
          example: '2026-06-13T19:30:00'
          description: |
            Local date-time in the outlet's timezone, written without a timezone
            designator (format `YYYY-MM-DDThh:mm:ss`, e.g. `2026-06-13T19:30:00`
            — no trailing `Z` or offset). Required when `type = CUSTOM`; ignored
            / omitted when `type = ASAP`.
    DeliveryAtType:
      type: string
      description: |
        Timing mode:
        - ASAP: prepare as soon as possible.
        - CUSTOM: prepare for the time given in `datetime`.
      enum:
        - ASAP
        - CUSTOM
    OrderingType:
      type: string
      description: >
        Fulfilment type of the order (the `OrderingType` enum on `Order.type`).

        Per-value meaning:

        - PICKUP: customer collects at the counter.

        - DELIVERY: courier delivery to `address` (address + coordinates
        required).

        - EAT_IN: dine-in.

        - CATERING: catering order. Code-backed but not shown in the public docs
        —
          documented; verify against POS model before relying on it.
        - DRIVE_THROUGH: drive-through pickup.


        > **Note:** For dine-in at a table, use `EAT_IN` with

        > `additionalInfo.tableNumber`.
      enum:
        - PICKUP
        - DELIVERY
        - EAT_IN
        - CATERING
        - DRIVE_THROUGH
    Address:
      type: object
      description: |
        Delivery address. Used when `type = DELIVERY`. `coordinates` and
        `addressString1` are required for delivery.
      properties:
        coordinates:
          $ref: '#/components/schemas/Coordinates'
        zipCode:
          type: string
        country:
          type: string
        city:
          type: string
        street:
          type: string
        house:
          type: string
        flat:
          type: string
        addressString1:
          type: string
          description: Primary single-line address string.
        addressString2:
          type: string
          description: >-
            Secondary single-line address string (e.g. unit, entrance).
            Optional.
    Coordinates:
      type: object
      description: Geographic coordinates of the delivery address.
      properties:
        latitude:
          type: number
        longitude:
          type: number
    PaymentInfo:
      type: object
      description: How the order is paid and the charge breakdown.
      required:
        - charge
      properties:
        method:
          $ref: '#/components/schemas/PaymentMethod'
        charge:
          $ref: '#/components/schemas/Charge'
    PaymentMethod:
      type: object
      description: Payment instrument.
      properties:
        type:
          $ref: '#/components/schemas/PaymentType'
        cardInfo:
          allOf:
            - $ref: '#/components/schemas/CardInfo'
          description: Card details. Present only for card payment types.
    PaymentType:
      type: string
      description: |
        Payment instrument type:
        - CASH: cash on collection/delivery.
        - BANK_CARD_IN_APP: card charged inside the LoyaltyPlant app.
        - BANK_CARD_IN_STORE: card charged at the POS.
      enum:
        - CASH
        - BANK_CARD_IN_APP
        - BANK_CARD_IN_STORE
    CardInfo:
      type: object
      description: Masked card details (for card payment types).
      properties:
        cardHolderName:
          type: string
        pan:
          type: string
          description: Masked card number (PAN). Never a full clear PAN.
    Charge:
      type: object
      description: |
        Monetary breakdown of the order. On `createOrder`, `tax`, `subtotal` and
        `total` are required. All values are decimals.
      required:
        - tax
        - subtotal
        - total
      properties:
        tips:
          type: number
          minimum: 0
          description: Tip amount. Optional.
        deliveryFee:
          type: number
          minimum: 0
          description: Delivery fee. Optional (typically only for `DELIVERY`).
        discount:
          type: number
          minimum: 0
          description: Total discount applied. Optional.
        subtotal:
          type: number
          minimum: 0
          description: Sum of line items before tax/fees.
        tax:
          type: number
          minimum: 0
          description: Total tax.
        total:
          type: number
          minimum: 0
          description: Grand total the customer pays.
    OrderItem:
      type: object
      description: An order line item (paid `items[]` or free `presents[]`).
      required:
        - id
      properties:
        id:
          type: string
          description: Menu item id (maps to a menu item's `id`). Required.
        price:
          type: number
          minimum: 0
          description: Unit price. For `presents[]` (rewards) the POS must use 0.
        count:
          type: integer
          minimum: 1
          default: 1
          description: Quantity. Defaults to 1.
        modifiers:
          type: array
          items:
            $ref: '#/components/schemas/OrderItemModifier'
    OrderItemModifier:
      type: object
      description: A modifier applied to an order line item.
      required:
        - id
      properties:
        id:
          type: string
          description: Modifier id (maps to a menu modifier's `id`). Required.
        price:
          type: number
          minimum: 0
          description: Unit price of the modifier.
        count:
          type: integer
          minimum: 1
          default: 1
          description: Quantity. Defaults to 1.
    AdditionalInfo:
      type: object
      description: Order metadata.
      properties:
        tableNumber:
          type: integer
          description: Table number for dine-in orders. Optional.
        loyaltyPlantOrderId:
          type: integer
          description: LoyaltyPlant's internal numeric order id. Optional.
        customFields:
          type: object
          additionalProperties:
            type: string
          description: Free-form string key/value pairs agreed during onboarding.
    OrderState:
      type: object
      description: |
        Lifecycle state of an order.
      required:
        - status
      properties:
        status:
          $ref: '#/components/schemas/OrderStatus'
        reason:
          type: string
          description: |
            Reason for the state. **Required when `status` is `CANCELED` or
            `FAILED`.** Internal information, not shown to the customer.
        posErrorType:
          allOf:
            - $ref: '#/components/schemas/PosErrorType'
          description: >
            Optional **structured** failure reason, meaningful only when
            `status`

            is `CANCELED` or `FAILED`. Complements the free-text `reason`:

            LoyaltyPlant records it against the order and surfaces it in CRM /

            reports. Omit it when the order did not fail at the POS. See the

            `PosErrorType` enum and the **POS error reasons** section of the

            Message & Error Codes guide.
    OrderStatus:
      type: string
      description: >
        The nine order lifecycle states. On the webhook this string is parsed by

        an exact enum match — an unknown value (typo, wrong case, unrecognized

        status) is **not** rejected: it is logged and skipped for that order,
        the

        webhook still returns HTTP 200 `accepted: true`, and that order's state
        is

        not advanced (a silent per-order no-op, detectable only via the poll
        path).

        Per-value meaning:

        - PROCESSING: order received at the POS.

        - CONFIRMED_WAITING_FOR_COOKING: accepted and sent to the kitchen.

        - COOKING: being prepared.

        - AWAITING_PICKUP: ready, awaiting customer pickup.

        - AWAITING_DELIVERY: ready, awaiting a courier.

        - BEING_DELIVERED: handed to the courier / out for delivery.

        - PROCESSED: successfully closed at the POS (terminal success).

        - CANCELED: canceled at the POS (terminal; set `reason`).

        - FAILED: error creating/processing the order (terminal; set `reason`).
      enum:
        - PROCESSING
        - CONFIRMED_WAITING_FOR_COOKING
        - COOKING
        - AWAITING_PICKUP
        - AWAITING_DELIVERY
        - BEING_DELIVERED
        - PROCESSED
        - CANCELED
        - FAILED
    PosErrorType:
      type: string
      description: >
        Structured, **POS-reportable** reason an order was rejected at injection

        (`createOrder`) or cancelled/failed later (`CANCELED` / `FAILED`). Wire

        string literals (not a numeric catalog). This is a **curated
        allow-list**

        of reasons a POS can meaningfully self-report; LoyaltyPlant infers the

        transport/availability/lifecycle reasons itself and does not accept them

        here.


        The list is curated but **not frozen** — if your POS can report a

        meaningful reason not covered below, request its addition. Until it is

        added, send `UNKNOWN` together with a descriptive free-text `reason`

        rather than an unlisted value (an unrecognized value is treated as

        `UNKNOWN`).


        Per-value meaning:

        - ITEM_UNAVAILABLE: an ordered item is unknown to the POS
        (wrong/unmapped product id) or out of stock.

        - ITEM_MODIFIER_INVALID: a modifier is not valid for its item.

        - ITEM_MODIFIER_MISSING: a required modifier group is not satisfied
        (below minimum / missing mandatory).

        - ITEM_MODIFIER_EXCESS: more modifiers sent for a group than it allows.

        - ITEM_SIZE_INVALID: an item's size/variant is invalid or unknown.

        - ITEM_PRICE_MISMATCH: an item's price disagrees between LoyaltyPlant
        and the POS.

        - DISCOUNT_INVALID: a discount/coupon could not be applied by the POS.

        - DISCOUNT_MISMATCH: the POS-computed discount amount disagrees with
        LoyaltyPlant's.

        - DELIVERY_ADDRESS_INVALID: delivery address is malformed / invalid /
        too long.

        - DELIVERY_ADDRESS_UNAVAILABLE: address is not within any configured POS
        delivery zone.

        - DELIVERY_AREA_UNAVAILABLE: no delivery zone is configured for the
        establishment.

        - TIME_UNAVAILABLE: requested pickup/delivery time is invalid or cannot
        be fulfilled.

        - BAD_REQUEST: order rejected as malformed/invalid (generic) with no
        more specific cause.

        - UNKNOWN: a POS-stage failure the POS cannot classify.
      enum:
        - ITEM_UNAVAILABLE
        - ITEM_MODIFIER_INVALID
        - ITEM_MODIFIER_MISSING
        - ITEM_MODIFIER_EXCESS
        - ITEM_SIZE_INVALID
        - ITEM_PRICE_MISMATCH
        - DISCOUNT_INVALID
        - DISCOUNT_MISMATCH
        - DELIVERY_ADDRESS_INVALID
        - DELIVERY_ADDRESS_UNAVAILABLE
        - DELIVERY_AREA_UNAVAILABLE
        - TIME_UNAVAILABLE
        - BAD_REQUEST
        - UNKNOWN
    CreateOrderResponse:
      type: object
      description: >
        Result of `createOrder`. Either an **acceptance** — `posId` (+

        `queueNumber`) — or a **structured rejection** — `state` with

        `status: FAILED`, a `reason`, and an optional `posErrorType`. Exactly
        one

        of the two shapes is sent; a rejection carries no `posId`. In **both**

        cases the response MUST carry the `AuthorizationToken` header

        (`SHA256-hex(apiKeyResponse)`) — a rejection without it is discarded as
        a

        transport failure, not read as a reason.
      properties:
        posId:
          type: string
          description: |
            The POS's own id for the created order. LoyaltyPlant stores it and
            uses it on `getOrders` and the webhook. **Present on acceptance;
            omitted on a rejection.**
        queueNumber:
          type: string
          description: |
            Customer-facing queue / pickup number assigned by the POS. May be a
            numeric value; declared as string here (numeric is accepted).
        state:
          allOf:
            - $ref: '#/components/schemas/OrderState'
          description: >
            **Rejection only.** When the POS cannot accept the order, return

            `state` with `status: FAILED`, a free-text `reason`, and
            (optionally)

            a structured `posErrorType`. LoyaltyPlant then fails the order and

            records the reason against it. Omit on acceptance.
    GetOrdersRequest:
      type: object
      description: Order-state lookup by POS ids.
      required:
        - orderPosIds
      properties:
        orderPosIds:
          type: array
          description: >-
            POS order ids (the `posId` values you returned from `createOrder`)
            to look up.
          items:
            type: string
    GetOrdersResponse:
      type: object
      description: |
        Current state of the requested orders.
      required:
        - orders
      properties:
        orders:
          type: array
          description: >-
            One full `Order` per requested id; `state` carries the current
            status.
          items:
            $ref: '#/components/schemas/Order'
        requestId:
          type: string
          format: uuid
          description: Server-generated correlation id (UUID).
    OrdersWebhookRequest:
      type: object
      description: >
        Body of the order-status webhook you POST to LoyaltyPlant. LoyaltyPlant

        **acts only on** `orders[].id`, `orders[].queueNumber`, and

        `orders[].state.status`. Additional order fields (address, payment,
        items,

        presents, deliveryAt, comment) are **accepted and ignored** — send the

        minimal shape below.
      required:
        - orders
      properties:
        orders:
          type: array
          description: One entry per order whose status changed.
          items:
            $ref: '#/components/schemas/WebhookOrder'
    WebhookOrder:
      type: object
      description: >
        A single order's status update. Only `id`, `queueNumber` and
        `state.status`

        are consumed by LoyaltyPlant.
      required:
        - id
        - queueNumber
        - state
      properties:
        id:
          type: string
          description: The POS order id (the `posId` you returned from `createOrder`).
        queueNumber:
          type: string
          description: Customer-facing queue / pickup number.
        state:
          $ref: '#/components/schemas/WebhookOrderState'
    WebhookOrderState:
      type: object
      description: The new state.
      required:
        - status
      properties:
        status:
          $ref: '#/components/schemas/OrderStatus'
        reason:
          type: string
          description: |
            Free-text reason for the state. Recommended when `status` is
            `CANCELED` or `FAILED`. Internal information, not shown to the
            customer.
        posErrorType:
          allOf:
            - $ref: '#/components/schemas/PosErrorType'
          description: >
            Optional **structured** failure reason on a `CANCELED` / `FAILED`

            push. LoyaltyPlant records it against the order. Omit otherwise.

            (Historically the webhook acted only on `id` / `queueNumber` /

            `status`; `posErrorType`, when present on a terminal-failure push,
            is

            now also consumed and stored.)
    WebhookResponse:
      type: object
      description: |
        LoyaltyPlant's reply to your status webhook. On success: HTTP 200 with
        `accepted: true` (no `errors`). On failure: HTTP 400 or 500 with
        `accepted: false` and one `errors` entry.
      required:
        - accepted
      properties:
        accepted:
          type: boolean
          description: '`true` if LoyaltyPlant accepted and processed the update.'
        errors:
          type: array
          description: Present only when `accepted` is `false`.
          items:
            $ref: '#/components/schemas/WebhookErrorDetail'
    WebhookErrorDetail:
      type: object
      description: A single webhook processing error.
      required:
        - code
        - description
      properties:
        code:
          $ref: '#/components/schemas/WebhookErrorCode'
        description:
          type: string
          description: Free-text explanation of the error.
    WebhookErrorCode:
      type: string
      description: >
        Error codes LoyaltyPlant returns on a failed status webhook. These are

        wire string literals (not a Java enum). Per-value meaning and HTTP
        status:

        - READ_ERROR: the request body could not be read (HTTP 400).

        - INVALID_JSON: the body was malformed or incomplete JSON (HTTP 400).

        - VALIDATION_ERROR: header/auth validation failed — missing
          `AuthorizationToken` or `SalesOutletId`, non-numeric `SalesOutletId`,
          unknown outlet, or a wrong `AuthorizationToken` (HTTP 400).
        - INTERNAL_ERROR: a genuinely unexpected server-side failure while
          processing the request (HTTP 500). An unknown `state.status` does **not**
          map here — it is skipped per-order and the webhook still returns 200.
      enum:
        - READ_ERROR
        - INVALID_JSON
        - VALIDATION_ERROR
        - INTERNAL_ERROR
  examples:
    HealthcheckAllUpExample:
      summary: All services up
      value:
        POS: UP
        DB: UP
        KITCHEN: UP
    HealthcheckDownExample:
      summary: POS down — LoyaltyPlant will not order
      value:
        POS: DOWN
        DB: UP
    MenuRevisionExample:
      summary: Current revision
      value:
        revision: 1718280000
    MenuResponseExample:
      summary: Minimal v2 menu — one category, one item, one modifier group
      value:
        requestId: f0c1a2b3-4d5e-6f70-8a9b-0c1d2e3f4a5b
        menu:
          revision: 1718280000
          categories:
            - refId: cat-burgers
              id: '100'
              code: BURGERS
              title:
                en: Burgers
                es: Hamburguesas
              root: true
              rootSortOrder: 1
          modifiers:
            - refId: mod-bacon
              id: '9001'
              code: BACON
              title:
                en: Extra bacon
              pricesByOrderingType:
                PICKUP:
                  value: 1.5
                  taxes: []
          modifierGroups:
            - refId: grp-extras
              id: '500'
              code: EXTRAS
              title:
                en: Extras
              min: 0
              max: 3
              free: 0
              modifiers:
                - modifier: mod-bacon
                  selected: false
                  sortOrder: 1
          items:
            - refId: item-cheeseburger
              id: '1'
              code: CB01
              title:
                en: Cheeseburger
                es: Hamburguesa con queso
              description:
                en: Beef patty with cheese
              sortOrder: 1
              pricesByOrderingType:
                PICKUP:
                  value: 6.9
                  taxes:
                    - 10
                EAT_IN:
                  value: 7.5
                  taxes:
                    - 10
              categories:
                - category: cat-burgers
                  sortOrder: 1
              modifierGroups:
                - refId: emb-extras-cb01
                  basedOn: grp-extras
                  id: '500'
                  code: EXTRAS
                  title:
                    en: Extras
                  min: 0
                  max: 2
                  free: 1
                  modifiers:
                    - modifier: mod-bacon
                      selected: false
                      sortOrder: 1
    CreateOrderPickupExample:
      summary: Pickup order (cash)
      value:
        order:
          id: LP-ORD-1001
          type: PICKUP
          client:
            name: Maria
            lastName: Lopez
            email: maria@example.com
            phoneNumber: '+34911223344'
          deliveryAt:
            type: ASAP
          comment: No onions please
          payment:
            method:
              type: CASH
            charge:
              subtotal: 6.9
              tax: 0.69
              total: 7.59
          additionalInfo:
            loyaltyPlantOrderId: 5550001
            customFields:
              source: app
          items:
            - id: '1'
              price: 6.9
              count: 1
              modifiers:
                - id: '9001'
                  price: 0
                  count: 1
    CreateOrderDeliveryExample:
      summary: Delivery order (card in app, with reward present)
      value:
        order:
          id: LP-ORD-1002
          type: DELIVERY
          client:
            name: John
            lastName: Smith
            email: john@example.com
            phoneNumber: '+14155550123'
          deliveryAt:
            type: CUSTOM
            datetime: '2026-06-13T19:30:00'
          address:
            coordinates:
              latitude: 40.41678
              longitude: -3.70379
            country: ES
            city: Madrid
            street: Gran Via
            house: '28'
            flat: 4B
            addressString1: Gran Via 28, 4B
            zipCode: '28013'
          payment:
            method:
              type: BANK_CARD_IN_APP
              cardInfo:
                cardHolderName: JOHN SMITH
                pan: 411111******1111
            charge:
              subtotal: 13.8
              deliveryFee: 2.5
              tax: 1.38
              tips: 2
              total: 19.68
          additionalInfo:
            loyaltyPlantOrderId: 5550002
          presents:
            - id: '42'
              price: 0
              count: 1
          items:
            - id: '1'
              price: 6.9
              count: 2
    CreateOrderEatInExample:
      summary: Eat-in order at a table (card in store)
      value:
        order:
          id: LP-ORD-1003
          type: EAT_IN
          client:
            name: Aisha
            email: aisha@example.com
            phoneNumber: '+971501234567'
          deliveryAt:
            type: ASAP
          payment:
            method:
              type: BANK_CARD_IN_STORE
            charge:
              subtotal: 7.5
              tax: 0.75
              total: 8.25
          additionalInfo:
            tableNumber: 12
            loyaltyPlantOrderId: 5550003
          items:
            - id: '1'
              price: 7.5
              count: 1
    CreateOrderResponseExample:
      summary: Order accepted
      value:
        posId: POS-9F3A21
        queueNumber: A17
    CreateOrderRejectedExample:
      summary: Order rejected at injection (structured reason)
      value:
        state:
          status: FAILED
          reason: Item 42 is out of stock
          posErrorType: ITEM_UNAVAILABLE
    GetOrdersRequestExample:
      summary: Look up two orders by POS id
      value:
        orderPosIds:
          - POS-9F3A21
          - POS-9F3A22
    GetOrdersResponseExample:
      summary: Two orders, one cooking, one ready for pickup
      value:
        requestId: 7e9c2a14-3b6d-4f80-91ac-5d2e6f7a8b90
        orders:
          - id: POS-9F3A21
            queueNumber: A17
            type: PICKUP
            state:
              status: COOKING
            items:
              - id: '1'
                count: 1
          - id: POS-9F3A22
            queueNumber: A18
            type: PICKUP
            state:
              status: AWAITING_PICKUP
            items:
              - id: '1'
                count: 2
    GetOrdersCanceledExample:
      summary: A canceled order (reason required, structured type optional)
      value:
        requestId: 1f2e3d4c-5b6a-4790-8123-456789abcdef
        orders:
          - id: POS-9F3A23
            queueNumber: A19
            type: DELIVERY
            state:
              status: CANCELED
              reason: Item out of stock; customer notified
              posErrorType: ITEM_UNAVAILABLE
    WebhookRequestExample:
      summary: Status push — order is now cooking
      value:
        orders:
          - id: POS-9F3A21
            queueNumber: A17
            state:
              status: COOKING
    WebhookRequestCanceledExample:
      summary: Status push — order canceled (with a structured reason)
      value:
        orders:
          - id: POS-9F3A23
            queueNumber: A19
            state:
              status: CANCELED
              reason: Kitchen 86'd the item
              posErrorType: ITEM_UNAVAILABLE
    WebhookAcceptedExample:
      summary: Accepted
      value:
        accepted: true
    WebhookValidationErrorExample:
      summary: Rejected — auth/validation failure
      value:
        accepted: false
        errors:
          - code: VALIDATION_ERROR
            description: Invalid AuthorizationToken for salesOutletId=4198
  callbacks:
    OrderStatusWebhook:
      '{$request.header.X-LP-Webhook-Url}/digitalordering/pos/v2/integrated':
        post:
          summary: Push order status to LoyaltyPlant (vendor → LoyaltyPlant)
          operationId: orderStatusWebhook
          security:
            - AuthorizationToken: []
          description: >
            After `createOrder`, POST this to LoyaltyPlant every time an order's

            status changes. The deployed path is

            `{lpBaseUrl}/digitalordering/pos/v2/integrated` (the URL expression

            shown is a placeholder — LoyaltyPlant gives you the exact base URL

            during onboarding, e.g.

            `https://release.loyaltyplant.com/digitalordering/pos/v2/integrated`).

            Authenticate it with the `AuthorizationToken` security scheme plus
            the

            `SalesOutletId` header (both required).


            LoyaltyPlant consumes only `orders[].id`, `orders[].queueNumber`,
            and

            `orders[].state.status`; any extra fields are accepted and ignored.


            Validation order on LoyaltyPlant's side, each producing

            `VALIDATION_ERROR` (HTTP 400): missing `AuthorizationToken` →
            missing

            `SalesOutletId` → non-numeric `SalesOutletId` → no group/integrated/

            settings for the outlet → token mismatch

            (`Invalid AuthorizationToken for salesOutletId=X`). A `state.status`

            that is not one of the nine `OrderStatus` values does **not** fail

            the webhook: it is caught per-order, logged, and skipped (that

            order's state is not advanced), while the webhook still returns

            HTTP 200 `accepted: true`. Detect a skipped order via the
            `getOrders`

            poll path; do not retry it with backoff. `INTERNAL_ERROR` (HTTP 500)

            is reserved for a genuinely uncaught processing failure.
          parameters:
            - $ref: '#/components/parameters/SalesOutletIdHeader'
            - $ref: '#/components/parameters/AuthorizationTokenHeader'
          requestBody:
            required: true
            content:
              application/json:
                schema:
                  $ref: '#/components/schemas/OrdersWebhookRequest'
                examples:
                  cooking:
                    $ref: '#/components/examples/WebhookRequestExample'
                  canceled:
                    $ref: '#/components/examples/WebhookRequestCanceledExample'
          responses:
            '200':
              description: Update accepted.
              content:
                application/json:
                  schema:
                    $ref: '#/components/schemas/WebhookResponse'
                  examples:
                    accepted:
                      $ref: '#/components/examples/WebhookAcceptedExample'
            '400':
              description: |
                Rejected — `READ_ERROR`, `INVALID_JSON`, or `VALIDATION_ERROR`.
              content:
                application/json:
                  schema:
                    $ref: '#/components/schemas/WebhookResponse'
                  examples:
                    validationError:
                      $ref: '#/components/examples/WebhookValidationErrorExample'
            '500':
              description: >
                `INTERNAL_ERROR` — a genuinely unexpected server-side processing

                failure. An unknown `state.status` does **not** cause this — it
                is

                skipped per-order and the webhook still returns 200 `accepted:
                true`.
              content:
                application/json:
                  schema:
                    $ref: '#/components/schemas/WebhookResponse'
