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.
Ton site web ne tourne peut-être sur aucune des plateformes que GrowWriter connecte nativement. Aucun souci. Avec l'intégration webhook, GrowWriter livre chaque article directement sur ton site au moment où tu cliques sur Publier. Stack maison, CMS sur mesure, site statique, peu importe. S'il peut recevoir une requête web, il peut recevoir tes articles.
Ce guide t'accompagne dans toute la configuration. Aucune expérience des webhooks n'est nécessaire. À la fin, publier un article dans GrowWriter créera ou mettra à jour un billet sur ton propre site automatiquement.
C'est quoi, un webhook ?
Un webhook est la façon la plus simple pour deux applications de se parler : quand quelque chose se passe dans l'une, elle envoie un message à une adresse web que tu choisis.
Imagine un service de livraison. Tu donnes ton adresse à GrowWriter (une URL de ton site web). Quand un article est prêt, GrowWriter frappe à cette porte et remet l'article complet sous forme de données structurées. Ton site fait le reste : l'enregistrer, le mettre en forme, le publier à ta façon.
Pourquoi c'est mieux que les alternatives :
- Zéro copier-coller. L'article arrive avec son titre, son contenu, ses images et ses champs SEO déjà organisés.
- Zéro vérification permanente. Ton site n'a pas besoin de demander « du nouveau ? » toutes les cinq minutes. GrowWriter n'appelle que lorsqu'il y a quelque chose à livrer.
- Zéro dépendance à une plateforme. Les webhooks parlent HTTPS et JSON purs, que toute stack web comprend.
Comment ça marche
Le flux complet se comprend en vingt secondes :
- Tu donnes à GrowWriter une URL de ton site, et GrowWriter te donne une clé secrète en échange.
- Quand tu publies un article, GrowWriter envoie une requête
POSTà ton URL. Le corps de la requête est l'article complet en JSON, et la requête est signée avec ton secret pour que tu puisses vérifier qu'elle vient bien de GrowWriter. - Ton site vérifie la signature, enregistre l'article et répond avec un
statut de succès (n'importe quel
2xx). C'est tout.
La configuration, étape par étape
Connecte le webhook dans GrowWriter
Ouvre la page Publier dans GrowWriter et repère la carte Webhook dans le groupe Automatisation. Clique sur Connecter.
Saisis ton URL de l'endpoint. C'est l'adresse de ton site web qui recevra
les articles, par exemple https://example.com/webhooks/growwriter. Deux
règles : elle doit commencer par https:// et être accessible depuis
internet (une adresse localhost ne fonctionnera pas).
L'endpoint n'existe pas encore ? Pas de problème. GrowWriter ne teste pas l'URL à ce stade, tu peux donc connecter d'abord et écrire le code juste après.
Clique sur Créer la connexion.

Copie ton secret de signature
La boîte de dialogue affiche maintenant ton Secret de signature. Il
commence par whsec_ et prouve qu'une requête vient réellement de
GrowWriter.
Clique sur Copier le secret et range-le en lieu sûr sur ton serveur, en général dans une variable d'environnement :
GROWWRITER_WEBHOOK_SECRET=whsec_ton_secret_iciCe secret ne s'affiche qu'une seule fois
GrowWriter le stocke chiffré et ne peut pas le réafficher. Si tu le perds, déconnecte puis reconnecte le webhook pour en obtenir un nouveau. Se reconnecter crée toujours un nouveau secret, et l'ancien cesse de fonctionner.

Construis ton récepteur
Ajoute maintenant l'endpoint à ton site web. Il doit faire quatre choses :
lire le corps brut de la requête, vérifier la signature, répondre vite avec
un statut 2xx et enregistrer l'article.
File vers les exemples de code plus bas et copie celui qui correspond à ta stack. Déploie-le à l'URL saisie à l'étape 1.
Envoie un événement de test
De retour dans la boîte de dialogue GrowWriter, clique sur Envoyer un
événement de test. GrowWriter envoie un petit ping signé à ton endpoint.
Il ressemble à ceci :
{
"version": "2026-08-01",
"event": "ping",
"deliveryId": "5f0c9a1e-4d2b-4f6a-9c3e-8b7d6a5e4f3c",
"sentAt": "2026-09-01T09:30:00.000Z"
}Remarque : il n'y a pas d'article dedans. C'est comme ça que ton code distingue un test d'une vraie livraison.
Quand ton endpoint répond avec un 2xx, la boîte de dialogue affiche
Événement de test livré. Clique sur Terminé et te voilà connecté. Si
tu vois Échec de l'événement de test à la place, consulte la
section de dépannage et réessaie. Un test raté ne casse jamais
la connexion, tu peux donc réessayer autant de fois que tu veux.
Publie un article
Ouvre un de tes articles terminés et publie-le. Choisis ton webhook comme
destination et GrowWriter livrera l'article complet à ton endpoint sous forme
d'événement article.published.
Publie le même article plus tard (après des modifications, par exemple) et
ton endpoint recevra un événement article.updated avec le même id
d'article. C'est cet id qui permet à ton site de mettre à jour le billet
existant au lieu de créer un doublon.
Exemples de code
Les deux exemples ci-dessous font le travail complet : vérifier la signature, répondre au ping de test, répondre vite et confier l'article à ta propre logique d'enregistrement.
Une règle compte plus que toutes les autres : vérifie la signature sur le corps brut de la requête, exactement tel qu'il est arrivé. Si ton framework parse le JSON d'abord et que tu le re-sérialises, les octets changent et la vérification de signature échoue.
import { createHmac, timingSafeEqual } from 'node:crypto';
// Âge maximal d'une livraison, en secondes, avant de la rejeter.
// Protège contre le rejeu d'une ancienne requête capturée.
const MAX_AGE_SECONDS = 5 * 60;
function verifySignature(rawBody: string, signatureHeader: string, secret: string) {
// Le header ressemble à : t=1756718400,v1=5257a869e7...
const parts = Object.fromEntries(
signatureHeader.split(',').map((part) => part.split('='))
);
// 1. Le timestamp doit être récent. GrowWriter re-signe chaque nouvelle
// tentative, une livraison fraîche porte donc toujours un timestamp frais.
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!(age < MAX_AGE_SECONDS)) {
return false;
}
// 2. Recalcule la signature : HMAC-SHA256 de "timestamp.corpsBrut"
// avec ton secret de signature, encodé en hexadécimal.
const expected = createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
// 3. Compare en temps constant pour que la vérification ne soit pas mesurable.
const provided = Buffer.from(parts.v1 ?? '', 'hex');
const wanted = Buffer.from(expected, 'hex');
return provided.length === wanted.length && timingSafeEqual(provided, wanted);
}
export async function POST(request: Request) {
// Lis le corps en texte brut EN PREMIER. N'utilise pas request.json()
// avant la vérification : la signature couvre exactement ces octets.
const rawBody = await request.text();
const signature = request.headers.get('x-growwriter-signature') ?? '';
const secret = process.env.GROWWRITER_WEBHOOK_SECRET ?? '';
if (!verifySignature(rawBody, signature, secret)) {
return new Response('Invalid signature', { status: 401 });
}
const payload = JSON.parse(rawBody);
// Le bouton « Envoyer un événement de test » envoie un ping sans article.
// Répondre 200 ici, c'est ce qui fait réussir le test.
if (payload.event === 'ping') {
return new Response('pong', { status: 200 });
}
// GrowWriter réessaie les livraisons échouées avec le MÊME id de livraison.
// Si tu as déjà traité cet id, réponds simplement OK à nouveau.
const deliveryId = request.headers.get('x-growwriter-delivery');
// if (await alreadyProcessed(deliveryId)) return new Response('OK');
const { article } = payload;
// Upsert sur article.id : "article.published" signifie qu'il est nouveau,
// "article.updated" signifie que tu as déjà vu cet id.
// Enregistre ce dont tu as besoin. article.content fournit le même article
// en markdown, en HTML et en blocs structurés. Choisis-en un.
await saveArticle({
externalId: article.id, // stable d'une republication à l'autre
title: article.title,
slug: article.slug,
html: article.content.html,
metaDescription: article.metaDescription,
publishedAt: article.dates.firstPublishedAt,
updatedAt: article.dates.modifiedAt,
});
// GrowWriter attend au maximum 10 secondes. Réponds vite ; fais le
// travail lourd (comme télécharger les images) en tâche de fond après.
return new Response('OK', { status: 200 });
}Ce que contient le payload
Chaque livraison est un POST avec Content-Type: application/json et trois
headers qui t'intéressent :
| Header | Ce qu'il t'apprend |
|---|---|
X-GrowWriter-Event | article.published, article.updated ou ping |
X-GrowWriter-Delivery | Id unique de cette livraison. Les réessais gardent le même id, utilise-le pour éviter de traiter deux fois |
X-GrowWriter-Signature | t=<timestamp>,v1=<signature> pour la vérification ci-dessus |
Voici un exemple raccourci du corps pour un vrai article :
{
"version": "2026-08-01",
"event": "article.published",
"deliveryId": "9d2f7c1e-6a3b-4c8d-b5e0-1f2a3b4c5d6e",
"sentAt": "2026-09-01T09:30:00.000Z",
"publishRecord": {
"firstPublishedAt": "2026-09-01T09:30:00.000Z",
"lastPublishedAt": "2026-09-01T09:30:00.000Z"
},
"article": {
"id": "cme8x2k9r0001l7042n5q8w3v",
"language": "fr",
"title": "Comment choisir des chaussures de running vraiment adaptées",
"slug": "comment-choisir-chaussures-running",
"metaDescription": "Apprends à choisir des chaussures de running adaptées à ton pied et à ta foulée, avec des vérifications simples à faire en magasin.",
"summary": "Un guide pratique pour trouver des chaussures de running...",
"keywords": {
"primary": "comment choisir chaussures running",
"secondary": ["chaussures running adaptées", "guide chaussures running"]
},
"takeaways": ["L'ajustement compte plus que la marque", "..."],
"coverImage": {
"url": "https://cdn.example.com/articleimages/.../cover.jpg",
"altText": "Coureur laçant ses chaussures sur un banc de parc"
},
"images": [
{
"url": "https://cdn.example.com/articleimages/.../gait-check.jpg",
"altText": "Vue latérale d'un coureur en pleine foulée",
"caption": "Un rapide contrôle de la foulée en dit plus que n'importe quelle fiche technique.",
"sectionSlug": "controle-ta-foulee"
}
],
"content": {
"markdown": "Trouver la bonne paire commence par...",
"html": "<p>Trouver la bonne paire commence par...</p>",
"blocks": [{ "type": "paragraph", "children": [{ "type": "text", "text": "..." }] }]
},
"dates": {
"createdAt": "2026-08-28T14:00:00.000Z",
"firstPublishedAt": "2026-09-01T09:30:00.000Z",
"modifiedAt": "2026-09-01T09:29:45.000Z"
},
"brand": {
"name": "Stride Lab",
"websiteUrl": "https://stridelab.example.com"
}
}
}Les champs qui font le gros du travail :
| Champ | Comment l'utiliser |
|---|---|
article.id | Ta clé stable. Garde-la, et mets à jour le billet existant quand le même id revient |
article.content | L'article en trois formats : markdown, html prêt à l'emploi et blocks structurés pour tes propres renderers. Prends celui qui convient à ton site |
article.title | Le titre. Il n'est pas répété dans le contenu, affiche-le donc toi-même |
article.metaDescription | 160 caractères maximum, écrit pour ta balise <meta name="description"> |
article.dates | Associe firstPublishedAt à ta date de publication et modifiedAt à ta date de mise à jour |
article.coverImage / article.images | La couverture plus chaque image du texte dans l'ordre, avec texte alternatif et légende quand ils existent |
Bon à savoir
- Télécharge les images. Les URLs d'images sont publiques et fonctionnent
au moment de la livraison, mais GrowWriter n'est pas un hébergeur d'images
à long terme. Enregistre chaque image (couverture et images du texte) dans
ton propre stockage, et n'oublie pas que les mêmes URLs apparaissent aussi
dans
content.htmletcontent.markdown: réécris-les là aussi. - Les livraisons se réessaient toutes seules. Si ton endpoint est
brièvement indisponible, GrowWriter réessaie jusqu'à 3 fois (en attendant 1
seconde, puis 3). Chaque tentative est signée à nouveau et garde le même id
X-GrowWriter-Delivery. - Tout
2xxcompte comme un succès. Les redirections, non. Si ton site répond301ou308, la livraison échoue : pointe le webhook directement vers l'URL finale. - Il n'y a pas d'événement de suppression. GrowWriter ne fait que créer et mettre à jour. Retirer un billet de ton site reste toujours ta décision.
- Une connexion, un secret. Déconnecter puis reconnecter le webhook crée un nouveau secret et retire l'ancien. Mets à jour ta variable d'environnement à ce moment-là.
Dépannage
« Ton endpoint n'a pas répondu à temps » : GrowWriter attend 10 secondes
par tentative. Si ton code télécharge des images ou appelle d'autres services
avant de répondre, déplace ce travail dans une tâche de fond et réponds 200
dès que l'article est enregistré.
« Ton endpoint a rejeté la livraison » : ton endpoint a répondu autre
chose qu'un 2xx. Causes fréquentes : la vérification de signature a échoué
parce que le corps a été parsé avant la vérification (utilise le corps brut),
le mauvais secret est configuré (compare-le avec celui de la boîte de
dialogue), ou l'URL redirige (utilise directement l'adresse finale).
« Échec de la livraison après plusieurs tentatives » : GrowWriter n'a pas
pu joindre ton endpoint du tout. Vérifie que l'URL est en ligne, utilise
https:// et est accessible publiquement. Les adresses de réseaux privés ou
localhost sont bloquées pour des raisons de sécurité. Pendant le
développement local, un service de tunnel peut donner à ton serveur une
adresse HTTPS publique temporaire.
Le test réussit mais les vrais articles échouent : le ping est minuscule ; un article complet peut peser quelques centaines de kilo-octets. Vérifie que ton serveur accepte des corps JSON de cette taille et que ta logique d'enregistrement gère le payload complet.
Toujours bloqué ? Écris-nous depuis l'application et on trouve la solution ensemble.