# BPP Partner API, OpenAPI-3.1-Referenzspezifikation.
# Gehört zu bpp-partner-api-guidelines.md.
openapi: 3.1.0

info:
  title: Anbindung g&o brokernet Plattform (BPP)
  version: 1.7.2
  # x-logo: wird von Scalar/Redoc als Logo in der Seitenleiste angezeigt (Klick führt zur Firmenseite).
  x-logo:
    url: https://go-brokernet.at/wp-content/uploads/2025/04/Logo_guo.png
    altText: g&o brokernet GmbH
    href: https://go-brokernet.at
  summary: API für die Integration von Partnern in die BPP-Plattform.
  description: |
    <a href="https://lipsum-soft.com/"><img src="https://lipsum-soft.com/assets/img/lipsum-logo.png" alt="Lipsum-Soft" width="240"></a>

    Bereitgestellt von der [Lipsum-Soft GmbH](https://lipsum-soft.com/) &middot; [lipsum-soft.com](https://lipsum-soft.com/)

    Diese Spezifikation ist ein Leitfaden, wie sich externe CRM-Systeme und andere Partnerlösungen in die g&o brokernet Plattform (BPP) integrieren können. 
    Sie beschreibt die Schnittstellen, die für den Import von Kunden, Verträgen, Schäden, Dokumenten und anderen relevanten Daten erforderlich sind.
  
  contact:
    name: Dev-Team
    url: https://lipsum-soft.com/
    email: contact@lipsum-soft.com

# Ein Eintrag pro Umgebung, damit generierte Clients ohne Nachbearbeitung funktionieren.
servers:
  - url: https://sandbox.api.provider.example/partner/v1
    description: Sandbox, getrennt von der Produktion, mit zurücksetzbaren Testdaten. Alle Lese- und Schreibaufrufe lassen sich hier ohne Risiko testen.
  - url: https://api.provider.example/partner/v1
    description: Produktion.

security:
  - oauth2ClientCredentials: []

tags:
  - name: meta
    description: Version, Health-Check und Betrieb. Ohne Anmeldung aufrufbar.
  - name: catalogs
    description: Wertelisten, die der Mandant pflegt. Gelten für den ganzen Mandanten.
  - name: customers
    description: Kunden mit Adressen, Bankkonten, Kontaktinfos und Risiken. Lesen, anlegen und ändern
  - name: contracts
    description: Verträge mit ihren Sparten. Lesen, anlegen und ändern.
  - name: claims
    description: Schäden lesen und anlegen.
  - name: documents
    description: Dokument-Metadaten, Download und Upload.
  - name: deletions
    description: Feed gelöschter und zusammengeführter Datensätze. Pflicht, wenn Löschungen nur daran erkennbar sind, dass ein Eintrag in der Liste fehlt; bei Soft-Delete empfohlen.

paths:

  /meta/version:
    get:
      tags: [meta]
      operationId: getVersion
      summary: API- und Spec-Version
      description: Ohne Anmeldung aufrufbar.
      security: []
      responses:
        "200":
          description: Aktuelle Versionen.
          content:
            application/json:
              schema:
                type: object
                required: [apiVersion, specVersion]
                properties:
                  apiVersion:
                    type: string
                    description: Version der laufenden API (Semver).
                    examples: ["1.4.2"]
                  specVersion:
                    type: string
                    description: Version des veröffentlichten OpenAPI-Dokuments (Semver).
                    examples: ["1.4.0"]
                  deprecations:
                    type: array
                    description: Angekündigte Deprecations mit Sunset-Datum.
                    items:
                      type: object
                      required: [feature, sunsetAt]
                      properties:
                        feature: { type: string }
                        sunsetAt: { type: string, format: date }

  /health:
    get:
      tags: [meta]
      operationId: getHealth
      summary: Health-Check
      description: Liveness und Readiness. Ohne Anmeldung aufrufbar, liefert keine Kundendaten.
      security: []
      responses:
        "200":
          description: Service läuft (bei `degraded` mit Details).
          content:
            application/json:
              schema:
                type: object
                required: [status]
                properties:
                  status:
                    type: string
                    enum: [up, degraded, maintenance]
                  subsystems:
                    type: object
                    additionalProperties:
                      type: string
                      enum: [up, degraded, down]
                  maintenanceWindow:
                    type: object
                    description: Angekündigte Wartung, etwa zum Prüfen vor einem Bulk-Lauf.
                    properties:
                      from: { type: string, format: date-time }
                      until: { type: string, format: date-time }

  /catalogs:
    get:
      tags: [catalogs]
      operationId: listCatalogDomains
      summary: Kataloge auflisten
      description: Gilt für den ganzen Mandanten.
      responses:
        "200":
          description: Alle Kataloge dieser API.
          content:
            application/json:
              schema:
                type: object
                required: [items]
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      required: [domain, updatedAt, version]
                      properties:
                        domain:
                          type: string
                          description: Schlüssel für `/catalogs/{domain}`.
                          examples: [documentTypes, contractStatuses, claimStatuses, productTypes, insurers, participantRoles, sparten, riskTypes, damageCauses, relationRoles, legalForms, regions]
                        updatedAt: { type: string, format: date-time }
                        version: { type: integer }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        default: { $ref: "#/components/responses/Problem" }

  /catalogs/{domain}:
    get:
      tags: [catalogs]
      operationId: getCatalog
      summary: Katalog abrufen
      description: |
        Liefert alle Werte eines Katalogs. Kataloge gelten für den ganzen Mandanten.

        Clients sollten mit dem stabilen `code` arbeiten, nicht mit dem Anzeigetext. Deaktivierte Werte bleiben
        mit `active: false` in der Liste.
      parameters:
        - name: domain
          in: path
          required: true
          schema: { type: string, maxLength: 64 }
      responses:
        "200":
          description: Katalogwerte samt `updatedAt` und `version` des Katalogs.
          content:
            application/json:
              schema:
                type: object
                required: [domain, updatedAt, version, values]
                properties:
                  domain: { type: string }
                  updatedAt: { type: string, format: date-time }
                  version: { type: integer }
                  values:
                    type: array
                    items: { $ref: "#/components/schemas/CatalogValue" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        default: { $ref: "#/components/responses/Problem" }

  /customers:
    get:
      tags: [customers]
      operationId: listCustomers
      summary: Kunden suchen und auflisten
      description: |
        Liefert Kunden.

        Alle Suchparameter sind optional; gibt man mehrere an,
        müssen alle zutreffen. Ohne Parameter kommt die erste Seite aller Kunden.

        Mit `updatedSince` ist das der Delta-Sync für Kunden. Er erfasst auch Änderungen an Adressen,
        Bankkonten und Kontaktinfos, weil diese `metadata.updatedAt` des Kunden erhöhen.
      parameters:
        - $ref: "#/components/parameters/UpdatedSince"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
        - name: query
          in: query
          description: Freitextsuche über Name und Kundennummer.
          schema: { type: string, maxLength: 256 }
        - name: customerNumber
          in: query
          description: Filter auf die Kundennummer, nur genaue Treffer.
          schema: { type: string, maxLength: 32 }
        - name: lastName
          in: query
          description: Filter auf den Nachnamen.
          schema: { type: string, maxLength: 256 }
        - name: firstName
          in: query
          description: Filter auf den Vornamen.
          schema: { type: string, maxLength: 256 }
        - name: birthDate
          in: query
          description: Filter auf das Geburtsdatum.
          schema: { type: string, format: date }
        - name: email
          in: query
          description: Filter auf eine E-Mail-Adresse aus den Kontaktinfos.
          schema: { type: string, format: email, maxLength: 256 }
        - name: iban
          in: query
          description: Filter auf eine IBAN aus den Bankkonten.
          schema: { type: string, maxLength: 34 }
      responses:
        "200":
          description: Sortiert nach `updatedAt`, dann `id`. Jeder Eintrag enthält dieselben Felder wie der Detail-Endpoint.
          headers:
            RateLimit-Remaining: { $ref: "#/components/headers/RateLimit-Remaining" }
          content:
            application/json:
              schema:
                type: object
                required: [items, page]
                properties:
                  items:
                    type: array
                    items: { $ref: "#/components/schemas/Customer" }
                  page: { $ref: "#/components/schemas/PageInfo" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "429": { $ref: "#/components/responses/Problem429" }
        default: { $ref: "#/components/responses/Problem" }
    post:
      tags: [customers]
      operationId: createCustomer
      summary: Kunde anlegen
      description: |
        Legt einen Kunden an, optional gleich mit Adressen, Bankkonten und Kontaktinfos. Der Kunde gehört
        danach dem angemeldeten Benutzer.

        Das Anlegen ist atomar: Wird ein Eintrag abgelehnt, wird gar nichts angelegt. Weitere Einträge kommen
        später über die Unter-Endpoints dazu (`POST /customers/{customerId}/addresses` usw.).

        `customerType` bestimmt, welche Felder erlaubt sind. Privatkunden brauchen `lastName`, Firmenkunden
        `companyName`. Ein Feld, das nicht zum Typ passt, führt zu 422 `customer.typeFieldMismatch`.

        Werte werden in derselben Schreibweise angenommen, in der `GET /customers/{customerId}` sie liefert.
        `legalFormCode` kommt aus `/catalogs/legalForms`, `regionCode` aus `/catalogs/regions` und muss zum
        `countryCode` der Adresse passen. Unbekannte oder deaktivierte Codes führen zu 422; die Antwort nennt
        den Wert und die erlaubten Codes.

        Weitere Prüfungen (jeweils 422):
        - mehr als ein Eintrag mit `isDefault: true` in einer Liste: `customer.multipleDefaults`
        - IBAN mit falscher Prüfziffer: `bankAccount.invalidIban`
        - dieselbe IBAN zweimal: `bankAccount.duplicateIban`
        - Kontaktinfo ohne `email` und `phone`: `contactInfo.empty`

        Jeder Fehler ist ein eigener Eintrag in `errors[]`. `field` enthält den Pfad im Body samt Index,
        z. B. `bankAccounts[1].iban`.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CustomerCreate" }
      responses:
        "201":
          description: |
            Kunde angelegt, mit allen vergebenen ids. Adressen, Bankkonten und Kontaktinfos stehen in derselben
            Reihenfolge wie im Request, damit sich die neuen ids zuordnen lassen. Danach zählt nur noch die id.
            Der Kunde ist sofort per id und über `GET /customers` lesbar.
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Customer" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "422": { $ref: "#/components/responses/Problem422" }
        "429": { $ref: "#/components/responses/Problem429" }
        default: { $ref: "#/components/responses/Problem" }

  /customers/{customerId}:
    get:
      tags: [customers]
      operationId: getCustomer
      summary: Kunde abrufen
      description: Kunde per Id abrufen.
      parameters:
        - $ref: "#/components/parameters/CustomerId"
      responses:
        "200":
          description: Kunde.
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Customer" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        "410": { $ref: "#/components/responses/Problem410" }
        default: { $ref: "#/components/responses/Problem" }
    put:
      tags: [customers]
      operationId: updateCustomer
      summary: Kunde ändern
      description: |
        Partielles Update: Ein Feld, das fehlt, bleibt unverändert. Ein Feld mit `null` wird gelöscht.

        `id` und `customerType` lassen sich nicht ändern. Aus einem Privatkunden wird kein Firmenkunde und
        umgekehrt.

        Adressen, Bankkonten und Kontaktinfos gehören nicht zum Body und bleiben bei diesem Aufruf unverändert.
        Sie werden über ihre eigenen Endpoints angelegt und geändert.

        Je nach gespeichertem `customerType` lässt sich `lastName` (Privatkunde) bzw. `companyName`
        (Firmenkunde) nicht auf `null` setzen (422). Ein Feld, das nicht zum Typ passt, führt zu 422
        `customer.typeFieldMismatch`. Katalogprüfung wie beim Anlegen.
      parameters:
        - $ref: "#/components/parameters/CustomerId"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/CustomerUpdate" }
      responses:
        "200":
          description: Geänderter Kunde, sofort lesbar.
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Customer" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        "410": { $ref: "#/components/responses/Problem410" }
        "412": { $ref: "#/components/responses/Problem412" }
        "422": { $ref: "#/components/responses/Problem422" }
        "429": { $ref: "#/components/responses/Problem429" }
        default: { $ref: "#/components/responses/Problem" }

  /customers/{customerId}/addresses:
    post:
      tags: [customers]
      operationId: createCustomerAddress
      summary: Adresse anlegen
      description: |
        Legt eine Adresse für einen Kunden an.

        Mit `isDefault: true` wird die neue Adresse zur Default-Adresse, die bisherige verliert den Status.
        Ohne `isDefault: true` bleibt der Default, wie er ist; der Server setzt ihn nie von sich aus.

        Ein unbekannter `countryCode` führt zu 422. `regionCode` muss ein aktiver Code aus `/catalogs/regions`
        sein und zu `countryCode` passen, sonst 422.

        Das Anlegen erhöht `metadata.updatedAt` des Kunden.
      parameters:
        - $ref: "#/components/parameters/CustomerId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AddressCreate" }
      responses:
        "201":
          description: Adresse angelegt, sofort per id und im Kunden lesbar.
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Address" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        "410": { $ref: "#/components/responses/Problem410" }
        "422": { $ref: "#/components/responses/Problem422" }
        "429": { $ref: "#/components/responses/Problem429" }
        default: { $ref: "#/components/responses/Problem" }

  /customers/{customerId}/addresses/{addressId}:
    get:
      tags: [customers]
      operationId: getCustomerAddress
      summary: Adresse abrufen
      description: |
        Liefert eine Adresse per id, auch wenn sie nicht die Default-Adresse ist. Nur für Kunden des
        angemeldeten Benutzers.
      parameters:
        - $ref: "#/components/parameters/CustomerId"
        - name: addressId
          in: path
          required: true
          schema: { type: string, maxLength: 64 }
      responses:
        "200":
          description: Adresse.
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Address" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        "410": { $ref: "#/components/responses/Problem410" }
        default: { $ref: "#/components/responses/Problem" }
    put:
      tags: [customers]
      operationId: updateCustomerAddress
      summary: Adresse ändern
      description: |
        Ändert eine Adresse eines Kunden. Partielles Update: Ein fehlendes Feld
        bleibt unverändert, `null` löscht den Wert. `street`, `postalCode`, `city` und `countryCode` lassen sich
        nicht löschen (422).

        Gehört `{addressId}` nicht zu `{customerId}`, kommt 404 `resource.notFound` und nichts wird geändert.

        `isDefault: true` macht die Adresse zur Default-Adresse und nimmt der bisherigen den Status.
        `isDefault: false` nimmt ihr den Status, der Kunde hat dann keine Default-Adresse.

        Einen DELETE-Endpoint gibt es nicht. Eine veraltete Adresse bleibt als Eintrag ohne Default bestehen.

        `regionCode` muss auch nach der Änderung zu `countryCode` passen. Die Änderung erhöht
        `metadata.updatedAt` des Kunden.
      parameters:
        - $ref: "#/components/parameters/CustomerId"
        - name: addressId
          in: path
          required: true
          schema: { type: string, maxLength: 64 }
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AddressUpdate" }
      responses:
        "200":
          description: Geänderte Adresse, sofort per id und im Kunden lesbar.
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Address" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        "410": { $ref: "#/components/responses/Problem410" }
        "412": { $ref: "#/components/responses/Problem412" }
        "422": { $ref: "#/components/responses/Problem422" }
        "429": { $ref: "#/components/responses/Problem429" }
        default: { $ref: "#/components/responses/Problem" }

  /customers/{customerId}/bank-accounts:
    post:
      tags: [customers]
      operationId: createCustomerBankAccount
      summary: Bankkonto anlegen
      description: |
        Legt ein Bankkonto für einen Kunden an.

        `iban` im elektronischen Format (Großbuchstaben, ohne Leerzeichen). Eine IBAN mit falscher Prüfziffer
        führt zu 422 `bankAccount.invalidIban`. Hat der Kunde schon ein Konto mit dieser IBAN, kommt 422
        `bankAccount.duplicateIban`.

        Mit `isDefault: true` wird das neue Konto zum Default-Konto, das bisherige verliert den Status. Ohne
        `isDefault: true` bleibt der Default, wie er ist; der Server setzt ihn nie von sich aus.

        Das Anlegen erhöht `metadata.updatedAt` des Kunden.
      parameters:
        - $ref: "#/components/parameters/CustomerId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BankAccountCreate" }
      responses:
        "201":
          description: Bankkonto angelegt, sofort per id und im Kunden lesbar.
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BankAccount" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        "410": { $ref: "#/components/responses/Problem410" }
        "422": { $ref: "#/components/responses/Problem422" }
        "429": { $ref: "#/components/responses/Problem429" }
        default: { $ref: "#/components/responses/Problem" }

  /customers/{customerId}/bank-accounts/{bankAccountId}:
    get:
      tags: [customers]
      operationId: getCustomerBankAccount
      summary: Bankkonto abrufen
      description: |
        Liefert ein Bankkonto per id, auch wenn es nicht das Default-Konto ist. Nur für Kunden des
        angemeldeten Benutzers.
      parameters:
        - $ref: "#/components/parameters/CustomerId"
        - name: bankAccountId
          in: path
          required: true
          schema: { type: string, maxLength: 64 }
      responses:
        "200":
          description: Bankkonto.
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BankAccount" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        "410": { $ref: "#/components/responses/Problem410" }
        default: { $ref: "#/components/responses/Problem" }
    put:
      tags: [customers]
      operationId: updateCustomerBankAccount
      summary: Bankkonto ändern
      description: |
        Ändert ein Bankkonto eines Kunden. Partielles Update: Ein fehlendes Feld
        bleibt unverändert, `null` löscht den Wert.

        `iban` lässt sich nicht ändern. Eine andere IBAN ist ein anderes Konto und wird neu angelegt. Gehört
        `{bankAccountId}` nicht zu `{customerId}`, kommt 404 `resource.notFound` und nichts wird geändert.

        `isDefault: true` macht das Konto zum Default-Konto und nimmt dem bisherigen den Status.
        `isDefault: false` nimmt ihm den Status, der Kunde hat dann kein Default-Konto.

        Einen DELETE-Endpoint gibt es nicht. Ein nicht mehr genutztes Konto bleibt als Eintrag ohne Default
        bestehen. Die Änderung erhöht `metadata.updatedAt` des Kunden.
      parameters:
        - $ref: "#/components/parameters/CustomerId"
        - name: bankAccountId
          in: path
          required: true
          schema: { type: string, maxLength: 64 }
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/BankAccountUpdate" }
      responses:
        "200":
          description: Geändertes Bankkonto, sofort per id und im Kunden lesbar.
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/BankAccount" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        "410": { $ref: "#/components/responses/Problem410" }
        "412": { $ref: "#/components/responses/Problem412" }
        "422": { $ref: "#/components/responses/Problem422" }
        "429": { $ref: "#/components/responses/Problem429" }
        default: { $ref: "#/components/responses/Problem" }

  /customers/{customerId}/contact-infos:
    post:
      tags: [customers]
      operationId: createCustomerContactInfo
      summary: Kontaktinfo anlegen
      description: |
        Legt eine Kontaktinfo für einen Kunden an.

        Mindestens eines von `email` und `phone` muss gesetzt sein, sonst 422 `contactInfo.empty`. Eine
        ungültige E-Mail-Adresse führt zu 422 mit Fehler auf `email`.

        Mit `isDefault: true` wird die neue Kontaktinfo zum Default, die bisherige verliert den Status. Ohne
        `isDefault: true` bleibt der Default, wie er ist; der Server setzt ihn nie von sich aus.

        Das Anlegen erhöht `metadata.updatedAt` des Kunden.
      parameters:
        - $ref: "#/components/parameters/CustomerId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContactInfoCreate" }
      responses:
        "201":
          description: Kontaktinfo angelegt, sofort per id und im Kunden lesbar.
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ContactInfo" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        "410": { $ref: "#/components/responses/Problem410" }
        "422": { $ref: "#/components/responses/Problem422" }
        "429": { $ref: "#/components/responses/Problem429" }
        default: { $ref: "#/components/responses/Problem" }

  /customers/{customerId}/contact-infos/{contactInfoId}:
    get:
      tags: [customers]
      operationId: getCustomerContactInfo
      summary: Kontaktinfo abrufen
      description: |
        Liefert eine Kontaktinfo per id, auch wenn sie nicht der Default ist. Nur für Kunden des angemeldeten
        Benutzers.
      parameters:
        - $ref: "#/components/parameters/CustomerId"
        - name: contactInfoId
          in: path
          required: true
          schema: { type: string, maxLength: 64 }
      responses:
        "200":
          description: Kontaktinfo.
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ContactInfo" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        "410": { $ref: "#/components/responses/Problem410" }
        default: { $ref: "#/components/responses/Problem" }
    put:
      tags: [customers]
      operationId: updateCustomerContactInfo
      summary: Kontaktinfo ändern
      description: |
        Ändert eine Kontaktinfo eines Kunden. Partielles Update: Ein fehlendes Feld
        bleibt unverändert, `null` löscht den Wert. Danach muss mindestens eines von `email` und `phone`
        gesetzt sein, sonst 422 `contactInfo.empty`.

        Gehört `{contactInfoId}` nicht zu `{customerId}`, kommt 404 `resource.notFound` und nichts wird
        geändert.

        `isDefault: true` macht die Kontaktinfo zum Default und nimmt der bisherigen den Status.
        `isDefault: false` nimmt ihr den Status, der Kunde hat dann keine Default-Kontaktinfo.

        Einen DELETE-Endpoint gibt es nicht. Eine veraltete Kontaktinfo bleibt als Eintrag ohne Default
        bestehen. Die Änderung erhöht `metadata.updatedAt` des Kunden.
      parameters:
        - $ref: "#/components/parameters/CustomerId"
        - name: contactInfoId
          in: path
          required: true
          schema: { type: string, maxLength: 64 }
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContactInfoUpdate" }
      responses:
        "200":
          description: Geänderte Kontaktinfo, sofort per id und im Kunden lesbar.
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ContactInfo" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        "410": { $ref: "#/components/responses/Problem410" }
        "412": { $ref: "#/components/responses/Problem412" }
        "422": { $ref: "#/components/responses/Problem422" }
        "429": { $ref: "#/components/responses/Problem429" }
        default: { $ref: "#/components/responses/Problem" }

  /customers/{customerId}/risks:
    get:
      tags: [customers]
      operationId: listCustomerRisks
      summary: Risiken eines Kunden auflisten
      description: |
        Vertragssparten verweisen über `ContractSparte.riskIds` auf
        diese Risiken. Mit `updatedSince` ist das der Delta-Sync für Risiken.
      parameters:
        - $ref: "#/components/parameters/CustomerId"
        - $ref: "#/components/parameters/UpdatedSince"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Eine Seite Risiken.
          content:
            application/json:
              schema:
                type: object
                required: [items, page]
                properties:
                  items:
                    type: array
                    items: { $ref: "#/components/schemas/Risk" }
                  page: { $ref: "#/components/schemas/PageInfo" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        default: { $ref: "#/components/responses/Problem" }
    post:
      tags: [customers]
      operationId: createCustomerRisk
      summary: Risiko anlegen
      description: |
        Legt ein Risiko für einen Kunden an. `typeCode` muss ein Code aus
        `/catalogs/riskTypes` sein, sonst 422 mit den erlaubten Codes.

        Bei `typeCode: person` verweist `personId` auf einen Kunden mit dieser id beim Anbieter; eine eigene
        Personen-Ressource gibt es nicht. `displayName` und `birthDate` der Person stehen direkt im Risiko.
      parameters:
        - $ref: "#/components/parameters/CustomerId"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/RiskCreate" }
      responses:
        "201":
          description: Risiko angelegt, sofort über `GET /customers/{customerId}/risks` lesbar.
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Risk" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        "422": { $ref: "#/components/responses/Problem422" }
        "429": { $ref: "#/components/responses/Problem429" }
        default: { $ref: "#/components/responses/Problem" }

  /customers/{customerId}/risks/{riskId}:
    put:
      tags: [customers]
      operationId: updateCustomerRisk
      summary: Risiko ändern
      description: |
        Ändert ein Risiko eines Kunden. Partielles Update: Ein fehlendes Feld
        bleibt unverändert, `null` löscht den Wert.

        `typeCode` lässt sich nicht ändern. Für einen anderen Typ ein neues Risiko anlegen und das alte mit
        `isActive: false` stilllegen. Katalogprüfung wie beim Anlegen.

        Ein Detail-GET für Risiken gibt es nicht, den aktuellen Stand liefert `GET /customers/{customerId}/risks`.
      parameters:
        - $ref: "#/components/parameters/CustomerId"
        - name: riskId
          in: path
          required: true
          schema: { type: string, maxLength: 64 }
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/RiskUpdate" }
      responses:
        "200":
          description: Geändertes Risiko, sofort über `GET /customers/{customerId}/risks` lesbar.
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Risk" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        "410": { $ref: "#/components/responses/Problem410" }
        "412": { $ref: "#/components/responses/Problem412" }
        "422": { $ref: "#/components/responses/Problem422" }
        "429": { $ref: "#/components/responses/Problem429" }
        default: { $ref: "#/components/responses/Problem" }

  /customers/{customerId}/relations:
    get:
      tags: [customers]
      operationId: listCustomerRelations
      summary: Beziehungen eines Kunden auflisten
      description: |
        Liefert Beziehungen zu anderen Kunden (Haushaltsmitglieder,
        verbundene Partner usw.). Jede Beziehung hat einen `roleCode` aus `/catalogs/relationRoles`. Die Art der
        Beziehung ergibt sich aus diesem Code, nicht schon daraus, dass der Kunde in der Liste steht. Die
        Rollencodes sind mit denen der Vertragsbeteiligten konsistent.
      parameters:
        - $ref: "#/components/parameters/CustomerId"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Eine Seite Kundenbeziehungen.
          content:
            application/json:
              schema:
                type: object
                required: [items, page]
                properties:
                  items:
                    type: array
                    items: { $ref: "#/components/schemas/CustomerRelation" }
                  page: { $ref: "#/components/schemas/PageInfo" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        default: { $ref: "#/components/responses/Problem" }

  /contracts:
    get:
      tags: [contracts]
      operationId: listContracts
      summary: Verträge auflisten
      description: |
        Liefert Verträge von Kunden. `customerId` filtert auf die Verträge
        eines Kunden. Mit `updatedSince` ist das der Delta-Sync für Verträge.
      parameters:
        - $ref: "#/components/parameters/UpdatedSince"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
        - name: customerId
          in: query
          schema: { type: string, maxLength: 64 }
      responses:
        "200":
          description: Paginierte Verträge.
          content:
            application/json:
              schema:
                type: object
                required: [items, page]
                properties:
                  items:
                    type: array
                    items: { $ref: "#/components/schemas/Contract" }
                  page: { $ref: "#/components/schemas/PageInfo" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        default: { $ref: "#/components/responses/Problem" }
    post:
      tags: [contracts]
      operationId: createContract
      summary: Vertrag anlegen
      description: |
        Legt einen Vertrag für einen Kunden an. Eine `customerId`, die nicht zu
        diesem Benutzer gehört, führt zu 403.

        Gegen die Kataloge geprüft werden `statusCode` (`/catalogs/contractStatuses`), `productCode`
        (`/catalogs/productTypes`), `insurerCode` (`/catalogs/insurers`), der `spartenCode` jeder Sparte
        (`/catalogs/sparten`) und der `roleCode` jedes Beteiligten (`/catalogs/participantRoles`). Ein
        unbekannter Wert führt zu 422 mit den erlaubten Codes.

        Die `riskIds` einer Sparte dürfen nur auf Risiken des Kunden aus `customerId` zeigen, sonst 422
        `contract.riskCustomerMismatch`.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContractCreate" }
      responses:
        "201":
          description: Vertrag angelegt, sofort per id abrufbar.
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Contract" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "422": { $ref: "#/components/responses/Problem422" }
        "429": { $ref: "#/components/responses/Problem429" }
        default: { $ref: "#/components/responses/Problem" }

  /contracts/{contractId}:
    get:
      tags: [contracts]
      operationId: getContract
      summary: Vertrag abrufen
      description: |
        Liefert einen Vertrag, sofern sein Kunde dem angemeldeten Benutzer gehört. Die Beteiligten sind mit
        Rolle, `displayName` und `birthDate` eingebettet.
      parameters:
        - $ref: "#/components/parameters/ContractId"
      responses:
        "200":
          description: Vertrag.
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Contract" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        "410": { $ref: "#/components/responses/Problem410" }
        default: { $ref: "#/components/responses/Problem" }
    put:
      tags: [contracts]
      operationId: updateContract
      summary: Vertrag ändern
      description: |
        Ändert einen Vertrag eines Kunden. Partielles Update: Ein fehlendes Feld
        bleibt unverändert, `null` löscht den Wert. Ein Vertrag lässt sich nicht auf einen anderen Kunden
        umhängen.

        `sparten` ist optional. Fehlt das Feld, bleiben die Sparten unverändert. Ist es gesetzt, gilt es als
        die komplette neue Liste:
        - Eintrag mit `id`: diese Sparte wird geändert
        - Eintrag ohne `id`: eine neue Sparte wird angelegt
        - bestehende Sparte, deren `id` fehlt: wird gelöscht (die Löschung erscheint auch in `GET /deletions`)

        `participants` funktioniert genauso, mit der Beteiligungs-`id` als Schlüssel.

        Katalog- und Risikoprüfung wie beim Anlegen.
      parameters:
        - $ref: "#/components/parameters/ContractId"
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/IfMatch"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ContractUpdate" }
      responses:
        "200":
          description: Geänderter Vertrag, sofort per id abrufbar.
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Contract" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        "410": { $ref: "#/components/responses/Problem410" }
        "412": { $ref: "#/components/responses/Problem412" }
        "422": { $ref: "#/components/responses/Problem422" }
        "429": { $ref: "#/components/responses/Problem429" }
        default: { $ref: "#/components/responses/Problem" }

  /claims:
    get:
      tags: [claims]
      operationId: listClaims
      summary: Schäden auflisten
      description: |
        Liefert nur Schäden zu Verträgen von Kunden. `contractId` filtert auf die
        Schäden eines Vertrags, `customerId` auf die eines Kunden. Mit `updatedSince` ist das der Delta-Sync
        für Schäden.
      parameters:
        - $ref: "#/components/parameters/UpdatedSince"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
        - name: contractId
          in: query
          schema: { type: string, maxLength: 64 }
        - name: customerId
          in: query
          description: |
            Filtert auf die Schäden eines Kunden. Ein Schaden verweist nur auf seinen Vertrag (`contractId`),
            der Filter geht daher über den Vertrag.
          schema: { type: string, maxLength: 64 }
      responses:
        "200":
          description: Paginierte Schäden. Jeder Eintrag enthält dieselben Felder wie der Detail-Endpoint.
          content:
            application/json:
              schema:
                type: object
                required: [items, page]
                properties:
                  items:
                    type: array
                    items: { $ref: "#/components/schemas/Claim" }
                  page: { $ref: "#/components/schemas/PageInfo" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        default: { $ref: "#/components/responses/Problem" }
    post:
      tags: [claims]
      operationId: createClaim
      summary: Schaden melden
      description: |
        Legt einen Schaden zu einem Vertrag an. Der Vertrag muss zu einem Kunden des angemeldeten Benutzers
        gehören.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ClaimCreate" }
      responses:
        "201":
          description: Schaden angelegt, sofort per id abrufbar.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Claim" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "422": { $ref: "#/components/responses/Problem422" }
        "429": { $ref: "#/components/responses/Problem429" }
        default: { $ref: "#/components/responses/Problem" }

  /claims/{claimId}:
    get:
      tags: [claims]
      operationId: getClaim
      summary: Schaden abrufen
      description: |
        Liefert einen Schaden, sofern sein Vertrag zu einem Kunden des angemeldeten Benutzers gehört.
      parameters:
        - name: claimId
          in: path
          required: true
          schema: { type: string, maxLength: 64 }
      responses:
        "200":
          description: Schaden.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Claim" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        "410": { $ref: "#/components/responses/Problem410" }
        default: { $ref: "#/components/responses/Problem" }

  /documents:
    get:
      tags: [documents]
      operationId: listDocuments
      summary: Dokumente auflisten
      description: |
        Liefert die Metadaten aller Dokumente, die über Kunde, Vertrag oder Schaden dem angemeldeten Benutzer
        gehören. Größe, MIME-Typ und Hash sind so vor dem Download bekannt. Jeder Eintrag verweist über
        `customerId`, `contractId` bzw. `claimId` auf seine Zuordnung; die gleichnamigen Query-Parameter
        filtern danach.
      parameters:
        - $ref: "#/components/parameters/UpdatedSince"
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
        - name: customerId
          in: query
          schema: { type: string, maxLength: 64 }
        - name: contractId
          in: query
          schema: { type: string, maxLength: 64 }
        - name: claimId
          in: query
          schema: { type: string, maxLength: 64 }
      responses:
        "200":
          description: Alle Metadaten samt `sizeBytes`, `mimeType` und `sha256`, ohne Dateiinhalt.
          content:
            application/json:
              schema:
                type: object
                required: [items, page]
                properties:
                  items:
                    type: array
                    items: { $ref: "#/components/schemas/DocumentMetadata" }
                  page: { $ref: "#/components/schemas/PageInfo" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        default: { $ref: "#/components/responses/Problem" }
    post:
      tags: [documents]
      operationId: uploadDocument
      summary: Dokument hochladen
      description: |
        Lädt ein Dokument als Multipart-Request hoch, der Dateiteil wird gestreamt. Das Dokument gehört danach
        über Kunde, Vertrag oder Schaden dem angemeldeten Benutzer.

        Die Metadaten müssen mindestens eine von `customerId`, `contractId` und `claimId` setzen. Bei einem
        Schadendokument reicht `claimId` (Schaden -> Vertrag -> Kunde). Fehlen alle drei, kommt 422
        `document.ownerMissing`.

        `documentTypeCode` muss ein aktiver Code aus `/catalogs/documentTypes` sein, sonst 422 mit den
        erlaubten Codes.

        Erlaubte MIME-Typen:
        - `application/pdf`
        - `image/jpeg`, `image/png`, `image/tiff`
        - `message/rfc822`
        - `application/vnd.openxmlformats-officedocument.wordprocessingml.document` (docx)
        - `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` (xlsx)

        Andere Typen führen zu 422 `document.unsupportedMediaType`. Die Maximalgröße ist 50 MiB, darüber
        kommt 413 `document.tooLarge`. Sehr große Dateien besser über den fortsetzbaren Upload unter
        `/documents/uploads` hochladen.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [metadata, content]
              properties:
                metadata:
                  $ref: "#/components/schemas/DocumentUploadMetadata"
                content:
                  type: string
                  format: binary
                  description: Rohe Dateibytes.
      responses:
        "201":
          description: Metadaten des angelegten Dokuments.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DocumentMetadata" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "413":
          description: Datei größer als die Maximalgröße von 50 MiB. Code `document.tooLarge`.
          content:
            application/problem+json:
              schema: { $ref: "#/components/schemas/Problem" }
        "422": { $ref: "#/components/responses/Problem422" }
        default: { $ref: "#/components/responses/Problem" }

  /documents/uploads:
    post:
      tags: [documents]
      operationId: initDocumentUpload
      summary: Fortsetzbaren Upload starten
      description: |
        Startet einen Upload in Teilen (Chunks) für sehr große Dateien. Ablauf: hier die Session anlegen,
        die Chunks mit `PUT /documents/uploads/{uploadId}/content` senden, mit
        `POST /documents/uploads/{uploadId}/complete` abschließen.

        Der Body enthält dieselben Metadaten wie der Multipart-Upload (auch hier muss mindestens eine von
        `customerId`, `contractId`, `claimId` gesetzt sein), die erwartete Gesamtgröße `sizeBytes` und
        optional den erwarteten `sha256`. Beides wird beim Abschluss geprüft. Das fertige Dokument gehört
        über Kunde, Vertrag oder Schaden dem angemeldeten Benutzer.
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [metadata, sizeBytes]
              properties:
                metadata: { $ref: "#/components/schemas/DocumentUploadMetadata" }
                sizeBytes:
                  type: integer
                  format: int64
                  description: Erwartete Gesamtgröße in Bytes, wird beim Abschluss geprüft.
                sha256:
                  type: [string, "null"]
                  maxLength: 64
                  description: Erwarteter SHA-256 als Kleinbuchstaben-Hex, wird beim Abschluss geprüft.
      responses:
        "201":
          description: Upload-Session angelegt.
          content:
            application/json:
              schema:
                type: object
                required: [uploadId]
                properties:
                  uploadId: { type: string, maxLength: 64 }
                  expiresAt:
                    type: [string, "null"]
                    format: date-time
                    description: Zeitpunkt, ab dem eine nicht abgeschlossene Session verworfen wird.
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "422": { $ref: "#/components/responses/Problem422" }
        default: { $ref: "#/components/responses/Problem" }

  /documents/uploads/{uploadId}/content:
    put:
      tags: [documents]
      operationId: uploadDocumentChunk
      summary: Chunk hochladen
      description: |
        Sendet einen zusammenhängenden Byte-Bereich der Datei als `application/octet-stream`.
        `Content-Range` gibt den Bereich und die Gesamtgröße an.

        Chunks dürfen wiederholt werden, ein schon empfangener Bereich wird ignoriert. So lässt sich ein
        abgebrochener Upload fortsetzen.
      parameters:
        - name: uploadId
          in: path
          required: true
          schema: { type: string, maxLength: 64 }
        - name: Content-Range
          in: header
          required: true
          description: Byte-Bereich dieses Chunks und Gesamtgröße, z. B. `bytes 0-1048575/5242880`.
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema:
              type: string
              format: binary
      responses:
        "204":
          description: Chunk angenommen. Danach den nächsten Bereich senden oder, wenn alle Bytes da sind, `POST /documents/uploads/{uploadId}/complete` aufrufen.
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        "422": { $ref: "#/components/responses/Problem422" }
        default: { $ref: "#/components/responses/Problem" }

  /documents/uploads/{uploadId}/complete:
    post:
      tags: [documents]
      operationId: completeDocumentUpload
      summary: Upload abschließen
      description: |
        Schließt die Session ab, wenn alle Chunks angekommen sind. Nur für Sessions des angemeldeten
        Benutzers. Der Server prüft die Gesamtgröße und, falls beim Start angegeben, den `sha256`. Weicht
        etwas ab, kommt 422 `document.hashMismatch`. Danach ist der Upload ein normales Dokument mit
        denselben Metadaten wie beim Multipart-Upload.
      parameters:
        - name: uploadId
          in: path
          required: true
          schema: { type: string, maxLength: 64 }
      responses:
        "201":
          description: Metadaten des angelegten Dokuments.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DocumentMetadata" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        "422": { $ref: "#/components/responses/Problem422" }
        default: { $ref: "#/components/responses/Problem" }

  /documents/{documentId}:
    get:
      tags: [documents]
      operationId: getDocumentMetadata
      summary: Dokument-Metadaten abrufen
      description: |
        Liefert die Metadaten eines Dokuments des angemeldeten Benutzers, ohne Dateiinhalt.
      parameters:
        - $ref: "#/components/parameters/DocumentId"
      responses:
        "200":
          description: Nur Metadaten.
          headers:
            ETag: { $ref: "#/components/headers/ETag" }
          content:
            application/json:
              schema: { $ref: "#/components/schemas/DocumentMetadata" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        "410": { $ref: "#/components/responses/Problem410" }
        default: { $ref: "#/components/responses/Problem" }

  /documents/{documentId}/content:
    get:
      tags: [documents]
      operationId: downloadDocument
      summary: Dokument herunterladen
      description: |
        Liefert den Dateiinhalt eines Dokuments. Der Inhalt wird gestreamt, auch
        bei sehr großen Dateien. Abgebrochene Downloads lassen sich per `Range` fortsetzen.
      parameters:
        - $ref: "#/components/parameters/DocumentId"
        - name: Range
          in: header
          description: Byte-Bereich nach HTTP-Standard, zum Fortsetzen eines Downloads.
          schema: { type: string }
      responses:
        "200":
          description: Ganzer Inhalt. `Content-Length` ist immer gesetzt.
          headers:
            Content-Length:
              schema: { type: integer, format: int64 }
            ETag: { $ref: "#/components/headers/ETag" }
            Accept-Ranges:
              schema: { type: string, examples: ["bytes"] }
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        "206":
          description: Teilinhalt für einen `Range`-Request.
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }
        default: { $ref: "#/components/responses/Problem" }
    head:
      tags: [documents]
      operationId: headDocument
      summary: Größe, Typ und Hash abfragen
      description: |
        Liefert Größe, MIME-Typ und SHA-256 des Inhalts als Header, ohne Body. Nur für Dokumente des
        angemeldeten Benutzers.
      parameters:
        - $ref: "#/components/parameters/DocumentId"
      responses:
        "200":
          description: Dieselben Header wie bei `GET`, kein Body.
          headers:
            Content-Length:
              schema: { type: integer, format: int64 }
            Content-Type:
              schema: { type: string }
            ETag: { $ref: "#/components/headers/ETag" }
            X-Content-Sha256:
              schema: { type: string }
              description: SHA-256 des Inhalts als Kleinbuchstaben-Hex.
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        "404": { $ref: "#/components/responses/Problem404" }

  /deletions:
    get:
      tags: [deletions]
      operationId: listDeletions
      summary: Löschungen und Merges abrufen
      description: |
        Liefert Löschungen und Merges, aber nur für Daten.

        Der Anbieter zeigt Löschungen für alle Entitätstypen auf dieselbe Art an, nach Strategie A oder B:
        - A: Listen enthalten immer alle aktuellen Einträge, ein fehlender Eintrag gilt als gelöscht.
        - B: Gelöschte Einträge bleiben abrufbar und sind über `metadata.deletedAt` bzw. `status` markiert.

        Bei Strategie A muss es diesen Feed geben, weil eine Löschung dort bei `updatedSince` nicht auftaucht.
        Bei Strategie B sollte es ihn geben. Er deckt dann endgültige Löschungen ab (z. B. nach DSGVO), die
        auch den markierten Eintrag entfernen.

        Bei einem Merge steht die überlebende id in `mergedIntoId`. `deletedSince` verhält sich wie
        `updatedSince`: Ein erneuter Aufruf mit demselben Zeitpunkt ist sicher.

        Risiken (`entityType=risk`) haben kein Detail-GET. Für sie ist dieser Feed das einzige Löschsignal.
      parameters:
        - name: deletedSince
          in: query
          required: true
          schema: { type: string, format: date-time }
        - name: entityType
          in: query
          schema:
            type: string
            enum: [customer, contract, claim, document, risk]
        - $ref: "#/components/parameters/Cursor"
        - $ref: "#/components/parameters/Limit"
      responses:
        "200":
          description: Paginierte Lösch-Einträge, sortiert nach `deletedAt`, dann `id`.
          content:
            application/json:
              schema:
                type: object
                required: [items, page]
                properties:
                  items:
                    type: array
                    items: { $ref: "#/components/schemas/Deletion" }
                  page: { $ref: "#/components/schemas/PageInfo" }
        "401": { $ref: "#/components/responses/Problem401" }
        "403": { $ref: "#/components/responses/Problem403" }
        default: { $ref: "#/components/responses/Problem" }

components:

  securitySchemes:
    oauth2ClientCredentials:
      type: oauth2
      description: |
        Der hier gezeigte OAuth2-Client-Credentials-Flow ist ein Beispiel. Gescopte API-Keys oder ein
        Username/Password-Grant (Resource Owner) sind ebenso zulässig. Der Mechanismus muss ein dokumentierter
        Standard sein. BPP speichert die Zugangsdaten pro Mandant und pro Benutzer.

        Die Dokumentation muss den ganzen Ablauf beschreiben:
        - ob ein Client-Zertifikat (mTLS) nötig ist und wie man es bekommt und installiert
        - Token-Endpoint, Grant-Type und Parameter, mit Beispiel-Request und -Response
        - Lebensdauer des Tokens (sollte mindestens 15 Minuten sein) und wie es erneuert wird
      flows:
        clientCredentials:
          tokenUrl: https://auth.provider.example/oauth2/token
          scopes:
            partner.read: Kunden, Verträge, Schäden, Dokumente, Kataloge lesen
            partner.write: Schäden erstellen, Dokumente hochladen sowie Kunden (samt Adressen, Bankkonten, Kontaktinfos), Verträge und Risiken anlegen/aktualisieren

  parameters:
    CustomerId:
      name: customerId
      in: path
      required: true
      schema: { type: string, maxLength: 64 }
      description: Unveränderlich, wird nie wiederverwendet.
    ContractId:
      name: contractId
      in: path
      required: true
      schema: { type: string, maxLength: 64 }
    DocumentId:
      name: documentId
      in: path
      required: true
      schema: { type: string, maxLength: 64 }
    UpdatedSince:
      name: updatedSince
      in: query
      description: |
        Liefert alle Ressourcen mit `metadata.updatedAt >= updatedSince`, sortiert nach `updatedAt`, dann `id`.
        Ein erneuter Aufruf mit demselben Zeitpunkt ist gefahrlos möglich.
      schema: { type: string, format: date-time }
    Cursor:
      name: cursor
      in: query
      description: Opaker Cursor aus `page.nextCursor` der vorigen Seite.
      schema: { type: string, maxLength: 512 }
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 500, default: 100 }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: |
        Wird ein Schreibvorgang mit demselben Key erneut gesendet, kommt das ursprüngliche Ergebnis zurück
        (gleicher Status, gleicher Body). Es entsteht kein Duplikat. Keys gelten pro Client und werden
        mindestens 24 Stunden aufbewahrt.
      schema: { type: string, maxLength: 128 }
    IfMatch:
      name: If-Match
      in: header
      required: false
      description: |
        ETag aus dem letzten GET. Ist er angegeben, wird nur geändert, wenn die Ressource seitdem unverändert
        ist. Sonst kommt 412 mit `resource.versionConflict`. Ohne den Header wird immer geändert
        (Last-Write-Wins).
      schema: { type: string, maxLength: 256 }

  headers:
    ETag:
      description: Ändert sich genau dann, wenn sich die Ressource ändert.
      schema: { type: string }
    RateLimit-Remaining:
      description: |
        Verbleibende Requests im aktuellen Zeitfenster. Die Dokumentation muss die Limits nennen (Requests pro
        Sekunde, Burst, pro Mandant oder global, Folgen für Bulk-Syncs) oder angeben, dass es keine gibt.
      schema: { type: integer }
    Retry-After:
      description: Wartezeit in Sekunden bis zum nächsten Versuch.
      schema: { type: integer }

  responses:
    Problem:
      description: Fehler nach RFC 7807.
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    Problem401:
      description: |
        Nicht oder nicht mehr authentifiziert (Credentials fehlen, sind abgelaufen oder ungültig). `code` ist
        `auth.tokenExpired` oder `auth.invalidToken`. Vor dem nächsten Versuch neu authentifizieren.
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    Problem403:
      description: |
        Authentifiziert, aber der Aufrufer (Mandant bzw. angemeldeter Benutzer) hat keinen Zugriff auf diese
        Ressource. `code` ist `auth.forbidden`.
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    Problem404:
      description: |
        Nicht gefunden. `code` ist `resource.notFound` (hat nie existiert) oder `resource.merged`
        (zusammengeführt, mit Angabe von `mergedIntoId`).
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    Problem410:
      description: |
        Gelöscht, `code` ist `resource.deleted`. Gelöschte Kunden, Verträge, Schäden, Dokumente und Risiken
        stehen auch im Lösch-Feed `/deletions`. Adressen, Bankkonten und Kontaktinfos haben dort keinen
        Eintrag: Sie fehlen danach in der Liste des Kunden, und dessen `metadata.updatedAt` steigt.
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    Problem412:
      description: |
        Der `If-Match`-ETag passt nicht mehr zur aktuellen Ressource. Neu lesen und mit dem aktuellen ETag
        erneut senden. `code` ist `resource.versionConflict`.
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    Problem422:
      description: Validierungsfehler. `errors[]` nennt für jeden Fehler das betroffene Feld und den Grund.
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }
    Problem429:
      description: Zu viele Requests. Die geltenden Limits müssen dokumentiert sein.
      headers:
        Retry-After: { $ref: "#/components/headers/Retry-After" }
      content:
        application/problem+json:
          schema: { $ref: "#/components/schemas/Problem" }

  schemas:

    Metadata:
      type: object
      description: Audit-Metadaten, gleich aufgebaut auf jeder Ressource.
      required: [createdAt, updatedAt]
      properties:
        createdAt:
          type: string
          format: date-time
          description: Vom Server gesetzt, in UTC.
        updatedAt:
          type: string
          format: date-time
          description: Steigt bei jeder Änderung und sinkt nie. Der Delta-Sync über `updatedSince` filtert danach.
        createdBy: { $ref: "#/components/schemas/Actor" }
        updatedBy: { $ref: "#/components/schemas/Actor" }
        deletedAt:
          type: [string, "null"]
          format: date-time
          description: |
            Für Anbieter, die gelöschte Einträge weiter ausliefern (Soft Delete): gesetzt, sobald der Eintrag
            beim Anbieter gelöscht ist. Die Löschung erhöht `updatedAt`.

            Verschwinden gelöschte Einträge stattdessen aus den Listen, ist das Feld `null` oder fehlt.
            In beiden Fällen steht die Löschung auch im Lösch-Feed `/deletions`.

    Actor:
      type: object
      required: [id]
      properties:
        id: { type: string, maxLength: 64 }
        displayName: { type: string, maxLength: 256 }

    CatalogValue:
      type: object
      description: Ein Wert aus einem Katalog.
      required: [code, label, active]
      properties:
        code:
          type: string
          maxLength: 64
          description: Ändert sich nie. Datensätze verweisen auf den Code, nicht auf das Label.
        label:
          type: string
          maxLength: 256
          description: Anzeigetext, darf sich ändern und lokalisiert sein. Der Server kürzt ihn nicht.
        active:
          type: boolean
          description: Inaktive Werte bleiben für bestehende Datensätze auflösbar.

    PageInfo:
      type: object
      description: Paginierungsangaben einer Liste.
      required: [hasMore]
      properties:
        nextCursor:
          type: [string, "null"]
          maxLength: 512
          description: Opak; `null`, wenn `hasMore` false ist.
        hasMore: { type: boolean }
        total:
          type: integer
          description: Genaue Anzahl aller Treffer für den Filter, nicht gedeckelt.

    Customer:
      type: object
      description: |
        Ein Kunde ist eine natürliche Person (`private`) oder eine Organisation (`company`), unterschieden über
        `customerType`. Es gibt ein Schema für beide Typen, kein `oneOf`. Welche Felder für welchen Typ
        gelten, steht bei `customerType`.

        Führt der Anbieter Personendaten, Adressen und Kontakte eines Privatkunden auf einer eigenen
        Personen-Ebene, gehören sie hier zum Kunden. Das gilt auch beim Schreiben: `CustomerCreate`,
        `CustomerUpdate` und die Unter-Endpoints arbeiten auf dem Kunden.
      required: [id, customerNumber, customerType, metadata]
      properties:
        id:
          type: string
          maxLength: 64
          description: Global eindeutig, unveränderlich, wird nie wiederverwendet.
        customerNumber:
          type: string
          maxLength: 32
          description: Kundennummer zur Anzeige.
        customerType:
          type: string
          enum: [private, company]
          description: |
            Discriminator, nach dem Anlegen unveränderlich.

            `private` (natürliche Person): `lastName` ist Pflicht. Außerdem erlaubt sind `firstName`,
            `birthDate`, `genderCode`, `titlePrefix`, `titleSuffix`, `occupation`, `nationalityCode`,
            `maritalStatusCode` und `birthPlace`. Alle Firmenfelder müssen `null` sein.

            `company` (Organisation): `companyName` ist Pflicht. Außerdem erlaubt sind `legalFormCode`,
            `companyRegisterNumber`, `vatId`, `businessActivity`, `balanceSheetDay` und `balanceSheetMonth`.
            Alle Privatkundenfelder müssen `null` sein.

            `customerNumber`, `correspondenceLanguageCode`, `addresses`, `bankAccounts` und `contactInfos`
            gelten für beide Typen.
        firstName:
          type: [string, "null"]
          maxLength: 256
          description: |
            Nur bei Privatkunden, bei Firmenkunden `null`. Unbekannt ist `null`, kein Leerstring und kein
            Platzhaltertext.
        lastName:
          type: [string, "null"]
          maxLength: 256
          description: |
            Bei Privatkunden Pflicht, bei Firmenkunden `null`.
        birthDate:
          type: [string, "null"]
          format: date
          description: Nur bei Privatkunden, bei Firmenkunden `null`.
        genderCode:
          type: [string, "null"]
          maxLength: 32
          description: |
            `female`, `male` oder `diverse`, kein lokalisierter Text. `null`, wenn unbekannt. Nur bei
            Privatkunden, bei Firmenkunden `null`.
        titlePrefix:
          type: [string, "null"]
          maxLength: 256
          description: Titel vor dem Namen (z. B. Dr., Mag.). Nur bei Privatkunden, bei Firmenkunden `null`.
        titleSuffix:
          type: [string, "null"]
          maxLength: 256
          description: Titel nach dem Namen (z. B. BSc, MBA). Nur bei Privatkunden, bei Firmenkunden `null`.
        occupation:
          type: [string, "null"]
          maxLength: 256
          description: Beruf als Freitext. Nur bei Privatkunden, bei Firmenkunden `null`.
        nationalityCode:
          type: [string, "null"]
          maxLength: 2
          description: |
            ISO 3166-1 alpha-2 in Großbuchstaben, Kosovo als `XK`. Nur bei Privatkunden, bei Firmenkunden `null`.
        maritalStatusCode:
          type: [string, "null"]
          maxLength: 32
          description: |
            `single`, `married`, `divorced`, `widowed`, `registeredPartnership`, `dissolvedRegisteredPartnership`
            oder `survivingRegisteredPartner`, kein lokalisierter Text. `null`, wenn unbekannt. Nur bei
            Privatkunden, bei Firmenkunden `null`.
        birthPlace:
          type: [string, "null"]
          maxLength: 256
          description: Geburtsort als Freitext. Nur bei Privatkunden, bei Firmenkunden `null`.
        companyName:
          type: [string, "null"]
          maxLength: 256
          description: |
            Rechtlicher Name der Organisation. Bei Firmenkunden Pflicht, bei Privatkunden `null`.
        legalFormCode:
          type: [string, "null"]
          maxLength: 64
          description: |
            Rechtsform als Code aus `/catalogs/legalForms`, nicht als Label oder Freitext (z. B.
            `einzelunternehmen`, `gmbh`, `gmbhCoKg`, `ag`, `og`, `kg`, `verein`). Bei Privatkunden `null`.
        companyRegisterNumber:
          type: [string, "null"]
          maxLength: 64
          description: Firmenbuchnummer. Bei Privatkunden `null`.
        vatId:
          type: [string, "null"]
          maxLength: 32
          description: Umsatzsteuer-Identifikationsnummer (UID). Bei Privatkunden `null`.
        businessActivity:
          type: [string, "null"]
          maxLength: 256
          description: Gewerbewortlaut bzw. Unternehmensgegenstand als Freitext. Bei Privatkunden `null`.
        balanceSheetDay:
          type: [integer, "null"]
          minimum: 1
          maximum: 31
          description: |
            Tag des Bilanzstichtags. Muss im `balanceSheetMonth` existieren (Februar: bis 29, unabhängig vom
            Jahr). Tag und Monat sind beide gesetzt oder beide `null`. Ein ungültiges oder halb gesetztes Paar
            führt beim Schreiben zu 422 mit Feldfehler. Bei Privatkunden `null`.
        balanceSheetMonth:
          type: [integer, "null"]
          minimum: 1
          maximum: 12
          description: Monat des Bilanzstichtags. Bei Privatkunden `null`.
        correspondenceLanguageCode:
          type: [string, "null"]
          maxLength: 2
          description: Korrespondenzsprache, ISO 639-1 in Kleinbuchstaben (z. B. `de`, `en`). Für beide Kundentypen.
        addresses:
          type: array
          description: |
            Höchstens eine Adresse hat `isDefault: true`; BPP verwendet diese, falls vorhanden. Frühere
            Adressen bleiben abrufbar. Anlegen mit `POST /customers/{customerId}/addresses` oder beim Anlegen
            des Kunden, ändern mit `PUT /customers/{customerId}/addresses/{addressId}`.
          items: { $ref: "#/components/schemas/Address" }
        bankAccounts:
          type: array
          description: |
            Keine doppelten IBANs, der Anbieter dedupliziert. Höchstens ein Konto hat `isDefault: true`, das ist
            das aktuelle Standardkonto. Anlegen mit `POST /customers/{customerId}/bank-accounts` oder beim
            Anlegen des Kunden, ändern mit `PUT /customers/{customerId}/bank-accounts/{bankAccountId}`.
          items: { $ref: "#/components/schemas/BankAccount" }
        contactInfos:
          type: array
          description: |
            E-Mail- und Telefonkontakte. Höchstens einer hat `isDefault: true`. Ersetzt das frühere Feld
            `email`. Anlegen mit `POST /customers/{customerId}/contact-infos` oder beim Anlegen des Kunden,
            ändern mit `PUT /customers/{customerId}/contact-infos/{contactInfoId}`.
          items: { $ref: "#/components/schemas/ContactInfo" }
        metadata:
          $ref: "#/components/schemas/Metadata"
          description: |
            `updatedAt` steigt bei jeder Änderung am Kunden oder an seinen Adressen, Bankkonten und
            Kontaktinfos. Das gilt für Änderungen über diese API und beim Anbieter, auch für das Entfernen
            eines Eintrags. Diese Einträge haben keine eigene Änderungsabfrage; Änderungen daran kommen über
            `GET /customers?updatedSince`.

    CustomerCreate:
      type: object
      description: |
        Body für `POST /customers`. Wie `Customer`, aber ohne die vom Server gesetzten Felder (`id`,
        `metadata`, ids der Einträge). Adressen, Bankkonten und Kontaktinfos können gleich mitgeschickt
        werden; alles wird atomar angelegt.

        `customerType` bestimmt die erlaubten Felder (siehe `Customer.customerType`). Privatkunden brauchen
        `lastName`, Firmenkunden `companyName`. Ein Feld, das nicht zum Typ passt und einen Wert hat, führt zu
        422 `customer.typeFieldMismatch`.

        Ein unbekannter Code führt zu 422 mit `allowedCodes`.
      required: [customerType]
      properties:
        customerType:
          type: string
          enum: [private, company]
          description: Nach dem Anlegen unveränderlich. Feldregeln wie bei `Customer.customerType`.
        customerNumber:
          type: [string, "null"]
          maxLength: 32
          description: |
            Fehlt die Nummer, vergibt der Anbieter eine eigene (steht in der 201-Antwort). Sonst übernimmt er
            den Wert. Ein Anbieter, der keine vorgegebenen Nummern annimmt, dokumentiert das und ignoriert den
            Wert immer.
        firstName:
          type: [string, "null"]
          maxLength: 256
          description: Nur bei Privatkunden.
        lastName:
          type: [string, "null"]
          maxLength: 256
          description: Bei Privatkunden Pflicht. Bei Firmenkunden `null` oder weglassen.
        birthDate:
          type: [string, "null"]
          format: date
          description: Nur bei Privatkunden.
        genderCode:
          type: [string, "null"]
          maxLength: 32
          description: |
            `female`, `male` oder `diverse`. Nur bei Privatkunden.
        titlePrefix:
          type: [string, "null"]
          maxLength: 256
          description: Titel vor dem Namen. Nur bei Privatkunden.
        titleSuffix:
          type: [string, "null"]
          maxLength: 256
          description: Titel nach dem Namen. Nur bei Privatkunden.
        occupation:
          type: [string, "null"]
          maxLength: 256
          description: Beruf. Nur bei Privatkunden.
        nationalityCode:
          type: [string, "null"]
          maxLength: 2
          description: ISO 3166-1 alpha-2 in Großbuchstaben, Kosovo als `XK`. Nur bei Privatkunden.
        maritalStatusCode:
          type: [string, "null"]
          maxLength: 32
          description: Codes wie bei `Customer.maritalStatusCode`. Nur bei Privatkunden.
        birthPlace:
          type: [string, "null"]
          maxLength: 256
          description: Geburtsort. Nur bei Privatkunden.
        companyName:
          type: [string, "null"]
          maxLength: 256
          description: Bei Firmenkunden Pflicht. Bei Privatkunden `null` oder weglassen.
        legalFormCode:
          type: [string, "null"]
          maxLength: 64
          description: |
            Code aus `/catalogs/legalForms`. Ein unbekannter oder inaktiver Code führt zu 422 mit `allowedCodes`.
            Nur bei Firmenkunden.
        companyRegisterNumber:
          type: [string, "null"]
          maxLength: 64
          description: Firmenbuchnummer. Nur bei Firmenkunden.
        vatId:
          type: [string, "null"]
          maxLength: 32
          description: Umsatzsteuer-Identifikationsnummer (UID). Nur bei Firmenkunden.
        businessActivity:
          type: [string, "null"]
          maxLength: 256
          description: Gewerbewortlaut bzw. Unternehmensgegenstand. Nur bei Firmenkunden.
        balanceSheetDay:
          type: [integer, "null"]
          minimum: 1
          maximum: 31
          description: |
            Tag des Bilanzstichtags. Nur bei Firmenkunden. Regeln wie bei `Customer.balanceSheetDay`: Der Tag
            muss im Monat existieren, Tag und Monat sind beide gesetzt oder beide `null`, sonst 422.
        balanceSheetMonth:
          type: [integer, "null"]
          minimum: 1
          maximum: 12
          description: Monat des Bilanzstichtags. Nur bei Firmenkunden.
        correspondenceLanguageCode:
          type: [string, "null"]
          maxLength: 2
          description: ISO 639-1 in Kleinbuchstaben (z. B. `de`, `en`). Für beide Kundentypen.
        addresses:
          type: array
          description: |
            Optional. Höchstens eine Adresse mit `isDefault: true`, sonst 422 `customer.multipleDefaults`.
          items: { $ref: "#/components/schemas/AddressCreate" }
        bankAccounts:
          type: array
          description: |
            Optional. Höchstens ein Konto mit `isDefault: true`, sonst 422 `customer.multipleDefaults`. Dieselbe
            IBAN zweimal führt zu 422 `bankAccount.duplicateIban`.
          items: { $ref: "#/components/schemas/BankAccountCreate" }
        contactInfos:
          type: array
          description: |
            Optional. Höchstens eine Kontaktinfo mit `isDefault: true`, sonst 422 `customer.multipleDefaults`.
          items: { $ref: "#/components/schemas/ContactInfoCreate" }

    CustomerUpdate:
      type: object
      description: |
        Body für `PUT /customers/{customerId}`. Partielles Update: Ein fehlendes Feld bleibt unverändert, ein
        Feld mit `null` wird gelöscht. Alle Felder sind optional.

        `id` und `customerType` sind unveränderlich und nicht Teil des Bodys. Adressen, Bankkonten und
        Kontaktinfos ebenfalls nicht: Sie werden nur über ihre eigenen Endpoints angelegt und geändert und
        bleiben bei diesem Request unverändert.

        Die Typregeln gelten wie beim Anlegen, bezogen auf den gespeicherten `customerType`. `lastName`
        (Privatkunde) bzw. `companyName` (Firmenkunde) darf nicht auf `null` gesetzt werden (422). Ein Feld,
        das nicht zum Typ passt und einen Wert hat, führt zu 422 `customer.typeFieldMismatch`.
      properties:
        customerNumber:
          type: string
          maxLength: 32
          description: Änderbar, aber nicht löschbar.
        firstName:
          type: [string, "null"]
          maxLength: 256
          description: Nur bei Privatkunden.
        lastName:
          type: [string, "null"]
          maxLength: 256
          description: |
            Bei Privatkunden nicht löschbar (`null` führt zu 422). Bei Firmenkunden nur `null` erlaubt, sonst
            422 `customer.typeFieldMismatch`.
        birthDate:
          type: [string, "null"]
          format: date
          description: Nur bei Privatkunden.
        genderCode:
          type: [string, "null"]
          maxLength: 32
          description: |
            `female`, `male` oder `diverse`. Nur bei Privatkunden.
        titlePrefix:
          type: [string, "null"]
          maxLength: 256
          description: Nur bei Privatkunden.
        titleSuffix:
          type: [string, "null"]
          maxLength: 256
          description: Nur bei Privatkunden.
        occupation:
          type: [string, "null"]
          maxLength: 256
          description: Nur bei Privatkunden.
        nationalityCode:
          type: [string, "null"]
          maxLength: 2
          description: ISO 3166-1 alpha-2, Kosovo als `XK`. Nur bei Privatkunden.
        maritalStatusCode:
          type: [string, "null"]
          maxLength: 32
          description: Codes wie bei `Customer.maritalStatusCode`. Nur bei Privatkunden.
        birthPlace:
          type: [string, "null"]
          maxLength: 256
          description: Nur bei Privatkunden.
        companyName:
          type: [string, "null"]
          maxLength: 256
          description: |
            Bei Firmenkunden nicht löschbar (`null` führt zu 422). Bei Privatkunden nur `null` erlaubt, sonst
            422 `customer.typeFieldMismatch`.
        legalFormCode:
          type: [string, "null"]
          maxLength: 64
          description: |
            Code aus `/catalogs/legalForms`. Ein unbekannter Code führt zu 422 mit `allowedCodes`. Nur bei
            Firmenkunden.
        companyRegisterNumber:
          type: [string, "null"]
          maxLength: 64
          description: Nur bei Firmenkunden.
        vatId:
          type: [string, "null"]
          maxLength: 32
          description: Nur bei Firmenkunden.
        businessActivity:
          type: [string, "null"]
          maxLength: 256
          description: Nur bei Firmenkunden.
        balanceSheetDay:
          type: [integer, "null"]
          minimum: 1
          maximum: 31
          description: |
            Nur bei Firmenkunden. Regeln wie bei `Customer.balanceSheetDay`. Ist nach dem Update nur Tag oder
            Monat gesetzt, kommt 422 mit Feldfehler. Der Server ergänzt oder löscht den anderen Wert nicht selbst.
        balanceSheetMonth:
          type: [integer, "null"]
          minimum: 1
          maximum: 12
          description: Nur bei Firmenkunden.
        correspondenceLanguageCode:
          type: [string, "null"]
          maxLength: 2
          description: ISO 639-1. Für beide Kundentypen.

    Address:
      type: object
      required: [id, isDefault]
      properties:
        id:
          type: string
          maxLength: 64
          description: Eigene stabile id der Adresse.
        street:
          type: [string, "null"]
          maxLength: 256
          description: Nur der Straßenname, die Hausnummer steht in `houseNumber`.
        houseNumber: { type: [string, "null"], maxLength: 256 }
        postalCode: { type: [string, "null"], maxLength: 16 }
        city: { type: [string, "null"], maxLength: 256 }
        countryCode:
          type: [string, "null"]
          maxLength: 2
          description: ISO 3166-1 alpha-2 in Großbuchstaben, für Kosovo `XK`.
        regionCode:
          type: [string, "null"]
          maxLength: 64
          description: |
            Code aus `/catalogs/regions`, nicht das Anzeige-Label. Der Katalog enthält die Bundesländer von
            Österreich und Deutschland mit ihrem ISO-3166-2-Code. Das Länderpräfix muss zu `countryCode` passen.
            `null`, wenn die Region unbekannt ist oder es für das Land keinen Katalogwert gibt.

            Österreich: `AT-1` Burgenland, `AT-2` Kärnten, `AT-3` Niederösterreich, `AT-4` Oberösterreich,
            `AT-5` Salzburg, `AT-6` Steiermark, `AT-7` Tirol, `AT-8` Vorarlberg, `AT-9` Wien.

            Deutschland: `DE-BW` Baden-Württemberg, `DE-BY` Bayern, `DE-BE` Berlin, `DE-BB` Brandenburg,
            `DE-HB` Bremen, `DE-HH` Hamburg, `DE-HE` Hessen, `DE-MV` Mecklenburg-Vorpommern,
            `DE-NI` Niedersachsen, `DE-NW` Nordrhein-Westfalen, `DE-RP` Rheinland-Pfalz, `DE-SL` Saarland,
            `DE-SN` Sachsen, `DE-ST` Sachsen-Anhalt, `DE-SH` Schleswig-Holstein, `DE-TH` Thüringen.
        isDefault:
          type: boolean
          description: |
            Höchstens eine Adresse pro Kunde ist Default. Die übrigen bleiben über ihre id abrufbar.

    AddressCreate:
      type: object
      description: |
        Body zum Anlegen einer Adresse, inline in `CustomerCreate.addresses` oder über
        `POST /customers/{customerId}/addresses`. Die id vergibt der Server. Der Kunde ergibt sich aus dem Pfad
        bzw. dem umschließenden Kunden, nie aus dem Body.

        `street`, `postalCode`, `city` und `countryCode` sind beim Anlegen Pflicht, auch wenn Bestandsadressen
        beim Lesen dort `null` haben können. `countryCode` und `regionCode` werden in derselben Schreibweise
        angenommen, in der sie gelesen werden.
      required: [street, postalCode, city, countryCode]
      properties:
        street:
          type: string
          maxLength: 256
          description: Nur der Straßenname, die Hausnummer steht in `houseNumber`.
        houseNumber: { type: [string, "null"], maxLength: 256 }
        postalCode: { type: string, maxLength: 16 }
        city: { type: string, maxLength: 256 }
        countryCode:
          type: string
          maxLength: 2
          description: ISO 3166-1 alpha-2 in Großbuchstaben, für Kosovo `XK`. Ein unbekannter Code führt zu 422.
        regionCode:
          type: [string, "null"]
          maxLength: 64
          description: |
            Code aus `/catalogs/regions`, passend zu `countryCode` (z. B. `AT-9` für Wien, `DE-BY` für Bayern;
            Liste siehe `Address.regionCode`). Ein unbekannter, inaktiver oder nicht zu `countryCode` passender
            Code führt zu 422, die Antwort nennt die erlaubten Codes.
        isDefault:
          type: boolean
          default: false
          description: |
            `true` macht diese Adresse zum Default und setzt den bisherigen Default im selben Schreibvorgang auf
            `false`. Weglassen oder `false` ändert den Default nicht. Der Server setzt `isDefault` nie von sich aus.

    AddressUpdate:
      type: object
      description: |
        Body für eine partielle Änderung einer Adresse. Ein fehlendes Feld bleibt unverändert, `null` löscht
        den Wert bei nullbaren Feldern. id und Zuordnung zum Kunden sind nicht änderbar.

        `street`, `postalCode`, `city` und `countryCode` nehmen kein `null` an. Hat eine Bestandsadresse dort
        `null`, das Feld im Body weglassen.
      properties:
        street:
          type: string
          maxLength: 256
          description: Fehlt = unverändert. `null` führt zu 422.
        houseNumber:
          type: [string, "null"]
          maxLength: 256
          description: Fehlt = unverändert, `null` löscht.
        postalCode:
          type: string
          maxLength: 16
          description: Fehlt = unverändert. `null` führt zu 422.
        city:
          type: string
          maxLength: 256
          description: Fehlt = unverändert. `null` führt zu 422.
        countryCode:
          type: string
          maxLength: 2
          description: Fehlt = unverändert. ISO 3166-1 alpha-2, für Kosovo `XK`. `null` führt zu 422.
        regionCode:
          type: [string, "null"]
          maxLength: 64
          description: |
            Fehlt = unverändert, `null` löscht. Code aus `/catalogs/regions`. Nach der Änderung muss
            `regionCode` zu `countryCode` passen, sonst 422.
        isDefault:
          type: boolean
          description: |
            Fehlt = unverändert. `true` macht diese Adresse zum Default und setzt den bisherigen Default im
            selben Schreibvorgang auf `false`. `false` nimmt ihr den Default-Status, der Kunde hat dann keine
            Default-Adresse.

    BankAccount:
      type: object
      required: [id, iban, isDefault]
      properties:
        id: { type: string, maxLength: 64 }
        iban:
          type: string
          maxLength: 34
          description: Elektronisches Format nach ISO 13616 (Großbuchstaben, ohne Leerzeichen).
        bic:
          type: [string, "null"]
          maxLength: 11
          description: ISO 9362, 8 oder 11 Zeichen, Großbuchstaben ohne Leerzeichen. `null`, wenn der Anbieter keine BIC führt.
        accountHolderName:
          type: [string, "null"]
          maxLength: 256
          description: Name des Kontoinhabers. `null`, wenn der Anbieter ihn nicht führt.
        bankName:
          type: [string, "null"]
          maxLength: 256
          description: Name der Bank. `null`, wenn der Anbieter ihn nicht führt.
        isDefault:
          type: boolean
          description: |
            Höchstens ein Bankkonto pro Kunde ist Default. Die übrigen bleiben über ihre id abrufbar.

    BankAccountCreate:
      type: object
      description: |
        Body zum Anlegen eines Bankkontos, inline in `CustomerCreate.bankAccounts` oder über
        `POST /customers/{customerId}/bank-accounts`. Felder wie `BankAccount`, die id vergibt der Server.

        Eine IBAN mit falscher Prüfziffer führt zu 422 `bankAccount.invalidIban`. Eine IBAN, die der Kunde
        schon hat, führt zu 422 `bankAccount.duplicateIban`.
      required: [iban]
      properties:
        iban:
          type: string
          maxLength: 34
          pattern: "^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$"
          description: Elektronisches Format nach ISO 13616, so wie die IBAN beim Lesen geliefert wird.
        bic:
          type: [string, "null"]
          maxLength: 11
          pattern: "^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$"
          description: BIC nach ISO 9362.
        accountHolderName:
          type: [string, "null"]
          maxLength: 256
          description: Name des Kontoinhabers.
        bankName:
          type: [string, "null"]
          maxLength: 256
          description: Name der Bank.
        isDefault:
          type: boolean
          default: false
          description: |
            `true` macht dieses Bankkonto zum Default und setzt den bisherigen Default im selben Schreibvorgang
            auf `false`. Weglassen oder `false` ändert den Default nicht. Der Server setzt `isDefault` nie von
            sich aus.

    BankAccountUpdate:
      type: object
      description: |
        Body für eine partielle Änderung eines Bankkontos. Ein fehlendes Feld bleibt unverändert, `null` löscht
        den Wert.

        Nicht änderbar sind id, Zuordnung zum Kunden und `iban`. Eine andere IBAN ist ein anderes Konto und
        wird neu angelegt.
      properties:
        bic:
          type: [string, "null"]
          maxLength: 11
          pattern: "^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$"
          description: Fehlt = unverändert, `null` löscht. BIC nach ISO 9362.
        accountHolderName:
          type: [string, "null"]
          maxLength: 256
          description: Fehlt = unverändert, `null` löscht.
        bankName:
          type: [string, "null"]
          maxLength: 256
          description: Fehlt = unverändert, `null` löscht.
        isDefault:
          type: boolean
          description: |
            Fehlt = unverändert. `true` macht dieses Bankkonto zum Default und setzt den bisherigen Default im
            selben Schreibvorgang auf `false`. `false` nimmt ihm den Default-Status, der Kunde hat dann kein
            Default-Bankkonto.

    ContactInfo:
      type: object
      description: |
        E-Mail-Adresse und/oder Telefonnummer eines Kunden. Es gibt kein Feld für die Art des Kontakts, die
        Rolle ergibt sich aus dem Kontext. Mindestens eines von `email` und `phone` ist gesetzt; leere
        Einträge liefert der Anbieter nicht aus.
      required: [id, isDefault]
      properties:
        id: { type: string, maxLength: 64 }
        email: { type: [string, "null"], format: email, maxLength: 256 }
        phone: { type: [string, "null"], maxLength: 64 }
        isDefault:
          type: boolean
          description: |
            Höchstens eine Kontaktinfo pro Kunde ist Default. Die übrigen bleiben über ihre id abrufbar.

    ContactInfoCreate:
      type: object
      description: |
        Body zum Anlegen einer Kontaktinfo, inline in `CustomerCreate.contactInfos` oder über
        `POST /customers/{customerId}/contact-infos`. Felder wie `ContactInfo`, die id vergibt der Server.
        Mindestens eines von `email` und `phone` muss gesetzt sein, sonst 422 `contactInfo.empty`.
      properties:
        email:
          type: [string, "null"]
          format: email
          maxLength: 256
          description: Eine ungültige Adresse führt zu 422, der Eintrag in `errors[]` nennt das Feld.
        phone:
          type: [string, "null"]
          maxLength: 64
          description: Telefonnummer als Freitext.
        isDefault:
          type: boolean
          default: false
          description: |
            `true` macht diese Kontaktinfo zum Default und setzt den bisherigen Default im selben Schreibvorgang
            auf `false`. Weglassen oder `false` ändert den Default nicht. Der Server setzt `isDefault` nie von
            sich aus.

    ContactInfoUpdate:
      type: object
      description: |
        Body für eine partielle Änderung einer Kontaktinfo. Ein fehlendes Feld bleibt unverändert, `null` löscht
        den Wert. id und Zuordnung zum Kunden sind nicht änderbar.

        Nach der Änderung muss mindestens eines von `email` und `phone` gesetzt sein, sonst 422
        `contactInfo.empty`.
      properties:
        email:
          type: [string, "null"]
          format: email
          maxLength: 256
          description: Fehlt = unverändert, `null` löscht.
        phone:
          type: [string, "null"]
          maxLength: 64
          description: Fehlt = unverändert, `null` löscht.
        isDefault:
          type: boolean
          description: |
            Fehlt = unverändert. `true` macht diese Kontaktinfo zum Default und setzt den bisherigen Default im
            selben Schreibvorgang auf `false`. `false` nimmt ihr den Default-Status, der Kunde hat dann keine
            Default-Kontaktinfo.

    CustomerRelation:
      type: object
      description: |
        Beziehung eines Kunden zu einem anderen Kunden (Haushaltsmitglied, verbundener Partner usw.) mit Rolle.
        Die Rollen-Codes sind mit denen der Vertragsbeteiligten konsistent.

        Das Ziel ist immer ein Kunde (`relatedCustomerId`). Zeigt eine Beziehung beim Anbieter auf eine Person,
        die kein Kunde ist, sollte er sie in den verbundenen Kunden einfließen lassen oder weglassen, statt
        Kunden-ids zu erfinden. Die Beziehung gehört zu dem Kunden, in dessen `/relations`-Liste sie erscheint.
      required: [id, relatedCustomerId, roleCode]
      properties:
        id:
          type: string
          maxLength: 64
          description: Stabile id dieser Beziehung.
        relatedCustomerId:
          type: string
          maxLength: 64
          description: Der verbundene Kunde, abrufbar über `GET /customers/{customerId}`.
        roleCode:
          type: string
          maxLength: 64
          description: Code aus `/catalogs/relationRoles`, kein Freitext.
        isActive:
          type: [boolean, "null"]
          description: Ob die Beziehung derzeit aktiv ist. `null`, wenn der Anbieter das nicht führt.

    Contract:
      type: object
      required: [id, contractNumber, customerId, statusCode, participants, sparten, metadata]
      properties:
        id: { type: string, maxLength: 64 }
        contractNumber: { type: string, maxLength: 64 }
        customerId:
          type: string
          maxLength: 64
          description: Kunde, dem der Vertrag gehört (Versicherungsnehmer).
        statusCode:
          type: string
          maxLength: 64
          description: Code aus `/catalogs/contractStatuses`. Der Katalog enthält auch vom Mandanten definierte Status.
        productCode:
          type: [string, "null"]
          maxLength: 64
          description: Code aus `/catalogs/productTypes`, kein Produktname als Freitext.
        insurerCode:
          type: [string, "null"]
          maxLength: 64
          description: Code aus `/catalogs/insurers`.
        validFrom: { type: [string, "null"], format: date }
        validUntil: { type: [string, "null"], format: date }
        mainDueDay:
          type: [integer, "null"]
          minimum: 1
          maximum: 31
          description: |
            Tag der Hauptfälligkeit. `null`, wenn der Anbieter keine Fälligkeit führt.
        mainDueMonth:
          type: [integer, "null"]
          minimum: 1
          maximum: 12
          description: Monat der Hauptfälligkeit.
        premium:
          $ref: "#/components/schemas/Premium"
          description: |
            Gesamtprämie des Vertrags über alle Sparten. Sie darf von der Summe der Spartenprämien abweichen.
            Dann muss der Anbieter angeben, wie die beiden zusammenhängen.
        participants:
          type: array
          description: |
            Alle Personen, die mit dem Vertrag verknüpft sind, jede mit eigenem `roleCode`. Die Rolle ergibt
            sich nur aus `roleCode`, nicht aus der Zugehörigkeit zur Liste.
          items: { $ref: "#/components/schemas/Participant" }
        sparten:
          type: array
          description: |
            Eine oder mehrere Sparten (Versicherungszweige) des Vertrags. Prämie, Versicherungssumme,
            Selbstbehalt, Gültigkeit und Risiken hängen an der Sparte. Auch ein Vertrag mit nur einer Deckung
            hat eine Sparte.
          items: { $ref: "#/components/schemas/ContractSparte" }
        metadata: { $ref: "#/components/schemas/Metadata" }

    ContractSparte:
      type: object
      description: |
        Eine Sparte (Versicherungszweig, Deckung) eines Vertrags mit eigener Prämie, Versicherungssumme,
        Selbstbehalt und Gültigkeit. Risiken werden hier verknüpft, nicht am Vertrag. Eine Sparte darf nur
        Risiken des Vertragskunden referenzieren.
      required: [id, spartenCode, premium]
      properties:
        id: { type: string, maxLength: 64 }
        spartenCode:
          type: string
          maxLength: 64
          description: Code aus `/catalogs/sparten`. Jeder Code kommt pro Vertrag nur einmal vor.
        spartenLabel:
          type: [string, "null"]
          maxLength: 256
          description: Nur zur Anzeige. Für die Zuordnung zählt nur `spartenCode`.
        validFrom: { type: [string, "null"], format: date }
        validUntil: { type: [string, "null"], format: date }
        premium:
          $ref: "#/components/schemas/Premium"
          description: Prämie dieser Sparte.
        insuredSum:
          $ref: "#/components/schemas/MonetaryAmount"
          description: Versicherungssumme (maximale Auszahlung) dieser Sparte.
        deductible:
          $ref: "#/components/schemas/MonetaryAmount"
          description: Selbstbehalt dieser Sparte.
        riskIds:
          type: array
          description: |
            ids der Risiken, die diese Sparte deckt, abrufbar über `GET /customers/{customerId}/risks`. Die
            Risiken gehören dem Kunden.
          items: { type: string, maxLength: 64 }

    ContractCreate:
      type: object
      description: |
        Body zum Anlegen eines Vertrags. Katalog-Codes werden gegen den jeweiligen Katalog geprüft; ein
        unbekannter Code führt zu 422 mit den erlaubten Codes (`allowedCodes`).
      required: [customerId, statusCode, sparten]
      properties:
        customerId:
          type: string
          maxLength: 64
          description: Versicherungsnehmer. Muss ein Kunde des angemeldeten Benutzers sein, sonst 403.
        contractNumber:
          type: [string, "null"]
          maxLength: 64
          description: |
            Optional. Fehlt sie, vergibt der Anbieter eine eigene Nummer und liefert sie in der 201-Response.
            Ist sie gesetzt, übernimmt der Anbieter den Wert. Ein Anbieter, der keine vom Client vergebenen
            Nummern annimmt, dokumentiert das und ignoriert das Feld dann immer.
        statusCode:
          type: string
          maxLength: 64
          description: Code aus `/catalogs/contractStatuses`.
        productCode:
          type: [string, "null"]
          maxLength: 64
          description: Code aus `/catalogs/productTypes`.
        insurerCode:
          type: [string, "null"]
          maxLength: 64
          description: Code aus `/catalogs/insurers`.
        validFrom: { type: [string, "null"], format: date }
        validUntil: { type: [string, "null"], format: date }
        mainDueDay:
          type: [integer, "null"]
          minimum: 1
          maximum: 31
          description: Tag der Hauptfälligkeit.
        mainDueMonth:
          type: [integer, "null"]
          minimum: 1
          maximum: 12
          description: Monat der Hauptfälligkeit.
        premium:
          $ref: "#/components/schemas/Premium"
          description: Gesamtprämie des Vertrags, in derselben Form wie beim Lesen.
        participants:
          type: array
          description: |
            Mit dem Vertrag verknüpfte Personen, jede mit `roleCode` aus `/catalogs/participantRoles`. Die
            Rolle ergibt sich nur aus `roleCode`.
          items: { $ref: "#/components/schemas/ParticipantCreate" }
        sparten:
          type: array
          minItems: 1
          description: |
            Sparten des Vertrags. Auch ein Vertrag mit nur einer Deckung braucht eine Sparte.
          items: { $ref: "#/components/schemas/ContractSparteCreate" }

    ContractSparteCreate:
      type: object
      description: |
        Eine Sparte beim Anlegen eines Vertrags. Prämie, Versicherungssumme, Selbstbehalt und Gültigkeit sind
        optional.
      required: [spartenCode]
      properties:
        spartenCode:
          type: string
          maxLength: 64
          description: Code aus `/catalogs/sparten`. Jeder Code darf pro Vertrag nur einmal vorkommen.
        spartenLabel:
          type: [string, "null"]
          maxLength: 256
          description: Nur zur Anzeige. Für die Zuordnung zählt nur `spartenCode`.
        validFrom: { type: [string, "null"], format: date }
        validUntil: { type: [string, "null"], format: date }
        premium:
          $ref: "#/components/schemas/Premium"
          description: Prämie dieser Sparte.
        insuredSum:
          $ref: "#/components/schemas/MonetaryAmount"
          description: Versicherungssumme (maximale Auszahlung) dieser Sparte.
        deductible:
          $ref: "#/components/schemas/MonetaryAmount"
          description: Selbstbehalt dieser Sparte.
        riskIds:
          type: array
          description: |
            ids der Risiken, die diese Sparte deckt, abrufbar über `GET /customers/{customerId}/risks`. Erlaubt
            sind nur Risiken des Vertragskunden, jede andere id führt zu 422 `contract.riskCustomerMismatch`.
          items: { type: string, maxLength: 64 }

    ParticipantCreate:
      type: object
      description: |
        Ein Vertragsbeteiligter. Der Body enthält nur `personId` und `roleCode`. Die id der Beteiligung
        vergibt der Server, `displayName` und `birthDate` ergänzt er beim Lesen. `roleCode` wird gegen
        `/catalogs/participantRoles` geprüft.
      required: [personId, roleCode]
      properties:
        personId:
          type: string
          maxLength: 64
          description: Abrufbare Kunden- oder Personen-id.
        roleCode:
          type: string
          maxLength: 64
          description: Code aus `/catalogs/participantRoles`, z. B. `insurant`, `insuredPerson`, `premiumPayer`, `beneficiary`.

    ContractUpdate:
      type: object
      description: |
        Body für eine partielle Änderung eines Vertrags. Ein fehlendes Feld bleibt unverändert, `null` löscht
        den Wert.

        `id` und `customerId` sind nicht Teil des Bodys: Ein Vertrag lässt sich nicht auf einen anderen Kunden
        umhängen. Katalogprüfung wie beim Anlegen.
      properties:
        contractNumber:
          type: [string, "null"]
          maxLength: 64
          description: Fehlt = unverändert, sonst wird die Vertragsnummer ersetzt.
        statusCode:
          type: [string, "null"]
          maxLength: 64
          description: Fehlt = unverändert. Code aus `/catalogs/contractStatuses`.
        productCode:
          type: [string, "null"]
          maxLength: 64
          description: Fehlt = unverändert, `null` löscht. Code aus `/catalogs/productTypes`.
        insurerCode:
          type: [string, "null"]
          maxLength: 64
          description: Fehlt = unverändert, `null` löscht. Code aus `/catalogs/insurers`.
        validFrom:
          type: [string, "null"]
          format: date
          description: Fehlt = unverändert, `null` löscht.
        validUntil:
          type: [string, "null"]
          format: date
          description: Fehlt = unverändert, `null` löscht.
        mainDueDay:
          type: [integer, "null"]
          minimum: 1
          maximum: 31
          description: Fehlt = unverändert, `null` löscht. Tag der Hauptfälligkeit.
        mainDueMonth:
          type: [integer, "null"]
          minimum: 1
          maximum: 12
          description: Fehlt = unverändert, `null` löscht. Monat der Hauptfälligkeit.
        premium:
          $ref: "#/components/schemas/Premium"
          description: Fehlt = unverändert. Gesamtprämie des Vertrags.
        participants:
          type: array
          description: |
            Fehlt = Beteiligte unverändert. Ist die Liste vorhanden, ist sie der neue Stand aller Beteiligten,
            zugeordnet über die id der Beteiligung:

            - Eintrag mit `id` ändert diese Beteiligung.
            - Eintrag ohne `id` legt eine neue an.
            - Eine bestehende Beteiligung, deren id in der Liste fehlt, wird entfernt.
          items: { $ref: "#/components/schemas/ParticipantUpdate" }
        sparten:
          type: array
          description: |
            Fehlt = alle Sparten bleiben unverändert. Ist die Liste vorhanden, ist sie der neue Stand aller
            Sparten, zugeordnet über die Sparten-id:

            - Eintrag mit `id` ändert diese Sparte.
            - Eintrag ohne `id` legt eine neue an.
            - Eine bestehende Sparte, deren id in der Liste fehlt, wird gelöscht. Die Löschung erscheint auch
              in `GET /deletions`.
          items: { $ref: "#/components/schemas/ContractSparteUpdate" }

    ContractSparteUpdate:
      type: object
      description: |
        Eine Sparte in `ContractUpdate.sparten`. Der Eintrag gibt den gewünschten Zustand der Sparte an,
        `spartenCode` ist wie beim Anlegen Pflicht. Katalog- und Risikoprüfung wie bei `ContractSparteCreate`.
      required: [spartenCode]
      properties:
        id:
          type: [string, "null"]
          maxLength: 64
          description: id einer bestehenden Sparte, die geändert wird. Weglassen oder `null` legt eine neue Sparte an.
        spartenCode:
          type: string
          maxLength: 64
          description: Code aus `/catalogs/sparten`. Jeder Code darf pro Vertrag nur einmal vorkommen.
        spartenLabel:
          type: [string, "null"]
          maxLength: 256
          description: Nur zur Anzeige. Für die Zuordnung zählt nur `spartenCode`.
        validFrom: { type: [string, "null"], format: date }
        validUntil: { type: [string, "null"], format: date }
        premium:
          $ref: "#/components/schemas/Premium"
          description: Prämie dieser Sparte.
        insuredSum:
          $ref: "#/components/schemas/MonetaryAmount"
          description: Versicherungssumme (maximale Auszahlung) dieser Sparte.
        deductible:
          $ref: "#/components/schemas/MonetaryAmount"
          description: Selbstbehalt dieser Sparte.
        riskIds:
          type: array
          description: |
            ids der Risiken, die diese Sparte deckt. Erlaubt sind nur Risiken des Vertragskunden, jede andere
            id führt zu 422 `contract.riskCustomerMismatch`.
          items: { type: string, maxLength: 64 }

    ParticipantUpdate:
      type: object
      description: |
        Ein Beteiligter in `ContractUpdate.participants`. `roleCode` wird gegen `/catalogs/participantRoles`
        geprüft.
      required: [personId, roleCode]
      properties:
        id:
          type: [string, "null"]
          maxLength: 64
          description: id einer bestehenden Beteiligung, die geändert wird. Weglassen oder `null` legt eine neue an.
        personId:
          type: string
          maxLength: 64
          description: Abrufbare Kunden- oder Personen-id.
        roleCode:
          type: string
          maxLength: 64
          description: Code aus `/catalogs/participantRoles`.

    MonetaryAmount:
      type: object
      description: Geldbetrag als Zahl mit Währung, kein String. Wird für Versicherungssummen und Selbstbehalte verwendet.
      required: [amount, currency]
      properties:
        amount:
          type: number
          description: Dezimalzahl mit zwei Nachkommastellen.
        currency:
          type: string
          maxLength: 3
          description: Währungscode nach ISO 4217.

    Premium:
      type: object
      description: |
        Prämie mit Währung. Mindestens eines von `netAmount` und `grossAmount` muss gesetzt sein. Ist nur eines
        gesetzt, muss auch `insuranceTaxRate` gesetzt sein, damit sich der andere Wert berechnen lässt.
      required: [currency]
      properties:
        currency:
          type: string
          maxLength: 3
          description: Währungscode nach ISO 4217.
        netAmount:
          type: [number, "null"]
          description: Nettoprämie, zwei Nachkommastellen.
        grossAmount:
          type: [number, "null"]
          description: Bruttoprämie inklusive Versicherungssteuer, zwei Nachkommastellen.
        insuranceTaxRate:
          type: [number, "null"]
          description: |
            Angewandter Versicherungssteuersatz in Prozent, z. B. `11.0`. Muss gesetzt sein, wenn nur `netAmount`
            oder nur `grossAmount` geliefert wird.
        paymentIntervalCode:
          type: [string, "null"]
          maxLength: 32
          description: Einer von `monthly`, `quarterly`, `yearly`, `once`.

    Participant:
      type: object
      description: Am Vertrag beteiligte Person mit ihrer Rolle. Name und Geburtsdatum stehen direkt im Eintrag, ein zusätzlicher Personenabruf ist nicht nötig.
      required: [id, personId, roleCode, displayName]
      properties:
        id:
          type: string
          maxLength: 64
          description: Id der Beteiligung selbst, nicht der Person. Bleibt über Syncs hinweg gleich.
        personId:
          type: string
          maxLength: 64
          description: Auflösbare Kunden- bzw. Personen-id.
        roleCode:
          type: string
          maxLength: 64
          description: Code aus `/catalogs/participantRoles`, z. B. `insurant`, `insuredPerson`, `premiumPayer`, `beneficiary`.
        displayName:
          type: string
          maxLength: 512
          description: Anzeigename der Person.
        birthDate: { type: [string, "null"], format: date }

    Risk:
      type: object
      required: [id, customerId, typeCode, isActive, metadata]
      properties:
        id:
          type: string
          maxLength: 64
          description: Eigene id des Risikos beim Anbieter, kein zusammengesetzter Schlüssel.
        customerId:
          type: string
          maxLength: 64
          description: |
            Kunde, dem das Risiko gehört. Vertragssparten verweisen über `ContractSparte.riskIds` auf Risiken. Eine
            Sparte darf nur Risiken des Vertragskunden referenzieren.
        typeCode:
          type: string
          maxLength: 32
          description: Code aus `/catalogs/riskTypes`, z. B. `person`, `vehicle`, `property`.
        personId:
          type: [string, "null"]
          maxLength: 64
          description: |
            Nur bei Personenrisiken (`typeCode` `person`) gesetzt, mit der Kunden- bzw. Personen-id. Eine eigene
            Personen-Ressource gibt es nicht; Name und Geburtsdatum stehen deshalb direkt im Risiko.
        displayName:
          type: [string, "null"]
          maxLength: 512
          description: |
            Name der Person bei Personenrisiken, zusammen mit `personId` gesetzt. Bei anderen Risiken `null`.
        birthDate:
          type: [string, "null"]
          format: date
          description: |
            Geburtsdatum der Person bei Personenrisiken. Bei anderen Risiken `null`.
        description: { type: [string, "null"], maxLength: 512 }
        isActive:
          type: boolean
          description: |
            Ob das Risiko aktiv ist. Inaktive Risiken bleiben abrufbar.
        metadata: { $ref: "#/components/schemas/Metadata" }

    RiskCreate:
      type: object
      description: |
        Body zum Anlegen eines Risikos. `customerId` kommt aus dem Pfad, nicht aus dem Body.
      required: [typeCode]
      properties:
        typeCode:
          type: string
          maxLength: 32
          description: |
            Code aus `/catalogs/riskTypes`, z. B. `person`, `vehicle`, `property`. Ein unbekannter Code führt zu
            422; die Antwort nennt die erlaubten Codes in `allowedCodes`.
        personId:
          type: [string, "null"]
          maxLength: 64
          description: |
            Nur bei Personenrisiken (`typeCode` `person`). Muss beim Anbieter als Kunde mit dieser id auflösbar
            sein. Bei anderen Risiken `null`.
        displayName:
          type: [string, "null"]
          maxLength: 512
          description: Name der Person bei Personenrisiken, zusammen mit `personId`. Bei anderen Risiken `null`.
        birthDate:
          type: [string, "null"]
          format: date
          description: Geburtsdatum der Person bei Personenrisiken. Bei anderen Risiken `null`.
        description: { type: [string, "null"], maxLength: 512 }
        isActive:
          type: boolean
          default: true
          description: Ob das Risiko aktiv ist.

    RiskUpdate:
      type: object
      description: |
        Body für eine teilweise Änderung eines Risikos. Fehlt ein Feld, bleibt der gespeicherte Wert unverändert;
        `null` löscht ihn.

        `id`, `customerId` und `typeCode` lassen sich nicht ändern. Für einen anderen Risikotyp ein neues Risiko
        anlegen und das alte mit `isActive: false` stilllegen.
      properties:
        personId:
          type: [string, "null"]
          maxLength: 64
          description: Fehlt = unverändert; `null` löscht. Id der Person bei Personenrisiken.
        displayName:
          type: [string, "null"]
          maxLength: 512
          description: Fehlt = unverändert; `null` löscht. Name der Person bei Personenrisiken.
        birthDate:
          type: [string, "null"]
          format: date
          description: Fehlt = unverändert; `null` löscht. Geburtsdatum der Person bei Personenrisiken.
        description:
          type: [string, "null"]
          maxLength: 512
          description: Fehlt = unverändert; `null` löscht.
        isActive:
          type: [boolean, "null"]
          description: Fehlt = unverändert. `false` legt das Risiko still, z. B. wenn es wegen eines anderen Typs ersetzt wird.

    Claim:
      type: object
      description: |
        Minimales Schadenmodell (ein `dateOfDamage`, kein KFZ-spezifischer Block). Anbieter können es je
        Fachgebiet erweitern. Für zusätzliche Felder gelten dieselben Regeln wie für alle anderen: Metadaten,
        Delta-Sync, Löschung, Kataloge und Fehlerformat.
      required: [id, claimNumber, contractId, statusCode, metadata]
      properties:
        id: { type: string, maxLength: 64 }
        claimNumber:
          type: string
          maxLength: 32
          description: Schadennummer.
        contractId: { type: string, maxLength: 64 }
        riskIds:
          type: array
          description: |
            Ids der betroffenen Risiken des Vertragskunden, abrufbar über `/customers/{customerId}/risks` (wie
            `ContractSparte.riskIds`). Leer oder nicht vorhanden, wenn der Anbieter Schäden nicht mit Risiken
            verknüpft.
          items: { type: string, maxLength: 64 }
        statusCode:
          type: string
          maxLength: 64
          description: Code aus `/catalogs/claimStatuses`.
        causeOfDamageCode:
          type: [string, "null"]
          maxLength: 64
          description: |
            Schadenursache als Code aus `/catalogs/damageCauses`. Zum Abgleich diesen Code verwenden, nicht
            `causeOfDamage`. `null`, wenn der Anbieter nur Freitext führt.
        causeOfDamage:
          type: [string, "null"]
          maxLength: 1024
          description: Ursache als Freitext zur Anzeige, in Liste und Detail enthalten. Zum Abgleich `causeOfDamageCode` verwenden.
        dateOfDamage: { type: [string, "null"], format: date }
        lossAmount:
          $ref: "#/components/schemas/MonetaryAmount"
          description: Gesamte Schadenhöhe; `null`, wenn unbekannt.
        paidAmount:
          $ref: "#/components/schemas/MonetaryAmount"
          description: Bisher ausgezahlter Betrag; `null`, wenn unbekannt.
        metadata: { $ref: "#/components/schemas/Metadata" }

    ClaimCreate:
      type: object
      description: |
        Body zum Melden eines Schadens.
      required: [contractId, dateOfDamage, description]
      properties:
        contractId: { type: string, maxLength: 64 }
        dateOfDamage: { type: string, format: date }
        description: { type: string, maxLength: 4096 }
        causeOfDamageCode:
          type: [string, "null"]
          maxLength: 64
          description: Code aus `/catalogs/damageCauses`. Ein unbekannter Code wird abgelehnt, die Antwort nennt die erlaubten Codes in `allowedCodes`.

    DocumentMetadata:
      type: object
      description: |
        Metadaten eines Dokuments ohne den Inhalt. Sie stehen fest, bevor der Inhalt heruntergeladen wird.

        Mindestens eines von `customerId`, `contractId` und `claimId` ist gesetzt. BPP ordnet das Dokument der
        spezifischsten gesetzten id zu (`claimId`, sonst `contractId`, sonst `customerId`). Bei einem
        Schadendokument genügt `claimId`; Vertrag und Kunde ergeben sich über den Schaden.
      required: [id, fileName, sizeBytes, mimeType, sha256, documentTypeCode, metadata]
      properties:
        id:
          type: string
          maxLength: 64
          description: Ändert sich während der Lebensdauer des Dokuments nicht.
        fileName: { type: string, maxLength: 512 }
        sizeBytes:
          type: integer
          format: int64
          description: Tatsächliche Größe des Inhalts in Bytes.
        mimeType:
          type: string
          maxLength: 128
          description: Vom Server festgelegt, nicht aus dem Dateinamen abgeleitet.
        sha256:
          type: string
          maxLength: 64
          description: SHA-256 des Inhalts als Hex in Kleinbuchstaben. Ändert sich genau dann, wenn sich der Inhalt ändert.
        documentTypeCode:
          type: string
          maxLength: 64
          description: Code aus `/catalogs/documentTypes`.
        description: { type: [string, "null"], maxLength: 1024 }
        customerId:
          type: [string, "null"]
          maxLength: 64
          description: Zugeordneter Kunde, falls vorhanden.
        contractId:
          type: [string, "null"]
          maxLength: 64
          description: Zugeordneter Vertrag, falls vorhanden.
        claimId:
          type: [string, "null"]
          maxLength: 64
          description: Zugeordneter Schaden, falls vorhanden.
        validFrom: { type: [string, "null"], format: date }
        validUntil: { type: [string, "null"], format: date }
        metadata: { $ref: "#/components/schemas/Metadata" }

    DocumentUploadMetadata:
      type: object
      description: |
        Metadaten für einen Upload. Mindestens eines von `customerId`, `contractId` und `claimId` muss gesetzt
        sein, sonst 422 `document.ownerMissing`. Alle drei dürfen zusammen gesetzt sein (Schaden, Vertrag,
        Kunde). Zugeordnet wird zur spezifischsten gesetzten id (`claimId`, sonst `contractId`, sonst
        `customerId`).
      required: [fileName, mimeType, documentTypeCode]
      properties:
        fileName: { type: string, maxLength: 512 }
        mimeType:
          type: string
          maxLength: 128
          description: Muss einer der erlaubten MIME-Typen sein, sonst 422 `document.unsupportedMediaType`.
        documentTypeCode:
          type: string
          maxLength: 64
          description: Aktiver Code aus `/catalogs/documentTypes`.
        description: { type: [string, "null"], maxLength: 1024 }
        customerId:
          type: [string, "null"]
          maxLength: 64
          description: Zugeordneter Kunde.
        contractId:
          type: [string, "null"]
          maxLength: 64
          description: Zugeordneter Vertrag.
        claimId:
          type: [string, "null"]
          maxLength: 64
          description: Zugeordneter Schaden.
        sha256:
          type: [string, "null"]
          maxLength: 64
          description: Optional, vom Client berechnet. Der Server prüft den Hash und lehnt den Upload bei Abweichung ab.

    Deletion:
      type: object
      description: Eintrag im Lösch-Feed `GET /deletions`, für eine Löschung oder einen Merge.
      required: [entityType, id, deletedAt]
      properties:
        entityType:
          type: string
          enum: [customer, contract, claim, document, risk]
        id: { type: string, maxLength: 64 }
        deletedAt: { type: string, format: date-time }
        mergedIntoId:
          type: [string, "null"]
          maxLength: 64
          description: Bei einem Merge die id des verbleibenden Datensatzes. Sollte dann gesetzt sein.

    Problem:
      type: object
      description: Fehler-Body nach RFC 7807, ergänzt um einen stabilen Fehlercode in `code`.
      required: [title, status, code]
      properties:
        type:
          type: string
          format: uri
          description: Aufrufbarer Link auf die Dokumentation dieser Fehlerart.
        title: { type: string, maxLength: 256 }
        status: { type: integer }
        detail:
          type: string
          maxLength: 2048
          description: Text für Menschen, nicht zur maschinellen Auswertung.
        code:
          type: string
          maxLength: 128
          description: |
            Stabiler, maschinenlesbarer Fehlercode aus dem veröffentlichten Fehlercode-Katalog, z. B.
            `resource.notFound`, `resource.deleted`, `resource.merged`, `resource.versionConflict`,
            `catalog.unknownCode`, `customer.typeFieldMismatch`, `customer.multipleDefaults`,
            `bankAccount.invalidIban`, `bankAccount.duplicateIban`, `contactInfo.empty`,
            `contract.riskCustomerMismatch`, `document.tooLarge`, `document.unsupportedMediaType`,
            `document.ownerMissing`, `document.hashMismatch`, `auth.tokenExpired`, `auth.invalidToken`,
            `auth.forbidden`.
        errors:
          type: array
          description: Validierungsfehler, ein Eintrag pro Fehler.
          items:
            type: object
            required: [field, code, message]
            properties:
              field: { type: string, maxLength: 256 }
              code: { type: string, maxLength: 128 }
              message: { type: string, maxLength: 1024 }
              rejectedValue: { type: [string, "null"], maxLength: 512 }
              allowedCodes:
                type: array
                description: Bei abgelehnten Katalogwerten die aktuell erlaubten Codes.
                items: { type: string, maxLength: 64 }
        traceId:
          type: string
          maxLength: 64
          description: Korrelations-id für den Support.
