Legacy-API-Kunden mit Keys beider Altportale — vor allem connektar — senden jede Meldung an presseecho.de UND businessportal24.de. Weil die Firma ihr Portal vom User und die Meldung es von der Firma erbte, landeten beide Kopien im selben Portal, die zweite mit Slug-Suffix "-2". Verifiziert wurde das über die Token-Hashes gegen die Legacy-Datenbanken. Das Portal wird jetzt in fester Reihenfolge aufgelöst: expliziter portal-Parameter, sonst die aufgerufene Domain, sonst das Portal des Firmenprofils, zuletzt Portal::Both — genau der Default, den beide Spalten ohnehin tragen. Die Auflösung ist damit nicht-nullbar; vorher konnte sie null liefern, was als SQL-Fehler und damit als 500er beim Alt-Client gelandet wäre. Ein Update über die v1-API verschiebt eine bestehende Meldung ausschließlich über den expliziten Parameter, nie über die aufgerufene Domain. press-releases:fix-api-portal-duplicates repariert die Altdaten: Es findet Paare (gleicher User, Titel, Sprache und Portal, beide per API eingereicht, kurz nacheinander) und verschiebt die zweite Kopie samt ihrer exklusiven Firma ins jeweils andere Portal, mit neu vergebenem Slug. Web-Doppel-Submits ohne Status-Log source=api und Legacy-Sync-Daten mit gesetzter legacy_id werden geschont. Auf Live ausgeführt und verifiziert. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
353 lines
13 KiB
YAML
353 lines
13 KiB
YAML
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:
|
|
portal:
|
|
type: string
|
|
enum: [presseecho, businessportal24]
|
|
description: >
|
|
Ziel-Portal der Veröffentlichung. Optional: Ohne Angabe wird das
|
|
Portal aus der aufgerufenen Domain bestimmt (presseecho.de bzw.
|
|
businessportal24.com); ein explizit gesetzter Wert hat Vorrang
|
|
vor der Domain. Fehlen beide (z. B. Aufruf über pressekonto.de),
|
|
gilt das Portal des Firmenprofils. Beim PATCH wechselt das Portal
|
|
ausschließlich über diesen Parameter — die aufgerufene Domain
|
|
verschiebt eine bestehende Mitteilung nicht.
|
|
company_id:
|
|
type: integer
|
|
description: ID eines eigenen Firmenprofils — bestimmt den Newsroom
|
|
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 }
|