Publication par API
Publiez des articles sur votre site ou CMS via des requêtes API signées.
Papigrows envoie des requêtes POST signées au récepteur HTTPS que vous hébergez. Votre récepteur enregistre et publie l'article, puis répond avec son URL publique.
Configurer la publication par APIConnecter votre service de publication
Créez un service qui enregistre et publie les articles sur votre site. Ajoutez son adresse et le secret partagé dans les paramètres CMS. Tester et enregistrer envoie connection.test avec des données vides ; répondez par HTTP 200 et ok: true. Seuls les propriétaires et administrateurs peuvent modifier cette connexion.
Utilisez une adresse HTTPS publique sans paramètres, fragment ni identifiants intégrés.
Un secret aléatoire est prêt. Vous pouvez le remplacer par votre propre secret de 32 à 256 caractères. Copiez-le dans votre récepteur et conservez-le sur le serveur.
{
"version": 1,
"event": "connection.test",
"id": "<event-id>",
"data": {}
}
HTTP/1.1 200 OK
Content-Type: application/json
{ "ok": true }Laissez le récepteur accepter l’ancien et le nouveau secret pendant le test et l’enregistrement. Papigrows utilise le secret enregistré jusqu’à ce que la nouvelle connexion réussisse le test et soit enregistrée. Retirez l’ancien secret une fois les livraisons en cours terminées.
En-têtes de requête
Ces en-têtes accompagnent le test de connexion et les livraisons d'articles. Leurs noms ne sont pas sensibles à la casse.
| Nom de l'en-tête | Exemple de valeur | Signification |
|---|---|---|
Content-Type | application/json | Corps de la requête au format JSON. |
Accept | application/json | Une confirmation au format JSON est attendue. |
X-Papigrows-Event-Id | <event-id> | Correspond à l'id de l'enveloppe. |
X-Papigrows-Timestamp | <unix-seconds> | Heure Unix en secondes pour cette tentative. |
X-Papigrows-Signature | v1=<hex-hmac-sha256> | v1= suivi du HMAC-SHA256 hexadécimal de l'horodatage + '.' + le corps brut exact. |
Idempotency-Key | <event-id> | Le même id stable de l'enveloppe lors des nouvelles tentatives. |
Vérifier chaque requête
Avant de lire le JSON, vérifiez la signature v1 HMAC-SHA256 sur l’horodatage, un point et le corps brut exact. Comparez en temps constant, refusez les horodatages de plus de cinq minutes ou futurs et gardez le secret sur le serveur. HTTPS et la signature ne remplacent pas la protection contre les doublons.
Exemple de requête
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" }
]
}
}Champs du corps de la requête
Ces champs décrivent article.upsert. Pour connection.test, data est vide. La version du protocole de l'enveloppe est distincte de la version du contenu de l'article.
| Nom du champ | Type de valeur | Présence | Signification |
|---|---|---|---|
version | integer | Toujours | Version du protocole : 1 pour le format d’origine ou 2 après une mise à niveau explicite du récepteur. |
event | string | Toujours | Type d'événement : article.upsert ou connection.test. |
id | string | Toujours | Identifiant de livraison stable partagé avec les deux en-têtes d'identité. Enregistrez-le pour éviter les écritures en double. |
data | object | Toujours | Contenu de l'événement. Vide pour connection.test. |
data.id | string | Toujours | Identifiant permanent de cette variante linguistique. Utilisez-le pour créer ou mettre à jour. |
data.version | integer | Toujours | Révision enregistrée du contenu, un entier positif ou nul. Refusez les révisions antérieures. |
data.title | string | Toujours | Titre de l'article. |
data.html | string | Toujours | HTML à publier, avec les liens hreflang disponibles. |
data.slug | string | Si disponible | Slug URL suggéré. Conservez une URL publique stable lors des modifications ultérieures. |
data.excerpt | string | Si disponible | Extrait de l'article, si disponible. |
data.coverImageUrl | string | Si disponible | URL de l'image de couverture, si disponible. |
data.locale | string | Si disponible | Code de langue de cette variante de l'article. |
data.hreflangAlternates | array | Si disponible | Autres variantes linguistiques publiées, si disponibles. |
data.hreflangAlternates[].locale | string | Pour chaque variante linguistique | Code de langue de la variante liée. |
data.hreflangAlternates[].url | string | Pour chaque variante linguistique | URL publique de cette variante publiée. |
Version 2 et compatibilité
La version 2 inclut le texte brut, les identifiants d'espace de travail et d'audience, les résumés, les sections et les étiquettes. Le récepteur doit confirmer explicitement la version 2 lors du test de connexion. Les récepteurs existants conservent la version 1 jusqu'au choix, au test réussi et à l'enregistrement d'une autre version.
{
"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 version 2 conserve les champs d’origine et ajoute ces champs obligatoires. Les identifiants d’audience restent null pour les anciens articles sans ciblage enregistré.
| Nom du champ | Type de valeur | Signification |
|---|---|---|
data.groupId | UUID | Groupe stable partagé par les variantes linguistiques. |
data.workspaceId | UUID | Identifiant enregistré de l’espace de travail propriétaire. |
data.primaryAudienceId | UUID | null | Identifiant de l’audience principale enregistré, ou null pour un ancien article sans ciblage. |
data.businessAreaId | UUID | null | Identifiant du domaine d’activité enregistré, ou null s’il est absent. |
data.text | string | Texte brut dérivé du HTML nettoyé, pas du Markdown d’origine. Les liens ne sont pas chargés. |
data.summary | string | null | Résumé enregistré de l’article, ou null s’il est absent. |
data.section | string | null | Section enregistrée pour cette publication, ou null. |
data.tags | string[] | Étiquettes enregistrées pour cette publication ; une liste vide signifie aucune étiquette. |
Flux public de contenu
Activez la connexion API Papigrows pour exposer les articles publiés. Demandez explicitement la version 2 ; sans version, la réponse d’origine est conservée. Les espaces privés et les articles non publiés restent inaccessibles.
/api/content/feed/<workspace-slug>?version=2&locale=en&limit=20Chaque article inclut ses identifiants enregistrés, la version du contenu, le texte brut et le résumé. La section et les étiquettes proviennent uniquement de la destination enregistrée à la publication. Les étiquettes valent null en l’absence de cette preuve. Conservez version=2 et locale dans chaque requête avec curseur.
Dans le flux public, text vaut null si la conversion est indisponible. textUnavailable vaut body_too_large au-delà de 1 Mio ou conversion_failed en cas d’erreur ; sinon null. Le HTML et la pagination sont conservés.
Confirmer la publication
Une fois l’article public, renvoyez HTTP 200 ou 201 avec le même identifiant, le statut published et son URL HTTPS publique. Une mise en file, HTTP 202 ou 204, une redirection, un brouillon ou une réponse invalide ne valent pas publication. Confirmez uniquement après enregistrement et publication.
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "<permanent-article-id>",
"status": "published",
"url": "https://example.com/blog/example"
}Livraison et nouvelles tentatives
article.upsert crée ou modifie une version linguistique avec data.id comme clé permanente. Enregistrez data.version et l’identifiant d’événement atomiquement avec la modification. Les doublons reçoivent la confirmation enregistrée ; une ancienne version ne doit jamais écraser une plus récente. Jusqu’à trois tentatives de cinq secondes chacune. Les erreurs réseau, HTTP 429 et HTTP 5xx sont réessayées après 250 ms et 1 seconde. Le corps et l’identifiant restent identiques ; l’horodatage et la signature sont renouvelés.
Les corps des livraisons sont limités à 1 MiB et les réponses JSON à 16 KiB. Limitez le corps brut de la requête avant de l'analyser.
Périmètre de cette intégration
Uniquement les articles, y compris les modifications republiées depuis Papigrows. Les événements de sujets, suppressions, files en arrière-plan et relecture des modifications distantes ne sont pas inclus. L’édition par réponse e-mail ne peut pas modifier un webhook en toute sécurité ; modifiez et publiez dans Papigrows. Les règles d’abonnement, de qualité et d’automatisation restent applicables. Après un échec incertain, vérifiez le destinataire avant une relance manuelle ; aucune boucle infinie n’est prévue.
Exemple de vérification de signature
Cet utilitaire Node.js vérifie seulement l’authenticité. Le destinataire doit aussi limiter la taille, valider le schéma, gérer durablement les doublons et versions, publier puis renvoyer la confirmation ci-dessous.
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; }
}