Pubblicazione tramite API
Pubblica articoli sul tuo sito o CMS tramite richieste API firmate.
Papigrows invia richieste POST firmate al ricevitore HTTPS che gestisci. Il ricevitore salva e pubblica l'articolo, poi risponde con il suo URL pubblico.
Configura la pubblicazione APICollega il tuo endpoint di pubblicazione
Crea un ricevitore che salvi e pubblichi gli articoli sul tuo sito. Aggiungi endpoint e segreto condiviso nelle impostazioni CMS. Verifica e salva invia connection.test con dati vuoti; rispondi con HTTP 200 e ok: true. Solo proprietari e amministratori possono modificare la connessione.
Usa un endpoint HTTPS pubblico senza parametri, frammenti o credenziali incorporate.
È pronto un segreto casuale. Puoi sostituirlo con uno tuo da 32 a 256 caratteri. Copialo nel ricevitore e conservalo sul server.
{
"version": 1,
"event": "connection.test",
"id": "<event-id>",
"data": {}
}
HTTP/1.1 200 OK
Content-Type: application/json
{ "ok": true }Consenti al ricevitore di accettare il vecchio e il nuovo segreto durante il test e il salvataggio. Papigrows usa il segreto salvato finché la nuova connessione non supera il test e viene salvata. Rimuovi il vecchio segreto quando le consegne in corso sono terminate.
Intestazioni della richiesta
Queste intestazioni accompagnano sia il test di connessione sia le consegne degli articoli. I nomi non distinguono maiuscole e minuscole.
| Nome intestazione | Valore di esempio | Significato |
|---|---|---|
Content-Type | application/json | Corpo della richiesta in JSON. |
Accept | application/json | È prevista una conferma in JSON. |
X-Papigrows-Event-Id | <event-id> | Corrisponde all'id della busta. |
X-Papigrows-Timestamp | <unix-seconds> | Ora Unix in secondi per questo tentativo. |
X-Papigrows-Signature | v1=<hex-hmac-sha256> | v1= seguito dall'HMAC-SHA256 esadecimale di timestamp + '.' + il corpo grezzo esatto. |
Idempotency-Key | <event-id> | Lo stesso id stabile della busta nei nuovi tentativi. |
Verifica ogni richiesta
Prima di interpretare il JSON, verifica la firma v1 HMAC-SHA256 su timestamp, un punto e il corpo grezzo esatto. Confronta in tempo costante, rifiuta timestamp più vecchi di cinque minuti o futuri e conserva il segreto sul server. HTTPS e firma non sostituiscono la protezione dai duplicati.
Esempio di richiesta
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" }
]
}
}Campi del corpo della richiesta
Questi campi descrivono article.upsert. Per connection.test, data è vuoto. La versione del protocollo della busta è distinta dalla versione del contenuto dell'articolo.
| Nome campo | Tipo di valore | Presenza | Significato |
|---|---|---|---|
version | integer | Sempre | Versione del protocollo: 1 per il formato originale oppure 2 dopo un aggiornamento esplicito del ricevitore. |
event | string | Sempre | Tipo di evento: article.upsert o connection.test. |
id | string | Sempre | ID stabile della consegna condiviso con entrambe le intestazioni di identità . Salvalo per evitare scritture duplicate. |
data | object | Sempre | Contenuto dell'evento. Vuoto per connection.test. |
data.id | string | Sempre | ID permanente di questa variante linguistica. Usalo come chiave per creare o aggiornare. |
data.version | integer | Sempre | Revisione salvata del contenuto, un intero non negativo. Rifiuta le revisioni precedenti. |
data.title | string | Sempre | Titolo dell'articolo. |
data.html | string | Sempre | HTML da pubblicare, inclusi i link hreflang disponibili. |
data.slug | string | Se disponibile | Slug URL suggerito. Mantieni un URL pubblico stabile nelle modifiche successive. |
data.excerpt | string | Se disponibile | Estratto dell'articolo, se disponibile. |
data.coverImageUrl | string | Se disponibile | URL dell'immagine di copertina, se disponibile. |
data.locale | string | Se disponibile | Codice lingua di questa variante dell'articolo. |
data.hreflangAlternates | array | Se disponibile | Altre varianti linguistiche pubblicate, se disponibili. |
data.hreflangAlternates[].locale | string | Per ogni variante linguistica | Codice lingua della variante collegata. |
data.hreflangAlternates[].url | string | Per ogni variante linguistica | URL pubblico di quella variante pubblicata. |
Versione 2 e compatibilitÃ
La versione 2 include testo semplice, ID di spazio di lavoro e pubblico, riepiloghi, sezioni e tag. Il ricevitore deve confermare esplicitamente la versione 2 durante il test di connessione. I ricevitori esistenti mantengono la versione 1 finché non scegli, testi e salvi un'altra versione.
{
"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" }
]
}
}La versione 2 mantiene i campi originali e aggiunge questi campi obbligatori. Gli ID del pubblico restano null per gli articoli precedenti senza destinazione salvata.
| Nome campo | Tipo di valore | Significato |
|---|---|---|
data.groupId | UUID | Gruppo stabile condiviso dalle versioni linguistiche. |
data.workspaceId | UUID | ID salvato dello spazio di lavoro proprietario. |
data.primaryAudienceId | UUID | null | ID del pubblico principale salvato, oppure null per un vecchio articolo senza pubblico. |
data.businessAreaId | UUID | null | ID dell’area aziendale salvato, oppure null se assente. |
data.text | string | Testo semplice derivato dall’HTML ripulito, non dal Markdown originale. I link non vengono caricati. |
data.summary | string | null | Riepilogo dell’articolo salvato, oppure null se assente. |
data.section | string | null | Sezione acquisita per questa pubblicazione, oppure null. |
data.tags | string[] | Tag acquisiti per questa pubblicazione; un elenco vuoto indica nessun tag. |
Feed pubblico dei contenuti
Attiva la connessione API di Papigrows per rendere disponibili gli articoli pubblicati. Richiedi esplicitamente la versione 2; senza versione, la risposta resta quella originale. Spazi privati e articoli non pubblicati restano inaccessibili.
/api/content/feed/<workspace-slug>?version=2&locale=en&limit=20Ogni articolo include gli ID salvati, la versione del contenuto, il testo semplice e il riepilogo. Sezione e tag provengono solo dalla destinazione acquisita alla pubblicazione. I tag sono null quando tale prova manca. Mantieni version=2 e locale in ogni richiesta con cursore.
Nel feed pubblico, text è null se la conversione non è disponibile. textUnavailable vale body_too_large oltre 1 MiB o conversion_failed in caso di errore; altrimenti è null. HTML e paginazione restano intatti.
Conferma la pubblicazione
Quando l’articolo è pubblico, restituisci HTTP 200 o 201 con lo stesso ID, stato published e URL HTTPS pubblico. Conferme di coda, HTTP 202 o 204, reindirizzamenti, bozze e risposte non valide non contano come pubblicazione. Conferma solo dopo salvataggio e pubblicazione.
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "<permanent-article-id>",
"status": "published",
"url": "https://example.com/blog/example"
}Consegna e nuovi tentativi
article.upsert crea o aggiorna una variante linguistica usando data.id come chiave permanente. Salva data.version e ID evento atomicamente con la modifica. I duplicati ricevono la conferma salvata; le versioni vecchie non devono mai sovrascrivere quelle nuove. Fino a tre tentativi di cinque secondi ciascuno. Errori di rete, HTTP 429 e HTTP 5xx vengono ritentati dopo 250 ms e 1 secondo. Corpo e ID restano uguali; timestamp e firma si rinnovano.
I corpi delle consegne sono limitati a 1 MiB e le risposte JSON a 16 KiB. Limita il corpo grezzo della richiesta prima di interpretarlo.
Cosa comprende l’integrazione
Solo articoli, incluse modifiche ripubblicate da Papigrows. Eventi sui temi, eliminazioni, code in background e lettura di modifiche remote non sono inclusi. La modifica via risposta e-mail non può aggiornare un webhook in sicurezza; modifica e pubblica in Papigrows. Restano validi piano, qualità e impostazioni di automazione. Dopo un errore incerto, controlla il ricevitore prima di riprovare manualmente; non esistono tentativi infiniti.
Esempio di verifica della firma
Questo helper Node.js verifica solo l’autenticità . Il ricevitore deve anche limitare la dimensione, validare lo schema, gestire duplicati e versioni in archiviazione persistente, pubblicare e restituire la conferma seguente.
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; }
}