Publicering via API
Publicera artiklar på din egen webbplats eller i ditt CMS via signerade API-anrop.
Papigrows skickar signerade POST-anrop till HTTPS-mottagaren som du driver. Mottagaren sparar och publicerar artikeln och svarar sedan med dess publika URL.
Konfigurera API-publiceringAnslut din publiceringsendpoint
Bygg en mottagare som sparar och publicerar artiklar på din sajt. Lägg till endpoint och delad hemlighet i CMS-inställningarna. Testa och spara skickar connection.test med tom data; svara med HTTP 200 och ok: true. Bara ägare och administratörer kan ändra anslutningen.
Använd en publik HTTPS-endpoint utan frågeparametrar, fragment eller inbäddade inloggningsuppgifter.
En slumpmässig hemlighet är klar att använda. Du kan ersätta den med en egen på 32 till 256 tecken. Kopiera den till mottagaren och förvara den på servern.
{
"version": 1,
"event": "connection.test",
"id": "<event-id>",
"data": {}
}
HTTP/1.1 200 OK
Content-Type: application/json
{ "ok": true }Låt mottagaren godta både den gamla och den nya hemligheten medan du testar och sparar. Papigrows använder den sparade hemligheten tills den nya anslutningen har klarat testet och sparats. Ta bort den gamla när pågående leveranser är klara.
Anropshuvuden
Dessa huvuden följer med både anslutningstestet och artikelleveranserna. Huvudnamn är inte skiftlägeskänsliga.
| Huvudnamn | Exempelvärde | Betydelse |
|---|---|---|
Content-Type | application/json | Anropskropp i JSON. |
Accept | application/json | Ett bekräftelsesvar i JSON förväntas. |
X-Papigrows-Event-Id | <event-id> | Matchar kuvertets id. |
X-Papigrows-Timestamp | <unix-seconds> | Unix-tid i sekunder för detta försök. |
X-Papigrows-Signature | v1=<hex-hmac-sha256> | v1= följt av hexkodad HMAC-SHA256 för tidsstämpeln + '.' + den exakta råa anropskroppen. |
Idempotency-Key | <event-id> | Samma stabila kuvert-id vid nya försök. |
Verifiera varje anrop
Verifiera v1-signaturen med HMAC-SHA256 över tidsstämpeln, en punkt och den exakta råa anropskroppen innan JSON tolkas. Jämför signaturer i konstant tid, avvisa tidsstämplar som är mer än fem minuter gamla eller ligger i framtiden och förvara hemligheten på servern. HTTPS och signatur ersätter inte skydd mot dubbletter.
Exempel på anrop
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" }
]
}
}Fält i anropskroppen
Fälten beskriver article.upsert. För connection.test är data tomt. Protokollversionen i kuvertet är separat från artikelns innehållsversion.
| Fältnamn | Värdetyp | Ingår | Betydelse |
|---|---|---|---|
version | integer | Alltid | Protokollversion: 1 för det ursprungliga formatet eller 2 efter en uttrycklig uppgradering av mottagaren. |
event | string | Alltid | Händelsetypen: article.upsert eller connection.test. |
id | string | Alltid | Stabilt leverans-id som delas med båda identitetshuvudena. Spara det för att förhindra dubbla skrivningar. |
data | object | Alltid | Händelsens innehåll. Tomt för connection.test. |
data.id | string | Alltid | Permanent id för just denna språkvariant. Använd nyckeln för att skapa eller uppdatera. |
data.version | integer | Alltid | Sparad innehållsrevision som ett icke-negativt heltal. Avvisa äldre revisioner. |
data.title | string | Alltid | Artikelns titel. |
data.html | string | Alltid | HTML att publicera, inklusive tillgängliga hreflang-länkar. |
data.slug | string | När tillgängligt | Föreslagen URL-slug. Behåll en stabil publik URL vid senare ändringar. |
data.excerpt | string | När tillgängligt | Artikelutdrag när det finns. |
data.coverImageUrl | string | När tillgängligt | Omslagsbildens URL när den finns. |
data.locale | string | När tillgängligt | Språkkod för denna artikelvariant. |
data.hreflangAlternates | array | När tillgängligt | Andra publicerade språkvarianter när de finns. |
data.hreflangAlternates[].locale | string | För varje språkvariant | Språkkod för den länkade varianten. |
data.hreflangAlternates[].url | string | För varje språkvariant | Den publicerade variantens publika URL. |
Version 2 och kompatibilitet
Version 2 innehåller vanlig text, arbetsyte- och målgrupps-ID:n, sammanfattningar, sektioner och taggar. Mottagaren måste uttryckligen bekräfta version 2 vid anslutningstestet. Befintliga mottagare behåller version 1 tills du väljer en annan version, testar och sparar den.
{
"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åller de ursprungliga artikelfälten och lägger till dessa obligatoriska fält. Målgrupps-ID:n förblir null för äldre artiklar utan sparad inriktning.
| Fältnamn | Värdetyp | Betydelse |
|---|---|---|
data.groupId | UUID | Stabil grupp som delas av språkversionerna. |
data.workspaceId | UUID | ID för artikelns sparade arbetsyta. |
data.primaryAudienceId | UUID | null | Sparat primärt målgrupps-ID, eller null för en äldre artikel utan målgrupp. |
data.businessAreaId | UUID | null | Sparat verksamhetsområdes-ID, eller null om det saknas. |
data.text | string | Vanlig text från rensad artikel-HTML, inte ursprunglig Markdown. Länkar hämtas inte. |
data.summary | string | null | Sparad artikelsammanfattning, eller null om den saknas. |
data.section | string | null | Sektion som sparades för publiceringen, eller null. |
data.tags | string[] | Taggar som sparades för publiceringen; en tom lista betyder inga taggar. |
Publikt innehållsflöde
Aktivera Papigrows API-anslutningen för att visa publicerade artiklar. Begär version 2 uttryckligen; utan versionsval behålls det ursprungliga svaret. Privata arbetsytor och opublicerade artiklar är inte tillgängliga.
/api/content/feed/<workspace-slug>?version=2&locale=en&limit=20Varje artikel innehåller sparade ID:n, innehållsversion, vanlig text och sammanfattning. Sektion och taggar hämtas endast från den sparade publiceringsdestinationen. Taggar är null när sådan information saknas. Behåll version=2 och locale vid varje begäran med sidmarkör.
I det publika flödet är text null om konverteringen inte är tillgänglig. textUnavailable är body_too_large över 1 MiB eller conversion_failed vid konverteringsfel; annars är värdet null. HTML och sidindelning bevaras.
Bekräfta publicering
När artikeln är publik svarar du med HTTP 200 eller 201, samma ID, status published och dess publika HTTPS-URL. Köbekräftelser, HTTP 202 eller 204, omdirigeringar, utkast eller ogiltiga svar räknas inte som publicering. Bekräfta inte framgång innan artikeln har sparats och publicerats.
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "<permanent-article-id>",
"status": "published",
"url": "https://example.com/blog/example"
}Leverans och nya försök
article.upsert skapar eller uppdaterar en språkversion med data.id som permanent nyckel. Spara data.version och händelse-ID atomärt med ändringen; dubbletter ska få den sparade bekräftelsen och äldre versioner får aldrig skriva över nyare innehåll. Upp till tre försök görs med fem sekunders tidsgräns per försök. Nätverksfel, HTTP 429 och HTTP 5xx ger nya försök efter 250 ms och 1 sekund. Kropp och händelse-ID förblir oförändrade, men tidsstämpel och signatur förnyas.
Leveransens anropskropp får vara högst 1 MiB och JSON-svaret högst 16 KiB. Begränsa den råa anropskroppen innan du tolkar den.
Vad integrationen omfattar
Endast artiklar, inklusive ändringar som publiceras igen från Papigrows. Ämneshändelser, raderingshändelser, leveransköer och läsning av externa ändringar ingår inte. Svar via e-post kan inte säkert uppdatera en webhook-mottagare; redigera och publicera i Papigrows i stället. Befintliga inställningar för abonnemang, kvalitet och automatisk publicering gäller fortfarande. Kontrollera mottagaren före ett manuellt nytt försök efter ett osäkert fel; det finns ingen oändlig försöksslinga.
Exempel på signaturverifiering
Den här Node.js-hjälparen verifierar bara autenticitet. Din mottagare måste även begränsa anropsstorlek, validera händelseschemat, hantera dubbletter och versioner i beständig lagring, publicera artikeln och returnera bekräftelsen nedan.
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; }
}