openapi: 3.1.0
info:
  title: Deweloper GOV REST API
  version: 2.1.0
  description: >-
    REST API PRO: katalog dla zewnętrznych frontendów, status, importy,
    publikacja i podpisany webhook właściwości. Endpointy katalogowe używają
    Application Password użytkownika z dg_read_catalog. Operacje administracyjne
    wymagają manage_options. Webhook używa niezależnego podpisu HMAC-SHA256.
  license:
    name: Proprietary
    url: https://deweloper-gov.pl/regulamin
servers:
  - url: https://twoja-domena.pl/wp-json/deweloper-gov/v2
security:
  - applicationPassword: []
paths:
  /catalog:
    get:
      operationId: getCatalog
      summary: Pobiera opis kontraktu i dostępnych zasobów
      responses:
        '200': { $ref: '#/components/responses/Catalog' }
        '304': { description: Niezmienione dane }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '503': { $ref: '#/components/responses/Unavailable' }
  /catalog/investments:
    get:
      operationId: listInvestments
      summary: Pobiera opublikowane inwestycje
      parameters:
        - $ref: '#/components/parameters/page'
        - $ref: '#/components/parameters/perPage'
        - $ref: '#/components/parameters/fields'
        - { name: search, in: query, schema: { type: string } }
      responses:
        '200': { $ref: '#/components/responses/InvestmentCollection' }
        '304': { description: Niezmienione dane }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '503': { $ref: '#/components/responses/Unavailable' }
  /catalog/investments/{id}:
    get:
      operationId: getInvestment
      summary: Pobiera szczegóły opublikowanej inwestycji
      parameters:
        - $ref: '#/components/parameters/id'
        - $ref: '#/components/parameters/fields'
      responses:
        '200':
          description: Szczegóły inwestycji
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Investment' }
        '304': { description: Niezmienione dane }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '503': { $ref: '#/components/responses/Unavailable' }
  /catalog/properties:
    get:
      operationId: listProperties
      summary: Pobiera opublikowane oferty
      parameters:
        - $ref: '#/components/parameters/page'
        - $ref: '#/components/parameters/perPage'
        - $ref: '#/components/parameters/fields'
        - { name: investment, in: query, schema: { type: integer } }
        - { name: status, in: query, schema: { type: string } }
        - { name: offer_kind, in: query, schema: { type: string } }
        - { name: type, in: query, schema: { type: string } }
        - { name: rooms, in: query, schema: { type: integer } }
        - { name: floor, in: query, schema: { type: integer } }
        - { name: area_min, in: query, schema: { type: string } }
        - { name: area_max, in: query, schema: { type: string } }
        - { name: price_min, in: query, schema: { type: integer, description: Kwota w groszach } }
        - { name: price_max, in: query, schema: { type: integer, description: Kwota w groszach } }
        - { name: search, in: query, schema: { type: string } }
        - { name: modified_after, in: query, schema: { type: string, format: date-time } }
        - { name: orderby, in: query, schema: { type: string, enum: [modified, title, price, area] } }
        - { name: order, in: query, schema: { type: string, enum: [ASC, DESC] } }
      responses:
        '200': { $ref: '#/components/responses/PropertyCollection' }
        '304': { description: Niezmienione dane }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '503': { $ref: '#/components/responses/Unavailable' }
  /catalog/properties/{id}:
    get:
      operationId: getProperty
      summary: Pobiera kompletne dane opublikowanej oferty
      parameters:
        - $ref: '#/components/parameters/id'
        - $ref: '#/components/parameters/fields'
      responses:
        '200':
          description: Szczegóły oferty
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Property' }
        '304': { description: Niezmienione dane }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '503': { $ref: '#/components/responses/Unavailable' }
  /catalog/properties/{id}/price-history:
    get:
      operationId: getPropertyPriceHistory
      summary: Pobiera pełną historię cen oferty
      parameters:
        - $ref: '#/components/parameters/id'
        - $ref: '#/components/parameters/fields'
      responses:
        '200':
          description: Pełna historia cen
          content:
            application/json:
              schema:
                type: object
                required: [property_id, items]
                properties:
                  property_id: { type: integer }
                  items: { type: array, items: { $ref: '#/components/schemas/PriceHistoryEntry' } }
        '304': { description: Niezmienione dane }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '503': { $ref: '#/components/responses/Unavailable' }
  /catalog/properties/{id}/related:
    get:
      operationId: getRelatedProperties
      summary: Pobiera podobne oferty z tej samej inwestycji
      parameters:
        - $ref: '#/components/parameters/id'
        - $ref: '#/components/parameters/page'
        - $ref: '#/components/parameters/perPage'
        - $ref: '#/components/parameters/fields'
      responses:
        '200': { $ref: '#/components/responses/PropertyCollection' }
        '304': { description: Niezmienione dane }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/TooManyRequests' }
        '503': { $ref: '#/components/responses/Unavailable' }
  /status:
    get:
      operationId: getSystemStatus
      summary: Pobiera status wtyczki, licencji, kontraktu i ostatniej publikacji
      description: Wymaga konta WordPress z capability manage_options oraz aktywnego API PRO.
      responses:
        '200':
          description: Status systemu
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SystemStatus' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/ProForbidden' }
  /imports/offer:
    post:
      operationId: previewOrExecuteOfferImport
      summary: Przygotowuje podgląd albo wykonuje import oferty
      description: >-
        Bez job_id przyjmuje multipart CSV/XLSX, mapping i tryb. Z job_id wykonuje
        wcześniej zwalidowany plan. Wykonanie wymaga Idempotency-Key.
      parameters:
        - $ref: '#/components/parameters/idempotencyKey'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file: { type: string, format: binary }
                mode: { type: string, enum: [patch, snapshot] }
                mapping:
                  oneOf:
                    - { type: string, description: Obiekt JSON canonical_field -> nagłówek pliku }
                    - { type: object, additionalProperties: { type: string } }
                job_id: { type: string, pattern: '^[a-f0-9]{32}$' }
                confirm_archiving: { type: boolean, default: false }
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [job_id]
              properties:
                job_id: { type: string, pattern: '^[a-f0-9]{32}$' }
                confirm_archiving: { type: boolean, default: false }
      responses:
        '200':
          description: Podgląd poprawny albo import zakończony
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ImportOperationResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/ProForbidden' }
        '404': { $ref: '#/components/responses/ImportNotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422':
          description: Niepoprawny podgląd albo błąd wykonania
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ImportOperationResponse' }
  /imports/report:
    post:
      operationId: previewOrExecuteGovernmentReportImport
      summary: Przygotowuje albo publikuje gotowy raport urzędowy
      description: Import raportu nie modyfikuje ofert ani frontendu WordPressa.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              properties:
                file: { type: string, format: binary }
                job_id: { type: string, pattern: '^[a-f0-9]{32}$' }
                confirm: { type: boolean }
                date: { type: string, format: date }
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [job_id, confirm]
              properties:
                job_id: { type: string, pattern: '^[a-f0-9]{32}$' }
                confirm: { type: boolean }
                date: { type: string, format: date }
      responses:
        '200':
          description: Podgląd poprawny albo raport opublikowany
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ImportOperationResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/ProForbidden' }
        '404': { $ref: '#/components/responses/ImportNotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }
  /imports/{jobId}:
    get:
      operationId: getImportJob
      summary: Pobiera trwały stan i stronicowane pozycje joba importu
      parameters:
        - $ref: '#/components/parameters/jobId'
        - $ref: '#/components/parameters/page'
        - name: per_page
          in: query
          schema: { type: integer, minimum: 1, maximum: 200, default: 100 }
      responses:
        '200':
          description: Stan joba
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ImportJob' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/ProForbidden' }
        '404': { $ref: '#/components/responses/ImportNotFound' }
  /publications/preflight:
    post:
      operationId: publicationPreflight
      summary: Waliduje pełny snapshot przed publikacją
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                date: { type: string, format: date }
                verify_portal: { type: boolean, default: false }
      responses:
        '200':
          description: Preflight poprawny
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Preflight' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/ProForbidden' }
        '422':
          description: Preflight zablokowany przez błędy
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Preflight' }
  /publications:
    post:
      operationId: publishGovernmentArtifacts
      summary: Uruchamia wspólny atomowy pipeline publikacji
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                date: { type: string, format: date }
      responses:
        '200':
          description: Publikacja zakończona
          content:
            application/json:
              schema: { type: object, additionalProperties: true }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/ProForbidden' }
        '422': { $ref: '#/components/responses/Unprocessable' }
  /webhooks/properties:
    post:
      operationId: importPropertyWebhook
      summary: Aktualizuje mieszkanie albo dom przez podpisany webhook HMAC
      description: >-
        Podpis obejmuje timestamp.nonce.idempotency-key.raw-body. Timestamp ma
        tolerancję 300 sekund, nonce jest chronione przez 600 sekund, a wynik
        idempotentnego wykonania jest przechowywany 7 dni.
      security:
        - webhookSignature: []
          webhookTimestamp: []
          webhookNonce: []
          webhookIdempotencyKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PropertyWebhookPayload' }
      responses:
        '200':
          description: Zwrócono wcześniejszy wynik bez ponownego zapisu
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebhookResponse' }
        '202':
          description: Zdarzenie zapisane
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebhookResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/WebhookUnauthorized' }
        '403': { $ref: '#/components/responses/ProForbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '503': { $ref: '#/components/responses/Unavailable' }
components:
  securitySchemes:
    applicationPassword:
      type: http
      scheme: basic
      description: WordPress username oraz Application Password.
    webhookSignature:
      type: apiKey
      in: header
      name: X-DG-Signature
      description: 64-znakowy, szesnastkowy HMAC-SHA256.
    webhookTimestamp:
      type: apiKey
      in: header
      name: X-DG-Timestamp
      description: Dziesięciocyfrowy Unix timestamp z tolerancją ±300 sekund.
    webhookNonce:
      type: apiKey
      in: header
      name: X-DG-Nonce
      description: Unikalne 16–128 znaków; ochrona replay przez 600 sekund.
    webhookIdempotencyKey:
      type: apiKey
      in: header
      name: Idempotency-Key
      description: Stabilny klucz logicznego zdarzenia, 8–128 znaków.
  parameters:
    id: { name: id, in: path, required: true, schema: { type: integer, minimum: 1 } }
    jobId: { name: jobId, in: path, required: true, schema: { type: string, pattern: '^[a-f0-9]{32}$' } }
    idempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: Wymagany podczas wykonania importu oferty.
      schema: { type: string, minLength: 8, maxLength: 128, pattern: '^[A-Za-z0-9._:-]+$' }
    page: { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
    perPage: { name: per_page, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
    fields: { name: _fields, in: query, schema: { type: string }, description: 'Rozdzielona przecinkami lista pól odpowiedzi, także z notacją zagnieżdżoną, np. `id,price.cents`.' }
  schemas:
    Error:
      type: object
      properties:
        error: { type: string }
        code: { type: string }
        message: { type: string }
        feature: { type: string }
      additionalProperties: true
    SystemStatus:
      type: object
      required: [version, contract_id, license_status, continuity_mode, last_publication, public_dataset_id]
      properties:
        version: { type: string }
        contract_id: { type: string }
        license_status: { type: string }
        continuity_mode: { type: boolean }
        last_publication: { type: object, additionalProperties: true }
        public_dataset_id: { type: string }
    Money:
      type: object
      required: [cents, formatted]
      properties:
        cents: { oneOf: [{ type: integer, minimum: 0 }, { type: 'null' }] }
        formatted: { type: string }
        valid_from: { type: string }
    Media:
      type: object
      required: [id, url, mime_type, alt, title, width, height]
      properties:
        id: { type: integer }
        url: { type: string, format: uri }
        mime_type: { type: string }
        alt: { type: string }
        title: { type: string }
        caption: { type: string }
        width: { type: integer }
        height: { type: integer }
    InvestmentSummary:
      type: object
      required: [id, title, permalink, frontend_visible, gov_publication]
      properties:
        id: { type: integer }
        title: { type: string }
        permalink: { type: string, format: uri }
        location: { type: string }
        city: { type: string }
        thumbnail: { type: string }
        frontend_visible: { type: boolean }
        gov_publication: { type: boolean }
        modified_gmt: { type: string, format: date-time }
    Investment:
      allOf:
        - $ref: '#/components/schemas/InvestmentSummary'
        - type: object
          properties:
            content: { type: string }
            excerpt: { type: string }
            context: { $ref: '#/components/schemas/FrontContext' }
            media:
              type: object
              properties:
                thumbnail: { oneOf: [{ $ref: '#/components/schemas/Media' }, { type: 'null' }] }
                gallery: { type: array, items: { $ref: '#/components/schemas/Media' } }
            documents: { $ref: '#/components/schemas/Documents' }
            visibility: { type: object, additionalProperties: { type: boolean } }
            custom_meta: { type: object, additionalProperties: true }
    PropertySummary:
      type: object
      required: [id, title, code, permalink, investment_id, status, price]
      properties:
        id: { type: integer }
        title: { type: string }
        code: { type: string }
        permalink: { type: string, format: uri }
        investment_id: { type: integer }
        offer_kind: { type: string }
        property_type: { type: string }
        offer_type: { type: string }
        status: { type: string }
        status_label: { type: string }
        area: { type: string }
        rooms: { type: string }
        floor: { type: string }
        price: { $ref: '#/components/schemas/Money' }
        price_per_m2: { $ref: '#/components/schemas/Money' }
        thumbnail: { type: string }
        modified_gmt: { type: string, format: date-time }
    Property:
      type: object
      required: [id, title, offer, components, price_history, media, documents, contact, visibility, custom_meta]
      properties:
        id: { type: integer }
        title: { type: string }
        content: { type: string }
        excerpt: { type: string }
        permalink: { type: string, format: uri }
        modified_gmt: { type: string, format: date-time }
        offer: { $ref: '#/components/schemas/OfferData' }
        investment: { oneOf: [{ $ref: '#/components/schemas/InvestmentSummary' }, { type: 'null' }] }
        location:
          type: object
          required: [label, city]
          properties:
            label: { type: string }
            city: { type: string }
        components: { type: array, items: { $ref: '#/components/schemas/OfferComponent' } }
        price_history: { type: array, items: { $ref: '#/components/schemas/PriceHistoryEntry' } }
        media:
          type: object
          properties:
            thumbnail: { oneOf: [{ $ref: '#/components/schemas/Media' }, { type: 'null' }] }
            gallery: { type: array, items: { $ref: '#/components/schemas/Media' } }
        documents: { $ref: '#/components/schemas/Documents' }
        contact:
          type: object
          required: [developer_name, phone, email, sales_office_address]
          properties:
            developer_name: { type: string }
            phone: { type: string }
            email: { type: string }
            sales_office_address: { type: string }
        visibility:
          type: object
          required: [frontend, gov_publication]
          properties:
            frontend: { type: boolean }
            gov_publication: { type: boolean }
        custom_meta: { type: object, additionalProperties: true }
    OfferData:
      type: object
      required: [id, title, code, offer_kind, property_type, is_standalone_part, offer_type_label, url, status, status_label, area, area_label, rooms, floor, building, price, price_cents, price_m2, price_m2_cents, price_date, price_m2_date, variant, variant_label, variant_components, image, gallery, investment_id]
      properties:
        id: { type: integer }
        title: { type: string }
        code: { type: string }
        offer_kind: { type: string, enum: [property, standalone_part] }
        property_type: { type: string }
        is_standalone_part: { type: boolean }
        offer_type_label: { type: string }
        url: { type: string, format: uri }
        status: { type: string }
        status_label: { type: string }
        area: { type: string }
        area_label: { type: string }
        rooms: { type: string }
        floor: { type: string }
        building: { type: string }
        price: { type: string }
        price_cents: { type: integer, minimum: 0 }
        price_m2: { type: string }
        price_m2_cents: { type: integer, minimum: 0 }
        price_date: { type: string }
        price_m2_date: { type: string }
        variant: { type: string }
        variant_label: { type: string }
        variant_components: { type: array, items: { type: string } }
        image: { type: string }
        gallery: { type: array, items: { type: integer } }
        investment_id: { type: integer, minimum: 0 }
      additionalProperties: true
      description: Filtry dg_frontend_property_data mogą addytywnie dodać pola.
    OfferComponent:
      type: object
      required: [group, description, price, date, relation, requirement, variant, legal_title]
      properties:
        group: { type: string }
        description: { type: string }
        price: { type: string }
        date: { type: string }
        relation: { type: string, enum: [included, not_included, ''] }
        requirement: { type: string, enum: [required, optional, ''] }
        variant: { type: string }
        legal_title: { type: string }
    FrontContext:
      type: object
      required: [investment_id, investment_title, investment_url, listing_url, description, hero_image, location, city, developer_name, developer_legal, developer_nip, developer_address, phone, phone_href, email, sale_address, prospect_url, documents_url]
      properties:
        investment_id: { type: integer, minimum: 0 }
        investment_title: { type: string }
        investment_url: { type: string }
        listing_url: { type: string }
        description: { type: string }
        hero_image: { type: string }
        location: { type: string }
        city: { type: string }
        developer_name: { type: string }
        developer_legal: { type: string }
        developer_nip: { type: string }
        developer_address: { type: string }
        phone: { type: string }
        phone_href: { type: string }
        email: { type: string }
        sale_address: { type: string }
        prospect_url: { type: string }
        documents_url: { type: string }
      additionalProperties: true
      description: Filtry dg_frontend_investment_data mogą addytywnie dodać pola.
    Documents:
      type: object
      required: [prospect_url, documents_url, items]
      properties:
        prospect_url: { type: string }
        documents_url: { type: string }
        items: { type: array, items: { type: object, additionalProperties: true } }
    PriceHistoryEntry:
      type: object
      required: [record_type, price_type, before, after, correction]
      properties:
        record_type: { type: string }
        price_type: { type: string }
        valid_from: { type: string }
        recorded_at: { type: string }
        before: { type: object, additionalProperties: true }
        after: { type: object, additionalProperties: true }
        label: { type: string }
        reason: { type: string }
        correction: { type: boolean }
    ImportIssue:
      type: object
      required: [row, field, code, message]
      properties:
        row: { type: integer, minimum: 1 }
        field: { type: string }
        code: { type: string }
        message: { type: string }
    ImportOperation:
      type: object
      required: [action, key, values]
      properties:
        action: { type: string, enum: [upsert, archive] }
        key: { type: string }
        values: { type: object, additionalProperties: { type: string } }
    OfferImportPreview:
      type: object
      required: [valid, mode, operations, errors]
      properties:
        valid: { type: boolean }
        mode: { type: string, enum: [patch, snapshot] }
        operations: { type: array, items: { $ref: '#/components/schemas/ImportOperation' } }
        errors: { type: array, items: { $ref: '#/components/schemas/ImportIssue' } }
    ImportExecutionResult:
      type: object
      required: [status, processed, created, updated, archived, run_id]
      properties:
        status: { type: string, enum: [completed] }
        processed: { type: integer, minimum: 0 }
        created: { type: integer, minimum: 0 }
        updated: { type: integer, minimum: 0 }
        archived: { type: integer, minimum: 0 }
        run_id: { type: integer, minimum: 1 }
    ImportOperationResponse:
      type: object
      required: [id]
      properties:
        id: { type: string, pattern: '^[a-f0-9]{32}$' }
        preview:
          oneOf:
            - { $ref: '#/components/schemas/OfferImportPreview' }
            - { type: object, additionalProperties: true }
        result:
          oneOf:
            - { $ref: '#/components/schemas/ImportExecutionResult' }
            - { type: object, additionalProperties: true }
        error: { type: string }
        message: { type: string }
    ImportJobItem:
      type: object
      required: [index, type, payload]
      properties:
        index: { type: integer, minimum: 0 }
        type: { type: string, enum: [operation, error, payload] }
        payload: { type: object, additionalProperties: true }
    ImportJob:
      type: object
      required: [id, type, mode, status, operator_id, item_count, error_count, created_at, expires_at, result, items, page, per_page, total_items]
      properties:
        id: { type: string, pattern: '^[a-f0-9]{32}$' }
        type: { type: string, enum: [offer, report] }
        mode: { type: string }
        status: { type: string, enum: [preview_ready, invalid, running, completed, failed, recovery_required] }
        operator_id: { type: integer, minimum: 0 }
        item_count: { type: integer, minimum: 0 }
        error_count: { type: integer, minimum: 0 }
        created_at: { type: string }
        completed_at: { oneOf: [{ type: string }, { type: 'null' }] }
        expires_at: { type: string }
        result: { type: object, additionalProperties: true }
        items: { type: array, items: { $ref: '#/components/schemas/ImportJobItem' } }
        page: { type: integer, minimum: 1 }
        per_page: { type: integer, minimum: 1, maximum: 200 }
        total_items: { type: integer, minimum: 0 }
    PreflightIssue:
      type: object
      required: [code, message]
      properties:
        code: { type: string }
        message: { type: string }
    Preflight:
      type: object
      required: [valid, production_allowed, contract_id, contract_valid_until, checked_at, record_count, mode, artifact_status, delivery_status, delivery_checked_at, delivery_issues, portal_status, issues]
      properties:
        valid: { type: boolean }
        production_allowed: { type: boolean }
        contract_id: { type: string }
        contract_valid_until: { type: string, format: date }
        checked_at: { type: string }
        record_count: { type: integer, minimum: 0 }
        mode: { type: string }
        artifact_status: { type: string }
        delivery_status: { type: string }
        delivery_checked_at: { type: string }
        delivery_issues: { type: array, items: {} }
        portal_status: { type: string }
        issues: { type: array, items: { $ref: '#/components/schemas/PreflightIssue' } }
        portal: { oneOf: [{ type: object, additionalProperties: true }, { type: 'null' }] }
    PropertyWebhookPayload:
      type: object
      required: [source_investment_id, source_property_id]
      additionalProperties: false
      properties:
        source_investment_id: { type: string }
        source_property_id: { type: string }
        prop_no: { type: string }
        prop_type: { type: string, enum: [mieszkanie, dom] }
        property_status: { type: string }
        prop_area: { type: string }
        prop_rooms: { type: string, pattern: '^\d+$' }
        prop_floor: { type: string, pattern: '^-?\d+$' }
        price_m2: { type: string }
        price_m2_from: { type: string, format: date }
        price_total: { type: string }
        price_total_from: { type: string, format: date }
        price_combined: { type: string }
        price_combined_from: { type: string, format: date }
        price_combined_variant: { type: string }
        price_combined_status: { type: string }
        proj_voivod: { type: string }
        proj_powiat: { type: string }
        proj_gmina: { type: string }
        proj_city: { type: string }
        proj_street: { type: string }
        proj_build_no: { type: string }
        proj_local_no: { type: string }
        proj_zip: { type: string }
    WebhookResponse:
      type: object
      required: [accepted, result]
      properties:
        accepted: { type: boolean, const: true }
        result: { $ref: '#/components/schemas/ImportExecutionResult' }
        replayed: { type: boolean }
  responses:
    Catalog:
      description: Kontrakt i zasoby katalogu
      content:
        application/json:
          schema: { type: object, additionalProperties: true }
    InvestmentCollection:
      description: Lista inwestycji
      headers:
        X-WP-Total: { schema: { type: integer } }
        X-WP-TotalPages: { schema: { type: integer } }
      content:
        application/json:
          schema: { type: array, items: { $ref: '#/components/schemas/InvestmentSummary' } }
    PropertyCollection:
      description: Lista ofert
      headers:
        X-WP-Total: { schema: { type: integer } }
        X-WP-TotalPages: { schema: { type: integer } }
      content:
        application/json:
          schema: { type: array, items: { $ref: '#/components/schemas/PropertySummary' } }
    Unauthorized:
      description: Brak albo błędne uwierzytelnienie WordPress
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: Brak capability, PRO lub HTTPS
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    ProForbidden:
      description: Brak capability, aktywnego PRO albo wymaganego entitlementu
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: Zasób nie istnieje albo nie jest opublikowany
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    ImportNotFound:
      description: Job importu nie istnieje albo wygasł
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    BadRequest:
      description: Nieprawidłowy format żądania
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Conflict:
      description: Trwające wykonanie, konflikt idempotencji albo recovery_required
      headers:
        Retry-After: { schema: { type: integer, minimum: 1 } }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unprocessable:
      description: Dane nie przeszły walidacji biznesowej
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    WebhookUnauthorized:
      description: Brak, nieprawidłowy albo odtworzony podpis webhooka
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    TooManyRequests:
      description: Przekroczono limit 120 żądań na minutę dla użytkownika i adresu IP
      headers:
        Retry-After: { schema: { type: integer, minimum: 1 } }
    Unavailable:
      description: Nie udało się bezpiecznie sprawdzić limitu żądań
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
