Docs

Démarrage rapide des webhooks

Connecte GrowWriter à ton propre site web et fais livrer ton premier événement de test signé, sur une seule page, avec un seul fichier de code.

Dernière mise à jour le

Cette page t'amène de rien à Événement de test livré. Une stack, un fichier, zéro théorie. Quand le test passe, le guide te montre comment enregistrer les articles qui suivent, et la référence du payload liste tout ce qu'ils contiennent.

Cette page est pour la personne qui peut ajouter un fichier à ton site web et le déployer. Si ce n'est pas toi, envoie-lui ce lien ; ça lui prend un seul fichier. Le code est pour Next.js, la configuration derrière la plupart des sites Vercel et Netlify. Les autres stacks sont dans le guide ; les étapes de cette page restent les mêmes.

Connecte le webhook dans GrowWriter

Ouvre la page Publier, repère la carte Webhook dans le groupe Automatisation et clique sur Connecter.

Saisis l'adresse à laquelle le code répondra. Pour le fichier ci-dessous, c'est le domaine de ton site suivi de /webhooks/growwriter, par exemple https://www.example.com/webhooks/growwriter. Utilise l'adresse exacte que ton site sert : s'il redirige example.com vers www.example.com, saisis celle en www. Clique sur Créer la connexion.

La boîte de dialogue affiche maintenant ton Secret de signature, une chaîne qui commence par whsec_. Clique sur Copier le secret. Il ne s'affiche qu'une seule fois, garde donc la boîte de dialogue ouverte ou colle-le en lieu sûr.

Ajoute le fichier récepteur

Crée ce fichier dans ton projet. Il lit chaque requête exactement telle qu'elle est arrivée, vérifie que GrowWriter l'a signée et répond à l'événement de test.

app/webhooks/growwriter/route.ts
import { createHmac, timingSafeEqual } from 'node:crypto';

// Une requête signée plus vieille que ceci est rejetée, une requête
// capturée ne peut donc pas être rejouée plus tard.
const MAX_AGE_SECONDS = 5 * 60;

function isSignedByGrowWriter(rawBody: string, header: string, secret: string) {
  // Le header ressemble à : t=1756718400,v1=5257a869e7...
  const parts = Object.fromEntries(
    header.split(',').map((part) => part.split('='))
  );
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (!(age < MAX_AGE_SECONDS)) {
    return false;
  }
  // Recalcule l'empreinte sur "timestamp.corps" avec ton secret ENTIER
  // (préfixe whsec_ inclus) et compare en temps constant.
  const expected = createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');
  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 d'abord. Le parser avant de vérifier la
  // signature change les octets et la vérification échoue.
  const rawBody = await request.text();
  const signature = request.headers.get('x-growwriter-signature') ?? '';
  const secret = process.env.GROWWRITER_WEBHOOK_SECRET ?? '';

  if (!isSignedByGrowWriter(rawBody, signature, secret)) {
    return new Response('Invalid signature', { status: 401 });
  }

  const payload = JSON.parse(rawBody);

  // « Envoyer un événement de test » envoie un ping sans article dedans.
  // Répondre 200, c'est ce qui fait réussir le test.
  if (payload.event === 'ping') {
    return new Response('pong');
  }

  // Un vrai article (ou sa suppression : article.unpublished ne porte que
  // l'id). Pour l'instant, prouve juste qu'il est arrivé ; le guide montre
  // comment l'enregistrer. Une fois enregistré, Response.json({ url }) avec
  // l'URL publique du billet donne un lien « Voir l'article » à la boîte de
  // dialogue de publication.
  console.log(`${payload.event}: ${payload.article.id}`);
  return new Response('OK');
}

Donne le secret à ton site et déploie

Ton code lit le secret depuis une variable d'environnement : un réglage que ton hébergeur garde en dehors de ton code. Dans le tableau de bord de ton hébergeur (Vercel : Settings, puis Environment Variables), ajoute :

Variable d'environnement
GROWWRITER_WEBHOOK_SECRET=whsec_colle_ton_secret_ici

Colle la valeur entière, whsec_ inclus. Puis déploie ton site pour que le nouveau fichier et la nouvelle variable soient en ligne.

Envoie l'événement de test

De retour dans la boîte de dialogue GrowWriter, clique sur Envoyer un événement de test. GrowWriter signe un petit ping et le poste à ton adresse. Quand ton fichier répond, la boîte de dialogue affiche Événement de test livré. Clique sur Terminé.

Si ça échoue, la boîte de dialogue nomme la raison. Les deux que tu as le plus de chances de voir :

  • L'URL de ton endpoint redirige. Saisis l'adresse vers laquelle elle redirige et réessaie : ton site répond à une autre adresse (en général www contre sans www), et le message affiche laquelle utiliser. Clique sur Modifier l'URL sur la ligne du webhook pour la changer.
  • Ton endpoint a rejeté la livraison : avec un 401, le secret chez ton hébergeur ne correspond pas ; avec un 404, le fichier n'est pas à l'adresse que tu as saisie.

Tout le reste est sur la page de dépannage.

Prouve que la vérification fonctionne

Facultatif, mais ça vaut une minute. Ce script signe un ping exactement comme GrowWriter le fait et l'envoie à ton site :

send-test-ping.sh
SECRET="whsec_colle_ton_secret_ici"
URL="https://www.example.com/webhooks/growwriter"
BODY='{"version":"2026-09-06","event":"ping","deliveryId":"9d2f7c1e-6a3b-4c8d-b5e0-1f2a3b4c5d6e","sentAt":"2026-09-01T09:30:00.000Z"}'

TIMESTAMP=$(date +%s)
SIGNATURE=$(printf '%s.%s' "$TIMESTAMP" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')

curl -X POST "$URL" \
  -H "Content-Type: application/json" \
  -H "X-GrowWriter-Event: ping" \
  -H "X-GrowWriter-Delivery: 9d2f7c1e-6a3b-4c8d-b5e0-1f2a3b4c5d6e" \
  -H "X-GrowWriter-Signature: t=$TIMESTAMP,v1=$SIGNATURE" \
  --data "$BODY"

Tu dois obtenir pong. Maintenant, change un seul caractère dans BODY et relance : tu dois obtenir un 401. C'est ta vérification de signature qui refuse une requête modifiée, et c'est tout l'intérêt.

Ce que tu as maintenant

Un endpoint public qui n'accepte que les requêtes signées par GrowWriter, et une connexion qui affiche Connecté sur la page Canaux. Publier un article le livrera à ce fichier sous forme d'événement article.published, et la ligne console.log affichera son id dans les logs de ton hébergeur.

Ensuite : enregistre l'article

Le fichier ci-dessus se contente de logger l'article. Pour le stocker, lis Enregistre l'article dans ton CMS dans le guide : six règles courtes sur les ids, les dates, l'état publié, les mots-clés, les images et l'échappement, chacune apprise d'un vrai récepteur qui s'était trompé. Les exemples complets de récepteurs qui s'y trouvent appliquent les six.

Tu te demandes pourquoi la signature et le secret existent ? Le guide l'explique en quatre étapes simples.

Sur cette page