openapi: 3.1.0

# =============================================================================
# Die Lifecycle-API des Batteriepasses nach EN 18222 Tabelle 17.
#
# Diese Datei ist die Übergabe an Integratoren: sie beschreibt, was ein
# Marktteilnehmer aufrufen kann, ohne unseren Code zu lesen.
#
# Zwei Dinge, die man wissen muss, bevor man loslegt:
#
#   1. LESEN BRAUCHT KEINEN SCHLÜSSEL. Anhang XIII Punkt 1 der VO (EU) 2023/1542
#      ist per Definition öffentlich, und EN 18222 kennt dafür keinen
#      Zugangsvorbehalt. Ein Schlüssel hebt die Stufe, er schaltet nicht frei.
#
#   2. DIE ANTWORT TRÄGT KEINEN UMSCHLAG. Wer ein DPP-Objekt erwartet, findet
#      DigitalProductPassportID unter `data`, nicht unter `data.data`. Das
#      Ergebnisobjekt nach Tabelle 14 steht daneben unter `result`.
# =============================================================================

info:
  title: Battery Passport Lifecycle API (EN 18222)
  version: '1.0.0'
  description: |
    Pflichtmethoden nach EN 18222 Tabelle 17, umgesetzt über dem Datenmodell aus
    EN 18223. Das Serialisierungsformat ist JSON-LD (EN 18216 5 c) — der
    `@context` liegt unter `/api/dpp/v1/context` und ist für alle Antworten
    derselbe.

    **Normstand:** Die EN-Dokumente sind Entwürfe (prEN). Eine
    Vermutungswirkung nach Anhang ZA besteht erst nach Zitierung im
    EU-Amtsblatt. Die Felder `DPPSchemaVersion` in jeder Antwort nennt die
    Fassung, gegen die die jeweilige Passversion gebaut wurde.
  contact:
    name: Elektro DPP
    url: https://elektro-beta.vercel.app/de/dpp

servers:
  - url: https://elektro-beta.vercel.app
    description: Produktion

tags:
  - name: read
    description: |
      Schlüssellos aufrufbar. Ohne Schlüssel wird die öffentliche Stufe
      geliefert (Anhang XIII Punkt 1); mit Schlüssel die Stufe, die seine
      Berechtigungen hergeben. Ein ungültiger Schlüssel führt auf die
      öffentliche Stufe, nicht auf 401 — die öffentlichen Angaben stehen auch
      dem zu, der sich vergeblich auszuweisen versucht hat.
  - name: write
    description: |
      Braucht einen Schlüssel mit `dpp:publish`. Eine Änderung am Pass ist eine
      Aussage des Wirtschaftsakteurs, und die kann nur er selbst treffen.

# ---------------------------------------------------------------------------
# Content Negotiation (EN 18216:2026 Klausel 5)
# ---------------------------------------------------------------------------
#
# Jede Leseoperation handelt die Form über `Accept` aus:
#
#   application/ld+json   (Standard)  JSON mit Kontext — die reichere Form
#   application/json                  identischer Rumpf, nur anderer Content-Type
#   application/xml, text/xml         EN 18223:2026 Anhang B (normativ)
#   text/html                         303 auf /{locale}/passport/{uid}
#
# Bei gleichem Qualitätsfaktor gewinnt die reichere Form. Ein Typ, den wir nicht
# haben, bekommt die Pflichtform (JSON) statt 406 — JSON ist das `shall` und damit
# nie die falsche Antwort, und Tabelle 15 kennt keinen Statusnamen für 406.
#
# Jede Antwort trägt `Vary: Accept`.
#
# Ein FEHLER kommt immer als JSON, auch bei `Accept: application/xml`: Anhang B
# beschreibt ein DigitalProductPassport, keinen Fehlerumschlag.

paths:

  /api/dpp/v1/dpps/{dppId}:
    get:
      security: []
      tags: [read]
      operationId: ReadDPPById
      summary: Einen Pass über seine Kennung lesen
      parameters:
        - $ref: '#/components/parameters/DppId'
        - $ref: '#/components/parameters/Representation'
        - $ref: '#/components/parameters/Accept'
        - $ref: '#/components/parameters/CorrelationId'
      responses:
        '200':
          $ref: '#/components/responses/DppObject'
        '400': { $ref: '#/components/responses/Fehler' }
        '404':
          description: |
            Unbekannter Pass — oder ein Entwurf. Beides ist von außen nicht zu
            unterscheiden, und das ist Absicht: die Existenz eines Entwurfs ist
            keine öffentliche Information.
          content:
            application/ld+json:
              schema: { $ref: '#/components/schemas/FehlerAntwort' }
        '429': { $ref: '#/components/responses/RateLimit' }

    patch:
      tags: [write]
      operationId: UpdateDPP
      summary: Werte eines Passes ändern (RFC 7396, alles oder nichts)
      description: |
        Merge-Patch nach RFC 7396:

        - Ein Feld mit einem Wert wird gesetzt.
        - Ein Feld mit `null` wird **gelöscht**.
        - Ein Feld, das nicht im Patch steht, bleibt unberührt.

        Der Unterschied zwischen `null` und "nicht erwähnt" ist wesentlich: ohne
        ihn können Sie einen Wert nicht zurücknehmen, nur überschreiben.

        **Alles oder nichts (EN 18222 4.7).** Ist ein einziges Feld unbekannt
        oder ausgemustert, scheitert der ganze Vorgang und **kein** Wert wird
        geschrieben. Das unterscheidet diese Route von unserem SAP-Ingest, der
        einzelne Felder abweist und den Rest schreibt.

        Die Änderung wirkt auf die Entwurfswerte. Sie erscheint erst im Pass,
        wenn er neu veröffentlicht wird.
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/DppId'
        - $ref: '#/components/parameters/CorrelationId'
        - $ref: '#/components/parameters/DppDataStatus'
        - $ref: '#/components/parameters/DppSourceSystem'
        - $ref: '#/components/parameters/DppExtractedAt'
        - $ref: '#/components/parameters/DppConfidence'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              description: Katalogschlüssel auf Wert. `null` löscht.
            examples:
              setzen:
                summary: Zwei Felder setzen
                value: { a_battery_serial: 'ABC-12345', e_capacity_fade: 3.5 }
              loeschen:
                summary: Ein Feld zurücknehmen
                value: { e_capacity_fade: null }
      responses:
        '200':
          description: Übernommen.
          content:
            application/ld+json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Umschlag'
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          updated: { type: integer }
                          cleared: { type: integer }
        '401': { $ref: '#/components/responses/Fehler' }
        '403': { $ref: '#/components/responses/Fehler' }
        '404': { $ref: '#/components/responses/Fehler' }
        '422':
          description: |
            Abgelehnt — **nichts** wurde geschrieben. `result.text` nennt das
            Feld, an dem es lag.
          content:
            application/ld+json:
              schema: { $ref: '#/components/schemas/FehlerAntwort' }

    delete:
      tags: [write]
      operationId: DeleteDPPById
      summary: Entwurf löschen, ausgestellten Pass archivieren
      description: |
        **Ein in Verkehr gebrachter Pass wird nicht gelöscht, sondern
        archiviert.** Art. 77 macht ihn zu einem Dokument über eine Batterie, die
        es gibt, und DSGVO Art. 17(3)(b) nimmt ihn ausdrücklich von der Löschung
        aus. Die Antwort ist dann `200` mit `messageType: Warning` und dem Code
        `ARCHIVED` — kein Fehler, sondern eine andere Auskunft.

        Nur ein Entwurf verschwindet wirklich. Der Vorgang wird mit Grund im
        Audit protokolliert.
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/DppId'
        - $ref: '#/components/parameters/CorrelationId'
      responses:
        '200':
          description: |
            Gelöscht (`DELETED`), archiviert (`ARCHIVED`) oder bereits
            archiviert (`ALREADY_ARCHIVED`) — `result.code` sagt, was geschah.
          content:
            application/ld+json:
              schema: { $ref: '#/components/schemas/FehlerAntwort' }
        '401': { $ref: '#/components/responses/Fehler' }
        '403': { $ref: '#/components/responses/Fehler' }
        '404': { $ref: '#/components/responses/Fehler' }

  /api/dpp/v1/dpps/{dppId}/collections/{elementId}:
    get:
      security: []
      tags: [read]
      operationId: ReadDataElementCollection
      summary: Eine Anhang-XIII-Gruppe lesen (EN 18222 6.2)
      description: |
        `elementId` ist der Anhang-XIII-Gruppenbuchstabe, zum Beispiel `L`.

        Eine Gruppe, von der die aufrufende Rolle kein einziges Feld sehen darf,
        antwortet mit **404** und nicht mit einer leeren Sammlung: eine leere
        Sammlung wäre die Auskunft, dass es sie gibt.
      parameters:
        - $ref: '#/components/parameters/DppId'
        - $ref: '#/components/parameters/ElementId'
        - $ref: '#/components/parameters/CorrelationId'
      responses:
        '200':
          description: Die Sammlung mit ihren DataElements.
          content:
            application/ld+json:
              schema:
                type: object
                properties:
                  '@context': { type: string, format: uri }
                  result: { $ref: '#/components/schemas/Result' }
                  data:
                    type: object
                    properties:
                      DigitalProductPassportID: { type: string, format: uri }
                      DPPStatus: { type: string, enum: [Active, Archived] }
                      collection:
                        type: object
                        description: |
                          Unsere Gruppenansicht — KEINE Normstruktur. EN 18223:2026
                          kennt `DataElementCollection` als Modellklasse, aber die
                          Anhang-XIII-Gruppe ist dort ein Metadatum im Wörterbuch
                          und kein Behälter in den Daten.
                        properties:
                          elementId: { type: string, example: L }
                          elements:
                            type: array
                            items: { $ref: '#/components/schemas/FullElement' }
        '400': { $ref: '#/components/responses/Fehler' }
        '404': { $ref: '#/components/responses/Fehler' }
        '429': { $ref: '#/components/responses/RateLimit' }
    patch:
      tags: [write]
      operationId: UpdateDataElementCollection
      summary: Eine Gruppe ändern, eingegrenzt auf die Gruppe (EN 18222 6.4)
      description: |
        Merge-Patch nach RFC 7396, aber **auf die Gruppe begrenzt**: ein
        Schlüssel aus einer anderen Gruppe lässt den ganzen Vorgang mit
        `ELEMENT_OUTSIDE_COLLECTION` scheitern.

        Das ist der Unterschied zur Wurzelroute. Ein Reparaturbetrieb, der
        `collections/L` schreibt, kann sich nicht an Gruppe B vergreifen — und
        erfährt es, statt dass es stillschweigend verworfen wird. Die feldweise
        Rechteprüfung (Migration 063) bleibt darunter aktiv.
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/DppId'
        - $ref: '#/components/parameters/ElementId'
        - $ref: '#/components/parameters/CorrelationId'
        - $ref: '#/components/parameters/DppDataStatus'
        - $ref: '#/components/parameters/DppSourceSystem'
        - $ref: '#/components/parameters/DppExtractedAt'
        - $ref: '#/components/parameters/DppConfidence'
      requestBody:
        required: true
        content:
          application/merge-patch+json:
            schema:
              type: object
              additionalProperties: true
              description: Katalogschlüssel dieser Gruppe auf Werte; `null` löscht.
            example: { l_repair_history: 'Zellmodul 3 getauscht 2026-09-01' }
      responses:
        '200':
          description: Zähler der geänderten und gelöschten Felder.
          content:
            application/ld+json:
              schema: { $ref: '#/components/schemas/PatchAntwort' }
        '400': { $ref: '#/components/responses/Fehler' }
        '401': { $ref: '#/components/responses/Fehler' }
        '403': { $ref: '#/components/responses/Fehler' }
        '404': { $ref: '#/components/responses/Fehler' }
        '422': { $ref: '#/components/responses/Fehler' }

  /api/dpp/v1/dpps/{dppId}/elements/{elementPath}:
    get:
      security: []
      tags: [read]
      operationId: ReadDataElement
      summary: Ein einzelnes Datenelement lesen (EN 18222 6.3)
      description: |
        `elementPath` ist der absolute Elementpfad `{Gruppe}/{Katalogschlüssel}`,
        zum Beispiel `L/l_repair_history`. Er enthält einen Schrägstrich und wird
        **nicht** kodiert.

        Der Gruppenbuchstabe ist redundant — er steckt im Schlüsselpräfix — und
        bleibt trotzdem im Pfad, weil die Norm einen absoluten Pfad verlangt und
        weil die Redundanz prüfbar ist: `A/l_repair_history` widerspricht sich
        und wird abgewiesen, nicht auf `L` aufgelöst.

        Ein Element, das die Rolle nicht sehen darf, ist von einem Element, das
        es nicht gibt, nicht zu unterscheiden. Beides ist 404, und das ist
        Absicht.
      parameters:
        - $ref: '#/components/parameters/DppId'
        - $ref: '#/components/parameters/ElementPath'
        - $ref: '#/components/parameters/CorrelationId'
      responses:
        '200':
          description: Das Element samt seiner Sammlung.
          content:
            application/ld+json:
              schema:
                type: object
                properties:
                  '@context': { type: string, format: uri }
                  result: { $ref: '#/components/schemas/Result' }
                  data:
                    type: object
                    properties:
                      DigitalProductPassportID: { type: string, format: uri }
                      DPPStatus: { type: string, enum: [Active, Archived] }
                      ElementPath: { type: string, example: 'L/l_repair_history' }
                      CollectionId: { type: string, example: 'L' }
                      CollectionName: { type: string }
                      element: { $ref: '#/components/schemas/DataElement' }
        '400': { $ref: '#/components/responses/Fehler' }
        '404': { $ref: '#/components/responses/Fehler' }
        '429': { $ref: '#/components/responses/RateLimit' }
    patch:
      tags: [write]
      operationId: UpdateDataElement
      summary: Ein einzelnes Datenelement ändern (EN 18222 6.5)
      description: |
        Der Rumpf ist `{"Value": <Wert>}`; `{"Value": null}` löscht den Wert
        (RFC 7396).

        Ein nackter Wert wird abgewiesen: `null` und "kein Rumpf" wären dann
        nicht zu unterscheiden, und ein Löschen darf nicht aus Versehen
        entstehen.
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/DppId'
        - $ref: '#/components/parameters/ElementPath'
        - $ref: '#/components/parameters/CorrelationId'
        - $ref: '#/components/parameters/DppDataStatus'
        - $ref: '#/components/parameters/DppSourceSystem'
        - $ref: '#/components/parameters/DppExtractedAt'
        - $ref: '#/components/parameters/DppConfidence'
      requestBody:
        required: true
        content:
          application/merge-patch+json:
            schema:
              type: object
              required: [Value]
              properties:
                Value:
                  description: Der neue Wert; `null` löscht das Element.
            example: { Value: 'Zellmodul 3 getauscht 2026-09-01' }
      responses:
        '200':
          description: Zähler plus der Pfad, der geschrieben wurde.
          content:
            application/ld+json:
              schema: { $ref: '#/components/schemas/PatchAntwort' }
        '400':
          description: |
            Ungültiger Pfad (`INVALID_ELEMENT_PATH`) oder ein Pfad, der sich
            selbst widerspricht (`ELEMENT_PATH_MISMATCH`).
          content:
            application/ld+json:
              schema: { $ref: '#/components/schemas/FehlerAntwort' }
        '401': { $ref: '#/components/responses/Fehler' }
        '403': { $ref: '#/components/responses/Fehler' }
        '404': { $ref: '#/components/responses/Fehler' }
        '422': { $ref: '#/components/responses/Fehler' }

  /api/dpp/v1/dpps:
    post:
      tags: [write]
      operationId: CreateDPP
      summary: Einen DPP-Entwurf aus einem DPP-Objekt anlegen (EN 18222:2026 4.6)
      description: |
        Nimmt ein **DigitalProductPassport** in der komprimierten Form
        (EN 18223:2026 5.2) — also so, wie `ReadDPPById` es ausliefert.
        `uniqueProductIdentifier` ist Pflicht und benennt das Batteriemodell.

        **Erzeugt einen Entwurf, keinen ausgestellten Pass** — und kann deshalb die
        `digitalProductPassportId` nicht zurückgeben, die Tabelle 5 als
        Ausgabeparameter nennt. Das folgt aus EN 18219:2026 4.1.2: eine ausgegebene
        Kennung darf nicht neu zugewiesen werden, deshalb entsteht sie genau
        einmal — beim Inverkehrbringen, nicht beim Anlegen eines Entwurfs, der
        noch verworfen werden kann. Die Antwort nennt stattdessen `draftId` und den
        Veröffentlichungsendpunkt.

        **Alles oder nichts:** ein unbekannter Katalogschlüssel lässt den ganzen
        Aufruf scheitern, und der Entwurf wird wieder entfernt.
      security:
        - ApiKeyAuth: []
      parameters:
        - $ref: '#/components/parameters/CorrelationId'
        - $ref: '#/components/parameters/DppDataStatus'
        - $ref: '#/components/parameters/DppSourceSystem'
        - $ref: '#/components/parameters/DppExtractedAt'
        - $ref: '#/components/parameters/DppConfidence'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CompressedDpp' }
            example:
              uniqueProductIdentifier: 95ea9f42-7637-450f-9789-4436d7534ccb
              granularity: item
              serialNumber: DEMO-0003
              a_chemical_composition: 'LFP'
      responses:
        '201':
          description: Entwurf angelegt. `payload.draftId` und der Veröffentlichungsweg.
          content:
            application/ld+json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Umschlag'
                  - type: object
                    properties:
                      payload:
                        type: object
                        properties:
                          draftId: { type: string, format: uuid }
                          uniqueProductIdentifier: { type: string }
                          dataElementsWritten: { type: integer }
                          publishEndpoint: { type: string }
        '400':
          description: |
            Kein DPP-Objekt (`INVALID_DPP`), fehlende Produktkennung
            (`PRODUCT_ID_REQUIRED`) oder unbekannte Katalogschlüssel
            (`UNKNOWN_DATA_ELEMENTS`).
          content:
            application/ld+json:
              schema: { $ref: '#/components/schemas/FehlerAntwort' }
        '401': { $ref: '#/components/responses/Fehler' }
        '403': { $ref: '#/components/responses/Fehler' }
        '404': { $ref: '#/components/responses/Fehler' }
        '422': { $ref: '#/components/responses/Fehler' }
        '429': { $ref: '#/components/responses/RateLimit' }

  /api/dpp/v1/dppsByIdAndDate/{dppId}:
    get:
      security: []
      tags: [read]
      operationId: ReadDPPVersionByIdAndDate
      summary: Die Version, die zu einem Zeitpunkt in Kraft war (EN 18222:2026 4.4)
      description: |
        **Diese Route ist neu, weil die finale Norm die Methode umbenannt und ihren
        Eingabeparameter gewechselt hat.** Der Entwurf hieß
        `ReadDPPVersionByProductIdAndDate` und nahm eine *Produkt*kennung;
        Tabelle 3 nennt sie `ReadDPPVersionByIdAndDate` mit
        `digitalProductPassportId`, Tabelle 16 legt diesen Pfad fest.

        Das ist keine Kosmetik: eine Produktkennung kann mehrere Pässe haben (ein
        Modell mit vielen Stücken), eine Passkennung genau einen. Die alte Route
        musste raten — sie nahm den jüngsten Pass zum Modell.

        Liefert die Version, die zum Zeitpunkt **in Kraft war**, nicht die an
        diesem Tag veröffentlichte. Schließt zugleich EN 18221:2026 4.2.

        Die alte Route bleibt bestehen, damit ein Integrator der Entwurfsfassung
        nicht bricht.
      parameters:
        - $ref: '#/components/parameters/DppId'
        - name: date
          in: query
          required: true
          description: Zeitpunkt nach ISO 8601-1, UTC-basiert.
          schema: { type: string, format: date-time }
        - $ref: '#/components/parameters/Representation'
        - $ref: '#/components/parameters/Accept'
        - $ref: '#/components/parameters/CorrelationId'
      responses:
        '200': { $ref: '#/components/responses/DppObject' }
        '303': { $ref: '#/components/responses/MenschenlesbarUmleitung' }
        '400':
          description: |
            Kennung unbrauchbar (`INVALID_DPP_ID`), Datum fehlt (`DATE_REQUIRED`)
            oder unlesbar (`INVALID_DATE`).
          content:
            application/ld+json:
              schema: { $ref: '#/components/schemas/FehlerAntwort' }
        '404':
          description: '`NO_VERSION_AT_DATE` — zu diesem Zeitpunkt war keine Version in Kraft.'
          content:
            application/ld+json:
              schema: { $ref: '#/components/schemas/FehlerAntwort' }
        '429': { $ref: '#/components/responses/RateLimit' }

  /api/dpp/v1/dppsByProductId/{productId}:
    get:
      security: []
      tags: [read]
      operationId: ReadDPPByProductId
      summary: Den Pass zu einer Produktkennung lesen
      description: |
        Solange keine GTIN gepflegt ist, **ist** die Passkennung die
        Produktkennung. Die Route nimmt beide: erst die Modellkennung, dann die
        Passkennung.
      parameters:
        - name: productId
          in: path
          required: true
          schema: { type: string, pattern: '^[A-Za-z0-9-]{2,64}$' }
        - $ref: '#/components/parameters/Representation'
        - $ref: '#/components/parameters/Accept'
        - $ref: '#/components/parameters/CorrelationId'
      responses:
        '200': { $ref: '#/components/responses/DppObject' }
        '303': { $ref: '#/components/responses/MenschenlesbarUmleitung' }
        '400': { $ref: '#/components/responses/Fehler' }
        '404': { $ref: '#/components/responses/Fehler' }
        '429': { $ref: '#/components/responses/RateLimit' }

  /api/dpp/v1/dppsByProductIdAndDate/{productId}:
    get:
      security: []
      tags: [read]
      operationId: ReadDPPVersionByProductIdAndDate
      deprecated: true
      summary: '[veraltet] Zeitpunktabfrage über die Produktkennung'
      description: |
        Liefert **nicht** die an diesem Tag veröffentlichte Version, sondern die,
        die zu diesem Zeitpunkt galt — also die letzte mit
        `published_at <= date`. Der Unterschied ist der ganze Zweck der Methode:
        wer wissen will, was der Pass am Tag eines Schadensfalls sagte, fragt
        nicht nach einer Veröffentlichung.

        Ohne `date` antwortet die Route mit `400` statt still die aktuelle
        Version zu liefern — sonst hätten Sie eine Zeitreise angefragt und die
        Gegenwart bekommen, ohne es zu merken.

        Schließt zugleich EN 18221 4.2 (archivierte Version zu einem Zeitpunkt
        abrufbar).
      parameters:
        - name: productId
          in: path
          required: true
          schema: { type: string, pattern: '^[A-Za-z0-9-]{2,64}$' }
        - name: date
          in: query
          required: true
          schema: { type: string, format: date-time }
          example: '2026-09-01T00:00:00Z'
        - $ref: '#/components/parameters/Representation'
        - $ref: '#/components/parameters/Accept'
        - $ref: '#/components/parameters/CorrelationId'
      responses:
        '200':
          description: |
            Die geltende Version. `result.text` nennt Versionsnummer und
            Veröffentlichungszeitpunkt.
          content:
            application/ld+json:
              schema: { $ref: '#/components/schemas/DppAntwort' }
        '400':
          description: '`date` fehlt oder ist kein ISO-8601-Zeitpunkt.'
          content:
            application/ld+json:
              schema: { $ref: '#/components/schemas/FehlerAntwort' }
        '404':
          description: Zu diesem Zeitpunkt gab es den Pass noch nicht.
          content:
            application/ld+json:
              schema: { $ref: '#/components/schemas/FehlerAntwort' }

  /api/dpp/v1/dppsByProductIds:
    post:
      security: []
      tags: [read]
      operationId: ReadDPPIdsByProductIds
      summary: Produktkennungen auf Passkennungen abbilden
      description: |
        Liefert Kennungen, keine Passinhalte — die liest man danach einzeln.

        **POST und nicht GET**, weil eine Liste von Produktkennungen nicht in
        eine URL gehört: sie wird lang, landet in Server-Logs und in der
        Browser-Historie, und es sind Ihre Betriebsdaten.

        Höchstens 500 Kennungen je Anfrage, höchstens 100 Ergebnisse je Seite.
        Diese Route läuft unter dem strengeren Kontingent (30/min): EN 18239
        5.2 (12) nennt Massenabfragen ausdrücklich als Hebel zum Absaugen des
        Bestands.

        Die Seitenschaltung nutzt einen Cursor (die letzte ausgegebene Kennung)
        statt `offset` — bei wachsendem Bestand überspringt `offset` Zeilen.
      parameters:
        - $ref: '#/components/parameters/CorrelationId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [productIds]
              properties:
                productIds:
                  type: array
                  maxItems: 500
                  items: { type: string }
                limit: { type: integer, minimum: 1, maximum: 100, default: 50 }
                cursor:
                  type: string
                  description: Die `nextCursor` der vorigen Seite.
      responses:
        '200':
          description: |
            Treffer. Werden Kennungen wegen ihrer Form verworfen, ist
            `result.messageType` gleich `Warning` und `result.text` nennt die
            Zahl — die Antwort ist trotzdem gültig.
          content:
            application/ld+json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/Umschlag'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          type: object
                          properties:
                            DigitalProductPassportID: { type: string, format: uri }
                            passportUid: { type: string }
                            ProductID: { type: string }
                            Granularity: { $ref: '#/components/schemas/Granularity' }
                            LastUpdate: { type: string, format: date-time }
                      nextCursor:
                        type: [string, 'null']
                        description: '`null`, wenn keine weitere Seite folgt.'
                      limit: { type: integer }
        '400': { $ref: '#/components/responses/Fehler' }
        '429': { $ref: '#/components/responses/RateLimit' }

  /api/dpp/v1/context:
    get:
      security: []
      tags: [read]
      operationId: ReadContext
      summary: Zeiger auf die laufende Fassung des JSON-LD-Kontexts
      description: |
        Leitet auf `/api/dpp/v1/context/{version}` um. Ein beweglicher Zeiger
        bekommt eine kurze Cache-Vorgabe (300 s), nicht die des Ziels — sonst
        liest ein Client nach einer Begriffsänderung bis zu sieben Tage lang
        jeden Pass mit der alten Bedeutung.
      responses:
        '302':
          description: Umleitung auf die laufende Fassung.
          headers:
            Location:
              schema: { type: string, format: uri }

  /api/dpp/v1/context/{version}:
    get:
      security: []
      tags: [read]
      operationId: ReadContextVersion
      summary: Der JSON-LD-Kontext, eine Fassung
      description: |
        Für alle Pässe derselbe — einmal holen, ablegen. Wer ihn ignoriert,
        liest weiterhin gültiges JSON (EN 18216 5 c verlangt genau das).

        Die Adresse trägt die Fassung, das Dokument unter ihr ist
        unveränderlich. Erst dadurch ist die lange Cache-Vorgabe eine Zusage.
        Eine unbekannte Fassung wird mit 404 beantwortet, nicht stillschweigend
        mit der aktuellen: ein falscher Kontext ist schlimmer als keiner.
      parameters:
        - name: version
          in: path
          required: true
          schema: { type: integer, minimum: 1 }
      responses:
        '200':
          description: Der Kontext dieser Fassung.
          content:
            application/ld+json: {}
        '404':
          description: Unbekannte Fassung.

  /api/dpp/v1/vocab:
    get:
      security: []
      tags: [read]
      operationId: ReadVocabulary
      summary: Das Vokabular hinter dem Präfix `dpp:`
      description: |
        Der Kontext bildet `dpp:` auf diese Adresse ab. Ein Dokument für alle
        Begriffe, adressiert über den Fragmentbezeichner
        (`…/vocab#granularity`).

        Keine Normfassung und kein Vokabular von CEN/CLC: jeder Begriff nennt
        seine Fundstelle in EN 18223.
      responses:
        '200':
          description: Das Vokabular.
          content:
            application/ld+json: {}

  /api/dpp/v1/dict/{key}/{version}:
    get:
      security: []
      tags: [read]
      operationId: ReadDataPointDefinition
      summary: Die semantische Definition eines Datenpunkts
      description: |
        Der entkoppelte Ansatz nach EN 18223 4.3. Jedes `DataElement` trägt eine
        `DictionaryReference` auf diese Adresse.

        **Die Version im Pfad ist keine Zierde.** Ändert sich die Bedeutung
        eines Feldes, steigt sie, und die alte URI löst weiter auf — sonst
        verweist jeder bereits ausgelieferte Pass auf eine Definition, die
        inzwischen etwas anderes sagt. Eine unbekannte Version antwortet mit
        `404` statt still die aktuelle zu liefern: eine falsche Definition ist
        schlimmer als keine.

        Auch ausgemusterte Felder lösen weiter auf.
      parameters:
        - name: key
          in: path
          required: true
          schema: { type: string, pattern: '^[a-z][a-z0-9_]{1,62}$' }
        - name: version
          in: path
          required: true
          schema: { type: string, pattern: '^\d{1,3}$' }
      responses:
        '200':
          description: Die Definition.
          content:
            application/ld+json: {}
        '404': { description: Unbekanntes Feld oder unbekannte Version. }

  /api/v1/dpps/{uid}/proof:
    get:
      security: []
      tags: [read]
      operationId: ReadProof
      summary: Integritäts- und Herkunftsnachweis
      description: |
        Schlüssellos und ohne Entgelt — EN 18246 4.7 verlangt genau das: wer
        einen Pass prüfen will, ist gerade kein Kunde.

        Liefert den `sha256` des veröffentlichten Snapshots, die Signatur als
        JWS und den öffentlichen Schlüssel. Ist kein Signierschlüssel
        konfiguriert, sind `signature` und `public_key` `null` und
        `verification.reason` ist `unsigned` — der Hash gilt trotzdem.

        `issuer` nennt den Aussteller zusätzlich als did:oyd (tenant_dids, ab
        17.09.2026) — siehe `IssuerAuskunft`.
      parameters:
        - $ref: '#/components/parameters/PassUid'
      responses:
        '200':
          description: |
            Der Nachweis. Diese Route liegt außerhalb des `{@context, statusCode,
            payload}`-Umschlags nach EN 18222 — sie antwortet mit dem
            `{success, data, api_version}`-Rumpf der Lifecycle-Routen, deshalb
            `application/json` statt `application/ld+json`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean }
                  data:
                    type: object
                    properties:
                      issuer: { $ref: '#/components/schemas/IssuerAuskunft' }
                  api_version: { type: string }
        '404': { description: Unbekannter oder nicht aktiver Pass. }

components:

  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: |
        Für Leseoperationen optional (hebt die Zugriffsstufe), für Schreiben
        erforderlich (`dpp:publish`).

  parameters:
    # Dieselbe Kennung wie DppId, aber unter dem Namen, den der Nachweis-Pfad
    # tatsaechlich traegt. Ein Parametername, der nicht zur Pfadschablone passt,
    # bricht jeden Codegenerator -- und genau das war hier der Fall.
    PassUid:
      name: uid
      in: path
      required: true
      schema: { type: string, pattern: '^[A-Z0-9]{2,62}$' }
      description: Die Passkennung (IAC + CIN + Serie nach ISO/IEC 15459).
      example: ATBAT0001
    DppId:
      name: dppId
      in: path
      required: true
      schema: { type: string, pattern: '^[A-Z0-9]{2,62}$' }
      description: Die Passkennung (IAC + CIN + Serie nach ISO/IEC 15459).
      example: ATBAT0001
    ElementId:
      name: elementId
      in: path
      required: true
      description: Anhang-XIII-Gruppenbuchstabe, ein Grossbuchstabe.
      schema: { type: string, pattern: '^[A-Za-z]$', example: L }
    ElementPath:
      name: elementPath
      in: path
      required: true
      description: |
        Absoluter Elementpfad als **RFC 9535 JSONPath** (EN 18222:2026 8.1).
        Kanonisch die Klammerform `$['a_gtin']`, prozentkodiert
        `$%5B'a_gtin'%5D`; die Punktform `$.a_gtin` wird ebenfalls angenommen.

        Filter, Slices und Wildcards werden abgewiesen: `ReadDataElement` liefert
        nach Tabelle 9 genau ein Element.

        Der Entwurfspfad `{Gruppe}/{key}` gilt **nicht mehr** und wird mit einer
        Meldung abgewiesen, die den neuen Pfad nennt.
      schema:
        type: string
        pattern: "^\\$(\\[\\s*'[a-z0-9_]{2,64}'\\s*\\]|\\.[a-z][a-z0-9_]{1,63})$"
        example: "$['l_repair_history']"
    Accept:
      name: Accept
      in: header
      required: false
      description: |
        Die gewünschte Darstellung (EN 18216:2026 Klausel 5). Ohne Angabe
        `application/ld+json`. `text/html` antwortet mit **303** auf die
        menschenlesbare Passseite.
      schema:
        type: string
        enum:
          - application/ld+json
          - application/json
          - application/xml
          - text/xml
          - text/html
        default: application/ld+json
    Representation:
      name: representation
      in: query
      required: false
      description: |
        Welche Serialisierung geliefert wird (EN 18222:2026 8.1).
        `compressed` ist der Standard und gilt auch ohne die Query.
      schema: { type: string, enum: [compressed, full], default: compressed }
    CorrelationId:
      name: X-Correlation-Id
      in: header
      required: false
      schema: { type: string, maxLength: 128 }
      description: |
        Ihre eigene Vorgangskennung. Sie kommt in `result.correlationId` und im
        gleichnamigen Antwort-Header zurück — so finden Sie Ihren Aufruf in
        unseren Protokollen wieder. Ohne Vorgabe vergeben wir eine UUID.

    DppDataStatus:
      name: X-DPP-Data-Status
      in: header
      required: false
      schema:
        type: string
        enum: [verified, partner_provided, ai_extracted, estimated]
      description: |
        Wofür Sie mit diesen Werten einstehen. Gilt für **alle** Felder des
        Aufrufs — ein Merge-Patch nach RFC 7396 hat keinen Platz für Angaben am
        einzelnen Schlüssel. Wer je Feld unterscheiden muss, nimmt den
        Umschlag-Ingest (`POST /api/v1/ingest/passports`).

        Ohne Angabe gilt `estimated`. Wir heben das nicht von selbst an: ein Wert
        wird nicht dadurch besser belegt, dass wir in Ihrem Namen etwas behaupten.

        `verified`, `partner_provided` und `ai_extracted` **verlangen**
        zusätzlich `X-DPP-Source-System`; `ai_extracted` außerdem
        `X-DPP-Extracted-At` und `X-DPP-Confidence`. Fehlt eines, scheitert der
        ganze Aufruf — eine Herkunftsbehauptung ohne Deckung wäre schlechter als
        gar keine.

    DppSourceSystem:
      name: X-DPP-Source-System
      in: header
      required: false
      schema: { type: string, minLength: 1, maxLength: 64 }
      description: |
        Das System, aus dem die Werte stammen: `SAP`, `weclapp`, `pruefstand-4`,
        `datenblatt-2026.pdf`. Steht im Pass als Herkunft neben dem Wert.

    DppExtractedAt:
      name: X-DPP-Extracted-At
      in: header
      required: false
      schema: { type: string, format: date-time }
      description: |
        Wann der Wert aus seiner Quelle gelesen wurde (RFC 3339, **mit**
        Zeitzonenversatz). Nicht der Zeitpunkt dieses Aufrufs — den kennen wir
        selbst. Ein Zeitpunkt ohne Versatz wird abgewiesen: er ist zweideutig,
        und im Streitfall zählen die zwei Stunden.

    DppConfidence:
      name: X-DPP-Confidence
      in: header
      required: false
      schema: { type: number, minimum: 0, maximum: 1 }
      description: |
        Wie sicher sich Ihre Quelle ist, zwischen 0 und 1. Bei `ai_extracted`
        Pflicht. `0` ist eine Aussage („völlig unsicher") und etwas anderes als
        keine Angabe.

  schemas:

    Granularity:
      type: string
      enum: [item, batch, model]
      description: |
        Bezugsebene nach EN 18223 4.1.3.1 und ESPR. Wird aus Serien- und
        Chargennummer abgeleitet und ist nicht von Hand setzbar.

    Message:
      type: object
      description: |
        Eine Meldung nach EN 18222:2026 **Tabelle 13**. `code` ist dort
        ausdrücklich „Technology-dependent" — dort stehen unsere sprechenden Codes.
      required: [messageType, text]
      properties:
        messageType:
          type: string
          enum: [Info, Warning, Error, Exception]
          description: |
            **Tabelle 14.** `Info` — nicht `Information`; der Entwurf hatte den
            langen Namen. `Exception` trennt den behandelten Fehler vom
            unerwarteten.
        text: { type: string }
        code: { type: string, example: DPP_NOT_FOUND }
        correlationId:
          type: string
          description: Übernimmt einen mitgegebenen `X-Correlation-Id`-Header und geht als Header zurück.
        timestamp: { type: string, format: date-time }

    Result:
      type: object
      description: |
        Ergebnisobjekt nach EN 18222:2026 **Tabelle 12**. `message` ist eine
        **Liste** (Kardinalität 0..*) — der Entwurf hatte die Felder flach am
        Ergebnis. Die Trennung erlaubt mehrere Meldungen zu einem Vorgang, etwa
        eine Warnung neben dem Erfolg.
      required: [message]
      properties:
        message:
          type: array
          items: { $ref: '#/components/schemas/Message' }

    GenericStatusCode:
      type: string
      description: |
        Der technologieunabhängige Statusname nach **Tabelle 15**, zusätzlich zum
        HTTP-Status. Fehlt bei 429: Tabelle 15 kennt dafür keinen Namen, und einen
        zu erfinden wäre schlechter als ihn weggelassen zu haben.
      enum:
        - Success
        - SuccessCreated
        - SuccessAccepted
        - SuccessNoContent
        - ClientErrorBadRequest
        - ClientNotAuthorized
        - ClientForbidden
        - ClientMethodNotAllowed
        - ClientErrorResourceNotFound
        - ClientResourceConflict
        - ServerInternalError
        - ServerNotImplemented
        - ServerErrorBadGateway

    Umschlag:
      type: object
      description: |
        Der Rumpf jeder Antwort: Kontext, generischer Statusname, Ergebnisobjekt,
        Nutzlast. Die Nutzlast heißt **`payload`** — so nennen die Methodentabellen
        (1 bis 10) den Ausgabeparameter. Bis zum 01.09.2026 hieß sie `data`.
      required: [result]
      properties:
        '@context': { type: string, format: uri }
        statusCode: { $ref: '#/components/schemas/GenericStatusCode' }
        result: { $ref: '#/components/schemas/Result' }

    FehlerAntwort:
      allOf:
        - $ref: '#/components/schemas/Umschlag'

    DigitalProductPassport:
      description: |
        Das DPP-Objekt nach **EN 18223:2026**. Zwei normative Formen, gesteuert
        über die Query `representation` (EN 18222:2026 8.1):

        - `compressed` (Standard, 18223 5.2): `elementId` ist der JSON-Schlüssel,
          Wörterbuchreferenz und Datentyp stehen im Wörterbuch und werden im
          Payload weggelassen.
        - `full` (18223 Anhang A): jedes Element als Objekt mit `objectType`,
          `dictionaryReference` und `valueDataType` unter `elements`.

        Alle Attribute in camelCase — 5.2.2 legt das fest. Der Entwurf
        (prEN 18223:2025) hatte PascalCase und einen Wrapper
        `DataElementCollections`; beides gilt nicht mehr.
      oneOf:
        - $ref: '#/components/schemas/CompressedDpp'
        - $ref: '#/components/schemas/FullDpp'

    DppHeader:
      type: object
      description: Kopf nach EN 18223:2026 4.1.2.1, Tabelle 1.
      required:
        - digitalProductPassportId
        - uniqueProductIdentifier
        - granularity
        - dppSchemaVersion
        - dppStatus
        - lastUpdated
      properties:
        digitalProductPassportId:
          type: string
          format: uri
          description: Global eindeutig, URI-basiert — dieselbe Adresse, unter der der Pass auch für Menschen erreichbar ist.
        uniqueProductIdentifier:
          type: string
          description: Produktkennung nach EN 18219:2026. Hieß im Entwurf `ProductID`.
        granularity: { $ref: '#/components/schemas/Granularity' }
        dppSchemaVersion:
          type: string
          example: 'EN 18223:2026'
          description: |
            Die Referenz**norm** des Instanzschemas, nicht unsere Schemaversion.
            Seit die Norm final ist, ohne `pr`-Präfix — die Vermutungswirkung
            hängt dagegen weiter an der Zitierung im EU-Amtsblatt.
        dppStatus:
          type: string
          enum: [active, inactive, archived, invalid]
          description: |
            Vier Werte nach Tabelle 1, klein geschrieben. Unsere sechs internen
            Zustände bilden feiner ab als im Entwurf: ein **Widerruf** ist
            `invalid` (Aussage über Fehlerhaftigkeit), ein **Ablauf** `inactive`
            (Aussage über Zeit). Ein Entwurf wird nicht ausgeliefert.
        lastUpdated:
          type: string
          format: date-time
          description: UTC nach ISO 8601-1. Hieß im Entwurf `LastUpdate`.
        economicOperatorId:
          type: string
          example: '0088:4000000000009'
          description: '`{ICD}:{OrgID}` nach ISO/IEC 6523. Weggelassen, wenn nicht gesetzt.'
        economicOperatorDid:
          type: string
          pattern: '^did:oyd:z[1-9A-HJ-NP-Za-km-z]+$'
          description: >
            did:oyd des verantwortlichen Wirtschaftsakteurs (OwnYourData DID-Methode).
            Auflösbar über https://resolver.ownyourdata.eu/1.0/identifiers/{did}; das
            DID-Dokument trägt die Signaturschlüssel des Passes als assertionMethod.
        facilityId:
          type: string
          description: Standortkennung nach EN 18219:2026. Neu im Kopf der finalen Norm.
        contentSpecificationIds:
          type: array
          items: { type: string }
          description: Verweise auf horizontale oder produkttypbezogene Inhaltsspezifikationen. Neu.

    CompressedDpp:
      description: |
        Komprimierte Form (18223 5.2). Der Kopf plus je Datenpunkt ein Feld,
        dessen Name der Katalogschlüssel ist. Werte in ihrem nativen JSON-Typ
        nach Tabelle 7 — `750.0`, nicht `"750.0"`.

        Die Anhang-XIII-Gruppe erscheint **nicht** als Ebene: sie ist nach 4.3 ein
        Metadatum des Feldes und steht im Wörterbuch. Unsere Schlüssel tragen den
        Gruppenbuchstaben ohnehin als Präfix.
      allOf:
        - $ref: '#/components/schemas/DppHeader'
        - type: object
          additionalProperties: true
          example:
            a_gtin: '04006381333931'
            e_capacity_fade: 12.5
            f_eu_doc: { resourceTitle: 'EU-Konformitätserklärung', contentType: application/pdf, url: 'https://example.org/doc.pdf' }

    FullDpp:
      description: Erweiterte Form (18223 Anhang A, normativ).
      allOf:
        - $ref: '#/components/schemas/DppHeader'
        - type: object
          required: [elements]
          properties:
            elements:
              type: array
              items: { $ref: '#/components/schemas/FullElement' }

    FullElement:
      type: object
      description: |
        Ein Datenelement in der erweiterten Form. `objectType` sagt, welche der
        fünf konkreten Unterklassen aus 4.1.2.3 vorliegt.
      required: [elementId, objectType]
      properties:
        elementId:
          type: string
          example: a_gtin
          description: |
            Im Kontext eindeutig (4.1.2.3). Der **absolute** Pfad dazu ist der
            JSONPath `$['a_gtin']` — siehe Parameter `elementPath`.
        objectType:
          type: string
          enum:
            - DataElementCollection
            - SingleValuedDataElement
            - MultiValuedDataElement
            - RelatedResource
            - MultiLanguageDataElement
        dictionaryReference:
          type: string
          format: uri
          description: Auflösbar (4.3). Zeigt auf ein fremdes Repository, wenn es eines gibt.
        valueDataType:
          type: string
          example: 'xsd:decimal'
          description: |
            XSD-Typ nach 4.1.2.9. Verboten sind Listentypen (ENTITIES, IDREFS,
            NMTOKENS), die von `xsd:string` **abgeleiteten** Built-in-Typen sowie
            NOTATION und QName.
        value:
          description: Der Wert, oder bei MultiValuedDataElement die Liste der Unterelemente.
        elements:
          type: array
          items: { $ref: '#/components/schemas/FullElement' }
          description: Nur bei DataElementCollection.
        contentType:
          type: string
          example: application/pdf
          description: 'Nur bei RelatedResource (4.1.2.7): IANA-Medientyp, dort Pflicht.'
        url: { type: string, format: uri, description: Nur bei RelatedResource. }
        resourceTitle: { type: string, description: Nur bei RelatedResource. }
        language:
          type: string
          example: de-AT
          description: Nur bei RelatedResource und MultiLanguageValue (4.1.2.8.2).

    DataElement:
      description: Ein Datenelement in der erweiterten Form.
      $ref: '#/components/schemas/FullElement'
    PatchAntwort:
      allOf:
        - $ref: '#/components/schemas/Umschlag'
        - type: object
          properties:
            payload:
              type: object
              properties:
                updated: { type: integer, description: Geschriebene Felder. }
                cleared: { type: integer, description: Geloeschte Felder. }
                elementIdPath:
                  type: string
                  description: Nur bei UpdateDataElement — der JSONPath, der geschrieben wurde.

    DppRegistryEntry:
      type: object
      description: |
        Der Anmeldedatensatz nach EN 18222:2026 **Tabelle 11**. Die Feldnamen
        kommen aus der Norm, nicht von uns.

        `dppApiEndpoint` ist die **Maschinen**-Adresse (`/api/dpp/v1/dpps/{dppId}`)
        und nicht die menschenlesbare Passansicht — ein Register fragt maschinell
        ab. Wir hatten vorher die Leseansicht gemeldet.
      required: [uniqueProductIdentifier, digitalProductPassportId, dppApiEndpoint]
      properties:
        uniqueProductIdentifier: { type: string }
        digitalProductPassportId: { type: string, format: uri }
        uniqueEconomicOperatorIdentifier:
          type: [string, 'null']
          example: '0088:4000000000009'
        backupUniqueEconomicOperatorIdentifier:
          type: [string, 'null']
          description: |
            Tabelle 11 nennt `uniqueEconomicOperatorIdentifier` zweimal — einmal für
            den Wirtschaftsakteur, einmal für den Back-up-Betreiber. Ein JSON-Objekt
            kann keinen Schlüssel zweimal tragen, deshalb hier mit Präfix. Heute
            immer `null`: ein Back-up-Dienstleister ist nicht gewählt (P16).
        dppApiEndpoint: { type: string, format: uri }
    DppAntwort:
      allOf:
        - $ref: '#/components/schemas/Umschlag'
        - type: object
          properties:
            payload: { $ref: '#/components/schemas/DigitalProductPassport' }

    IssuerAuskunft:
      type: object
      description: |
        Der Aussteller als did:oyd (tenant_dids, ab 17.09.2026), zusätzlich zur
        Betreibernummer — siehe `/api/v1/dpps/{uid}/proof` oben; dieselbe Form
        trägt auch `/api/verify/{uid}` (außerhalb dieser Datei dokumentiert;
        dieser Pfad ist kein Teil der EN-18222-Schnittstelle). Das
        DID-Dokument trägt den Signaturschlüssel des Nachweises als
        assertionMethod (EN 18239 5.2 (13)).
      required:
        - operator_id
        - did
        - did_document_url
        - resolver_url
        - key_in_did_document
        - did_in_payload
      properties:
        operator_id:
          type: [string, 'null']
          description: Betreibernummer aus dem signierten Payload (ISO/IEC 6523).
        did:
          type: [string, 'null']
          pattern: '^did:oyd:z[1-9A-HJ-NP-Za-km-z]+$'
          description: Die aktive DID des Mandanten, oder `null` ohne veröffentlichte DID.
        did_document_url:
          type: [string, 'null']
          format: uri
          description: Registry-Kopie des DID-Dokuments (OYDID-Form).
        resolver_url:
          type: [string, 'null']
          format: uri
          description: W3C Resolution Result über den öffentlichen Resolver — nicht bei uns.
        key_in_did_document:
          type: [boolean, 'null']
          description: |
            Steht der `kid` des Zertifikats/Nachweises als JWK in der gespeicherten
            Kopie des DID-Dokuments? `null`, wenn es keine DID oder keinen `kid` gibt.
        did_in_payload:
          type: boolean
          description: |
            Trug der SIGNIERTE Payload bereits die DID? Altbestand vor dem
            17.09.2026: `false` — die DID kommt dann nur aus der Tabelle, nicht
            aus dem Nachweis selbst.

  responses:

    DppObject:
      description: Das DPP-Objekt, gefiltert auf die Stufe des Aufrufers.
      headers:
        X-Correlation-Id:
          schema: { type: string }
        X-RateLimit-Limit:
          schema: { type: integer }
        X-RateLimit-Remaining:
          schema: { type: integer }
      content:
        application/ld+json:
          schema: { $ref: '#/components/schemas/DppAntwort' }

    MenschenlesbarUmleitung:
      description: |
        `Accept: text/html` — die menschenlesbare Darstellung ist eine eigene
        Seite und kein API-Format (EN 18216:2026 Klausel 5 d). `Location` nennt
        sie; die Sprache folgt `Accept-Language`, ohne Angabe Deutsch.

        **303 und nicht 302:** der Aufrufer hat einen Datensatz angefragt und
        bekommt eine andere Ressource genannt, die dasselbe darstellt.
      headers:
        Location:
          schema: { type: string, format: uri }
        Vary:
          schema: { type: string, example: 'Accept, Accept-Language' }

    Fehler:
      description: '`result` nennt Code und Klartext.'
      content:
        application/ld+json:
          schema: { $ref: '#/components/schemas/FehlerAntwort' }

    RateLimit:
      description: |
        Kontingent überschritten. `Retry-After` nennt die Wartezeit in Sekunden.
        Die Kontingentstände stehen auch in den erfolgreichen Antworten — Sie
        müssen die Bremse nicht auslösen, um sie zu kennen.
      headers:
        Retry-After:
          schema: { type: integer }
        X-RateLimit-Limit:
          schema: { type: integer }
        X-RateLimit-Remaining:
          schema: { type: integer }
