Docs

Référence du payload webhook

Chaque header et chaque champ que GrowWriter envoie à ton endpoint webhook, avec son type, sa présence, son format exact et ce sur quoi tu peux compter.

Dernière mise à jour le

Cette page est le contrat, écrite pour la personne qui construit ton endpoint. Si ce n'est pas toi, tu peux t'arrêter ici. Le guide couvre la configuration ; cette page liste exactement ce qui arrive, pour que personne n'ait à deviner.

Comment lire cette page

Chaque tableau a les mêmes cinq colonnes :

  • Champ : le chemin dans le corps JSON, en code. images[].url désigne l'url de chaque élément du tableau images.
  • Type : string, string[] (une liste de chaînes), object, object[], number, boolean.
  • Présence : un mot parmi trois. Toujours veut dire que la clé est dans chaque livraison. Toujours, parfois vide veut dire que la clé est là mais que la valeur peut être une chaîne vide ou une liste vide. Absent si non défini veut dire que la clé manque simplement dans le JSON quand il n'y a pas de valeur. Ce n'est jamais null.
  • Format et garanties : à quoi ressemble la valeur et ce sur quoi tu peux compter. Quand quelque chose n'est pas garanti, le tableau le dit.
  • Exemple : une valeur réaliste.

Deux règles s'appliquent partout. D'abord, la dernière livraison fait foi : si une valeur diffère entre deux livraisons du même article, utilise la plus récente. Ensuite, ignore les champs que tu ne reconnais pas : de nouveaux champs optionnels peuvent apparaître sans changement de version (vois Versionnage).

Un petit glossaire pour les termes des tableaux : une date ISO 8601 ressemble à 2026-09-01T09:30:00.000Z (année-mois-jour, un T, l'heure à la milliseconde, et Z pour UTC). Un UUID est un id aléatoire comme 9d2f7c1e-6a3b-4c8d-b5e0-1f2a3b4c5d6e.

La requête

GrowWriter envoie un POST par livraison à l'URL de l'endpoint que tu as saisie.

PropriétéValeur
MéthodePOST
Content-Typeapplication/json
User-AgentGrowWriter-Webhooks/1.0
Taille du corpsPas de plafond. Un article standard pèse en général quelques centaines de kilo-octets parce que le contenu voyage trois fois (markdown, HTML, blocs). Les articles longs peuvent dépasser 1 Mo.

Ce qui compte comme un succès, comment fonctionnent les réessais et quoi répondre sont dans Livraison. Les headers dont tu as besoin :

HeaderTypeFormat et garantiesExemple
X-GrowWriter-Eventstringping, article.published, article.updated ou article.unpublished. Même valeur que event dans le corps.article.published
X-GrowWriter-DeliverystringUUID v4. Identique sur toutes les tentatives d'une livraison. Même valeur que deliveryId dans le corps.9d2f7c1e-6a3b-4c8d-b5e0-1f2a3b4c5d6e
X-GrowWriter-Signaturestringt=<secondes unix>,v1=<64 caractères hex>. v1 est le HMAC-SHA256 de "<t>.<corps brut>" avec ton secret de signature entier comme clé. Nouveau à chaque tentative.t=1756718400,v1=5257a8…

Livraison

Une publication vers une connexion égale une livraison, et une livraison fait jusqu'à trois tentatives avant d'abandonner :

TentativeQuand elle a lieu
1Au moment où tu publies
21 seconde après l'échec de la tentative 1
33 secondes après l'échec de la tentative 2

Les règles sur lesquelles ton code peut compter :

  • Timeout. Chaque tentative attend ta réponse jusqu'à 10 secondes.
  • Succès. Tout statut 2xx. Réponds avec le JSON {"url": "https://…"} pour indiquer le billet en ligne et GrowWriter affiche un lien Voir l'article vers celui-ci (vois La réponse) ; tout autre corps de succès est ignoré. Quand une tentative est rejetée, GrowWriter affiche le statut et les 200 premiers caractères du corps dans sa boîte de dialogue, une courte raison en texte brut t'aide donc à déboguer.
  • Échec. Un timeout, tout 3xx (les redirections ne sont jamais suivies ; chaque tentative va à l'URL exacte que tu as saisie), ou tout autre statut.
  • Même id, signature neuve. Toutes les tentatives partagent un seul id X-GrowWriter-Delivery, et chaque tentative est signée avec son propre timestamp, un réessai ne déclenche donc jamais ta vérification des cinq minutes.
  • Au moins une fois. Un 2xx tardif peut arriver alors que GrowWriter a déjà réessayé, le même id de livraison peut donc t'atteindre deux fois. Traite un id répété comme déjà fait.
  • Rien après la troisième tentative. GrowWriter ne réessaie pas plus tard. Le canal affiche un bouton Réessayer dans la boîte de dialogue de publication, qui démarre une nouvelle livraison avec un nouvel id. Publier l'article à nouveau plus tard le livre aussi à nouveau, en article.updated.
  • Débit. La publication est limitée à 20 requêtes de publication par heure et par compte, une requête par canal. L'événement de test est limité à 60 par heure. Les livraisons elles-mêmes n'ont pas de limite de débit.

L'événement de test (Envoyer un événement de test dans la boîte de dialogue de connexion) suit les mêmes règles, un endpoint qui ne répond jamais met donc environ une demi-minute à échouer.

La réponse

GrowWriter n'a besoin que d'un statut 2xx de ta part. En option, dis-lui où le billet est en ligne pour que la boîte de dialogue de publication puisse y renvoyer :

Réponse de succès (corps optionnel)
{ "url": "https://www.example.com/blog/mon-article" }

Les règles :

  • URL absolue en http ou https. Les chemins relatifs et tout autre schéma sont ignorés, parce que la valeur est affichée comme un lien.
  • Les autres clés sont ignorées. Tu peux renvoyer l'url à côté de tes propres champs.
  • Rien n'est jamais une erreur. Un simple OK, un corps vide, du non-JSON ou du JSON sans url comptent tous comme un succès ; la boîte de dialogue n'affiche simplement aucun lien.
  • Reste léger. GrowWriter lit les 4 premiers Ko du corps.
  • Chaque livraison remplace le lien. Le lien stocké est celui qu'a renvoyé la dernière livraison réussie. Réponds sans url lors d'une republication et le lien disparaît.

Renvoie l'url quand ton handler crée le billet avant de répondre. Si tu réponds d'abord et crées le billet dans une tâche de fond, tu ne connais pas encore l'url : saute-la, et la boîte de dialogue n'affiche aucun lien pour cette publication.

L'enveloppe

Les champs de premier niveau, présents dans chaque livraison :

ChampTypePrésenceFormat et garantiesExemple
versionstringToujoursUne date qui nomme la forme du payload. Ne change que pour un changement incompatible ; vois Versionnage."2026-09-06"
eventstringToujoursping, article.published, article.updated ou article.unpublished."article.published"
deliveryIdstringToujoursUUID v4. Une valeur par livraison, partagée par ses réessais."9d2f7c1e-…"
sentAtstringToujoursISO 8601, UTC, millisecondes. Moment où le payload a été construit (une fois par livraison, pas par tentative)."2026-09-01T09:30:00.412Z"

Les quatre événements :

ÉvénementQuand il se déclencheCorps
pingTu cliques sur Envoyer un événement de test dans la boîte de dialogue de connexion.Enveloppe seule
article.publishedLa première livraison réussie d'un article vers cette connexion webhook.Enveloppe + l'article complet
article.updatedToute publication ultérieure du même article vers cette connexion, modifié ou non.Enveloppe + l'article complet
article.unpublishedL'article est supprimé dans GrowWriter, ou cette connexion webhook est déconnectée (une fois par article qu'elle avait publié).Enveloppe + article.id, article.slug, article.language seulement

« Cette connexion » a son importance : publier le même article vers une deuxième connexion webhook envoie article.published à cette connexion, parce qu'elle n'a jamais vu l'article. Une première tentative qui a échoué compte aussi comme jamais vu, le réessai est donc à nouveau article.published. Les deux événements article portent l'article complet, ton handler peut donc les traiter de la même façon : crée ou mets à jour le billet indexé sur article.id. Sur article.unpublished, masque ou supprime le billet qui porte cet id ; c'est toi qui choisis, et aucun contenu ne voyage avec une suppression.

L'événement ping

Envoyer un événement de test dans la boîte de dialogue de connexion livre un ping. Il ne porte que l'enveloppe, sans article :

ping
{
  "version": "2026-09-06",
  "event": "ping",
  "deliveryId": "5f0c9a1e-4d2b-4f6a-9c3e-8b7d6a5e4f3c",
  "sentAt": "2026-09-01T09:30:00.000Z"
}

L'événement unpublished

Supprimer un article dans GrowWriter, ou déconnecter le webhook, livre article.unpublished pour chaque article que cette connexion avait publié. Il identifie le billet et rien de plus :

article.unpublished
{
  "version": "2026-09-06",
  "event": "article.unpublished",
  "deliveryId": "0b7d3e2a-9c41-4f58-8e6d-2a1b3c4d5e6f",
  "sentAt": "2026-09-03T08:12:44.000Z",
  "article": {
    "id": "cme8x2k9r0001l7042n5q8w3v",
    "slug": "comment-choisir-chaussures-running",
    "language": "fr"
  }
}

Masque ou supprime le billet indexé sur article.id. Les mêmes règles de livraison s'appliquent (trois tentatives, puis rien), un récepteur qui était hors ligne rate donc la suppression : traite un article.published ultérieur pour un id que tu détiens encore comme l'état qui fait foi.

article : identité

ChampTypePrésenceFormat et garantiesExemple
article.idstringToujours25 caractères, lettres minuscules et chiffres, commence par c. Sûr dans les URLs et les noms de fichiers. Ne change jamais. C'est ta clé."cme8x2k9r0001l7042n5q8w3v"
article.languagestringToujoursDeux lettres minuscules, ISO 639-1. Jamais un tag de région (fr, jamais fr-FR). Le norvégien est nb. en par défaut."fr"
article.slugstringToujoursMinuscules, dérivé du titre au moment où le brief est généré. Jamais vide. Pas unique, pas de longueur maximale, peut contenir des lettres non latines."comment-choisir-chaussures-running"

Notes sur l'identité :

  • Chaque langue est un article à part entière. Une traduction est un brouillon séparé avec son propre id. Associe les langues avec tes propres données, en utilisant article.language.
  • Les 41 codes de langue possibles : ar bg ca cs da de el en es et fa fi fr he hi hr hu hy id is it ja ko lt lv nb nl pl pt ro ru sk sl sr sv th tl tr uk vi zh.
  • Comment le slug est construit. Les accents latins sont retirés (qué devient que), les espaces deviennent des tirets, la ponctuation est supprimée et les lettres des autres alphabets sont conservées, un titre japonais donne donc un slug japonais. Encode-le en pourcentage avant de le mettre dans une URL (encodeURIComponent(article.slug) en JavaScript).
  • Quand le slug change. Modifier le titre dans l'éditeur ne le change pas. Choisir un nouveau titre dans le writer régénère le brief et le slug, un article.updated peut donc porter un nouveau slug sous le même id.
  • N'utilise jamais le slug comme clé. Il peut changer lors d'une mise à jour et deux articles peuvent en partager un. Stocke-le comme donnée d'affichage à côté de l'id.

article : champs texte

ChampTypePrésenceFormat et garantiesExemple
article.titlestringToujoursUnicode. Pas de longueur maximale. Pas répété dans le contenu, affiche-le donc toi-même."Comment choisir des chaussures de running vraiment adaptées"
article.summarystringAbsent si non définiTexte brut, un paragraphe. Pas de longueur maximale."Un guide pratique pour…"
article.metaDescriptionstringAbsent si non définiTexte brut écrit pour faire 140 à 160 caractères. Jamais plus de 160 : tout dépassement est coupé à une limite de mot avant la livraison."Apprends à choisir des chaussures de running…"
article.takeawaysstring[]Toujours, parfois videPhrases en texte brut, 3 à 5 telles que générées ; considère le nombre comme non borné. Chacune est "<point> : <détail>" ou juste <détail>.["L'ajustement compte plus que la marque : …"]
article.keywordstringAbsent si non définiLe seul terme de recherche pour lequel l'article a été écrit, tel que l'auteur l'a tapé. Pour tes outils SEO ; pas un tag."comment choisir chaussures running"
article.relatedQuestionsstring[]Toujours, parfois videLes questions « Autres questions posées » de Google pour le mot-clé, dans la langue et le pays de l'article, capturées au moment où les titres ont été générés. Au plus 8, dans l'ordre de Google. Matière à FAQ : affiche-les sous l'article ou en balisage FAQPage. L'article ne promet pas de répondre à chacune, et ce ne sont pas des tags.["à quelle fréquence changer ses chaussures de running ?"]

article.coverImage et article.images

La couverture et chaque image du texte partagent une même forme d'objet :

ChampTypePrésenceFormat et garantiesExemple
urlstringToujoursURL https:// publique sur le stockage de GrowWriter. N'expire pas aujourd'hui, mais n'est pas un hébergement à long terme : télécharge-la."https://….public.blob.vercel-storage.com/…"
altTextstringAbsent si non définiTexte d'accessibilité."Vue latérale d'un coureur en pleine foulée"
captionstringAbsent si non définiLégende visible. Apparaît aussi dans content.markdown comme paragraphe en italique juste après l'image."Un rapide contrôle de la foulée…"
filenamestringAbsent si non définiNom suggéré sans extension. Non imposé : le générateur reçoit la consigne de 3 à 7 mots ASCII minuscules reliés par des tirets. Génère tes propres noms à partir de article.id si tu veux une certitude."runner-gait-check"
sectionSlugstringAbsent si non définiLa section du plan pour laquelle l'image a été générée, ou une valeur user-… pour les images insérées à la main par l'auteur. Jamais sur la couverture. Ne le parse pas, ne l'utilise pas comme clé."section-1-4f1a4848"
attributionobjectAbsent si non défini{ authorName?, pageUrl?, licenseShortName?, licenseUrl? }, chaque champ optionnel. Présent pour les photos Wikimedia Commons. Aussi rendu comme ligne en italique sous la légende dans content.markdown.vois l'exemple

Les règles qui diffèrent entre les deux :

  • article.coverImage est absent si non défini. Il n'a jamais de sectionSlug.
  • article.images est toujours présent et peut être vide. Il liste les images du texte dans l'ordre du document et n'inclut pas la couverture. Il n'y a pas de nombre maximal.
  • sectionSlug est stable pendant toute la vie de l'article ; seule la régénération du plan dans le writer produit de nouveaux ids.

Fichiers image

  • Format : JPEG, PNG, WebP ou AVIF. Jamais GIF ni SVG.
  • Taille : les fichiers sont stockés tels que récupérés, à leur résolution d'origine, sans ré-encodage. Les photos de banque d'images font couramment 1 à 6 Mo ; certains originaux dépassent 10 Mo. Redimensionne et compresse pour ton site.
  • Stabilité : une image qui n'a pas changé garde exactement la même URL dans les livraisons suivantes. Une image remplacée reçoit une nouvelle URL. Les anciennes URLs continuent de fonctionner.

article.content

ChampTypePrésenceFormat et garanties
article.content.markdownstringToujoursL'article en markdown de type CommonMark, normalisé par GrowWriter. Vois ce qui survit.
article.content.htmlstringToujoursHTML prêt à insérer, construit à partir de la même structure. Vois ce que contient le HTML.
article.content.blocksobject[]ToujoursL'article sous forme d'arbre JSON pour les renderers sur mesure. Vois le format blocks.

Les trois portent le même article. Choisis-en un au moment du rendu ; le titre n'est dans aucun des trois.

Ce qui survit en markdown

content.markdown est re-rendu par GrowWriter à partir de la structure de l'article, il est donc prévisible :

  • Présent : titres # à ######, paragraphes, listes ordonnées et non ordonnées (imbriquées avec deux espaces), citations, blocs de code délimités avec un langage, séparateurs ---, **strong**, *em*, ~~barré~~, `code`, [texte](https://…), ![alt](url). Les blocs sont séparés par des lignes vides.
  • Images : ![alt](url), puis un paragraphe en italique avec la légende, puis, seulement pour les photos attribuées, une ligne en italique comme *Photo: Jane Doe · [Source](url) · [CC BY-SA 4.0](url-licence)*. Seules les adresses d'image en http(s) sont conservées ; tout le reste est supprimé.
  • Absent : tableaux, notes de bas de page, listes de tâches. Si un auteur les a tapés, ils arrivent comme paragraphes simples.
  • HTML brut : si un auteur a collé du HTML dans l'éditeur, il arrive comme texte littéral, pas comme balisage.
  • Échappement : une prose ordinaire contient {, } et <. Échappe-les si tu compiles le markdown en MDX, en JSX ou dans un template.

Ce que contient le HTML

content.html est construit élément par élément, jamais copié d'ailleurs :

  • Balises utilisées : h1 à h6, p, ul, ol (avec start si besoin), li, blockquote, pre, code (avec class="language-…"), hr, figure, img, figcaption, a, strong, em, del, et un span utilisé comme séparateur dans les légendes. Aucune autre balise, pas d'ids, pas de styles inline, pas de scripts.
  • Le texte est toujours échappé (&, <, >, ").
  • Les liens ne sont conservés qu'avec des adresses http:// ou https:// ; tout le reste devient du texte brut.
  • Les images sont rendues en <figure><img src alt><figcaption>…</figcaption></figure>, sans le figcaption quand il n'y a ni légende ni attribution. Une image dont l'adresse n'est pas en http(s) est supprimée entièrement.
  • Pas d'élément englobant et pas de doctype : insère-le tel quel dans ta page.

article.dates

ChampTypePrésenceFormat et garantiesExemple
article.dates.createdAtstringToujoursISO 8601, UTC, millisecondes. Moment où le brouillon a été créé dans GrowWriter."2026-08-28T14:00:00.000Z"
article.dates.firstPublishedAtstringToujoursISO 8601. Moment où l'article a été publié pour la première fois vers cette connexion. Identique dans chaque livraison suivante."2026-09-01T09:30:00.000Z"
article.dates.modifiedAtstringToujoursISO 8601. Le moment où cette publication a été demandée. Avance à chaque publication, même sans modification. Jamais antérieur à firstPublishedAt."2026-09-01T09:30:00.000Z"

Comment les trois se relient :

  • Lors de la première livraison, firstPublishedAt et modifiedAt sont égaux.
  • modifiedAt n'est pas « dernière modification ». Modifier sans republier ne change rien ; republier sans modifier le fait avancer. Utilise l'event pour distinguer une première publication d'une mise à jour, pas une comparaison de dates.
  • Les champs de date sans heure d'un CMS veulent en général AAAA-MM-JJ : prends les dix premiers caractères.

article.brand

ChampTypePrésenceFormat et garantiesExemple
article.brandobjectAbsent si non définiPrésent quand l'article appartient à un kit de marque (le cas normal).
article.brand.namestringToujours dans brandLe nom d'affichage de la marque. Pour le balisage éditeur/organisation, pas un auteur."Stride Lab"
article.brand.websiteUrlstringAbsent si non définiURL https:// du site web de la marque."https://stridelab.example.com"

Le payload nomme une marque, jamais une personne. Si tes billets ont besoin d'un auteur, définis-le dans ta propre logique d'enregistrement.

Le format blocks

content.blocks, c'est l'article sous forme d'arbre JSON, pour les sites qui rendent le contenu eux-mêmes au lieu de faire confiance au HTML prêt à l'emploi. C'est un tableau d'objets block, et chaque bloc est de l'un de ces sept types :

BlocChamps
headinglevel (1 à 6) et children (nœuds inline)
paragraphchildren (nœuds inline)
listordered (booléen), un numéro start optionnel et items ; chaque item a son propre tableau blocks, les listes imbriquées apparaissent donc comme des blocs list à l'intérieur d'un item
blockquoteblocks : des blocs complets imbriqués dans la citation
codelanguage optionnel, et code en une seule chaîne
hrrien d'autre ; un séparateur thématique
imagesrc, plus alt, caption, filename, sectionSlug et attribution optionnels (même sens que dans images)

Partout où un bloc a des children, ce sont des nœuds inline : text (avec text), code (avec text), link (avec href et children) et strong / em / del (chacun avec children, les styles peuvent donc s'imbriquer).

Un paragraphe avec du texte en gras, par exemple :

{
  "type": "paragraph",
  "children": [
    { "type": "text", "text": "Tout le secret est là : " },
    { "type": "strong", "children": [{ "type": "text", "text": "l'ajustement d'abord" }] },
    { "type": "text", "text": ", tout le reste ensuite." }
  ]
}

Payload d'exemple complet

Une livraison article.published complète, générée par le code même qui appellera ton endpoint. Les URLs d'images sont des exemples : les vraies pointent vers le stockage public de GrowWriter.

Versionnage

La version actuelle est 2026-09-06, et cette page la décrit en entier. Le champ version est une date qui nomme la forme du payload. De nouveaux champs optionnels peuvent apparaître, et une clé qui était absente peut commencer à apparaître, sans que cette date change, écris donc ton endpoint pour qu'il ignore les champs qu'il ne reconnaît pas au lieu de les rejeter. Rien n'est retiré, renommé ni retypé au sein d'une version. La date ne change que pour un changement incompatible, et tout changement de ce genre est annoncé dans le journal des modifications avant d'atteindre ton endpoint.

Sur cette page