Publiceren via API
Publiceer artikelen op je eigen website of CMS via ondertekende API-verzoeken.
Papigrows stuurt ondertekende POST-verzoeken naar de HTTPS-ontvanger die jij beheert. Je ontvanger slaat het artikel op, publiceert het en antwoordt met de openbare URL.
API-publicatie instellenJe publicatie-endpoint verbinden
Bouw een ontvanger die artikelen opslaat en publiceert op je website. Voeg het endpoint en gedeelde geheim toe bij de CMS-instellingen. Testen en opslaan stuurt connection.test met lege data; antwoord met HTTP 200 en ok: true. Alleen eigenaren en beheerders kunnen deze verbinding wijzigen.
Gebruik een openbaar HTTPS-endpoint zonder queryparameters, fragmenten of ingesloten inloggegevens.
Er staat een willekeurig geheim klaar. Je kunt het vervangen door je eigen geheim van 32 tot 256 tekens. Kopieer het naar je ontvanger en bewaar het op de server.
{
"version": 1,
"event": "connection.test",
"id": "<event-id>",
"data": {}
}
HTTP/1.1 200 OK
Content-Type: application/json
{ "ok": true }Laat je ontvanger tijdens testen en opslaan zowel het oude als het nieuwe geheim accepteren. Papigrows gebruikt het opgeslagen geheim totdat de nieuwe verbinding de test doorstaat en is opgeslagen. Trek het oude geheim in nadat lopende leveringen zijn voltooid.
Verzoekheaders
Deze headers worden meegestuurd met de verbindingstest en artikelleveringen. Headernamen zijn niet hoofdlettergevoelig.
| Headernaam | Voorbeeldwaarde | Betekenis |
|---|---|---|
Content-Type | application/json | Verzoekinhoud in JSON. |
Accept | application/json | Er wordt een JSON-bevestiging verwacht. |
X-Papigrows-Event-Id | <event-id> | Komt overeen met de id van de envelop. |
X-Papigrows-Timestamp | <unix-seconds> | Unix-tijd in seconden voor deze poging. |
X-Papigrows-Signature | v1=<hex-hmac-sha256> | v1= gevolgd door de hexadecimale HMAC-SHA256 van de tijdstempel + '.' + de exacte ruwe verzoekinhoud. |
Idempotency-Key | <event-id> | Dezelfde vaste envelop-id bij herhaalde pogingen. |
Elk verzoek verifiëren
Verifieer vóór het parsen van JSON de v1-HMAC-SHA256-handtekening over het tijdstempel, een punt en de exacte onbewerkte body. Vergelijk in constante tijd, weiger tijdstempels ouder dan vijf minuten of in de toekomst en bewaar het geheim op de server. HTTPS en ondertekening vervangen geen bescherming tegen duplicaten.
Voorbeeldverzoek
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" }
]
}
}Velden in de verzoekinhoud
Deze velden beschrijven article.upsert. Bij connection.test is data leeg. De protocolversie van de envelop staat los van de inhoudsversie van het artikel.
| Veldnaam | Waardetype | Aanwezig | Betekenis |
|---|---|---|---|
version | integer | Altijd | Protocolversie: 1 voor het oorspronkelijke formaat of 2 na een uitdrukkelijke upgrade van de ontvanger. |
event | string | Altijd | Het gebeurtenistype: article.upsert of connection.test. |
id | string | Altijd | Vaste leverings-id die overeenkomt met beide identiteitsheaders. Sla deze op om dubbele schrijfacties te voorkomen. |
data | object | Altijd | Inhoud van de gebeurtenis. Leeg bij connection.test. |
data.id | string | Altijd | Permanente id voor deze taalvariant. Maak aan of werk bij met deze sleutel. |
data.version | integer | Altijd | Opgeslagen inhoudsrevisie als niet-negatief geheel getal. Weiger oudere revisies. |
data.title | string | Altijd | Titel van het artikel. |
data.html | string | Altijd | Te publiceren HTML, inclusief beschikbare hreflang-links. |
data.slug | string | Indien beschikbaar | Voorgestelde URL-slug. Houd bij latere wijzigingen een vaste openbare URL aan. |
data.excerpt | string | Indien beschikbaar | Artikeluittreksel, indien beschikbaar. |
data.coverImageUrl | string | Indien beschikbaar | URL van de omslagafbeelding, indien beschikbaar. |
data.locale | string | Indien beschikbaar | Taalcode voor deze artikelvariant. |
data.hreflangAlternates | array | Indien beschikbaar | Andere gepubliceerde taalvarianten, indien beschikbaar. |
data.hreflangAlternates[].locale | string | Voor elke taalvariant | Taalcode van de gekoppelde variant. |
data.hreflangAlternates[].url | string | Voor elke taalvariant | Openbare URL van die gepubliceerde variant. |
Versie 2 en compatibiliteit
Versie 2 bevat platte tekst, werkruimte- en doelgroep-ID's, samenvattingen, secties en tags. Je ontvanger moet versie 2 uitdrukkelijk bevestigen tijdens de verbindingstest. Bestaande ontvangers behouden versie 1 totdat je een andere versie kiest, succesvol test en opslaat.
{
"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" }
]
}
}Versie 2 behoudt de oorspronkelijke artikelvelden en voegt deze verplichte velden toe. Doelgroep-ID’s blijven null voor oudere artikelen zonder opgeslagen doelgroepinstellingen.
| Veldnaam | Waardetype | Betekenis |
|---|---|---|
data.groupId | UUID | Vaste groep die taalvarianten delen. |
data.workspaceId | UUID | Opgeslagen ID van de werkruimte van het artikel. |
data.primaryAudienceId | UUID | null | Opgeslagen primaire doelgroep-ID, of null voor een ouder artikel zonder doelgroep. |
data.businessAreaId | UUID | null | Opgeslagen bedrijfsgebied-ID, of null als dit ontbreekt. |
data.text | string | Platte tekst uit opgeschoonde artikel-HTML, geen oorspronkelijke Markdown. Links worden niet opgehaald. |
data.summary | string | null | Opgeslagen artikelsamenvatting, of null als deze ontbreekt. |
data.section | string | null | Sectie vastgelegd voor deze publicatie, of null. |
data.tags | string[] | Tags vastgelegd voor deze publicatie; een lege lijst betekent geen tags. |
Openbare inhoudsfeed
Schakel de Papigrows API-verbinding in om gepubliceerde artikelen beschikbaar te maken. Vraag expliciet versie 2 aan; zonder versie blijft het oorspronkelijke antwoord behouden. Privéwerkruimten en ongepubliceerde artikelen blijven ontoegankelijk.
/api/content/feed/<workspace-slug>?version=2&locale=en&limit=20Elk artikel bevat de opgeslagen ID’s, inhoudsversie, platte tekst en samenvatting. Secties en tags komen uitsluitend uit de vastgelegde publicatiebestemming. Tags zijn null als dat bewijs ontbreekt. Behoud version=2 en locale bij elk cursorverzoek.
In de openbare feed is text null als omzetting niet beschikbaar is. textUnavailable is body_too_large boven 1 MiB of conversion_failed bij een omzettingsfout; anders is de waarde null. HTML en paginering blijven behouden.
Publicatie bevestigen
Antwoord zodra het artikel openbaar is met HTTP 200 of 201, dezelfde ID, status published en een openbare HTTPS-URL. Wachtrijbevestigingen, HTTP 202 of 204, omleidingen, concepten en ongeldige antwoorden tellen niet als publicatie. Bevestig pas na opslaan en publiceren.
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "<permanent-article-id>",
"status": "published",
"url": "https://example.com/blog/example"
}Bezorging en herhalingen
article.upsert maakt of wijzigt één taalvariant met data.id als blijvende sleutel. Bewaar data.version en gebeurtenis-ID atomair met de wijziging. Duplicaten krijgen de bewaarde bevestiging; oudere versies mogen nieuwere inhoud nooit overschrijven. Maximaal drie pogingen van elk vijf seconden. Netwerkfouten, HTTP 429 en HTTP 5xx worden na 250 ms en 1 seconde herhaald. Body en gebeurtenis-ID blijven gelijk; tijdstempel en handtekening worden vernieuwd.
De inhoud van leveringen is beperkt tot 1 MiB en JSON-antwoorden tot 16 KiB. Begrens de ruwe verzoekinhoud voordat je deze verwerkt.
Wat de integratie omvat
Alleen artikelen, inclusief wijzigingen die opnieuw vanuit Papigrows worden gepubliceerd. Onderwerp- en verwijdergebeurtenissen, achtergrondwachtrijen en het teruglezen van externe wijzigingen zijn niet inbegrepen. Bewerken via e-mailantwoord kan een webhook niet veilig bijwerken; bewerk en publiceer in Papigrows. Bestaande abonnements-, kwaliteits- en automatiseringsinstellingen blijven gelden. Controleer na een onzekere fout de ontvanger voordat je handmatig opnieuw probeert; er is geen eindeloze herhaling.
Voorbeeld van handtekeningverificatie
Deze Node.js-helper controleert alleen de authenticiteit. Je ontvanger moet ook de verzoekgrootte beperken, het schema valideren, duplicaten en versies duurzaam opslaan, publiceren en de onderstaande bevestiging teruggeven.
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; }
}