openapi: 3.1.0

info:
  title: ProHolz Tirol — Netzwerk- und Unternehmensdaten
  version: 1.0.0-draft
  summary: Lesende Schnittstelle für Netzwerke, Branchen und Unternehmen der ProHolz-Tirol-Datenbank.
  description: |
    Diese Schnittstelle stellt die Unternehmenskooperationen von ProHolz Tirol —
    Cluster, Netzwerke und Branchengruppen — samt der zugehörigen Unternehmen zur
    Verfügung. Sie ist als Backend-Vertrag für die Einbindung in eine externe
    Website konzipiert: der Betreiber der Website entscheidet frei über
    Darstellung, Filterung und Navigation.

    Die Schnittstelle ist ausschließlich lesend. Datenpflege erfolgt weiterhin in
    den bestehenden ProHolz-Systemen.

    **Wichtig für die Bewertung des Aufwands:** Der Abschnitt `Befüllungsgrad` in
    der Beschreibung der einzelnen Felder gibt an, wie viele Datensätze das Feld
    tatsächlich befüllt haben. Mehrere Felder existieren technisch, sind aber
    redaktionell noch leer.

  contact:
    name: MINDSTREAM GmbH — Christian Klar
    email: christian.klar@mindstream.at

servers:
  - url: https://wms.proholz-tirol.at/api/v1
    description: Produktion

tags:
  - name: Networks
    description: |
      Netzwerke und Branchengruppen — die oberste Gliederungsebene. Jedes Netzwerk
      kann Kategorien (Untergruppen) enthalten.
  - name: Companies
    description: Unternehmen mit Adresse, Kontaktdaten, Geokoordinaten und Netzwerkzugehörigkeiten.
  - name: Media
    description: Logos und redaktionelle Bilder, mit Größenanpassung über Query-Parameter.

security:
  - ApiKeyAuth: []

paths:

  /networks:
    get:
      tags: [Networks]
      summary: Alle Netzwerke
      description: |
        Liefert alle Netzwerke und Branchengruppen mit ihren Kategorien als Baum.

        Dieser Endpoint ist der empfohlene Einstiegspunkt: Wenn die Navigation der
        Website aus dieser Antwort aufgebaut wird, erscheint ein neu angelegtes
        Netzwerk automatisch, ohne dass am Website-Code etwas geändert werden muss.
      parameters:
        - name: includeEmpty
          in: query
          description: |
            Ob Netzwerke ohne zugeordnete Unternehmen mitgeliefert werden.
            Standard ist `false` — leere Netzwerke werden also ausgeblendet.
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: Liste aller Netzwerke.
          content:
            application/json:
              schema:
                type: object
                properties:
                  syncedAt:
                    $ref: '#/components/schemas/SyncedAt'
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Network'
              examples:
                default:
                  $ref: '#/components/examples/NetworkList'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /networks/{networkId}:
    get:
      tags: [Networks]
      summary: Ein Netzwerk
      description: Liefert ein einzelnes Netzwerk samt seiner Kategorien.
      parameters:
        - $ref: '#/components/parameters/NetworkId'
      responses:
        '200':
          description: Das Netzwerk.
          content:
            application/json:
              schema:
                type: object
                properties:
                  syncedAt:
                    $ref: '#/components/schemas/SyncedAt'
                  data:
                    $ref: '#/components/schemas/Network'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /networks/{networkId}/companies:
    get:
      tags: [Networks]
      summary: Unternehmen eines Netzwerks
      description: |
        Liefert alle Unternehmen, die dem Netzwerk oder einer seiner Kategorien
        zugeordnet sind.

        Entspricht `/companies?network={networkId}` und existiert als bequemere
        Schreibweise für den Regelfall.
      parameters:
        - $ref: '#/components/parameters/NetworkId'
        - $ref: '#/components/parameters/Category'
        - $ref: '#/components/parameters/Query'
        - $ref: '#/components/parameters/HasHomepage'
        - $ref: '#/components/parameters/Bbox'
        - $ref: '#/components/parameters/Fields'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          $ref: '#/components/responses/CompanyList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /companies:
    get:
      tags: [Companies]
      summary: Unternehmen suchen
      description: |
        Liefert Unternehmen, optional gefiltert. Ohne Filter werden alle
        Unternehmen der Datenbank zurückgegeben — das sind rund 2.700 Datensätze
        und etwa 2 MB. Für die Darstellung einzelner Netzwerke ist der Filter
        `network` deutlich sparsamer.
      parameters:
        - name: network
          in: query
          description: Auf ein oder mehrere Netzwerke einschränken.
          schema:
            type: array
            items:
              type: integer
          style: form
          explode: true
          example: [55]
        - $ref: '#/components/parameters/Category'
        - $ref: '#/components/parameters/Query'
        - $ref: '#/components/parameters/HasHomepage'
        - $ref: '#/components/parameters/Bbox'
        - $ref: '#/components/parameters/Fields'
        - $ref: '#/components/parameters/Page'
        - $ref: '#/components/parameters/PerPage'
      responses:
        '200':
          $ref: '#/components/responses/CompanyList'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /companies/{companyId}:
    get:
      tags: [Companies]
      summary: Ein Unternehmen
      description: |
        Liefert ein einzelnes Unternehmen mit allen Detaildaten — inklusive
        Beschreibungstext, Ansprechpartnern je Netzwerk, Bildergalerien und
        Innovationen, sofern vorhanden.
      parameters:
        - name: companyId
          in: path
          required: true
          schema:
            type: integer
          example: 491
      responses:
        '200':
          description: Das Unternehmen.
          content:
            application/json:
              schema:
                type: object
                properties:
                  syncedAt:
                    $ref: '#/components/schemas/SyncedAt'
                  data:
                    $ref: '#/components/schemas/Company'
              examples:
                default:
                  $ref: '#/components/examples/CompanyDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /media/logo/{companyId}:
    get:
      tags: [Media]
      summary: Firmenlogo
      description: |
        Liefert das Logo eines Unternehmens als Bilddatei.

        Diese URL muss nicht selbst zusammengesetzt werden — sie steht fertig im
        Feld `logoUrl` jedes Unternehmens. Die Parameter zur Größenanpassung
        können an die gelieferte URL angehängt werden.
      security: []
      parameters:
        - name: companyId
          in: path
          required: true
          schema:
            type: integer
          example: 19163
        - $ref: '#/components/parameters/MediaWidth'
        - $ref: '#/components/parameters/MediaHeight'
        - $ref: '#/components/parameters/MediaFit'
        - $ref: '#/components/parameters/MediaFormat'
      responses:
        '200':
          description: Die Bilddatei.
          content:
            image/webp: {}
            image/jpeg: {}
            image/png: {}
        '404':
          description: Kein Logo hinterlegt oder nicht zur Veröffentlichung freigegeben.

  /media/photo/{photoId}:
    get:
      tags: [Media]
      summary: Redaktionelles Bild
      description: |
        Liefert ein redaktionell gepflegtes Bild — Galeriebild, Foto eines
        Ansprechpartners oder Bild zu einer Innovation.

        Auch diese URLs stehen fertig in den jeweiligen Datensätzen und müssen
        nicht selbst gebaut werden. Die `photoId` trägt ein Präfix, das die
        Herkunft kennzeichnet (`ba-` Galerie, `ct-` Ansprechpartner,
        `in-` Innovation).
      security: []
      parameters:
        - name: photoId
          in: path
          required: true
          schema:
            type: string
          example: ba-102
        - $ref: '#/components/parameters/MediaWidth'
        - $ref: '#/components/parameters/MediaHeight'
        - $ref: '#/components/parameters/MediaFit'
        - $ref: '#/components/parameters/MediaFormat'
      responses:
        '200':
          description: Die Bilddatei.
          content:
            image/webp: {}
            image/jpeg: {}
            image/png: {}
        '404':
          description: Bild nicht gefunden.

components:

  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: |
        Jeder Konsument erhält einen eigenen Schlüssel. Er wird bei jedem Aufruf
        im Header `X-API-Key` mitgeschickt.

        Getrennte Schlüssel je Konsument haben zwei Gründe: ein Schlüssel lässt
        sich einzeln tauschen, ohne andere Anbindungen zu stören, und in den
        Logs ist nachvollziehbar, welches System welche Last erzeugt.

        Bildabrufe unter `/media/` benötigen keinen Schlüssel — sonst könnten die
        URLs nicht direkt in `img`-Tags verwendet werden.

  parameters:

    NetworkId:
      name: networkId
      in: path
      required: true
      description: ID des Netzwerks, wie von `/networks` geliefert.
      schema:
        type: integer
      example: 55

    Category:
      name: category
      in: query
      description: |
        Auf eine oder mehrere Kategorien innerhalb der Netzwerke einschränken,
        etwa nur die Zimmereien innerhalb der Holzclusterpartner.
      schema:
        type: array
        items:
          type: integer
      style: form
      explode: true

    Query:
      name: q
      in: query
      description: |
        Volltextsuche über Firmenname, Ort und Postleitzahl.
        Mindestens zwei Zeichen.
      schema:
        type: string
        minLength: 2
      example: Zimmerei

    HasHomepage:
      name: hasHomepage
      in: query
      description: |
        Auf Unternehmen einschränken, die eine veröffentlichte Website haben.
        Nützlich, wenn die Darstellung primär als Verteiler auf die Firmenseiten
        gedacht ist.
      schema:
        type: boolean

    Bbox:
      name: bbox
      in: query
      description: |
        Auf einen Kartenausschnitt einschränken, als
        `minLng,minLat,maxLng,maxLat`.
      schema:
        type: string
      example: "10.0,46.6,13.0,47.8"

    Fields:
      name: fields
      in: query
      description: |
        Kommaseparierte Liste der gewünschten Felder. Ohne Angabe werden alle
        Felder geliefert. Reduziert die Antwortgröße bei Listenansichten
        erheblich.
      schema:
        type: string
      example: id,name,city,logoUrl,homepageUrl

    Page:
      name: page
      in: query
      schema:
        type: integer
        minimum: 1
        default: 1

    PerPage:
      name: perPage
      in: query
      description: Maximal 500.
      schema:
        type: integer
        minimum: 1
        maximum: 500
        default: 100

    MediaWidth:
      name: w
      in: query
      description: Zielbreite in Pixeln.
      schema:
        type: integer
        minimum: 1
        maximum: 2000

    MediaHeight:
      name: h
      in: query
      description: Zielhöhe in Pixeln.
      schema:
        type: integer
        minimum: 1
        maximum: 2000

    MediaFit:
      name: fit
      in: query
      description: |
        `contain` behält das Seitenverhältnis und beschneidet nicht — richtig für
        Logos. `cover` füllt die angegebene Fläche vollständig und beschneidet
        dafür — richtig für Bildkacheln in einem Raster.
      schema:
        type: string
        enum: [contain, cover]
        default: contain

    MediaFormat:
      name: format
      in: query
      description: |
        `auto` liefert WebP, wenn der Browser es akzeptiert, sonst das
        Originalformat.
      schema:
        type: string
        enum: [auto, jpeg, png, webp]
        default: auto

  schemas:

    SyncedAt:
      type: string
      format: date-time
      description: |
        Zeitpunkt, zu dem die Stammdaten zuletzt aus der ProHolz-Datenbank
        übernommen wurden. Siehe Abschnitt zur Aktualität.
      example: "2026-08-22T09:00:00+02:00"

    Network:
      type: object
      description: |
        Ein Netzwerk oder eine Branchengruppe — etwa `Holzclusterpartner` oder
        `Netzwerk Zirbe`.
      properties:
        id:
          type: integer
          example: 55
        name:
          type: string
          example: Holzclusterpartner
        companyCount:
          type: integer
          description: Anzahl der zugeordneten Unternehmen, inklusive aller Kategorien.
          example: 130
        categories:
          type: array
          description: Untergliederung des Netzwerks. Kann leer sein.
          items:
            $ref: '#/components/schemas/Category'

    Category:
      type: object
      description: |
        Eine Untergruppe innerhalb eines Netzwerks — etwa `Zimmereien` innerhalb
        der Holzclusterpartner.
      properties:
        id:
          type: integer
          example: 59
        name:
          type: string
          example: Zimmereien
        companyCount:
          type: integer
          example: 24

    Company:
      type: object
      properties:
        id:
          type: integer
          example: 491
        name:
          type: string
          example: Feldkircher GmbH
        description:
          type: [string, "null"]
          description: |
            Redaktioneller Beschreibungstext als HTML.

            **Befüllungsgrad: sehr gering.** Über alle Netzwerke hinweg sind
            derzeit nur zwei Unternehmen mit einem Text hinterlegt. Das Feld ist
            vorbereitet, die Inhalte müssten redaktionell erfasst werden.
        street:
          type: [string, "null"]
          example: Dr. Walter-Zumtobel-Straße 3
        zipcode:
          type: [string, "null"]
          example: "6850"
        city:
          type: [string, "null"]
          example: Dornbirn
        country:
          type: [string, "null"]
          example: Österreich
        latitude:
          type: [number, "null"]
          description: "**Befüllungsgrad: vollständig** — alle Unternehmen sind geokodiert."
          example: 47.433591
        longitude:
          type: [number, "null"]
          example: 9.746341
        phone:
          type: [string, "null"]
          description: |
            Nur wenn das Unternehmen die Veröffentlichung freigegeben hat.
            **Befüllungsgrad in den Netzwerken: hoch** (Holzclusterpartner 121
            von 130, Netzwerk Zirbe 62 von 63).
          example: "+43 5572 58356"
        email:
          type: [string, "null"]
          description: Nur bei Freigabe durch das Unternehmen.
        homepage:
          type: [string, "null"]
          description: Adresse ohne Protokoll, für die Anzeige.
          example: www.hubert-feldkircher.at
        homepageUrl:
          type: [string, "null"]
          description: |
            Vollständige URL, für das `href`-Attribut.
            **Befüllungsgrad in den Netzwerken: hoch** (Holzclusterpartner 112
            von 130, Netzwerk Zirbe 61 von 63). Ausnahmen sind `Design in Tirol`
            (3 von 17) und `Netzwerk HolzBauPlanung` (3 von 8).
          example: http://www.hubert-feldkircher.at
        logoUrl:
          type: [string, "null"]
          description: |
            Vollständige URL zum Firmenlogo. Größenparameter können angehängt
            werden, siehe Abschnitt Media.
            **Befüllungsgrad in den Netzwerken: hoch** (Holzclusterpartner 106
            von 130, Netzwerk Zirbe 60 von 63).
          example: https://wms.proholz-tirol.at/api/v1/media/logo/19163
        numberEmployees:
          type: [integer, "null"]
          description: |
            **Befüllungsgrad: leer.** Das Feld wird derzeit in keinem Datensatz
            zur Veröffentlichung freigegeben.
        trainees:
          type: [boolean, "null"]
          description: Ob es sich um einen Lehrbetrieb handelt, sofern freigegeben.
        memberships:
          type: array
          description: |
            Die Netzwerkzugehörigkeiten des Unternehmens. Ein Unternehmen kann in
            mehreren Netzwerken vertreten sein — und pro Zugehörigkeit eigene
            redaktionelle Inhalte haben.
          items:
            $ref: '#/components/schemas/Membership'
        innovations:
          type: array
          description: |
            **Befüllungsgrad: sehr gering** — 13 Unternehmen insgesamt, im
            Wesentlichen im Netzwerk `Innovationslandkarte`.
          items:
            $ref: '#/components/schemas/Innovation'

    Membership:
      type: object
      description: |
        Die Zugehörigkeit eines Unternehmens zu einem Netzwerk beziehungsweise
        einer Kategorie. Träger der netzwerkspezifischen redaktionellen Inhalte:
        dasselbe Unternehmen kann im Netzwerk Zirbe anders dargestellt werden als
        bei den Holzclusterpartnern.
      properties:
        id:
          type: integer
          example: 2617
        networkId:
          type: integer
          example: 69
        networkName:
          type: string
          example: Netzwerk Zirbe
        categoryId:
          type: [integer, "null"]
          example: 73
        categoryName:
          type: [string, "null"]
          example: Tischler & Innenraumgestaltung
        entryDate:
          type: [string, "null"]
          format: date
          description: Datum des Beitritts, sofern freigegeben.
        description:
          type: [string, "null"]
          description: |
            Netzwerkspezifischer Text als HTML.
            **Befüllungsgrad: gering** — rund 100 Zuordnungen insgesamt.
        contactName:
          type: [string, "null"]
          example: Hubert Feldkircher
        contactFunction:
          type: [string, "null"]
          example: Geschäftsinhaber und Tischlermeister
        contactStatement:
          type: [string, "null"]
          description: Zitat des Ansprechpartners als HTML.
        contactImageUrl:
          type: [string, "null"]
          description: |
            Foto des Ansprechpartners, vollständige URL mit Größenparametern.
            **Befüllungsgrad: gering** — im Netzwerk Zirbe 7 von 63, bei den
            Holzclusterpartnern 10 von 130.
        images:
          type: array
          description: Bildergalerie zu dieser Netzwerkzugehörigkeit.
          items:
            $ref: '#/components/schemas/Image'

    Image:
      type: object
      properties:
        id:
          type: string
          example: ba-102
        url:
          type: string
          example: https://wms.proholz-tirol.at/api/v1/media/photo/ba-102
        title:
          type: [string, "null"]
        copyright:
          type: [string, "null"]
          description: Sofern hinterlegt, bei der Anzeige auszuweisen.

    Innovation:
      type: object
      properties:
        id:
          type: integer
        type:
          type: string
          enum: [product_service, process, culture]
          description: |
            Produkt- beziehungsweise Dienstleistungsinnovation, Prozessinnovation
            oder Kulturinnovation.
        title:
          type: string
        description:
          type: [string, "null"]
          description: HTML.
        images:
          type: array
          items:
            $ref: '#/components/schemas/Image'

    Pagination:
      type: object
      properties:
        page:
          type: integer
        perPage:
          type: integer
        total:
          type: integer
        totalPages:
          type: integer

    Error:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
              example: invalid_parameter
            message:
              type: string
              example: "Parameter 'bbox' ist nicht im Format minLng,minLat,maxLng,maxLat."

  responses:

    CompanyList:
      description: Liste der Unternehmen.
      content:
        application/json:
          schema:
            type: object
            properties:
              syncedAt:
                $ref: '#/components/schemas/SyncedAt'
              pagination:
                $ref: '#/components/schemas/Pagination'
              data:
                type: array
                items:
                  $ref: '#/components/schemas/Company'
          examples:
            default:
              $ref: '#/components/examples/CompanyList'

    BadRequest:
      description: Ein Parameter ist ungültig.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    Unauthorized:
      description: Schlüssel fehlt oder ist ungültig.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

    NotFound:
      description: Die angeforderte Ressource existiert nicht.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

  examples:

    NetworkList:
      summary: Auszug aus der Netzwerkliste
      value:
        syncedAt: "2026-08-22T09:00:00+02:00"
        data:
          - id: 55
            name: Holzclusterpartner
            companyCount: 130
            categories:
              - { id: 56, name: Forstwirtschaft, companyCount: 9 }
              - { id: 57, name: "Säge- & Holzindustrie", companyCount: 21 }
              - { id: 58, name: Holzhandel, companyCount: 11 }
              - { id: 59, name: Zimmereien, companyCount: 24 }
          - id: 69
            name: Netzwerk Zirbe
            companyCount: 63
            categories:
              - { id: 73, name: "Tischler & Innenraumgestaltung", companyCount: 18 }
              - { id: 74, name: Saunabau, companyCount: 6 }

    CompanyList:
      summary: Unternehmen eines Netzwerks, auf Listenfelder reduziert
      value:
        syncedAt: "2026-08-22T09:00:00+02:00"
        pagination: { page: 1, perPage: 100, total: 63, totalPages: 1 }
        data:
          - id: 491
            name: Feldkircher GmbH
            city: Dornbirn
            zipcode: "6850"
            latitude: 47.433591
            longitude: 9.746341
            homepageUrl: http://www.hubert-feldkircher.at
            logoUrl: https://wms.proholz-tirol.at/api/v1/media/logo/19163

    CompanyDetail:
      summary: Ein Unternehmen mit redaktionellen Inhalten
      value:
        syncedAt: "2026-08-22T09:00:00+02:00"
        data:
          id: 491
          name: Feldkircher GmbH
          description: "<p>In unserer Manufaktur entstehen mit viel Enthusiasmus unsere Massivholzmöbel …</p>"
          street: Dr. Walter-Zumtobel-Straße 3
          zipcode: "6850"
          city: Dornbirn
          country: Österreich
          latitude: 47.433591
          longitude: 9.746341
          phone: "+43 5572 58356"
          email: info@hubert-feldkircher.at
          homepage: www.hubert-feldkircher.at
          homepageUrl: http://www.hubert-feldkircher.at
          logoUrl: https://wms.proholz-tirol.at/api/v1/media/logo/19163
          numberEmployees: null
          trainees: false
          memberships:
            - id: 2617
              networkId: 69
              networkName: Netzwerk Zirbe
              categoryId: 73
              categoryName: "Tischler & Innenraumgestaltung"
              entryDate: null
              description: null
              contactName: Hubert Feldkircher
              contactFunction: Geschäftsinhaber und Tischlermeister
              contactStatement: "<p>Massivholzmöbel, versehen mit natürlichen Oberflächen, regulieren Ihr Raumklima …</p>"
              contactImageUrl: https://wms.proholz-tirol.at/api/v1/media/photo/ct-2617
              images:
                - id: ba-102
                  url: https://wms.proholz-tirol.at/api/v1/media/photo/ba-102
                  title: null
                  copyright: null
          innovations: []
