openapi: 3.0.3
info:
  title: Verpackungsnachweis.de – Netzwerk-API
  version: "1.0"
  description: |
    REST-API für Netzwerkdruckereien und Großkunden: Auftraggeber (Mandanten)
    anlegen, Nachweise erzeugen, Konformitätsunterlagen (PDF) hochladen und
    druckfertige QR-Codes/Etiketten herunterladen.

    Authentifizierung: API-Schlüssel im Header `Authorization: Bearer vn_live_…`
    (alternativ `X-Api-Key`). Test-Schlüssel (`vn_test_…`) sind gedrosselt;
    das verbleibende Stundenkontingent steht in jeder Antwort im Header
    `X-RateLimit-Remaining`. Fehler kommen als JSON: `{"fehler": "…"}`.

    Einmal erzeugte Nachweis-URLs (`/n/{kurzcode}`) bleiben dauerhaft stabil –
    aufgedruckte QR-Codes brechen nie, auch wenn Dokumente aktualisiert werden.
  contact:
    email: info@verpackungsnachweis.de
servers:
  - url: https://verpackungsnachweis.de/api/v1
security:
  - schluessel: []
components:
  securitySchemes:
    schluessel:
      type: http
      scheme: bearer
      description: API-Schlüssel (vn_test_… oder vn_live_…)
  schemas:
    Fehler:
      type: object
      properties:
        fehler: { type: string }
    Auftraggeber:
      type: object
      properties:
        id: { type: string, format: uuid }
        anzeigeName: { type: string }
        erzeugerName: { type: string }
        freigabeStatus: { type: string, description: "Offen | Bestaetigt" }
    AuftraggeberNeu:
      type: object
      required: [anzeigeName, erzeugerName, erzeugerAnschrift, erzeugerKontakt]
      properties:
        anzeigeName: { type: string, maxLength: 200, description: "Marke des Auftraggebers (erscheint auf der Nachweisseite)" }
        erzeugerName: { type: string, maxLength: 200, description: "Pflichtangabe Art. 15 PPWR" }
        erzeugerAnschrift: { type: string, maxLength: 300 }
        erzeugerKontakt: { type: string, maxLength: 200, description: "elektronischer Kontakt (E-Mail/URL)" }
    Nachweis:
      type: object
      properties:
        id: { type: string, format: uuid }
        titel: { type: string }
        auftragsnummer: { type: string, nullable: true }
        kurzcode: { type: string }
        url: { type: string, description: "dauerhafte öffentliche Nachweis-URL (QR-Ziel)" }
        auftraggeberId: { type: string, format: uuid, nullable: true }
        status: { type: string }
        dokumente: { type: integer }
    NachweisDetail:
      type: object
      properties:
        id: { type: string, format: uuid }
        titel: { type: string }
        auftragsnummer: { type: string, nullable: true }
        kurzcode: { type: string }
        url: { type: string }
        auftraggeberId: { type: string, format: uuid, nullable: true }
        status: { type: string }
        erstelltAm: { type: string, format: date-time }
        dokumente:
          type: array
          items:
            type: object
            properties:
              id: { type: string, format: uuid }
              dateiname: { type: string }
              groesseBytes: { type: integer }
              version: { type: integer }
              hochgeladenAm: { type: string, format: date-time }
              status: { type: string }
    NachweisNeu:
      type: object
      required: [titel]
      properties:
        titel: { type: string, maxLength: 200 }
        auftragsnummer: { type: string, maxLength: 60, nullable: true, description: "Ihre interne Auftragsnummer – erscheint in der Abrechnungsaufstellung" }
        beschreibung: { type: string, nullable: true }
        auftraggeberId: { type: string, format: uuid, nullable: true, description: "Nachweis läuft unter dieser Marke (Mandant)" }
        direktZumDokument: { type: boolean, default: false, description: "QR öffnet direkt das neueste Dokument statt der Nachweisseite" }
paths:
  /auftraggeber:
    get:
      summary: Auftraggeber (Mandanten) auflisten
      responses:
        "200":
          description: Liste
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/Auftraggeber" } }
        "401": { description: Schlüssel fehlt/ungültig, content: { application/json: { schema: { $ref: "#/components/schemas/Fehler" } } } }
        "429": { description: Kontingent aufgebraucht }
    post:
      summary: Auftraggeber anlegen
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/AuftraggeberNeu" }
      responses:
        "201": { description: Angelegt, content: { application/json: { schema: { $ref: "#/components/schemas/Auftraggeber" } } } }
        "400": { description: Pflichtfelder fehlen }
  /nachweise:
    get:
      summary: Nachweise auflisten
      parameters:
        - { name: auftraggeberId, in: query, required: false, schema: { type: string, format: uuid } }
      responses:
        "200":
          description: Liste
          content:
            application/json:
              schema: { type: array, items: { $ref: "#/components/schemas/Nachweis" } }
    post:
      summary: Nachweis anlegen (liefert dauerhafte QR-URL)
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/NachweisNeu" }
      responses:
        "201": { description: Angelegt (id, kurzcode, url, qrPng) }
        "404": { description: auftraggeberId unbekannt }
  /nachweise/{id}:
    get:
      summary: Nachweis-Details inkl. Dokumentliste (vorhandene Unterlagen anzeigen)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        "200":
          description: Details
          content:
            application/json:
              schema: { $ref: "#/components/schemas/NachweisDetail" }
        "404": { description: Unbekannt oder fremde Firma }
  /nachweise/{id}/dokument:
    post:
      summary: PDF-Dokument hochladen (Virenscan + Prüfung automatisch)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
        - { name: dateiname, in: query, required: false, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/pdf:
            schema: { type: string, format: binary }
      responses:
        "201": { description: "Hochgeladen (dokumentId, sha256, version)" }
        "413": { description: "Zu groß (PDFs bis 25 MB)" }
        "422": { description: "Abgelehnt (kein PDF / Virenbefund)" }
  /nachweise/{id}/qr.{format}:
    get:
      summary: QR-Code herunterladen (druckfertig)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
        - { name: format, in: path, required: true, schema: { type: string, enum: [svg, png, eps, pdf] } }
        - { name: modul, in: query, required: false, schema: { type: string, enum: [Quadrat, Punkt, Abgerundet, Fliessend], default: Quadrat }, description: Modulform }
        - { name: augen, in: query, required: false, schema: { type: string, enum: [Eckig, Abgerundet, Kreis], default: Eckig }, description: Augenform }
        - { name: vg, in: query, required: false, schema: { type: string }, description: "Vordergrundfarbe #RRGGBB" }
        - { name: hg, in: query, required: false, schema: { type: string }, description: "Hintergrundfarbe #RRGGBB" }
        - { name: verlauf, in: query, required: false, schema: { type: string }, description: "Farbverlauf-Zielfarbe #RRGGBB (leer = kein Verlauf)" }
        - { name: rahmen, in: query, required: false, schema: { type: boolean, default: false }, description: Rahmen mit Text }
        - { name: lage, in: query, required: false, schema: { type: string, enum: [Unten, Oben, Links, Rechts], default: Unten }, description: Textposition am Rahmen }
        - { name: t1, in: query, required: false, schema: { type: string, maxLength: 40 }, description: "Rahmentext Zeile 1" }
        - { name: t2, in: query, required: false, schema: { type: string, maxLength: 40 }, description: "Rahmentext Zeile 2" }
        - { name: rf, in: query, required: false, schema: { type: string }, description: "Rahmenfarbe #RRGGBB (Standard Portalgrün)" }
        - { name: invers, in: query, required: false, schema: { type: boolean, default: false }, description: Text hell auf Rahmenfarbe }
        - { name: tg, in: query, required: false, schema: { type: number, minimum: 0.7, maximum: 1.6, default: 1.0 }, description: Textgröße (Faktor) }
      responses:
        "200": { description: Datei }
        "400": { description: Design nicht scanbar }
  /nachweise/{id}/etikett.pdf:
    get:
      summary: Etiketten-PDF in exakter Größe (mm)
      parameters:
        - { name: id, in: path, required: true, schema: { type: string, format: uuid } }
        - { name: b, in: query, required: false, schema: { type: number, minimum: 20, maximum: 150, default: 62 }, description: Breite in mm }
        - { name: h, in: query, required: false, schema: { type: number, minimum: 20, maximum: 150, default: 29 }, description: Höhe in mm }
        - { name: modul, in: query, required: false, schema: { type: string, enum: [Quadrat, Punkt, Abgerundet, Fliessend] } }
        - { name: augen, in: query, required: false, schema: { type: string, enum: [Eckig, Abgerundet, Kreis] } }
        - { name: vg, in: query, required: false, schema: { type: string } }
        - { name: hg, in: query, required: false, schema: { type: string } }
        - { name: verlauf, in: query, required: false, schema: { type: string } }
        - { name: rahmen, in: query, required: false, schema: { type: boolean } }
        - { name: lage, in: query, required: false, schema: { type: string, enum: [Unten, Oben, Links, Rechts] } }
        - { name: t1, in: query, required: false, schema: { type: string, maxLength: 40 } }
        - { name: t2, in: query, required: false, schema: { type: string, maxLength: 40 } }
        - { name: rf, in: query, required: false, schema: { type: string } }
        - { name: invers, in: query, required: false, schema: { type: boolean } }
        - { name: tg, in: query, required: false, schema: { type: number, minimum: 0.7, maximum: 1.6 } }
      responses:
        "200": { description: PDF }
        "400": { description: Design nicht scanbar }
