Veröffentlichung per API
Veröffentliche Artikel auf deiner eigenen Website oder in deinem CMS über signierte API-Anfragen.
Papigrows sendet signierte POST-Anfragen an den von dir betriebenen HTTPS-Empfänger. Dein Empfänger speichert und veröffentlicht den Artikel und antwortet mit dessen öffentlicher URL.
API-Veröffentlichung einrichtenVeröffentlichungsendpunkt verbinden
Erstelle einen Empfänger, der Artikel auf deiner Website speichert und veröffentlicht. Trage Endpunkt und gemeinsames Geheimnis in den CMS-Einstellungen ein. Testen und speichern sendet connection.test mit leeren Daten; antworte mit HTTP 200 und ok: true. Nur Inhaber und Administratoren können die Verbindung ändern.
Verwende einen öffentlichen HTTPS-Endpunkt ohne Abfrageparameter, Fragmente oder eingebettete Zugangsdaten.
Ein zufälliges Geheimnis ist einsatzbereit. Du kannst es durch ein eigenes mit 32 bis 256 Zeichen ersetzen. Kopiere es zum Empfänger und speichere es auf dem Server.
{
"version": 1,
"event": "connection.test",
"id": "<event-id>",
"data": {}
}
HTTP/1.1 200 OK
Content-Type: application/json
{ "ok": true }Lass den Empfänger während Test und Speichern sowohl das alte als auch das neue Geheimnis akzeptieren. Papigrows nutzt das gespeicherte Geheimnis, bis die neue Verbindung den Test besteht und gespeichert ist. Entferne das alte Geheimnis, sobald laufende Zustellungen abgeschlossen sind.
Anfrage-Header
Diese Header begleiten sowohl den Verbindungstest als auch Artikellieferungen. Bei Header-Namen spielt Groß- und Kleinschreibung keine Rolle.
| Header-Name | Beispielwert | Bedeutung |
|---|---|---|
Content-Type | application/json | Anfrageinhalt im JSON-Format. |
Accept | application/json | Eine JSON-Bestätigung wird erwartet. |
X-Papigrows-Event-Id | <event-id> | Entspricht der id im Umschlag. |
X-Papigrows-Timestamp | <unix-seconds> | Unix-Zeit in Sekunden für diesen Versuch. |
X-Papigrows-Signature | v1=<hex-hmac-sha256> | v1= gefolgt vom hexadezimalen HMAC-SHA256 aus Zeitstempel + '.' + dem exakten rohen Anfrageinhalt. |
Idempotency-Key | <event-id> | Dieselbe stabile Umschlag-ID bei erneuten Versuchen. |
Jeden Aufruf prüfen
Prüfe vor dem JSON-Parsen die v1-HMAC-SHA256-Signatur über Zeitstempel, Punkt und unveränderten Rohtext. Vergleiche in konstanter Zeit, lehne mehr als fünf Minuten alte oder zukünftige Zeitstempel ab und halte das Geheimnis auf dem Server. HTTPS und Signatur ersetzen keinen Duplikatschutz.
Beispielaufruf
POST /your-publishing-endpoint
Content-Type: application/json
X-Papigrows-Event-Id: <event-id>
X-Papigrows-Timestamp: <unix-seconds>
X-Papigrows-Signature: v1=<hex-hmac-sha256>
Idempotency-Key: <event-id>
{
"version": 1,
"event": "article.upsert",
"id": "<event-id>",
"data": {
"id": "<permanent-article-id>",
"version": 7,
"title": "Example",
"html": "<p>...</p>",
"slug": "example",
"excerpt": "...",
"coverImageUrl": "https://example.com/cover.jpg",
"locale": "en",
"hreflangAlternates": [
{ "locale": "sv", "url": "https://example.com/sv/blog/example" }
]
}
}Felder im Anfrageinhalt
Diese Felder beschreiben article.upsert. Bei connection.test ist data leer. Die Protokollversion des Umschlags ist von der Inhaltsversion des Artikels getrennt.
| Feldname | Werttyp | Enthalten | Bedeutung |
|---|---|---|---|
version | integer | Immer | Protokollversion: 1 für das ursprüngliche Format oder 2 nach einer ausdrücklichen Aktualisierung des Empfängers. |
event | string | Immer | Der Ereignistyp: article.upsert oder connection.test. |
id | string | Immer | Stabile Lieferungs-ID, die mit beiden Identitäts-Headern übereinstimmt. Speichere sie, um doppelte Schreibvorgänge zu verhindern. |
data | object | Immer | Ereignisinhalt. Leer bei connection.test. |
data.id | string | Immer | Dauerhafte ID dieser Sprachvariante. Verwende sie zum Anlegen oder Aktualisieren. |
data.version | integer | Immer | Gespeicherte Inhaltsrevision als nicht negative Ganzzahl. Weise ältere Revisionen zurück. |
data.title | string | Immer | Titel des Artikels. |
data.html | string | Immer | Zu veröffentlichendes HTML einschließlich verfügbarer hreflang-Links. |
data.slug | string | Wenn verfügbar | Vorgeschlagener URL-Slug. Behalte bei späteren Änderungen eine stabile öffentliche URL bei. |
data.excerpt | string | Wenn verfügbar | Artikelauszug, falls verfügbar. |
data.coverImageUrl | string | Wenn verfügbar | URL des Titelbilds, falls verfügbar. |
data.locale | string | Wenn verfügbar | Sprachcode dieser Artikelvariante. |
data.hreflangAlternates | array | Wenn verfügbar | Andere veröffentlichte Sprachvarianten, falls verfügbar. |
data.hreflangAlternates[].locale | string | Für jede Sprachvariante | Sprachcode der verlinkten Variante. |
data.hreflangAlternates[].url | string | Für jede Sprachvariante | Öffentliche URL dieser veröffentlichten Variante. |
Version 2 und Kompatibilität
Version 2 enthält Klartext, Arbeitsbereichs- und Zielgruppen-IDs, Zusammenfassungen, Bereiche und Tags. Der Empfänger muss Version 2 beim Verbindungstest ausdrücklich bestätigen. Bestehende Empfänger behalten Version 1, bis du eine andere Version auswählst, erfolgreich testest und speicherst.
{
"version": 2,
"event": "connection.test",
"id": "<event-id>",
"data": {}
}
HTTP/1.1 200 OK
Content-Type: application/json
{ "ok": true, "version": 2 }POST /your-publishing-endpoint
Content-Type: application/json
X-Papigrows-Event-Id: <event-id>
X-Papigrows-Timestamp: <unix-seconds>
X-Papigrows-Signature: v1=<hex-hmac-sha256>
Idempotency-Key: <event-id>
{
"version": 2,
"event": "article.upsert",
"id": "<event-id>",
"data": {
"id": "<permanent-article-id>",
"groupId": "<permanent-article-group-id>",
"workspaceId": "<owning-workspace-id>",
"primaryAudienceId": "<saved-audience-id>",
"businessAreaId": "<saved-business-area-id>",
"text": "...",
"summary": "...",
"section": "Advice",
"tags": ["Families"],
"version": 7,
"title": "Example",
"html": "<p>...</p>",
"slug": "example",
"excerpt": "...",
"coverImageUrl": "https://example.com/cover.jpg",
"locale": "en",
"hreflangAlternates": [
{ "locale": "sv", "url": "https://example.com/sv/blog/example" }
]
}
}Version 2 behält die ursprünglichen Artikelfelder bei und ergänzt diese Pflichtfelder. Zielgruppen-IDs bleiben bei älteren Artikeln ohne gespeicherte Zuordnung null.
| Feldname | Werttyp | Bedeutung |
|---|---|---|
data.groupId | UUID | Stabile Gruppe für die Sprachvarianten. |
data.workspaceId | UUID | Gespeicherte ID des zugehörigen Arbeitsbereichs. |
data.primaryAudienceId | UUID | null | Gespeicherte primäre Zielgruppen-ID oder null bei älteren Artikeln ohne Zuordnung. |
data.businessAreaId | UUID | null | Gespeicherte Geschäftsbereichs-ID oder null, wenn sie fehlt. |
data.text | string | Klartext aus bereinigtem Artikel-HTML, kein ursprüngliches Markdown. Links werden nicht abgerufen. |
data.summary | string | null | Gespeicherte Artikelzusammenfassung oder null, wenn sie fehlt. |
data.section | string | null | Bei dieser Veröffentlichung erfasster Bereich oder null. |
data.tags | string[] | Bei dieser Veröffentlichung erfasste Tags; eine leere Liste bedeutet keine Tags. |
Öffentlicher Inhaltsfeed
Aktiviere die Papigrows-API-Verbindung, um veröffentlichte Artikel bereitzustellen. Fordere Version 2 ausdrücklich an; ohne Versionsangabe bleibt die ursprüngliche Antwort erhalten. Private Arbeitsbereiche und unveröffentlichte Artikel sind nicht zugänglich.
/api/content/feed/<workspace-slug>?version=2&locale=en&limit=20Jeder Artikel enthält gespeicherte IDs, Inhaltsversion, Klartext und Zusammenfassung. Bereich und Tags stammen ausschließlich aus dem erfassten Veröffentlichungsziel. Tags sind null, wenn dieser Nachweis fehlt. Behalte version=2 und locale bei jeder Cursor-Anfrage bei.
Im öffentlichen Feed ist text null, wenn die Umwandlung nicht verfügbar ist. textUnavailable ist bei mehr als 1 MiB body_too_large oder bei einem Umwandlungsfehler conversion_failed; sonst null. HTML und Seitennavigation bleiben erhalten.
Veröffentlichung bestätigen
Sobald der Artikel öffentlich ist, antworte mit HTTP 200 oder 201, derselben ID, Status published und öffentlicher HTTPS-URL. Warteschlangenbestätigungen, HTTP 202 oder 204, Weiterleitungen, Entwürfe und ungültige Antworten gelten nicht als Veröffentlichung. Bestätige erst nach dem Speichern und Veröffentlichen.
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "<permanent-article-id>",
"status": "published",
"url": "https://example.com/blog/example"
}Zustellung und Wiederholungen
article.upsert erstellt oder aktualisiert eine Sprachvariante mit data.id als dauerhaftem Schlüssel. Speichere data.version und Ereignis-ID atomar mit der Änderung. Duplikate erhalten die gespeicherte Bestätigung; ältere Versionen dürfen neuere Inhalte nie überschreiben. Bis zu drei Versuche mit jeweils fünf Sekunden Frist. Netzwerkfehler, HTTP 429 und HTTP 5xx werden nach 250 ms und 1 Sekunde wiederholt. Text und Ereignis-ID bleiben gleich, Zeitstempel und Signatur werden erneuert.
Lieferinhalte sind auf 1 MiB und JSON-Antworten auf 16 KiB begrenzt. Begrenze den rohen Anfrageinhalt, bevor du ihn verarbeitest.
Umfang der Integration
Nur Artikel, einschließlich erneut veröffentlichter Änderungen aus Papigrows. Themen-, Löschereignisse, Hintergrundwarteschlangen und das Zurücklesen externer Änderungen sind nicht enthalten. Bearbeitung per E-Mail-Antwort kann Webhook-Ziele nicht sicher ändern; bearbeite und veröffentliche in Papigrows. Bestehende Tarif-, Qualitäts- und Automatisierungseinstellungen gelten weiter. Prüfe nach unklarem Fehler den Empfänger vor einem manuellen Neuversuch; es gibt keine endlose Wiederholung.
Beispiel zur Signaturprüfung
Diese Node.js-Hilfe prüft nur die Authentizität. Der Empfänger muss außerdem die Anfragegröße begrenzen, das Schema validieren, Duplikate und Versionen dauerhaft verwalten, veröffentlichen und die folgende Bestätigung zurückgeben.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyWebhook(headers, rawBody, secret) {
const timestamp = headers.get("x-papigrows-timestamp") ?? "";
const signature = headers.get("x-papigrows-signature") ?? "";
if (!/^\d{10}$/.test(timestamp)) return false;
const age = Math.floor(Date.now() / 1000) - Number(timestamp);
if (age < 0 || age > 300) return false;
if (!/^v1=[a-f0-9]{64}$/.test(signature)) return false;
const expected = createHmac("sha256", secret)
.update(timestamp + ".").update(rawBody).digest();
const received = Buffer.from(signature.slice(3), "hex");
if (received.length !== expected.length
|| !timingSafeEqual(received, expected)) return false;
try {
const event = JSON.parse(rawBody.toString());
return typeof event.id === "string"
&& /^[a-f0-9]{64}$/.test(event.id)
&& headers.get("x-papigrows-event-id") === event.id
&& headers.get("idempotency-key") === event.id;
} catch { return false; }
}