Docs

Pubblicare con i webhook

Invia ogni articolo che pubblichi in GrowWriter direttamente al tuo sito web. Configura l'integrazione webhook passo dopo passo, con codice pronto all'uso.

Il tuo sito web magari non gira su nessuna delle piattaforme che GrowWriter collega di serie. Nessun problema. Con l'integrazione webhook, GrowWriter consegna ogni articolo direttamente al tuo sito nel momento in cui premi Pubblica. Stack personalizzato, CMS fatto in casa, sito statico, qualsiasi cosa. Se può ricevere una richiesta web, può ricevere i tuoi articoli.

Questa guida ti accompagna in tutta la configurazione. Non serve esperienza con i webhook. Alla fine, pubblicare un articolo in GrowWriter creerà o aggiornerà un post sul tuo sito web in automatico.

Che cos'è un webhook?

Un webhook è il modo più semplice che due applicazioni hanno per parlarsi: quando succede qualcosa in una, questa invia un messaggio a un indirizzo web che scegli tu.

Pensa a un servizio di consegne. Dai a GrowWriter il tuo indirizzo (un URL del tuo sito web). Quando un articolo è pronto, GrowWriter bussa a quella porta e consegna l'articolo completo come dati strutturati. Il tuo sito fa il resto: salvarlo, formattarlo, pubblicarlo a modo tuo.

Perché batte le alternative:

  • Niente copia e incolla. L'articolo arriva con titolo, contenuto, immagini e campi SEO già organizzati.
  • Niente controlli continui. Il tuo sito non deve chiedere "c'è qualcosa di nuovo?" ogni pochi minuti. GrowWriter chiama solo quando c'è qualcosa da consegnare.
  • Niente vincoli di piattaforma. I webhook parlano HTTPS e JSON puri, che ogni stack web capisce.

Come funziona

L'intero flusso si capisce in venti secondi:

  1. Dai a GrowWriter un URL del tuo sito, e GrowWriter ti dà in cambio una chiave segreta.
  2. Quando pubblichi un articolo, GrowWriter invia una richiesta POST al tuo URL. Il corpo della richiesta è l'articolo completo in JSON, e la richiesta è firmata con il tuo secret così puoi essere certo che arrivi davvero da GrowWriter.
  3. Il tuo sito verifica la firma, salva l'articolo e risponde con uno stato di successo (un 2xx qualsiasi). Fatto.
Diagramma di flusso in tre passaggi: un clic su Pubblica in GrowWriter invia una richiesta JSON firmata che il tuo sito web riceve e salva come post pubblicato.
Un clic in GrowWriter, una richiesta al tuo sito, un nuovo post.

Configurazione passo dopo passo

Connetti il webhook in GrowWriter

Apri la pagina Pubblica in GrowWriter e cerca la scheda Webhook nel gruppo Automazione. Fai clic su Connetti.

Inserisci il tuo URL dell'endpoint. È l'indirizzo del tuo sito web che riceverà gli articoli, per esempio https://example.com/webhooks/growwriter. Due regole: deve iniziare con https:// e deve essere raggiungibile da internet (un indirizzo localhost non funzionerà).

Non hai ancora costruito l'endpoint? Nessun problema. GrowWriter non verifica l'URL in questa fase, quindi puoi connettere prima e scrivere il codice subito dopo.

Fai clic su Crea connessione.

La pagina Pubblica di GrowWriter con la scheda Webhook nel gruppo Automazione e il suo pulsante Connetti.
La scheda Webhook si trova sotto Automazione, nella pagina Pubblica.

Copia il tuo secret di firma

La finestra ora mostra il tuo Secret di firma. Inizia con whsec_ e dimostra che una richiesta arriva davvero da GrowWriter.

Fai clic su Copia secret e conservalo in un posto sicuro sul tuo server, di solito come variabile d'ambiente:

.env
GROWWRITER_WEBHOOK_SECRET=whsec_il_tuo_secret_qui

Questo secret viene mostrato una sola volta

GrowWriter lo conserva cifrato e non può mostrarlo di nuovo. Se lo perdi, disconnetti e riconnetti il webhook per ottenerne uno nuovo. Riconnettersi crea sempre un secret nuovo, e quello vecchio smette di funzionare.

Passaggio 2 della finestra Connetti webhook, con il campo Secret di firma e i pulsanti Copia secret e Invia evento di prova.
Copia il secret prima di chiudere la finestra. Viene mostrato una sola volta.

Costruisci il tuo ricevitore

Ora aggiungi l'endpoint al tuo sito web. Deve fare quattro cose: leggere il corpo grezzo della richiesta, verificare la firma, rispondere in fretta con uno stato 2xx e salvare l'articolo.

Vai agli esempi di codice qui sotto e copia quello che corrisponde al tuo stack. Distribuiscilo all'URL inserito nel passaggio 1.

Invia un evento di prova

Torna nella finestra di GrowWriter e fai clic su Invia evento di prova. GrowWriter invia un piccolo ping firmato al tuo endpoint. Ha questo aspetto:

Payload dell'evento di prova
{
  "version": "2026-08-01",
  "event": "ping",
  "deliveryId": "5f0c9a1e-4d2b-4f6a-9c3e-8b7d6a5e4f3c",
  "sentAt": "2026-09-01T09:30:00.000Z"
}

Nota che non contiene nessun articolo. È così che il tuo codice distingue una prova da una consegna vera.

Quando il tuo endpoint risponde con un 2xx, la finestra mostra Evento di prova consegnato. Fai clic su Fatto e sei connesso. Se invece vedi Evento di prova non riuscito, controlla la sezione risoluzione dei problemi e riprova. Una prova fallita non rompe mai la connessione, quindi puoi riprovare tutte le volte che vuoi.

Pubblica un articolo

Apri uno dei tuoi articoli finiti e pubblicalo. Scegli il tuo webhook come destinazione e GrowWriter consegnerà l'articolo completo al tuo endpoint come evento article.published.

Se ripubblichi lo stesso articolo più avanti (dopo una modifica, per esempio), il tuo endpoint riceverà un evento article.updated con lo stesso id dell'articolo. È quell'id che permette al tuo sito di aggiornare il post esistente invece di crearne un duplicato.

Esempi di codice

I due esempi qui sotto fanno il lavoro completo: verificano la firma, rispondono al ping di prova, rispondono in fretta e passano l'articolo alla tua logica di salvataggio.

Una regola conta più di tutte le altre: verifica la firma sul corpo grezzo della richiesta, esattamente com'è arrivato. Se il tuo framework fa il parsing del JSON prima e tu lo ri-serializzi, i byte cambiano e la verifica della firma fallisce.

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

// Età massima in secondi di una consegna prima di rifiutarla.
// Protegge dal replay di una vecchia richiesta catturata.
const MAX_AGE_SECONDS = 5 * 60;

function verifySignature(rawBody: string, signatureHeader: string, secret: string) {
  // L'header ha questa forma: t=1756718400,v1=5257a869e7...
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((part) => part.split('='))
  );

  // 1. Il timestamp deve essere recente. GrowWriter firma di nuovo ogni
  //    nuovo tentativo, quindi una consegna fresca porta sempre un
  //    timestamp fresco.
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (!(age < MAX_AGE_SECONDS)) {
    return false;
  }

  // 2. Ricalcola la firma: HMAC-SHA256 di "timestamp.corpoGrezzo"
  //    con il tuo secret di firma, in esadecimale.
  const expected = createHmac('sha256', secret)
    .update(`${parts.t}.${rawBody}`)
    .digest('hex');

  // 3. Confronta in tempo costante così il controllo non è misurabile.
  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) {
  // Leggi il corpo come testo grezzo PRIMA. Non usare request.json()
  // prima della verifica: la firma copre esattamente questi byte.
  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);

  // Il pulsante "Invia evento di prova" manda un ping senza articolo.
  // Rispondere 200 qui è ciò che fa riuscire la prova.
  if (payload.event === 'ping') {
    return new Response('pong', { status: 200 });
  }

  // GrowWriter ritenta le consegne fallite con lo STESSO id di consegna.
  // Se hai già elaborato questo id, rispondi semplicemente OK di nuovo.
  const deliveryId = request.headers.get('x-growwriter-delivery');
  // if (await alreadyProcessed(deliveryId)) return new Response('OK');

  const { article } = payload;

  // Upsert su article.id: "article.published" significa che è nuovo,
  // "article.updated" significa che hai già visto questo id.
  // Salva ciò che ti serve. article.content ti dà lo stesso articolo
  // come markdown, come HTML e come blocchi strutturati. Scegline uno.
  await saveArticle({
    externalId: article.id, // stabile tra le ripubblicazioni
    title: article.title,
    slug: article.slug,
    html: article.content.html,
    metaDescription: article.metaDescription,
    publishedAt: article.dates.firstPublishedAt,
    updatedAt: article.dates.modifiedAt,
  });

  // GrowWriter aspetta al massimo 10 secondi. Rispondi in fretta; fai il
  // lavoro pesante (come scaricare le immagini) in background dopo.
  return new Response('OK', { status: 200 });
}

Cosa contiene il payload

Ogni consegna è un POST con Content-Type: application/json e tre header che ti interessano:

HeaderCosa ti dice
X-GrowWriter-Eventarticle.published, article.updated oppure ping
X-GrowWriter-DeliveryId unico di questa consegna. I nuovi tentativi mantengono lo stesso id, usalo per non elaborare due volte
X-GrowWriter-Signaturet=<timestamp>,v1=<firma> per la verifica mostrata sopra

Ecco un esempio ridotto del corpo per un articolo vero:

Payload di article.published (ridotto)
{
  "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": "it",
    "title": "Come scegliere scarpe da running che calzano davvero bene",
    "slug": "come-scegliere-scarpe-da-running",
    "metaDescription": "Impara a scegliere scarpe da running adatte al tuo piede e alla tua falcata, con controlli semplici da fare in qualsiasi negozio.",
    "summary": "Una guida pratica per trovare scarpe da running...",
    "keywords": {
      "primary": "come scegliere scarpe da running",
      "secondary": ["calzata scarpe running", "guida scarpe running"]
    },
    "takeaways": ["La calzata batte la marca, sempre", "..."],
    "coverImage": {
      "url": "https://cdn.example.com/articleimages/.../cover.jpg",
      "altText": "Runner che si allaccia le scarpe su una panchina del parco"
    },
    "images": [
      {
        "url": "https://cdn.example.com/articleimages/.../gait-check.jpg",
        "altText": "Vista laterale di un runner a metà falcata",
        "caption": "Un rapido controllo della falcata dice più di qualsiasi scheda tecnica.",
        "sectionSlug": "controlla-la-tua-falcata"
      }
    ],
    "content": {
      "markdown": "Trovare il paio giusto comincia da...",
      "html": "<p>Trovare il paio giusto comincia da...</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"
    }
  }
}

I campi che fanno il grosso del lavoro:

CampoCome usarlo
article.idLa tua chiave stabile. Conservala e aggiorna il post esistente quando torna lo stesso id
article.contentL'articolo in tre formati: markdown, html pronto all'uso e blocks strutturati per renderer personalizzati. Usa quello adatto al tuo sito
article.titleIl titolo. Non si ripete dentro il contenuto, quindi rendilo tu
article.metaDescriptionAl massimo 160 caratteri, pensato per il tuo tag <meta name="description">
article.datesCollega firstPublishedAt alla tua data di pubblicazione e modifiedAt a quella di aggiornamento
article.coverImage / article.imagesLa copertina più ogni immagine del testo in ordine, con testo alternativo e didascalie quando disponibili

Buono a sapersi

  • Scarica le immagini. Gli URL delle immagini sono pubblici e funzionano al momento della consegna, ma GrowWriter non è un hosting di immagini a lungo termine. Salva ogni immagine (copertina e immagini nel testo) nel tuo storage, e ricorda che gli stessi URL compaiono anche dentro content.html e content.markdown, quindi riscrivili anche lì.
  • Le consegne si ritentano da sole. Se il tuo endpoint è giù per un attimo, GrowWriter riprova fino a 3 volte (aspettando 1 secondo, poi 3). Ogni tentativo viene firmato di nuovo e mantiene lo stesso id X-GrowWriter-Delivery.
  • Qualsiasi 2xx conta come successo. I redirect no. Se il tuo sito risponde 301 o 308, la consegna fallisce: punta il webhook direttamente all'URL finale.
  • Non esiste un evento di eliminazione. GrowWriter crea e aggiorna soltanto. Rimuovere un post dal tuo sito resta sempre una tua scelta.
  • Una connessione, un secret. Disconnettere e riconnettere il webhook crea un secret nuovo e ritira quello vecchio. Aggiorna la tua variabile d'ambiente quando lo fai.

Risoluzione dei problemi

"Il tuo endpoint non ha risposto in tempo": GrowWriter aspetta 10 secondi per tentativo. Se il tuo codice scarica immagini o chiama altri servizi prima di rispondere, sposta quel lavoro in un processo in background e rispondi 200 appena l'articolo è salvato.

"Il tuo endpoint ha rifiutato la consegna": il tuo endpoint ha risposto con qualcosa di diverso da un 2xx. Cause comuni: la verifica della firma è fallita perché il corpo è stato parsato prima della verifica (usa il corpo grezzo), è configurato il secret sbagliato (confrontalo con quello della finestra) oppure l'URL fa redirect (usa direttamente l'indirizzo finale).

"Consegna non riuscita dopo diversi tentativi": GrowWriter non è riuscito a raggiungere il tuo endpoint. Controlla che l'URL sia attivo, usi https:// e sia raggiungibile pubblicamente. Gli indirizzi di reti private o localhost sono bloccati per sicurezza. Durante lo sviluppo in locale, un servizio di tunnel può dare al tuo server un indirizzo HTTPS pubblico temporaneo.

La prova funziona ma gli articoli veri falliscono: il ping è minuscolo; un articolo completo può pesare qualche centinaio di kilobyte. Controlla che il tuo server accetti corpi JSON di quella dimensione e che la tua logica di salvataggio gestisca il payload completo.

Ancora bloccato? Scrivici dall'app e lo risolviamo insieme.

In questa pagina