openapi: 3.0.3
info:
  title: Realpad Landing Page & Tablet App API
  version: "10"
  description: |
    WS-style API endpoints for integrating with Realpad CRM. Covers project landing page integrations and the tablet app endpoints used by Project Phoenix.

    ## Terminology
    In this documentation, "developer" refers to a **real estate developer** — the primary customer of Realpad. In our multi-tenant SaaS architecture, each real estate developer is a **tenant**. The terms "developer" and "tenant" are used interchangeably. A developer/tenant ID identifies the tenant and scopes all data access.

    ## Authentication
    All endpoints require `login` and `password` sent as form-encoded POST parameters. There is no Bearer token or API key mechanism. Credentials are issued per project and per use case (e.g., `project-name-pricelist` / `project-name-leads`).

    ## Common Behavior
    - All endpoints accept **HTTP POST** only.
    - Parameters are form-encoded (`application/x-www-form-urlencoded`).
    - Error responses are **plain text** with appropriate HTTP status codes.
    - Unexpected parameters cause HTTP 400 (unless the endpoint explicitly allows extras).
    - Rate-limited endpoints return HTTP 429 with a `Retry-After` header.

    ## Date Format
    All dates in XML responses use ISO 8601 format with timezone offset, e.g. `2025-06-15T14:30:00.000+02:00`. Exception: `modification-time` in `/list-basket` uses UTC with `Z` suffix (e.g., `2026-02-18T05:46:15Z`).

    ## XML Response Notes
    Some successful XML responses include a trailing `<!-- Time taken: X ms. -->` HTML comment with server-side processing time. This is not part of the response schema and can be safely ignored by parsers. Error responses (HTTP 4xx/5xx) never include this comment.
  contact:
    name: Realpad Development Team
    url: https://dev.realpadsoftware.com
    email: support@realpadsoftware.com
servers:
  - url: https://test.realpad.eu/ws/v10
    description: Staging
  - url: https://cms.realpad.eu/ws/v10
    description: Production
  - url: https://crm-2.realpad.eu/ws/v10
    description: Production 2
tags:
  - name: Project Landing
    description: Endpoints for third-party project landing page integrations — pricelist data fetching and lead submission.
  - name: Tablet App
    description: Endpoints used by the Phoenix tablet app for sales presentations, customer management, favorites, and pre-reservations.
  - name: Tablet App (Deprecated)
    description: Legacy tablet app endpoints no longer used by Project Phoenix. Retained for backward compatibility with the legacy iOS app until it is removed from the App Store.
  - name: Internal
    description: Internal endpoints used by automated API tests (Runscope) for CRM exercise and internal tooling. These will be moved to a dedicated internal API in a future update.

# ===========================================================================
# PATHS
# ===========================================================================
paths:

  # =========================================================================
  # PROJECT LANDING
  # =========================================================================

  /get-projects-info:
    post:
      tags: [Project Landing]
      summary: Get projects and screens overview
      operationId: getProjectsInfo
      description: |
        Returns a JSON object containing the necessary information for integrators to work with the projects: the developer/tenant ID, a list of projects (ID and name), and a list of available screens (ID and name).

        Use this as a first call to discover which `projectid` and `screenid` values to pass to `/get-project` or `/get-all-projects`.

        **Permission required:** `READ_CMS_DATA`
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/AuthFields'
            example:
              login: project-acme-pricelist
              password: s3cret
      responses:
        '200':
          description: Projects and screens for the authenticated developer.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProjectsInfoResponse'
              example:
                developer-id: 3230279
                projects:
                  - project-id: 3356887
                    project-name: C PROJECT Riverside
                  - project-id: 3602022
                    project-name: Dolorsit Residence
                  - project-id: 3908900
                    project-name: SKY PARK TOWER
                screens:
                  - screen-id: 2
                    screen-name: iPad2
                  - screen-id: 350613
                    screen-name: Web client
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /get-project:
    post:
      tags: [Project Landing]
      summary: Get full project details
      operationId: getProject
      description: |
        Returns XML with full information on the requested project, including buildings, floors, units, pricing, attributes, galleries, and POIs.

        Hidden units are excluded unless `includehidden` is set to a truthy value.

        **Unit status codes (`flat_status` attribute):**
        - `0` — Free (available for sale)
        - `1` — Pre-reserved
        - `2` — Reserved
        - `3` — Sold
        - `4` — Not for sale
        - `5` — Delayed

        **Resource UIDs:** Various elements (galleries, plans, logos) reference resources by UID strings. These UIDs are immutable — once a resource is created, its UID never changes. Clients can construct resource URLs as `/resource/{uid}` and safely cache responses indefinitely based on the UID. To get a thumbnail: `/resource/{uid}/thumbnail`. To force a download: `/resource/{uid}/download`.

        **Errors:** if one of the IDs is invalid, HTTP 400 is returned.

        **Permission required:** `READ_CMS_DATA`
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/AuthFields'
                - type: object
                  required: [developerid, projectid, screenid]
                  properties:
                    developerid:
                      type: integer
                      description: Developer ID (from `/get-projects-info`).
                    projectid:
                      type: integer
                      description: Project ID (from `/get-projects-info`).
                    screenid:
                      type: integer
                      description: Screen configuration ID (from `/get-projects-info`).
                    includehidden:
                      type: boolean
                      default: false
                      description: If `true`, hidden units are included in the response.
            example:
              login: project-acme-pricelist
              password: s3cret
              developerid: 3230279
              projectid: 3356887
              screenid: 2
              includehidden: false
      responses:
        '200':
          description: Full project XML export.
          content:
            application/xml:
              schema:
                $ref: '#/components/schemas/XmlExportV08'
              example: |
                <export creation-time="2026-02-19T07:14:17+0100" developer-id="3230279">
                  <project id="3356887" name="C PROJECT Riverside" gps="6789099"
                           hypo-assistant="false" public="false" hidden="false"
                           demo="false" available-second-market-unit-count="0">
                    <locale iso="cs_CZ" name="Čeština"/>
                    <locale iso="en_US" name="English"/>
                    <building id="3356894" name="Gent" arrow-direction="0">
                      <floor id="3356895" floorNo="1" arrow-direction="0">
                        <flat id="3356898">
                          <flat-attribute key="flat_internal_id" value="G001"/>
                          <flat-attribute key="flat_area" value="80"/>
                          <flat-attribute key="flat_price" value="142886.48"/>
                          <flat-attribute key="flat_price_without_vat" value="118088"/>
                          <flat-attribute key="flat_status" value="3"/>
                          <flat-attribute key="flat_type" value="17"/>
                        </flat>
                      </floor>
                    </building>
                    <gallery id="3356893" creation-time="2023-02-07T01:00:00+0100"
                             modification-time="2023-02-07T01:00:00+0100" type="3">
                      <name locale="cs_CZ" text="Fotografie okolí"/>
                      <name locale="en_US" text="Surroundings"/>
                    </gallery>
                    <flat-attribute-type key="flat_price" type="5"
                                         required="true" filterable="true"
                                         show-in-pricelist="true" show-in-detail="true">
                      <name locale="cs_CZ" text="Cena bez DPH"/>
                      <name locale="en_US" text="Price w/o VAT"/>
                    </flat-attribute-type>
                    <project-attribute key="show_prices_vat" value="true"/>
                    <second-market/>
                  </project>
                </export>
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /create-lead:
    post:
      tags: [Project Landing]
      summary: Create a new lead (customer + inquiry)
      operationId: createLead
      description: |
        Creates or updates a lead (Customer + Inquiry) based on the provided contact information, and optionally links it to a unit, project, and campaign.

        **De-duplication:** The endpoint performs duplicate detection by matching on email and/or phone number. If a matching Customer already exists, the existing record is updated rather than creating a duplicate. The Inquiry attached to the Customer is also created or updated accordingly. The `previously_known` field in the JSON response indicates whether the Customer already existed.

        **Important rules:**
        - At least one of `email` or `phone` **must** be provided.
        - `language`, if set, must be a valid ISO 639-1 code (e.g., `en`, `cs`, `sk`).
        - **Unit identification:** Use `unitid` (database ID) to link the lead to a unit — this is the recommended approach. The `internalid` parameter (unit internal ID) is also accepted but is a legacy alternative. If both are provided, `internalid` takes precedence. Note: for backward compatibility, if `internalid` contains a value that looks like a database ID, it will be interpreted as one.
        - `preferencerooms` is a pipe-separated (`|`) list of room/disposition preferences.
        - Extra (undeclared) parameters are allowed — see **Custom attributes** below.

        **Custom attributes:**
        Extra parameters beyond the ones documented here are treated as custom attribute key-value pairs.
        The attribute key must match a configured attribute on the developer account. Supported types:
        - **Boolean**: standard boolean string representations (e.g., `true`, `false`)
        - **String**: any string value
        - **Integer**: valid integer
        - **Decimal**: valid decimal number (BigDecimal)
        - **Date**: valid date string
        - **Enum (single-select)**: integer ID of the option
        - **Option (multi-select)**: comma-separated integer IDs (e.g., `3,7,12`)
        - File and multi-file attributes are **not supported**.
        - If a custom attribute value cannot be parsed, it is silently skipped (logged server-side but does not fail the request).

        **Consent tracking:**
        - `consentsource`: free-form text describing the source of the consents. The original intention is to contain the IP address from which the lead submitted the contact form.
        - `consents` and `updateconsents` are **mutually exclusive** — providing both results in an error.
          - `consents`: **replaces** all existing consents. If the lead already existed and had consent records, they are cleared first before setting the new ones.
          - `updateconsents`: **merges** with existing consents — any consents not mentioned in this parameter are kept unchanged.
        - The exact consent IDs and their purpose are tenant-specific and will be provided by the Realpad support team.

        **Response format** depends on the `Accept` request header:
        - Default (no header or `application/xml`): returns the customer ID as plain text.
        - `Accept: application/json`: returns a JSON object with customer and salesman details.

        **Errors:** if one of the provided IDs is invalid, HTTP 400 is returned.

        **Permission required:** `WRITE_INQUIRY_DATA`
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/AuthFields'
                - $ref: '#/components/schemas/CreateLeadParams'
            examples:
              minimal:
                summary: Minimal lead with email
                value:
                  login: project-acme-leads
                  password: s3cret
                  name: Jan
                  surname: Novak
                  email: jan.novak@example.com
              full:
                summary: Full lead with unit and consents
                value:
                  login: project-acme-leads
                  password: s3cret
                  name: Jan
                  surname: Novak
                  email: jan.novak@example.com
                  phone: "+420777123456"
                  note: Interested in 3+kk on higher floor
                  campaign: google-ads-2025
                  language: cs
                  unitid: "501"
                  project: 101
                  consents: "5,12,18"
                  preferencerooms: "3+kk|4+kk"
                  preferencepricemax: 8000000
      responses:
        '200':
          description: Lead processed successfully. If the customer already existed (matched by email/phone), the existing customer ID is returned. Use the `Accept` header to control response format.
          content:
            text/plain:
              schema:
                type: integer
                description: Customer ID.
              example: 12345
            application/json:
              schema:
                $ref: '#/components/schemas/LeadResult'
              example:
                customer_id: 12345
                customer_name: Novak Jan
                previously_known: false
                salesman_id: 99
                salesman_name: Petr Svoboda
                salesman_email: petr.svoboda@acme.cz
                salesman_phone: null
        '201':
          description: New lead created. Only returned for specific developer accounts (`AFI_ROMANIA`, `GARTAL`) to distinguish new leads from existing ones.
          content:
            text/plain:
              schema:
                type: integer
              example: 12346
            application/json:
              schema:
                $ref: '#/components/schemas/LeadResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /set-customer-attribute:
    post:
      tags: [Project Landing]
      summary: Set a customer attribute value
      operationId: setCustomerAttribute
      description: |
        Sets the attribute with the specified `key` of the specified customer to the specified `value`.

        **Restrictions:**
        - The attribute must **not** be of type *file* or *multi-file* — those types are not supported and return HTTP 400.
        - The `value` must be coercible to the attribute's declared type. If not, HTTP 400 is returned.
        - For **boolean** attributes specifically, `value` must be exactly `"true"` or `"false"`.

        **Permission required:** `WRITE_CUSTOMER_DATA`
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/AuthFields'
                - type: object
                  required: [customerid, key, value]
                  properties:
                    customerid:
                      type: integer
                      description: Customer ID.
                    key:
                      type: string
                      description: Attribute key identifier.
                    value:
                      type: string
                      description: Value to set. Must be coercible to the attribute's type. For boolean attributes, use `"true"` or `"false"`.
            example:
              login: project-acme-leads
              password: s3cret
              customerid: 12345
              key: utm_source
              value: facebook
      responses:
        '200':
          description: Attribute set successfully. Response body is empty.
        '400':
          description: |
            Bad request. Possible causes:
            - `"Invalid attribute key."` — the key does not exist.
            - `"Value could not be coerced into the attribute type."` — type mismatch.
            - `"Setting value to attribute of type file/multi-file is not supported."` — unsupported type.
            - `"ID does not refer to a valid object."` — invalid `customerid`.
          content:
            text/plain:
              schema:
                type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/ServerError'

  /list-leads:
    post:
      tags: [Project Landing]
      summary: List recent leads created by the authenticated user
      operationId: listLeads
      description: |
        Returns a JSON array of leads (Customer + Inquiry pairs) created by the authenticated user within the last 7 days. No additional parameters are required beyond authentication.

        **Permission required:** `READ_INQUIRY_DATA`
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/AuthFields'
            example:
              login: project-acme-leads
              password: s3cret
      responses:
        '200':
          description: JSON array of recent leads.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/RecentLead'
              example:
                - inquiry_id: 9001
                  customer_id: 12345
                  customer_name: Jan Novak
                  timestamp: "2026-03-08T14:30:00.000+01:00"
                  projects:
                    - C PROJECT Riverside
                    - Dolorsit Residence
                  campaign: google-ads-2025
                - inquiry_id: 9002
                  customer_id: 12346
                  customer_name: Petr Kral
                  timestamp: "2026-03-09T10:15:00.000+01:00"
                  projects:
                    - SKY PARK TOWER
                  campaign: null
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /update-units:
    post:
      tags: [Project Landing]
      summary: Batch update unit prices
      operationId: updateUnits
      description: |
        Updates prices for one or more units in a single atomic operation. The request body is a JSON string sent as a form parameter `data`.

        **Validation rules (per unit):**
        - Unit must exist.
        - Unit must not be attached to a Deal.
        - Unit must not have an active Pre-reservation.
        - Unit must not be Reserved or Sold.
        - Price change must not exceed 10% for either price (with or without VAT).
        - Prices must be non-negative.

        **Atomicity:** The entire batch is rejected if any unit fails validation — no partial updates occur.

        **Permission required:** `WRITE_CMS_DATA`
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/AuthFields'
                - type: object
                  required: [data]
                  properties:
                    data:
                      type: string
                      description: JSON string containing the unit update request. See `UnitUpdateRequest` schema for the expected structure.
            example:
              login: project-acme-pricelist
              password: s3cret
              data: '{"units":[{"id":3356898,"priceWithoutVat":5000000,"priceWithVat":6050000}]}'
      responses:
        '200':
          description: All units updated successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UnitUpdateResult'
              example:
                success: true
                updatedCount: 1
                units:
                  - id: 3356898
                    priceWithoutVat: 5000000
                    priceWithVat: 6050000
        '400':
          description: |
            Validation failed or invalid JSON format. The response body contains details about which unit(s) failed validation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UnitUpdateResult'
              example:
                success: false
                error: "Unit 3356898: price change exceeds 10%."
                updatedCount: 0
                units: []
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /set-unit-pdf:
    post:
      tags: [Project Landing]
      summary: Upload a PDF document for a unit
      operationId: setUnitPdf
      description: |
        Uploads and attaches a PDF file to the specified unit. If a PDF was previously attached, it is replaced.

        **Permission required:** `WRITE_CMS_DATA`
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              allOf:
                - $ref: '#/components/schemas/AuthFields'
                - type: object
                  required: [unitid, file]
                  properties:
                    unitid:
                      type: integer
                      description: Unit ID.
                    file:
                      type: string
                      format: binary
                      description: PDF file to upload.
            example:
              login: project-acme-pricelist
              password: s3cret
              unitid: 3356898
      responses:
        '204':
          description: PDF uploaded successfully. No response body.
        '400':
          description: Invalid unit ID or unit not found.
          content:
            text/plain:
              schema:
                type: string
              example: "ID does not refer to a valid object."
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /set-unit-plan:
    post:
      tags: [Project Landing]
      summary: Upload a floor plan image for a unit
      operationId: setUnitPlan
      description: |
        Uploads a floor plan image for the specified unit and screen. Only PNG and JPEG files are accepted.

        Returns HTTP 201 if a new plan was created, or HTTP 200 if an existing plan was replaced.

        **Permission required:** `WRITE_CMS_DATA`
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              allOf:
                - $ref: '#/components/schemas/AuthFields'
                - type: object
                  required: [unitid, screenid, file]
                  properties:
                    unitid:
                      type: integer
                      description: Unit ID.
                    screenid:
                      type: integer
                      description: Screen configuration ID (from `/get-projects-info`).
                    file:
                      type: string
                      format: binary
                      description: Floor plan image (PNG or JPEG only).
            example:
              login: project-acme-pricelist
              password: s3cret
              unitid: 3356898
              screenid: 2
      responses:
        '201':
          description: New floor plan created. Empty response body.
        '200':
          description: Existing floor plan replaced. Empty response body.
        '400':
          description: |
            Bad request. Possible causes:
            - Invalid unit ID or unit not found.
            - Invalid screen ID.
            - File is not PNG or JPEG.
          content:
            text/plain:
              schema:
                type: string
              example: "ID does not refer to a valid object."
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /add-unit-gallery-image:
    post:
      tags: [Project Landing]
      summary: Add an image to a unit's gallery
      operationId: addUnitGalleryImage
      description: |
        Adds a new image to the gallery of the specified unit. Only PNG and JPEG files are accepted. The unit must have a gallery attached.

        Returns HTTP 201 with the newly created picture ID in the response body.

        **Permission required:** `WRITE_CMS_DATA`
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              allOf:
                - $ref: '#/components/schemas/AuthFields'
                - type: object
                  required: [unitid, screenid, name, file]
                  properties:
                    unitid:
                      type: integer
                      description: Unit ID.
                    screenid:
                      type: integer
                      description: Screen configuration ID (from `/get-projects-info`).
                    name:
                      type: string
                      description: Image name (in the developer's default locale).
                    file:
                      type: string
                      format: binary
                      description: Gallery image (PNG or JPEG only).
            example:
              login: project-acme-pricelist
              password: s3cret
              unitid: 3356898
              screenid: 2
              name: Living room view
      responses:
        '201':
          description: Image added to gallery. Response body contains the picture ID.
          content:
            text/plain:
              schema:
                type: integer
                description: Newly created picture ID.
              example: 456789
        '400':
          description: |
            Bad request. Possible causes:
            - Invalid unit ID or unit not found.
            - Invalid screen ID.
            - File is not PNG or JPEG.
            - Unit has no gallery attached.
          content:
            text/plain:
              schema:
                type: string
              example: "ID does not refer to a valid object."
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '500':
          $ref: '#/components/responses/ServerError'

  # =========================================================================
  # TABLET APP — ACTIVE
  # =========================================================================

  /login:
    post:
      tags: [Tablet App]
      summary: Validate credentials and get user + developer info
      operationId: login
      description: |
        Validates the provided credentials and returns an XML document with the user ID, role ID, full name, email, "test" flag, developer feature flags, and the number of developer projects. No session is created and no token is returned — this is a stateless credential check.

        No additional parameters are required beyond `login` and `password`. No specific permission is required — any valid user can call this.

        **Failed login protection:** The system tracks failed login attempts per IP address and per user account, with progressive banning:
        - **Per user:** after 2 failed attempts, bans start at 2 minutes and double with each subsequent attempt (2 → 4 → 8 min), capping at 24 hours after 5 attempts.
        - **Per IP:** after 10 failed attempts, bans start at 10 minutes and double (10 → 20 → 40 → 80 → 160 min), capping at 24 hours after 15 attempts.
        - A successful login clears all failed attempt records.
        - While banned, the endpoint returns HTTP 401 with no indication of ban duration.
        - Note: this banning applies to **all** authenticated endpoints, not just `/login`.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/AuthFields'
            example:
              login: jan.novak
              password: s3cret
      responses:
        '200':
          description: User profile and developer configuration.
          content:
            application/xml:
              schema:
                $ref: '#/components/schemas/XmlUserV05'
              example: |
                <user id="3355682" developer-id="3230279" role="4"
                      name="Jan Novak" email="jan.novak@acme.cz"
                      wipe="false" test="true"
                      allows-prereservations="true"
                      allows-prereservation-extension="true"
                      allows-extending-to-specific-date="true"
                      shift-queue="true"
                      sales-funnel-aggregate="true"
                      original-prices="true"
                      allows-multiple-flats-on-deal="true"
                      warn-unsaved-exit="true"
                      signed-required-document="false"
                      extended-availability="true"
                      hide-prereservation="false"
                      show-own-tags-only="false"
                      imperial-system="false"
                      sort-units-numerically="true"
                      notification-attachments="true"
                      first-payment-contains-reservation="true"
                      has-n-payments="true"
                      filter-by-vat="true"
                      developer-projects="25"
                      second-market-units="0">
                  <feature id="42"/>
                  <feature id="103"/>
                  <feature id="105"/>
                </user>
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /list-projects:
    post:
      tags: [Tablet App]
      summary: List all projects (lightweight)
      operationId: listProjects
      description: |
        Returns XML with a list of all non-hidden projects of the developer of the invoking user, including project metadata, buildings, units, attributes, galleries, and POIs. This is the same data structure as `/get-project` but for all projects at once.

        No additional parameters required.

        **Permission required:** `READ_CMS_DATA`
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/AuthFields'
            example:
              login: jan.novak
              password: s3cret
      responses:
        '200':
          description: Full XML export with all projects.
          content:
            application/xml:
              schema:
                $ref: '#/components/schemas/XmlExportV08'
              example: |
                <export creation-time="2026-02-19T07:16:24+0100" developer-id="3230279"
                        developer-name="Lorem Ipsum Development"
                        logo="ffbea236-6b3c-4bac-85ee-09521ba03679"
                        allows-reservations="true">
                  <project id="3356887" name="C PROJECT Riverside" gps="6789099"
                           available-flat-count="40" available-second-market-unit-count="0">
                    <description locale="cs_CZ" text="Rezidenční projekt u řeky"/>
                    <description locale="en_US" text="Residential project by the river"/>
                    <locale iso="cs_CZ" name="Čeština"/>
                    <locale iso="en_US" name="English"/>
                    <building id="3356894" name="Gent" arrow-direction="0">
                      <floor id="3356895" floorNo="1" arrow-direction="0">
                        <flat id="3356898">
                          <flat-attribute key="flat_internal_id" value="G001"/>
                          <flat-attribute key="flat_area" value="80"/>
                          <flat-attribute key="flat_price" value="142886.48"/>
                          <flat-attribute key="flat_status" value="3"/>
                        </flat>
                      </floor>
                    </building>
                    <second-market/>
                  </project>
                  <project id="3602022" name="Dolorsit Residence"
                           available-flat-count="12" available-second-market-unit-count="0">
                    <!-- ... -->
                  </project>
                </export>
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /get-all-projects:
    post:
      tags: [Tablet App]
      summary: Get all projects for the developer
      operationId: getAllProjects
      description: |
        Returns XML with full information on **all non-hidden projects** of the developer. Same structure as `/get-project` but for every project. Useful for landing pages of developers with multiple projects.

        Hidden units are excluded unless `includehidden` is set to a truthy value.

        **Errors:** if one of the IDs is invalid, HTTP 400 is returned.

        **Permission required:** `READ_CMS_DATA`
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/AuthFields'
                - type: object
                  required: [screenid]
                  properties:
                    screenid:
                      type: integer
                      description: Screen configuration ID (from `/get-projects-info`).
                    includehidden:
                      type: boolean
                      default: false
                      description: If `true`, hidden units are included in the response.
            example:
              login: project-acme-pricelist
              password: s3cret
              screenid: 2
      responses:
        '200':
          description: Full XML export of all projects.
          content:
            application/xml:
              schema:
                $ref: '#/components/schemas/XmlExportV08'
              example: |
                <export creation-time="2026-02-19T07:16:24+0100" developer-id="3230279"
                        developer-name="Lorem Ipsum Development"
                        logo="ffbea236-6b3c-4bac-85ee-09521ba03679"
                        allows-reservations="true">
                  <project id="3356887" name="C PROJECT Riverside" gps="6789099"
                           available-flat-count="40" available-second-market-unit-count="0">
                    <description locale="cs_CZ" text="Rezidenční projekt u řeky"/>
                    <description locale="en_US" text="Residential project by the river"/>
                    <!-- buildings, floors, units, galleries, attributes... -->
                  </project>
                  <project id="3602022" name="Dolorsit Residence"
                           available-flat-count="12" available-second-market-unit-count="0">
                    <!-- ... -->
                  </project>
                </export>
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /list-basket:
    post:
      tags: [Tablet App]
      summary: List favorite units for a customer
      operationId: listBasket
      description: |
        Returns XML with the favorite (watched) units of the specified customer.

        **Errors:** if one of the IDs is invalid, HTTP 400 is returned.

        **Permission required:** `READ_CUSTOMER_DATA`
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/AuthFields'
                - type: object
                  required: [customerid]
                  properties:
                    customerid:
                      type: integer
                      description: Customer ID.
            example:
              login: jan.novak
              password: s3cret
              customerid: 4136001
      responses:
        '200':
          description: List of favorite units.
          content:
            application/xml:
              schema:
                $ref: '#/components/schemas/XmlFlatsV01'
              example: |
                <flats>
                  <flat flat-developer-id="3230279" flat-id="3356898"
                        salesman-developer-id="3230279" salesman-id="3355682"
                        modification-time="2026-02-19T06:37:52Z"
                        project-id="3356887"/>
                  <flat flat-developer-id="3230279" flat-id="3356905"
                        salesman-developer-id="3230279" salesman-id="3355682"
                        modification-time="2026-02-19T06:38:18Z"
                        project-id="3356887"/>
                </flats>
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /add-to-basket:
    post:
      tags: [Tablet App]
      summary: Add a unit to customer's favorites
      operationId: addToBasket
      description: |
        Adds the specified unit to the favorites of the specified customer. **Idempotent:** if the unit is already a favorite, this is a no-op.

        **Errors:** if one of the IDs is invalid, HTTP 400 is returned. If there was a database or internal server error, HTTP 500 is returned.

        **Permission required:** `WRITE_CUSTOMER_DATA`
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/AuthFields'
                - type: object
                  required: [flatid, customerid]
                  properties:
                    developerid:
                      type: integer
                      description: Developer ID of the unit. Optional — defaults to the authenticated user's developer.
                    flatid:
                      type: integer
                      description: Unit ID.
                    customerid:
                      type: integer
                      description: Customer ID.
                    inquiryid:
                      type: integer
                      description: |
                        Inquiry ID. Optional for now — **will become required in a future version.**
                        When not provided, defaults to the customer's latest inquiry.
            example:
              login: jan.novak
              password: s3cret
              flatid: 3356898
              customerid: 4136001
      responses:
        '200':
          description: Unit added to favorites (or already was). Empty response body.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/ServerError'

  /remove-from-basket:
    post:
      tags: [Tablet App]
      summary: Remove a unit from customer's favorites
      operationId: removeFromBasket
      description: |
        Removes the specified unit from the favorites of the specified customer. **Idempotent:** if the unit was not a favorite, this is a no-op.

        **Errors:** if one of the IDs is invalid, HTTP 400 is returned. If there was a database or internal server error, HTTP 500 is returned.

        **Permission required:** `WRITE_CUSTOMER_DATA`
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/AuthFields'
                - type: object
                  required: [flatid, customerid]
                  properties:
                    developerid:
                      type: integer
                      description: Developer ID of the unit. Optional — defaults to the authenticated user's developer.
                    flatid:
                      type: integer
                      description: Unit ID.
                    customerid:
                      type: integer
                      description: Customer ID.
                    inquiryid:
                      type: integer
                      description: |
                        Inquiry ID. Optional for now — **will become required in a future version.**
                        When not provided, defaults to the customer's latest inquiry.
            example:
              login: jan.novak
              password: s3cret
              flatid: 3356898
              customerid: 4136001
      responses:
        '200':
          description: Unit removed from favorites (or was not a favorite). Empty response body.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/ServerError'

  /list-queue:
    post:
      tags: [Tablet App]
      summary: List pre-reservation queue for a unit
      operationId: listQueue
      description: |
        Lists the queue of the specified unit, showing each active pre-reservation's position, validity period, and owner.

        Customer details (`person-id`, `person-name`) are only included if the authenticated user has permission to view that customer's data. Otherwise, the current user's own ID and name are shown.

        **Permission required:** `READ_CUSTOMER_DATA`
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/AuthFields'
                - type: object
                  required: [developerid, flatid]
                  properties:
                    developerid:
                      type: integer
                      description: Developer ID of the unit.
                    flatid:
                      type: integer
                      description: Unit ID.
            example:
              login: jan.novak
              password: s3cret
              developerid: 3230279
              flatid: 4129126
      responses:
        '200':
          description: Pre-reservation queue for the unit.
          content:
            application/xml:
              schema:
                $ref: '#/components/schemas/XmlQueueV01'
              example: |
                <queue>
                  <prereservation queue-position="1"
                                  salesman-id="3355682" salesman-name="Matej Vitásek"
                                  person-id="4136001" person-name="Sonnet Claude"
                                  valid-from="2026-02-19T07:56:51+0100"
                                  valid-until="2026-02-24T23:00:00+0100"/>
                  <prereservation queue-position="2"
                                  salesman-id="3355682" salesman-name="Matej Vitásek"
                                  person-id="4136021" person-name="Haiku Claude"
                                  valid-from="2026-02-24T23:00:00+0100"
                                  valid-until="2026-03-01T23:00:00+0100"/>
                </queue>
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /make-prereservation:
    post:
      tags: [Tablet App]
      summary: Create a pre-reservation on a unit
      operationId: makePrereservation
      description: |
        Creates a pre-reservation for the specified customer on the specified unit. The unit's availability is set to "pre-reserved". **Idempotent:** if the unit is already pre-reserved for the same customer, this is a no-op and returns the current status.

        **Restrictions:**
        - Only the salesman who created the customer can call this endpoint.
        - The unit must be available — if already reserved or sold, HTTP 409 is returned.
        - If too many active pre-reservations exist on the unit, HTTP 419 is returned.

        **Note:** it **must not** be assumed that this new availability value will be sent "upstream" and visible later. It might be that the next synchronization of the project will re-set the availability to the original value.

        **Errors:** if one of the IDs is invalid, HTTP 400 is returned. If there was a database or internal server error, HTTP 500 is returned.

        **Permission required:** `WRITE_CUSTOMER_DATA`
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/AuthFields'
                - type: object
                  required: [flatid, customerid]
                  properties:
                    developerid:
                      type: integer
                      description: Developer ID of the unit. Optional — defaults to the authenticated user's developer.
                    flatid:
                      type: integer
                      description: Unit ID.
                    customerid:
                      type: integer
                      description: Customer ID.
                    inquiryid:
                      type: integer
                      description: |
                        Inquiry ID. Optional for now — **will become required in a future version.**
                        When not provided, defaults to the customer's latest inquiry.
            example:
              login: jan.novak
              password: s3cret
              flatid: 4129126
              customerid: 4136001
      responses:
        '200':
          description: Pre-reservation created or already existed.
          content:
            application/xml:
              schema:
                $ref: '#/components/schemas/XmlPrereservationStatusV01'
              example: |
                <prereservation availability="1"
                               valid-from="2026-02-19T07:54:29+0100"
                               valid-until="2026-02-24T23:00:00+0100"
                               queue-position="1"/>
        '400':
          description: Unit does not exist or invalid customer ID.
          content:
            text/plain:
              schema:
                type: string
              example: "Unit does not exist."
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          description: Conflict — the unit is already reserved or sold, or an associated unit (e.g., cellar linked to the same Deal) is already reserved.
          content:
            text/plain:
              schema:
                type: string
              example: "Unit A-301 already reserved."
        '419':
          description: Too many active pre-reservations on this unit. Custom HTTP status code.
          content:
            text/plain:
              schema:
                type: string
              example: "Too many pre-reservations on unit A-301."
        '500':
          $ref: '#/components/responses/ServerError'

  # =========================================================================
  # INTERNAL
  # =========================================================================

  /create-customer:
    post:
      tags: [Internal]
      summary: Create a new customer
      operationId: createCustomer
      description: |
        Used by Runscope API tests for CRM exercise. Use `/create-lead` for new integrations.

        **Note:** this endpoint will be moved to a dedicated internal API in a future update.

        Creates a new customer with the specified information. `creation-time` must be a valid Unix timestamp. If `projectids` is empty or `{}`, the projects of the customer's salesman will be used. Otherwise it must contain a comma-separated list of project IDs (e.g. from `/list-projects`).

        **Permission required:** `WRITE_CUSTOMER_DATA`
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/AuthFields'
                - type: object
                  required: [creation-time]
                  properties:
                    name:
                      type: string
                      description: Customer first name.
                    surname:
                      type: string
                      description: Customer last name.
                    email:
                      type: string
                      description: Customer email.
                    phone:
                      type: string
                      description: Customer phone.
                    referral:
                      type: string
                      description: Referral source.
                    note:
                      type: string
                      description: Free-text note.
                    projectids:
                      type: string
                      description: "Comma-separated project IDs, `{}` for empty set, or omit to use salesman's projects."
                    creation-time:
                      type: integer
                      description: Unix timestamp for the customer creation time.
            example:
              login: jan.novak
              password: s3cret
              name: Petr
              surname: Kral
              email: petr.kral@example.com
              phone: "+420777111222"
              creation-time: 1718450000
              projectids: "101,102"
      responses:
        '200':
          description: Newly created customer ID as plain text.
          content:
            text/plain:
              schema:
                type: integer
              example: 12347
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /update-customer:
    post:
      tags: [Internal]
      summary: Update an existing customer
      operationId: updateCustomer
      description: |
        Used by Runscope API tests for CRM exercise.

        **Note:** this endpoint will be moved to a dedicated internal API in a future update.

        Updates the information of the specified customer. **Warning:** you must send **all** data that needs to be saved, not just the fields that changed. Omitted fields will be cleared.

        `projectids` interpretation: comma-separated list → set those projects; `{}` → clear all projects (empty set); omitted/empty → keep the original project list.

        **Restriction:** only the salesman who created the customer can call this.

        **Permission required:** `WRITE_CUSTOMER_DATA`
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/AuthFields'
                - type: object
                  required: [customerid]
                  properties:
                    customerid:
                      type: integer
                      description: Customer ID to update.
                    name:
                      type: string
                    surname:
                      type: string
                    email:
                      type: string
                    phone:
                      type: string
                    referral:
                      type: string
                    note:
                      type: string
                    projectids:
                      type: string
                      description: "Comma-separated project IDs, `{}` for empty set, or omit to keep the original list."
            example:
              login: jan.novak
              password: s3cret
              customerid: 12345
              name: Petr
              surname: Kral
              email: petr.kral@newmail.com
              phone: "+420777111222"
      responses:
        '200':
          description: Customer updated successfully. Empty response body.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /delete-customer:
    post:
      tags: [Internal]
      summary: Delete (hide) a customer
      operationId: deleteCustomer
      description: |
        Used by Runscope API tests for CRM exercise.

        **Note:** this endpoint will be moved to a dedicated internal API in a future update.

        "Deletes" the specified customer. In fact it hides them, so that they are invisible for `/list-customers`. The customer is not permanently deleted. If the customer has a Deal or similar linked data, the operation fails.

        **Errors:** if one of the IDs is invalid, HTTP 400 is returned.

        **Restriction:** only the salesman who created the customer can call this.

        **Permission required:** `WRITE_CUSTOMER_DATA`
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/AuthFields'
                - type: object
                  required: [customerid]
                  properties:
                    customerid:
                      type: integer
                      description: Customer ID to delete (hide).
            example:
              login: jan.novak
              password: s3cret
              customerid: 12345
      responses:
        '200':
          description: Customer hidden successfully. Empty response body.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /list-events:
    post:
      tags: [Internal]
      summary: List events for a customer
      operationId: listEvents
      description: |
        Returns XML with the events of the specified customer. Used by Runscope API tests for CRM exercise.

        **Note:** this endpoint will be moved to a dedicated internal API in a future update.

        **Errors:** if one of the IDs is invalid, HTTP 400 is returned. If there was a database or internal server error, HTTP 500 is returned.

        **Restriction:** only the salesman who created the customer can call this.

        **Permission required:** `READ_CUSTOMER_DATA`
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/AuthFields'
                - type: object
                  required: [customerid]
                  properties:
                    customerid:
                      type: integer
                      description: Customer ID.
            example:
              login: jan.novak
              password: s3cret
              customerid: 12345
      responses:
        '200':
          description: Event history for the customer.
          content:
            application/xml:
              schema:
                $ref: '#/components/schemas/XmlEventsV03'
              example: |
                <events>
                  <event id="9001" type="999"
                         time="2025-06-15T14:30:00.000+02:00"
                         salesman-developer-id="42" salesman-id="99"
                         user-developer-id="42" user-id="12345"
                         text="Customer visited showroom"/>
                </events>
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/ServerError'

  /add-event:
    post:
      tags: [Internal]
      summary: Add a custom event to a customer's history
      operationId: addEvent
      description: |
        Logs a new event of type `OTHER` (ID 999) in the customer's event history. Used by Runscope API tests for CRM exercise. If the event is related to a unit, both `flatid` and `developerid` should be specified. If `time` is not specified, the current database time is used.

        **Note:** this endpoint will be moved to a dedicated internal API in a future update.

        **Important:** do **not** log events of any type other than `OTHER` — the remaining event types are logged automatically by their corresponding web services.

        **Errors:** if one of the IDs is invalid, HTTP 400 is returned. If there was a database or internal server error, HTTP 500 is returned.

        **Restriction:** only the salesman who created the customer can call this.

        **Permission required:** `WRITE_CUSTOMER_DATA`
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/AuthFields'
                - type: object
                  required: [customerid]
                  properties:
                    customerid:
                      type: integer
                      description: Customer ID.
                    time:
                      type: integer
                      description: Unix timestamp for the event. If omitted, the current database time is used.
                    developerid:
                      type: integer
                      description: Developer ID of the related unit. Should be provided together with `flatid`.
                    flatid:
                      type: integer
                      description: Unit ID of the related unit. Should be provided together with `developerid`.
                    text:
                      type: string
                      description: Event description text.
            example:
              login: jan.novak
              password: s3cret
              customerid: 12345
              time: 1718450000
              text: Customer visited the showroom
      responses:
        '200':
          description: Event logged successfully. Empty response body.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/ServerError'

  # =========================================================================
  # TABLET APP — DEPRECATED
  # Legacy tablet app endpoints no longer used by Project Phoenix.
  # Retained for backward compatibility with the legacy iOS app
  # until it is removed from the App Store.
  # =========================================================================

  /get-resource:
    post:
      tags: [Tablet App (Deprecated)]
      summary: Get a resource file by UID
      operationId: getResource
      deprecated: true
      description: |
        **Deprecated:** a better replacement exists. Retained for backward compatibility.

        Returns an HTTP 302 redirect to the requested resource on the `/resource` endpoint. The client should follow the redirect to download the actual file (image, floor plan, etc.).

        The `thumbnail` parameter is accepted but **no longer used**.

        **Permission required:** `READ_CMS_DATA`
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              allOf:
                - $ref: '#/components/schemas/AuthFields'
                - type: object
                  required: [uid]
                  properties:
                    uid:
                      type: string
                      description: Resource unique identifier.
                    thumbnail:
                      type: boolean
                      description: "**Deprecated, no longer used.** Accepted for backward compatibility but has no effect."
                      deprecated: true
            example:
              login: jan.novak
              password: s3cret
              uid: a1b2c3d4-e5f6-7890-abcd-ef1234567890
      responses:
        '302':
          description: Redirect to the resource download URL.
          headers:
            Location:
              description: URL of the resource file on the `/resource` endpoint.
              schema:
                type: string
        '401':
          $ref: '#/components/responses/Unauthorized'

  /get-screen:
    post:
      tags: [Tablet App (Deprecated)]
      summary: Get screen configuration ID
      operationId: getScreen
      deprecated: true
      description: |
        **Deprecated:** not used by Project Phoenix. Retained for legacy tablet app.

        Returns a `screenid` as a plain text number. If there is a perfectly matching screen for the given dimensions, that one is returned. Otherwise, an arbitrary screen ID is returned.

        **No authentication required** — this endpoint does not require a user session.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [width, height]
              properties:
                width:
                  type: integer
                  description: Screen width in pixels.
                height:
                  type: integer
                  description: Screen height in pixels.
            example:
              width: 2048
              height: 1536
      responses:
        '200':
          description: Screen ID as plain text number.
          content:
            text/plain:
              schema:
                type: integer
              example: 1

  /list-customers:
    post:
      tags: [Tablet App (Deprecated)]
      summary: List customers for the authenticated user
      operationId: listCustomers
      deprecated: true
      description: |
        **Deprecated:** not used by Project Phoenix. Retained for legacy tablet app and Runscope API tests.

        Returns XML with basic customer information, along with their favorites and pre-reservations. In legacy mode, this returns approximately the same set of customers as the user would see in the UI. In Multiple Inquiries mode, this returns a limited set of customers (the business logic is in flux).

        **Permission required:** `READ_CUSTOMER_DATA`
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/AuthFields'
            example:
              login: jan.novak
              password: s3cret
      responses:
        '200':
          description: List of customers with favorites and pre-reservations.
          content:
            application/xml:
              schema:
                $ref: '#/components/schemas/XmlPeopleV02'
              example: |
                <people>
                  <person id="12345" salesman-developer-id="42" salesman-id="99"
                          name="Petr" surname="Kral"
                          email="petr.kral@example.com" phone="+420777111222"
                          creation-time="2025-01-15T10:00:00.000+01:00"
                          note="Interested in 3+kk">
                    <basket-entry flat-developer-id="42" flat-id="501"
                                  salesman-developer-id="42" salesman-id="99"
                                  modification-time="2025-02-01T12:00:00.000+01:00"
                                  project-id="101"/>
                    <reservation flat-developer-id="42" flat-id="502"
                                 salesman-developer-id="42" salesman-id="99"
                                 modification-time="2025-03-01T09:00:00.000+01:00"
                                 project-id="101"
                                 expires="2025-03-15T23:59:59.000+01:00"
                                 queue-position="1"/>
                  </person>
                </people>
        '401':
          $ref: '#/components/responses/Unauthorized'

  /list-questions:
    post:
      tags: [Tablet App (Deprecated)]
      summary: List questions (always empty)
      operationId: listQuestions
      deprecated: true
      description: |
        **Deprecated:** returns just an empty response. This endpoint has been deprecated and the questions feature has been removed.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/AuthFields'
            example:
              login: jan.novak
              password: s3cret
      responses:
        '200':
          description: Always returns empty questions element.
          content:
            application/xml:
              schema:
                type: object
                xml:
                  name: questions
              example: |
                <questions></questions>
        '401':
          $ref: '#/components/responses/Unauthorized'

# ===========================================================================
# COMPONENTS
# ===========================================================================
components:

  # =========================================================================
  # SCHEMAS
  # =========================================================================
  schemas:

    # -----------------------------------------------------------------------
    # Auth
    # -----------------------------------------------------------------------
    AuthFields:
      type: object
      required: [login, password]
      properties:
        login:
          type: string
          description: API user login credential.
        password:
          type: string
          format: password
          description: API user password.

    # -----------------------------------------------------------------------
    # JSON schemas
    # -----------------------------------------------------------------------
    ProjectsInfoResponse:
      type: object
      properties:
        developer-id:
          type: integer
          description: Developer ID.
        projects:
          type: array
          items:
            type: object
            properties:
              project-id:
                type: integer
              project-name:
                type: string
        screens:
          type: array
          items:
            type: object
            properties:
              screen-id:
                type: integer
              screen-name:
                type: string

    CreateLeadParams:
      type: object
      required: [name, surname]
      properties:
        name:
          type: string
          description: Customer first name.
        surname:
          type: string
          description: Customer last name.
        email:
          type: string
          description: Customer email. At least one of `email` or `phone` **must** be provided.
        phone:
          type: string
          description: "Customer phone. At least one of `email` or `phone` **must** be provided. E.164 format is strongly recommended (e.g., `+420777123456`). The system will attempt to normalize other formats, but non-E.164 input may fail normalization for some country codes."
        note:
          type: string
          description: Free-text inquiry note.
        referral:
          type: integer
          description: Referral source ID.
        campaign:
          type: string
          description: Campaign identifier (e.g., `google-ads-2025`).
        language:
          type: string
          description: "ISO 639-1 language code (e.g., `en`, `cs`, `sk`). Must be a valid code if provided."
        unitid:
          type: string
          description: "**Recommended.** Unit database ID. Use this to link the lead to a specific unit."
        internalid:
          type: string
          description: "Unit internal ID (legacy alternative to `unitid`). Takes precedence over `unitid` if both are provided. For backward compatibility, if the value looks like a database ID, it is interpreted as one."
        productinterest:
          type: string
          description: Product interest description.
        tags:
          type: string
          description: Comma-separated customer tags.
        salesman:
          type: integer
          description: Sales agent user ID to assign.
        project:
          type: integer
          description: Project ID to associate the inquiry with.
        consentsource:
          type: string
          description: "Free-form text describing the source of the consents. The original intention is to contain the IP address from which the lead submitted the contact form."
        consents:
          type: string
          description: "Comma-separated Consent Type IDs (exact IDs and their purpose are tenant-specific — provided by the Realpad support team). **Replaces** all existing consents — if the lead already existed and had consent records, they are cleared first. Mutually exclusive with `updateconsents`."
        updateconsents:
          type: string
          description: "Comma-separated Consent Type IDs (same IDs as `consents`). **Merges** with existing consents — unmentioned ones are kept unchanged. Mutually exclusive with `consents`."
        preferencerooms:
          type: string
          description: "Pipe-separated (`|`) list of room/disposition preferences (e.g., `3+kk|4+kk`)."
        preferencepricemax:
          type: integer
          description: Maximum price preference.

    LeadResult:
      type: object
      properties:
        customer_id:
          type: integer
          description: Customer ID (new or existing).
        customer_name:
          type: string
          description: Full name of the customer.
        previously_known:
          type: boolean
          description: "`true` if the customer already existed, `false` if newly created."
        salesman_id:
          type: integer
          description: Assigned sales agent user ID.
        salesman_name:
          type: string
          nullable: true
          description: Sales agent full name.
        salesman_email:
          type: string
          nullable: true
          description: Sales agent email address.
        salesman_phone:
          type: string
          nullable: true
          description: Sales agent phone number.

    RecentLead:
      type: object
      description: A recent lead (Customer + Inquiry) created by the authenticated user within the last 7 days.
      properties:
        inquiry_id:
          type: integer
          description: Inquiry ID.
        customer_id:
          type: integer
          description: Customer ID.
        customer_name:
          type: string
          description: Customer full name.
        timestamp:
          type: string
          description: ISO 8601 datetime with timezone indicating when the lead was created.
        projects:
          type: array
          description: Names of projects associated with the inquiry.
          items:
            type: string
        campaign:
          type: string
          nullable: true
          description: Campaign identifier, or `null` if no campaign was associated.

    UnitUpdateRequest:
      type: object
      description: JSON structure expected in the `data` form parameter of `/update-units`.
      required: [units]
      properties:
        units:
          type: array
          items:
            type: object
            required: [id]
            properties:
              id:
                type: integer
                description: Unit database ID.
              priceWithoutVat:
                type: number
                description: New price without VAT. Must be non-negative. Change must not exceed 10%.
              priceWithVat:
                type: number
                description: New price with VAT. Must be non-negative. Change must not exceed 10%.

    UnitUpdateResult:
      type: object
      description: Result of a batch unit price update.
      properties:
        success:
          type: boolean
          description: "`true` if all units were updated, `false` if validation failed."
        updatedCount:
          type: integer
          description: Number of units successfully updated.
        error:
          type: string
          description: Error message if `success` is `false`. Absent on success.
        units:
          type: array
          description: List of units that were updated (empty on failure).
          items:
            type: object
            properties:
              id:
                type: integer
              priceWithoutVat:
                type: number
              priceWithVat:
                type: number

    # -----------------------------------------------------------------------
    # XML schemas — Project export
    # -----------------------------------------------------------------------
    XmlExportV08:
      type: object
      description: Root element of the project XML export. Contains project(s) with buildings, floors, units, attributes, galleries, and POIs.
      xml:
        name: export
      properties:
        creation-time:
          type: string
          description: ISO 8601 timestamp with timezone.
          xml:
            attribute: true
        developer-id:
          type: integer
          xml:
            attribute: true
        developer-name:
          type: string
          xml:
            attribute: true
        url:
          type: string
          xml:
            attribute: true
        logo:
          type: string
          description: Resource UID of the developer logo.
          xml:
            attribute: true
        allows-reservations:
          type: boolean
          xml:
            attribute: true
        projects:
          type: array
          xml:
            wrapped: false
          items:
            $ref: '#/components/schemas/XmlProjectV08'
        developer-attributes:
          type: array
          xml:
            wrapped: false
          items:
            $ref: '#/components/schemas/XmlAttributeValueV01'

    XmlProjectV08:
      type: object
      xml:
        name: project
      properties:
        id:
          type: integer
          xml:
            attribute: true
        name:
          type: string
          xml:
            attribute: true
        stage:
          type: string
          xml:
            attribute: true
        gps:
          type: string
          description: GPS coordinates.
          xml:
            attribute: true
        gps-center:
          type: string
          xml:
            attribute: true
        city:
          type: string
          xml:
            attribute: true
        project-group:
          type: string
          xml:
            attribute: true
        plan:
          type: string
          description: Resource UID of the project plan.
          xml:
            attribute: true
        coordinates:
          type: string
          xml:
            attribute: true
        zip:
          type: string
          xml:
            attribute: true
        icon:
          type: string
          description: Resource UID.
          xml:
            attribute: true
        logo:
          type: string
          description: Resource UID.
          xml:
            attribute: true
        standards:
          type: string
          description: Resource UID of standards document.
          xml:
            attribute: true
        offline-map:
          type: string
          description: Resource UID.
          xml:
            attribute: true
        pinpoint:
          type: string
          description: Resource UID.
          xml:
            attribute: true
        hypo-assistant:
          type: boolean
          xml:
            attribute: true
        public:
          type: boolean
          xml:
            attribute: true
        hidden:
          type: boolean
          xml:
            attribute: true
        demo:
          type: boolean
          xml:
            attribute: true
        currency:
          type: string
          description: Currency code (e.g., `CZK`, `EUR`).
          xml:
            attribute: true
        available-flat-count:
          type: integer
          xml:
            attribute: true
        available-second-market-unit-count:
          type: integer
          xml:
            attribute: true
        descriptions:
          type: array
          xml:
            wrapped: false
          items:
            $ref: '#/components/schemas/XmlLocalizedTextV01'
        buildings:
          type: array
          xml:
            wrapped: false
          items:
            $ref: '#/components/schemas/XmlBuildingV03'
        galleries:
          type: array
          xml:
            wrapped: false
          items:
            $ref: '#/components/schemas/XmlGalleryV02'
        pois:
          type: array
          xml:
            wrapped: false
          items:
            $ref: '#/components/schemas/XmlPoiV01'
        locales:
          type: array
          xml:
            wrapped: false
          items:
            $ref: '#/components/schemas/XmlLocaleV01'
        flat-attribute-types:
          type: array
          xml:
            wrapped: false
          items:
            $ref: '#/components/schemas/XmlAttributeV02'
        project-attributes:
          type: array
          xml:
            wrapped: false
          items:
            $ref: '#/components/schemas/XmlAttributeValueV01'

    XmlLocalizedTextV01:
      type: object
      description: A localized text value.
      xml:
        name: description
      properties:
        locale:
          type: string
          description: ISO 639-1 locale code.
          xml:
            attribute: true
        text:
          type: string
          xml:
            attribute: true

    XmlLocaleV01:
      type: object
      xml:
        name: locale
      properties:
        iso:
          type: string
          description: ISO 639-1 code.
          xml:
            attribute: true
        name:
          type: string
          description: Display name.
          xml:
            attribute: true

    XmlBuildingV03:
      type: object
      description: A building within a project containing floors and units.
      xml:
        name: building
      properties:
        id:
          type: integer
          xml:
            attribute: true
        name:
          type: string
          xml:
            attribute: true
        plan:
          type: string
          description: Resource UID of the building plan.
          xml:
            attribute: true
        coordinates:
          type: string
          xml:
            attribute: true

    XmlGalleryV02:
      type: object
      xml:
        name: gallery
      properties:
        id:
          type: integer
          xml:
            attribute: true
        name:
          type: string
          xml:
            attribute: true

    XmlPoiV01:
      type: object
      description: Point of Interest.
      xml:
        name: poi
      properties:
        id:
          type: integer
          xml:
            attribute: true
        name:
          type: string
          xml:
            attribute: true
        gps:
          type: string
          xml:
            attribute: true
        type:
          type: string
          xml:
            attribute: true

    XmlAttributeV02:
      type: object
      description: Attribute type definition.
      xml:
        name: flat-attribute-type
      properties:
        id:
          type: integer
          xml:
            attribute: true
        key:
          type: string
          xml:
            attribute: true
        type:
          type: string
          description: "Attribute data type (e.g., `INTEGER`, `NUMERIC`, `STRING`, `BOOLEAN`, `LOCALIZED_STRING`)."
          xml:
            attribute: true
        unit:
          type: string
          description: Unit of measurement (e.g., `m²`).
          xml:
            attribute: true

    XmlAttributeValueV01:
      type: object
      description: An attribute key-value pair.
      xml:
        name: attribute
      properties:
        key:
          type: string
          xml:
            attribute: true
        value:
          type: string
          xml:
            attribute: true

    # -----------------------------------------------------------------------
    # XML schemas — User (login response)
    # -----------------------------------------------------------------------
    XmlUserV05:
      type: object
      description: User profile and developer configuration returned by `/login`.
      xml:
        name: user
      properties:
        id:
          type: integer
          description: User ID.
          xml:
            attribute: true
        developer-id:
          type: integer
          description: Developer ID.
          xml:
            attribute: true
        role:
          type: integer
          description: "**Deprecated.** Always set to `4`. Retained for backward compatibility."
          xml:
            attribute: true
          deprecated: true
        name:
          type: string
          description: User full name.
          xml:
            attribute: true
        email:
          type: string
          xml:
            attribute: true
        wipe:
          type: boolean
          description: "**Deprecated.** Always `false`. Retained for backward compatibility."
          xml:
            attribute: true
          deprecated: true
        test:
          type: boolean
          description: Whether this is a test/staging user.
          xml:
            attribute: true
        allows-prereservations:
          type: boolean
          xml:
            attribute: true
        allows-prereservation-extension:
          type: boolean
          xml:
            attribute: true
        allows-extending-to-specific-date:
          type: boolean
          xml:
            attribute: true
        shift-queue:
          type: boolean
          xml:
            attribute: true
        sales-funnel-aggregate:
          type: boolean
          xml:
            attribute: true
        original-prices:
          type: boolean
          xml:
            attribute: true
        allows-multiple-flats-on-deal:
          type: boolean
          xml:
            attribute: true
        warn-unsaved-exit:
          type: boolean
          xml:
            attribute: true
        signed-required-document:
          type: boolean
          xml:
            attribute: true
        extended-availability:
          type: boolean
          xml:
            attribute: true
        hide-prereservation:
          type: boolean
          xml:
            attribute: true
        show-own-tags-only:
          type: boolean
          xml:
            attribute: true
        imperial-system:
          type: boolean
          xml:
            attribute: true
        sort-units-numerically:
          type: boolean
          xml:
            attribute: true
        notification-attachments:
          type: boolean
          xml:
            attribute: true
        first-payment-contains-reservation:
          type: boolean
          xml:
            attribute: true
        has-n-payments:
          type: boolean
          xml:
            attribute: true
        filter-by-vat:
          type: boolean
          description: Whether to filter/display prices with VAT.
          xml:
            attribute: true
        developer-projects:
          type: integer
          description: Number of projects for the developer.
          xml:
            attribute: true
        second-market-units:
          type: integer
          description: Count of second market units. Default `0`.
          xml:
            attribute: true
        features:
          type: array
          description: Developer feature flags.
          xml:
            wrapped: false
          items:
            $ref: '#/components/schemas/XmlFeatureV01'

    XmlFeatureV01:
      type: object
      description: A developer feature flag.
      xml:
        name: feature
      properties:
        id:
          type: integer
          description: Feature ID.
          xml:
            attribute: true

    # -----------------------------------------------------------------------
    # XML schemas — Favorites (basket)
    # -----------------------------------------------------------------------
    XmlFlatsV01:
      type: object
      description: List of favorite units.
      xml:
        name: flats
      properties:
        flats:
          type: array
          xml:
            wrapped: false
          items:
            $ref: '#/components/schemas/XmlFlatChangeV01'

    XmlFlatChangeV01:
      type: object
      xml:
        name: flat
      properties:
        flat-developer-id:
          type: integer
          xml:
            attribute: true
        flat-id:
          type: integer
          xml:
            attribute: true
        salesman-developer-id:
          type: integer
          xml:
            attribute: true
        salesman-id:
          type: integer
          xml:
            attribute: true
        modification-time:
          type: string
          description: "ISO 8601 datetime in UTC with `Z` suffix (e.g., `2026-02-18T05:46:15Z`)."
          xml:
            attribute: true
        project-id:
          type: integer
          xml:
            attribute: true

    # -----------------------------------------------------------------------
    # XML schemas — Queue
    # -----------------------------------------------------------------------
    XmlQueueV01:
      type: object
      description: Pre-reservation queue for a unit.
      xml:
        name: queue
      properties:
        prereservations:
          type: array
          xml:
            wrapped: false
          items:
            $ref: '#/components/schemas/XmlPrereservationV01'

    XmlPrereservationV01:
      type: object
      xml:
        name: prereservation
      properties:
        queue-position:
          type: integer
          description: Position in the queue (1-based).
          xml:
            attribute: true
        salesman-id:
          type: integer
          xml:
            attribute: true
        salesman-name:
          type: string
          xml:
            attribute: true
        person-id:
          type: integer
          description: Customer ID. Only present if the authenticated user has permission to view this customer.
          xml:
            attribute: true
        person-name:
          type: string
          description: Customer name. Only present if the authenticated user has permission to view this customer.
          xml:
            attribute: true
        valid-from:
          type: string
          description: ISO 8601 datetime with timezone.
          xml:
            attribute: true
        valid-until:
          type: string
          description: ISO 8601 datetime with timezone.
          xml:
            attribute: true

    # -----------------------------------------------------------------------
    # XML schemas — Pre-reservation status
    # -----------------------------------------------------------------------
    XmlPrereservationStatusV01:
      type: object
      description: Result of a pre-reservation creation.
      xml:
        name: prereservation
      properties:
        availability:
          type: integer
          description: Updated unit availability ID.
          xml:
            attribute: true
        valid-from:
          type: string
          description: ISO 8601 datetime with timezone.
          xml:
            attribute: true
        valid-until:
          type: string
          description: ISO 8601 datetime with timezone.
          xml:
            attribute: true
        queue-position:
          type: integer
          description: Position in the pre-reservation queue (1-based).
          xml:
            attribute: true

    # -----------------------------------------------------------------------
    # XML schemas — People (list-customers response)
    # -----------------------------------------------------------------------
    XmlPeopleV02:
      type: object
      description: List of customers with favorites and pre-reservations.
      xml:
        name: people
      properties:
        people:
          type: array
          xml:
            wrapped: false
          items:
            $ref: '#/components/schemas/XmlPersonV02'

    XmlPersonV02:
      type: object
      xml:
        name: person
      properties:
        id:
          type: integer
          description: Customer ID.
          xml:
            attribute: true
        salesman-developer-id:
          type: integer
          xml:
            attribute: true
        salesman-id:
          type: integer
          xml:
            attribute: true
        name:
          type: string
          xml:
            attribute: true
        surname:
          type: string
          xml:
            attribute: true
        email:
          type: string
          xml:
            attribute: true
        phone:
          type: string
          xml:
            attribute: true
        creation-time:
          type: string
          description: ISO 8601 datetime with timezone.
          xml:
            attribute: true
        referral:
          type: string
          xml:
            attribute: true
        note:
          type: string
          xml:
            attribute: true
        basket-entries:
          type: array
          description: Favorite/watched units.
          xml:
            wrapped: false
          items:
            $ref: '#/components/schemas/XmlFlatChangeV02'
        reservations:
          type: array
          description: Active pre-reservations.
          xml:
            wrapped: false
          items:
            $ref: '#/components/schemas/XmlFlatChangeV02'
        attributes:
          type: array
          xml:
            wrapped: false
          items:
            $ref: '#/components/schemas/XmlAttributeValueV01'

    XmlFlatChangeV02:
      type: object
      description: "A unit reference with timestamps. Used for both favorites (`basket-entry`) and pre-reservations (`reservation`)."
      xml:
        name: basket-entry
      properties:
        flat-developer-id:
          type: integer
          xml:
            attribute: true
        flat-id:
          type: integer
          xml:
            attribute: true
        salesman-developer-id:
          type: integer
          xml:
            attribute: true
        salesman-id:
          type: integer
          xml:
            attribute: true
        modification-time:
          type: string
          description: ISO 8601 datetime with timezone.
          xml:
            attribute: true
        project-id:
          type: integer
          xml:
            attribute: true
        expires:
          type: string
          description: ISO 8601 datetime with timezone. Only present for pre-reservations, not favorites.
          xml:
            attribute: true
        queue-position:
          type: integer
          description: Only present for pre-reservations.
          xml:
            attribute: true

    # -----------------------------------------------------------------------
    # XML schemas — Events
    # -----------------------------------------------------------------------
    XmlEventsV03:
      type: object
      description: List of customer events.
      xml:
        name: events
      properties:
        events:
          type: array
          xml:
            wrapped: false
          items:
            $ref: '#/components/schemas/XmlEventV03'

    XmlEventV03:
      type: object
      xml:
        name: event
      properties:
        id:
          type: integer
          description: Event ID.
          xml:
            attribute: true
        type:
          type: integer
          description: "Event type ID. Type `999` = `OTHER`."
          xml:
            attribute: true
        time:
          type: string
          description: ISO 8601 datetime with timezone.
          xml:
            attribute: true
        salesman-developer-id:
          type: integer
          xml:
            attribute: true
        salesman-id:
          type: integer
          xml:
            attribute: true
        user-developer-id:
          type: integer
          description: Customer's developer ID.
          xml:
            attribute: true
        user-id:
          type: integer
          description: Customer ID.
          xml:
            attribute: true
        flat-id:
          type: integer
          description: Unit ID if the event is related to a unit.
          xml:
            attribute: true
        text:
          type: string
          description: Event description text.
          xml:
            attribute: true

  # =========================================================================
  # RESPONSES
  # =========================================================================
  responses:
    Unauthorized:
      description: Authentication failed — invalid credentials, insufficient permissions, or IP/account restriction.
      content:
        text/plain:
          schema:
            type: string
          example: "Not authorized."

    BadRequest:
      description: Missing required parameters, invalid parameter format, unexpected parameters, or invalid IDs.
      content:
        text/plain:
          schema:
            type: string
          examples:
            missing:
              summary: Required parameter missing
              value: "Parameters missing."
            format:
              summary: Invalid parameter format
              value: "Incorrect parameter format."
            extra:
              summary: Unexpected parameters
              value: "Unexpected parameters found."
            invalidId:
              summary: Invalid object ID
              value: "ID does not refer to a valid object."

    Forbidden:
      description: User must accept the privacy/data processing consent in the Realpad web interface before calling the API.
      content:
        text/plain:
          schema:
            type: string
          example: >-
            Please log into Realpad web interface and provide your consent
            with us processing your private data.

    TooManyRequests:
      description: Rate limited. Wait for the duration indicated in the response body or the `Retry-After` header before retrying.
      content:
        text/plain:
          schema:
            type: string
          example: "You can call this endpoint again after 2025-06-15T14:35:00.000+02:00"
      headers:
        Retry-After:
          description: Seconds until the next allowed call.
          schema:
            type: integer

    ServerError:
      description: Internal server error.
      content:
        text/plain:
          schema:
            type: string
          example: "Server error, check logs."
