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[].urldésigne l'urlde chaque élément du tableauimages. - 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éthode | POST |
Content-Type | application/json |
User-Agent | GrowWriter-Webhooks/1.0 |
| Taille du corps | Pas 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 :
| Header | Type | Format et garanties | Exemple |
|---|---|---|---|
X-GrowWriter-Event | string | ping, article.published, article.updated ou article.unpublished. Même valeur que event dans le corps. | article.published |
X-GrowWriter-Delivery | string | UUID v4. Identique sur toutes les tentatives d'une livraison. Même valeur que deliveryId dans le corps. | 9d2f7c1e-6a3b-4c8d-b5e0-1f2a3b4c5d6e |
X-GrowWriter-Signature | string | t=<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 :
| Tentative | Quand elle a lieu |
|---|---|
| 1 | Au moment où tu publies |
| 2 | 1 seconde après l'échec de la tentative 1 |
| 3 | 3 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
2xxtardif 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 :
{ "url": "https://www.example.com/blog/mon-article" }Les règles :
- URL absolue en
httpouhttps. 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 sansurlcomptent 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 :
| Champ | Type | Présence | Format et garanties | Exemple |
|---|---|---|---|---|
version | string | Toujours | Une date qui nomme la forme du payload. Ne change que pour un changement incompatible ; vois Versionnage. | "2026-09-06" |
event | string | Toujours | ping, article.published, article.updated ou article.unpublished. | "article.published" |
deliveryId | string | Toujours | UUID v4. Une valeur par livraison, partagée par ses réessais. | "9d2f7c1e-…" |
sentAt | string | Toujours | ISO 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énement | Quand il se déclenche | Corps |
|---|---|---|
ping | Tu cliques sur Envoyer un événement de test dans la boîte de dialogue de connexion. | Enveloppe seule |
article.published | La première livraison réussie d'un article vers cette connexion webhook. | Enveloppe + l'article complet |
article.updated | Toute publication ultérieure du même article vers cette connexion, modifié ou non. | Enveloppe + l'article complet |
article.unpublished | L'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 :
{
"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 :
{
"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é
| Champ | Type | Présence | Format et garanties | Exemple |
|---|---|---|---|---|
article.id | string | Toujours | 25 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.language | string | Toujours | Deux 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.slug | string | Toujours | Minuscules, 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 utilisantarticle.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édevientque), 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.updatedpeut donc porter un nouveau slug sous le mêmeid. - 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
| Champ | Type | Présence | Format et garanties | Exemple |
|---|---|---|---|---|
article.title | string | Toujours | Unicode. 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.summary | string | Absent si non défini | Texte brut, un paragraphe. Pas de longueur maximale. | "Un guide pratique pour…" |
article.metaDescription | string | Absent si non défini | Texte 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.takeaways | string[] | Toujours, parfois vide | Phrases 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.keyword | string | Absent si non défini | Le 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.relatedQuestions | string[] | Toujours, parfois vide | Les 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 :
| Champ | Type | Présence | Format et garanties | Exemple |
|---|---|---|---|---|
url | string | Toujours | URL 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/…" |
altText | string | Absent si non défini | Texte d'accessibilité. | "Vue latérale d'un coureur en pleine foulée" |
caption | string | Absent si non défini | Lé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…" |
filename | string | Absent si non défini | Nom 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" |
sectionSlug | string | Absent si non défini | La 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" |
attribution | object | Absent 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.coverImageest absent si non défini. Il n'a jamais desectionSlug.article.imagesest 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.sectionSlugest 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
| Champ | Type | Présence | Format et garanties |
|---|---|---|---|
article.content.markdown | string | Toujours | L'article en markdown de type CommonMark, normalisé par GrowWriter. Vois ce qui survit. |
article.content.html | string | Toujours | HTML prêt à insérer, construit à partir de la même structure. Vois ce que contient le HTML. |
article.content.blocks | object[] | Toujours | L'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://…),. Les blocs sont séparés par des lignes vides. - Images :
, 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 enhttp(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(avecstartsi besoin),li,blockquote,pre,code(avecclass="language-…"),hr,figure,img,figcaption,a,strong,em,del, et unspanutilisé 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://ouhttps://; tout le reste devient du texte brut. - Les images sont rendues en
<figure><img src alt><figcaption>…</figcaption></figure>, sans lefigcaptionquand il n'y a ni légende ni attribution. Une image dont l'adresse n'est pas enhttp(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
| Champ | Type | Présence | Format et garanties | Exemple |
|---|---|---|---|---|
article.dates.createdAt | string | Toujours | ISO 8601, UTC, millisecondes. Moment où le brouillon a été créé dans GrowWriter. | "2026-08-28T14:00:00.000Z" |
article.dates.firstPublishedAt | string | Toujours | ISO 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.modifiedAt | string | Toujours | ISO 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,
firstPublishedAtetmodifiedAtsont égaux. modifiedAtn'est pas « dernière modification ». Modifier sans republier ne change rien ; republier sans modifier le fait avancer. Utilise l'eventpour 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
| Champ | Type | Présence | Format et garanties | Exemple |
|---|---|---|---|---|
article.brand | object | Absent si non défini | Présent quand l'article appartient à un kit de marque (le cas normal). | |
article.brand.name | string | Toujours dans brand | Le nom d'affichage de la marque. Pour le balisage éditeur/organisation, pas un auteur. | "Stride Lab" |
article.brand.websiteUrl | string | Absent si non défini | URL 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 :
| Bloc | Champs |
|---|---|
heading | level (1 à 6) et children (nœuds inline) |
paragraph | children (nœuds inline) |
list | ordered (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 |
blockquote | blocks : des blocs complets imbriqués dans la citation |
code | language optionnel, et code en une seule chaîne |
hr | rien d'autre ; un séparateur thématique |
image | src, 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.
Publier avec des webhooks
Envoie chaque article publié dans GrowWriter directement vers ton propre site web. Configure l'intégration webhook étape par étape, avec du code prêt à l'emploi.
Dépannage des webhooks
Résous un événement de test ou une livraison en échec par symptôme, avec les messages exacts que GrowWriter affiche, ce que chacun veut dire et quoi faire.