openapi: 3.1.0
info:
  title: pressekonto Publisher API
  version: "1.0"
  description: |
    REST-API des Publisher-Hubs (pressekonto) für presseecho.de und
    businessportal24.com. Pressemitteilungen werden als Entwurf angelegt,
    optional mit Bildern versehen und über die Submit-Route zur
    Inhaltsprüfung eingereicht — eine direkte Veröffentlichung über die
    API ist bewusst nicht möglich.

    Authentifizierung über einen persönlichen API-Token (Bearer). Jeder
    Token trägt Berechtigungen (Abilities), die den Zugriff auf die
    Endpunkte steuern. Tokens werden im Publisher-Konto unter „API-Tokens"
    selbst erstellt und widerrufen (Voraussetzung: aktive Buchung).
    Rate-Limit: 60 Anfragen pro Minute je Token (Header X-RateLimit-Limit /
    X-RateLimit-Remaining, bei 429 zusätzlich Retry-After).

    Migration: Bestehende API-Schlüssel der Altportale wurden übernommen
    und bleiben gültig — auch in der alten Übergabeform (X-ApiKey-Header)
    und auf den dokumentierten /service/…-Pfaden. Unbekannte Alt-Schlüssel
    erhalten 410 mit Migrationshinweis. Neue Integrationen nutzen bitte
    Bearer-Tokens auf /api/v1.
servers:
  - url: https://pressekonto.de/api/v1
security:
  - bearerToken: []
tags:
  - name: press-releases
    description: "Eigene Pressemitteilungen (Ability: press-releases:read bzw. press-releases:write)"
  - name: images
    description: "Bilder einer Pressemitteilung (Ability: press-release-images:write zum Schreiben)"
  - name: companies
    description: "Eigene Firmenprofile/Newsrooms (Ability: companies:read)"
  - name: categories
    description: "Aktive Rubriken (Ability: press-releases:read)"
  - name: newsletter
    description: "Newsletter-Anmeldungen (Ability: newsletter:subscribe)"
paths:
  /press-releases:
    get:
      tags: [press-releases]
      summary: Eigene Pressemitteilungen auflisten
      parameters:
        - name: status
          in: query
          description: Nach Status filtern (draft, review, published, rejected, archived)
          schema: { type: string }
        - name: per_page
          in: query
          description: Seitengröße, Standard 25, max. 100
          schema: { type: integer, maximum: 100 }
        - name: page
          in: query
          schema: { type: integer }
      responses:
        "200":
          description: Paginierte Liste
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/PressRelease" }
    post:
      tags: [press-releases]
      summary: Pressemitteilung anlegen (immer als Entwurf)
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PressReleaseInput" }
      responses:
        "201":
          description: Entwurf angelegt
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/PressRelease" }
        "403": { description: "Ability fehlt oder Firma gehört nicht zum Konto" }
        "422": { description: "Validierungsfehler" }
  /press-releases/{id}:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
    get:
      tags: [press-releases]
      summary: Pressemitteilung abrufen
      responses:
        "200":
          description: Mitteilung inkl. Firma, Rubrik und Bildern
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/PressRelease" }
        "403": { description: "Nicht gefunden oder fremdes Konto" }
    patch:
      tags: [press-releases]
      summary: Pressemitteilung ändern (nur draft/rejected)
      requestBody:
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PressReleaseInput" }
      responses:
        "200": { description: "Aktualisierte Mitteilung" }
        "409": { description: "Nur Entwürfe und abgelehnte Mitteilungen sind änderbar" }
    delete:
      tags: [press-releases]
      summary: Pressemitteilung löschen (nicht bei published)
      responses:
        "204": { description: "Gelöscht" }
        "409": { description: "Veröffentlichte Mitteilungen können nicht per API gelöscht werden" }
  /press-releases/{id}/submit:
    post:
      tags: [press-releases]
      summary: Zur Inhaltsprüfung einreichen
      description: |
        Der einzige API-Weg Richtung Veröffentlichung. Durchläuft denselben
        Funnel wie das Web-Formular (Richtlinien-Filter, Kontingent, Status-Log).

        Der Aufruf erklärt verbindlich den deklarierten KI-Grad des Textes
        (`ai_text_declaration`, Transparenzpflicht nach Art. 50 KI-VO) und
        schließt die Kenntnisnahme der dauerhaften Archivierung ein
        (AGB § 13: Depublikation nur als kostenpflichtiger Service,
        gesetzliche Ansprüche unberührt).
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ai_text_declaration]
              properties:
                ai_text_declaration:
                  type: string
                  enum: [none, assisted, mostly_ai, fully_ai]
                  description: >
                    Deklarierter KI-Grad des Textes: ohne generative KI (none),
                    KI als Formulierungshilfe mit redaktioneller Überarbeitung
                    (assisted), überwiegend KI-erstellt und geprüft (mostly_ai),
                    vollständig KI-generiert (fully_ai). Ab mostly_ai trägt die
                    Veröffentlichung ein öffentliches Transparenz-Label.
      responses:
        "200": { description: "Eingereicht (Status review)" }
        "402": { description: "Aktive Buchung erforderlich (Tarif oder Einzel-PM)" }
        "409": { description: "Nur draft/rejected kann eingereicht werden" }
        "422": { description: "KI-Deklaration fehlt/ungültig, Kontingent erschöpft oder Richtlinienverstoß" }
  /press-releases/{id}/images:
    parameters:
      - name: id
        in: path
        required: true
        schema: { type: integer }
    get:
      tags: [images]
      summary: Bilder einer Mitteilung auflisten
      responses:
        "200":
          description: Bilderliste
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/PressReleaseImage" }
    post:
      tags: [images]
      summary: Bild hochladen (nur bei draft/rejected)
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [image, ai_declaration]
              properties:
                image:
                  type: string
                  format: binary
                  description: JPG, PNG oder WebP, max. 8 MB
                ai_declaration:
                  type: string
                  enum: [original, ai_generated, ai_edited]
                  description: >
                    KI-Kennzeichnung des Bildes (Art. 50 KI-VO): Originalfoto,
                    KI-generiert oder KI-bearbeitet.
                title: { type: string, maxLength: 120 }
                description: { type: string, maxLength: 500 }
                copyright: { type: string, maxLength: 255 }
                is_preview:
                  type: boolean
                  description: Als Vorschaubild setzen (ersetzt ein vorhandenes)
      responses:
        "201": { description: "Bild gespeichert" }
        "409": { description: "Mitteilung ist nicht mehr änderbar" }
        "422": { description: "Ungültige Datei" }
  /press-release-images/{imageId}:
    delete:
      tags: [images]
      summary: Bild entfernen (nur bei draft/rejected)
      parameters:
        - name: imageId
          in: path
          required: true
          schema: { type: integer }
      responses:
        "204": { description: "Gelöscht" }
        "409": { description: "Mitteilung ist nicht mehr änderbar" }
  /companies:
    get:
      tags: [companies]
      summary: Eigene Firmenprofile auflisten
      parameters:
        - name: per_page
          in: query
          schema: { type: integer, maximum: 100 }
      responses:
        "200": { description: "Paginierte Liste eigener Firmen" }
  /companies/{id}:
    get:
      tags: [companies]
      summary: Firmenprofil abrufen
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
      responses:
        "200": { description: "Firmenprofil" }
        "403": { description: "Nicht gefunden oder fremdes Konto" }
  /categories:
    get:
      tags: [categories]
      summary: Aktive Rubriken auflisten
      responses:
        "200": { description: "Rubriken inkl. Übersetzungen" }
  /newsletter/subscribe:
    post:
      tags: [newsletter]
      summary: Newsletter-Anmeldung anlegen (Double-Opt-in folgt per Mail)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [portal, email]
              properties:
                portal:
                  type: string
                  enum: [presseecho, businessportal24]
                email: { type: string, format: email, maxLength: 190 }
                salutation_key: { type: string, maxLength: 20 }
                first_name: { type: string, maxLength: 80 }
                last_name: { type: string, maxLength: 80 }
      responses:
        "201": { description: "Anmeldung angelegt (unbestätigt)" }
components:
  securitySchemes:
    bearerToken:
      type: http
      scheme: bearer
      description: "Persönlicher API-Token aus dem Publisher-Konto (Selfservice unter „API-Tokens")."
  schemas:
    PressReleaseInput:
      type: object
      required: [company_id, category_id, language, title, text]
      properties:
        company_id:
          type: integer
          description: ID eines eigenen Firmenprofils — bestimmt Newsroom und Portal
        category_id: { type: integer }
        language:
          type: string
          minLength: 2
          maxLength: 2
          description: de oder en
        title: { type: string, maxLength: 255 }
        text:
          type: string
          description: "Erlaubtes HTML: p, br, h2, h3, strong, em, ul, ol, li, blockquote, a"
        backlink_url: { type: string, format: uri, maxLength: 255 }
        keywords: { type: string, maxLength: 255 }
        teaser_begin: { type: integer, minimum: 0 }
        teaser_end: { type: integer, minimum: 0 }
        no_export: { type: boolean }
    PressRelease:
      type: object
      properties:
        id: { type: integer }
        uuid: { type: string, format: uuid }
        portal: { type: string }
        language: { type: string }
        title: { type: string }
        slug: { type: string }
        text: { type: string }
        backlink_url: { type: [string, "null"] }
        keywords: { type: [string, "null"] }
        status:
          type: string
          enum: [draft, review, published, rejected, archived]
        hits: { type: integer }
        teaser:
          type: object
          properties:
            begin: { type: [integer, "null"] }
            end: { type: [integer, "null"] }
        no_export: { type: boolean }
        published_at: { type: [string, "null"], format: date-time }
        company: { type: object }
        category: { type: object }
        images:
          type: array
          items: { $ref: "#/components/schemas/PressReleaseImage" }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    PressReleaseImage:
      type: object
      properties:
        id: { type: integer }
        title: { type: [string, "null"] }
        description: { type: [string, "null"] }
        copyright: { type: [string, "null"] }
        is_preview: { type: boolean }
        width: { type: integer }
        height: { type: integer }
        mime: { type: string }
