Docs

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 :

  1. Tu donnes à GrowWriter une URL de ton site, et GrowWriter te donne une clé secrète en échange.
  2. 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.
  3. 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.
Schéma en trois étapes : un clic sur Publier dans GrowWriter envoie une requête JSON signée que ton site web reçoit et enregistre comme billet publié.
Un clic dans GrowWriter, une requête vers ton site, un nouveau billet.

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.

La page Publier de GrowWriter avec la carte Webhook dans le groupe Automatisation et son bouton Connecter.
La carte Webhook se trouve sous Automatisation, sur la page Publier.

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 :

.env
GROWWRITER_WEBHOOK_SECRET=whsec_ton_secret_ici

Ce 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.

Étape 2 de la boîte de dialogue Connecter un webhook, avec le champ Secret de signature et les boutons Copier le secret et Envoyer un événement de test.
Copie le secret avant de fermer la boîte de dialogue. Il ne s'affiche qu'une fois.

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 :

Payload de l'événement de test
{
  "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.

app/webhooks/growwriter/route.ts
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 :

HeaderCe qu'il t'apprend
X-GrowWriter-Eventarticle.published, article.updated ou ping
X-GrowWriter-DeliveryId unique de cette livraison. Les réessais gardent le même id, utilise-le pour éviter de traiter deux fois
X-GrowWriter-Signaturet=<timestamp>,v1=<signature> pour la vérification ci-dessus

Voici un exemple raccourci du corps pour un vrai article :

Payload article.published (raccourci)
{
  "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 :

ChampComment l'utiliser
article.idTa clé stable. Garde-la, et mets à jour le billet existant quand le même id revient
article.contentL'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.titleLe titre. Il n'est pas répété dans le contenu, affiche-le donc toi-même
article.metaDescription160 caractères maximum, écrit pour ta balise <meta name="description">
article.datesAssocie firstPublishedAt à ta date de publication et modifiedAt à ta date de mise à jour
article.coverImage / article.imagesLa 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.html et content.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 2xx compte comme un succès. Les redirections, non. Si ton site répond 301 ou 308, 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.

Sur cette page