Vai al contenuto

Guida

Collega qualsiasi sito web tramite webhook

Non usi WordPress? Nessun problema. BlogTend invia ogni articolo completato tramite POST come JSON firmato a un endpoint sotto il tuo controllo — qualsiasi stack, qualsiasi CMS, circa venti righe di codice da parte tua.

Cos'è il connettore webhook

Quando la generazione di un articolo termina — o arriva il momento programmato — BlogTend lo invia al tuo endpoint con un'unica richiesta POST in formato JSON: titolo, HTML completo, estratto, metadati SEO, nomi di categorie e tag, lingua, fonti, e un URL pubblico per l'immagine in evidenza. Il tuo endpoint lo salva secondo le modalità di archiviazione dei contenuti del tuo sito, e tutto il resto in BlogTend funziona esattamente come per i siti WordPress: automazioni, programmazione, bozze, pannello di controllo, indicizzazione su Google.

Ogni richiesta è firmata con un segreto che solo tu e BlogTend conoscete, così il tuo endpoint può verificare che l'articolo provenga davvero da noi e non sia stato alterato.

Cosa deve fare il tuo endpoint

  • Accetta una richiesta POST HTTPS con un corpo JSON a un URL pubblico.
  • Rispondi entro 15 secondi con un codice di stato 2xx. Esegui le operazioni lente (scaricamento delle immagini, ricostruzione della cache) dopo aver risposto, non prima.
  • Rispondi a ogni ping con un codice 2xx. Un ping non contiene articoli e non deve scrivere nulla, quindi puoi rispondere prima di verificarlo (o senza verificarlo): è questo che consente a Testa & connetti di andare a buon fine mentre il tuo segreto è ancora in fase di distribuzione.
  • Verifica X-BlogTend-Signature per ogni evento relativo a un articolo (codice qui sotto), e rispondi con 401 se non corrisponde. Non lo imponiamo, ma senza questa verifica chiunque trovi l'URL potrebbe inviare articoli falsi al tuo sito.
  • Usa article.id come chiave di idempotenza: se esiste già un articolo con quell'id, aggiornalo invece di creare un duplicato.
  • Quando rifiuti una richiesta, indica il motivo nel corpo (ad esempio {"error": "invalid signature"}). Mostriamo i primi 300 caratteri nel tuo registro delle consegne.

Come collegarlo

  1. Metti in produzione il tuo endpoint di ricezione (l'esempio in fondo è completo).
  2. In BlogTend, vai su Siti → Collega un sito → Webhook, inserisci un nome e l'URL dell'endpoint, e premi Avanti.
  3. Mostriamo la chiave segreta di firma whsec_…. Copiala subito nell'ambiente del tuo server e distribuisci l'applicazione: questa schermata è l'unico posto in cui viene mostrata.
  4. Premi Testa & connetti. Inviamo un ping firmato con quel segreto; una risposta 2xx salva la connessione. Se il tuo endpoint lo rifiuta, mostriamo il suo codice di stato e il suo messaggio di errore per permetterti di risolvere il problema e premere di nuovo Testa & connetti.

Collegare di nuovo lo stesso URL (per rinominare il sito, o dopo un errore) non cambia mai il segreto: la finestra di dialogo mostra quello attuale e il ping di prova viene firmato con quel segreto. Per ottenere un nuovo segreto, ruotalo.

Intestazioni inviate

Ogni richiesta, ping o articolo, include queste intestazioni:

IntestazioneValoreNote
X-BlogTend-Signaturet=<unix seconds>,v1=<hex>[,v1=<hex>]Firma HMAC-SHA256. Un v1 per ogni segreto attivo: due solo durante il periodo di tolleranza di 24 ore previsto per la rotazione, dal più recente.
X-BlogTend-Eventping | article.published | article.updatedUguale al campo event nel corpo.
X-BlogTend-Deliverywhd_…Uguale a delivery_id nel corpo. Nuovo a ogni tentativo, inclusi i tentativi successivi.
X-BlogTend-Test1Solo per i ping (Testa & connetti, Verifica di nuovo, aggiornamento delle categorie). Un ping non contiene articoli e non deve scrivere nulla.
Content-Typeapplication/jsonIl corpo è in formato JSON con codifica UTF-8.
User-AgentBlogTend-Webhook/1Si chiamava YoDon-Webhook/1 prima del cambio di nome.
X-Yodon-Signature, X-Yodon-Event, X-Yodon-DeliverydeprecatedInviati per i ricevitori creati prima del cambio di nome, con gli stessi valori, tranne che X-Yodon-Signature contiene sempre esattamente una firma v1, generata con il segreto più recente. I nuovi ricevitori dovrebbero leggere le intestazioni X-BlogTend-*.

Le intestazioni X-Yodon-* sono deprecate. Continuano a funzionare per i ricevitori creati prima del cambio di nome, ma solo X-BlogTend-Signature contiene entrambe le firme durante una rotazione, quindi passa ai nuovi nomi la prossima volta che modifichi il tuo ricevitore.

Il payload, campo per campo

Arrivano tre tipi di evento, distinti dal campo event e dall'intestazione X-BlogTend-Event: ping (test di connessione, nessun articolo), article.published (primo invio di un articolo) e article.updated (lo stesso articolo inviato di nuovo: dopo un nuovo tentativo che hai avviato, la pubblicazione di una bozza, o un aggiornamento di un articolo già online, nel qual caso url indica l'articolo online da sostituire e id è l'id che hai memorizzato per quell'articolo). La struttura completa:

{
  "event": "article.published",
  "delivery_id": "whd_5f0c9c1e-…",
  "site_id": "d2a41c3e-…",
  "sent_at": "2026-08-28T15:04:05.000Z",
  "article": {
    "id": "a81f6a02-…",
    "title": "How to Choose a Standing Desk",
    "slug": "how-to-choose-a-standing-desk",
    "status": "publish",
    "html": "<h2>…</h2><p>…</p>",
    "excerpt": "A practical buyer's guide…",
    "seo": {
      "title": "Standing Desk Buyer's Guide (2026)",
      "description": "Everything to check before…",
      "keyword": "standing desk"
    },
    "category": "Office Setup",
    "categories": ["Office Setup", "Buying Guides"],
    "tags": ["desks", "ergonomics"],
    "language": "en",
    "featured_image": {
      "url": "https://www.blogtend.com/api/images/…?sig=…",
      "mime": "image/webp"
    },
    "sources": [{ "title": "OSHA guidance", "url": "https://…" }],
    "published_at": "2026-08-28T15:04:05.000Z",
    "updated_at": "2026-08-28T15:04:05.000Z",
    "url": "https://example.com/blog/how-to-choose-a-standing-desk"
  }
}
  • article.status è lo stato che richiediamo: "publish" o "draft". Gli articoli programmati arrivano all'orario previsto con "publish".
  • article.html è il corpo completo dell'articolo. I riferimenti alle immagini al suo interno puntano già a URL pubblici.
  • category e categories sono nomi, non identificativi — associali alla tua tassonomia, oppure ignorali.
  • featured_image è null quando l'articolo non ha un'immagine. L'URL è firmato e stabile — puoi recuperarlo in qualsiasi momento.
  • delivery_id identifica questo singolo tentativo e cambia a ogni nuovo tentativo; article.id identifica l'articolo e non cambia mai. Usa article.id per eliminare i duplicati.
  • Il corpo di un ping contiene solo event, delivery_id, site_id e sent_at; nessun articolo.

Verifica della firma

Ogni richiesta include X-BlogTend-Signature: t=<unix seconds>,v1=<hex>. Ogni valore v1 è un HMAC-SHA256 della stringa "<t>.<raw body>" calcolato usando un segreto whsec_ come chiave. Verifica la firma sul corpo grezzo della richiesta, prima di qualsiasi analisi JSON, rifiuta i timestamp con uno scarto superiore a 5 minuti, confronta i valori con una funzione a tempo costante e accetta la richiesta se almeno un valore v1 corrisponde al tuo segreto: durante una rotazione ce ne sono due.

Node.js

import crypto from "crypto";

// header: the X-BlogTend-Signature value, e.g. "t=1767225600,v1=ab12…,v1=cd34…".
// During a secret rotation it holds one v1 per live secret: accept if ANY matches.
export function verifyBlogTendSignature(secret, header, rawBody) {
  const parts = (header || "").split(",").map((p) => p.trim());
  const t = parts.find((p) => p.startsWith("t="))?.slice(2);
  // Reject anything older than 5 minutes: replay protection.
  if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = Buffer.from(
    crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex")
  );
  return parts
    .filter((p) => p.startsWith("v1="))
    .map((p) => Buffer.from(p.slice(3)))
    .some((sig) => sig.length === expected.length && crypto.timingSafeEqual(sig, expected));
}

PHP

function verify_blogtend_signature(string $secret, string $header, string $rawBody): bool {
    $t = null;
    $sigs = [];
    foreach (explode(',', $header) as $part) {
        [$k, $v] = array_pad(explode('=', trim($part), 2), 2, '');
        if ($k === 't') $t = $v;
        if ($k === 'v1') $sigs[] = $v;                      // one per live secret
    }
    if (!ctype_digit((string) $t) || abs(time() - (int) $t) > 300) return false; // replay window
    $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
    foreach ($sigs as $sig) {
        if (hash_equals($expected, $sig)) return true;      // timing-safe
    }
    return false;
}

Python

import hashlib, hmac, time

def verify_blogtend_signature(secret: str, header: str, raw_body: bytes) -> bool:
    parts = [p.strip().split("=", 1) for p in (header or "").split(",") if "=" in p]
    t = next((v for k, v in parts if k == "t"), "")
    if not t.isdigit() or abs(time.time() - int(t)) > 300:   # replay window
        return False
    expected = hmac.new(
        secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    # One v1 per live secret during a rotation: accept if any matches.
    return any(hmac.compare_digest(expected, v) for k, v in parts if k == "v1")

Un ricevitore meno recente che legge X-Yodon-Signature con un modello esatto che prevede un solo v1 continua a funzionare: quell'intestazione contiene ancora esattamente un v1, generato con il segreto più recente.

Rotazione del segreto

Hai perso il segreto, o lo sostituisci periodicamente? Nella pagina Siti, apri la scheda del sito collegato tramite webhook e premi Sostituisci segreto. Il nuovo segreto viene mostrato una sola volta.

  • Per le prossime 24 ore ogni richiesta include due firme in X-BlogTend-Signature: una generata con il nuovo segreto, una con quello vecchio. Un destinatario che dispone di uno dei due segreti può verificare la firma, quindi tutto continua a funzionare mentre aggiorni il tuo server.
  • Dopo 24 ore viene usata per firmare solo la nuova chiave segreta. La scheda indica quando la vecchia smette di essere usata.
  • Se il vecchio segreto è stato esposto, seleziona "disattivalo subito" quando lo sostituisci: non è previsto alcun periodo di tolleranza, e gli invii falliscono con codice 401 finché il tuo server non ha il nuovo segreto.
  • I destinatari che leggono ancora l'intestazione deprecata X-Yodon-Signature vedono solo la firma del nuovo segreto, quindi per loro il passaggio è immediato.

Risposta da inviare

Qualsiasi codice 2xx indica una consegna riuscita; il corpo della risposta è facoltativo e può essere vuoto. Rispondi con JSON e BlogTend diventa più intelligente:

{ "ok": true, "id": "123", "url": "https://example.com/blog/my-post" }
  • url — memorizzato come collegamento all'articolo pubblicato: il pulsante Visualizza nel pannello di controllo lo apre, e la funzione di indicizzazione Google lo invia.
  • id — l'identificatore del tuo articolo, memorizzato per consentirti di associare anche nel tuo sistema i futuri eventi article.updated.
  • held — una breve motivazione se hai accettato l'articolo ma non l'hai pubblicato sul tuo sito (una coda editoriale, una regola interna). BlogTend lo mostra quindi come bozza con quella motivazione anziché come articolo pubblicato, e non invia l'email "pubblicato". Rispondere con status: "draft" produce lo stesso risultato senza una motivazione.

Anche per un ping basta qualsiasi risposta 2xx. L'unico campo che leggiamo dalla risposta a un ping è categories: rispondi con { ok: true, categories: ["Guide", "Notizie"] } (un array di massimo 100 nomi) e quei nomi appariranno come categorie effettivamente selezionabili nei moduli di creazione di BlogTend. L'elenco viene aggiornato a ogni nuova verifica e tramite il pulsante di aggiornamento del selettore delle categorie. Se ometti il campo, l'IA si limita invece a proporre nomi di categorie.

Errori, nuovi tentativi e duplicati

Come BlogTend interpreta ogni risposta alla consegna di un articolo:

La tua rispostaCosa significa per noiReinviato?
2xxConsegnato. Il corpo è facoltativo; un corpo JSON viene letto come descritto in Cosa rispondere.Fatto.
3xxOperazione non riuscita. I reindirizzamenti non vengono seguiti: collega invece l'URL finale.Non reinviato immediatamente.
401 / 403Non riuscito: indicato come "firma rifiutata". Il segreto sul tuo server non corrisponde. Incollalo di nuovo o ruotalo.Non reinviato immediatamente.
503Operazione non riuscita: viene mostrato "non ancora configurato", la risposta tipica di un ricevitore la cui chiave segreta non è impostata.Reinviato una sola volta, immediatamente.
Altri 4xxNon riuscito: hai compreso la richiesta e l'hai rifiutata.Non reinviato immediatamente.
Altri 5xxNon riuscito: il tuo endpoint si è arrestato in modo anomalo o non è disponibile.Reinviato una sola volta, immediatamente.
Nessuna risposta entro 15 s, o un errore di reteOperazione non riuscita: viene mostrato "impossibile raggiungere il tuo endpoint".Reinviato una sola volta, immediatamente.
  • Articoli che BlogTend pubblica autonomamente (automazioni e contenuti programmati): se la consegna continua a fallire, l'intero articolo viene inviato di nuovo nelle due esecuzioni successive, a circa un minuto di distanza, qualunque sia stata la risposta. Dopo il terzo tentativo fallito BlogTend si ferma, conserva l'articolo completato e ti invia il motivo via email. Pubblicalo di nuovo dopo aver risolto il problema del tuo endpoint.
  • Articoli che pubblichi manualmente: nessun nuovo tentativo automatico. Il motivo appare subito sull'articolo; premi di nuovo Pubblica, oppure Riprova in Invii recenti.
  • Un articolo non va mai perso: se la pubblicazione fallisce, resta in BlogTend con il motivo, pronto per essere pubblicato di nuovo.
  • Dove guardare: nella pagina Siti, la scheda di un sito con webhook contiene la sezione Invii recenti. Ogni invio mostra il codice di stato, il suo significato abituale e i primi 300 caratteri del tuo messaggio di errore; gli invii non riusciti hanno un pulsante Riprova. Il primo kilobyte di ogni risposta viene memorizzato.
  • A causa dei nuovi tentativi il tuo endpoint potrebbe ricevere lo stesso articolo due volte, ogni volta con un nuovo delivery_id. Per questo è importante inserire o aggiornare i dati in base ad article.id.

Un esempio completo di ricevitore

Una route di Next.js App Router che verifica, elimina i duplicati e salva i dati. Sostituisci savePost con il tuo sistema di persistenza ed è pronta per la produzione:

// app/api/articles/webhook/route.ts — a complete Next.js receiver
import { NextResponse } from "next/server";
import { verifyBlogTendSignature } from "./verify"; // the Node.js function above

const SECRET = process.env.BLOGTEND_WEBHOOK_SECRET; // the whsec_… shown when you connected

export async function POST(req: Request) {
  const body = await req.text(); // raw body — verify BEFORE parsing
  let payload;
  try {
    payload = JSON.parse(body);
  } catch {
    return NextResponse.json({ error: "invalid JSON" }, { status: 400 });
  }

  // A ping carries no article and writes nothing, so it is answered without
  // verifying: Test & connect then passes even before SECRET is deployed.
  // Return nothing private here.
  if (payload.event === "ping") {
    // Optional: advertise your categories so BlogTend's forms can offer them.
    return NextResponse.json({ ok: true, categories: await listMyCategories() });
  }

  // Every content event must be verified. Clear messages here show up in
  // BlogTend's delivery log, so "wrong secret" never looks like "down".
  if (!SECRET) {
    return NextResponse.json({ error: "receiver not configured: BLOGTEND_WEBHOOK_SECRET is not set" }, { status: 503 });
  }
  const header =
    req.headers.get("x-blogtend-signature") ?? req.headers.get("x-yodon-signature") ?? "";
  if (!verifyBlogTendSignature(SECRET, header, body)) {
    return NextResponse.json({ error: "invalid signature: check BLOGTEND_WEBHOOK_SECRET" }, { status: 401 });
  }

  const a = payload.article;
  // Upsert by a.id — retries and updates must not create duplicates.
  const post = await savePost({
    externalId: a.id,
    slug: a.slug,
    title: a.title,
    html: a.html,
    excerpt: a.excerpt,
    metaTitle: a.seo.title,
    metaDescription: a.seo.description,
    tags: a.tags,
    category: a.category,
    imageUrl: a.featured_image?.url ?? null,
    published: a.status === "publish",
  });

  // Answer with the live URL so the BlogTend dashboard links to the post.
  return NextResponse.json({ ok: true, id: post.id, url: post.url });
}

La stessa logica si adatta a qualsiasi framework in pochi minuti — le FAQ qui sotto rispondono alle domande più comuni sulle piattaforme.

Domande

Con quali piattaforme funziona?
Qualsiasi ambiente tecnologico in grado di ricevere una richiesta HTTPS POST: Next.js, Laravel, Django, Rails, Express, un Cloudflare Worker, uno strumento senza codice come Make o n8n, un CMS personalizzato — se ha un URL, può ricevere articoli.
Ho perso la chiave segreta di firma. Dove posso ritrovarla?
Da nessuna parte: lo conserviamo crittografato e lo mostriamo solo durante la connessione. Usa Ruota segreto sulla scheda del sito in Siti. Ottieni un nuovo segreto e quello precedente continua a firmare insieme al nuovo per 24 ore, così gli invii continuano a funzionare mentre aggiorni il tuo server.
Gli articoli programmati funzionano?
Sì. La programmazione resta gestita da BlogTend: al momento programmato inviamo l'articolo con stato "publish". Il tuo endpoint non deve mai implementare la programmazione.
E le bozze?
Un articolo inviato come bozza arriva con lo stato "draft". Sta a te decidere cosa significa sul tuo sito — la maggior parte dei destinatari salva il contenuto senza pubblicarlo. Quando poi premi Pubblica in BlogTend, lo stesso articolo arriva di nuovo come evento article.updated con lo stato "publish".
Come vengono ricevute le immagini?
L'immagine in evidenza viene fornita tramite un URL pubblico firmato sui nostri server (featured_image.url), e il codice HTML dell'articolo fa riferimento allo stesso URL. Scaricala e ospitala sui tuoi server, oppure distribuiscila direttamente dai nostri — entrambe le opzioni funzionano.
L'invio del payload viene ritentato? Vedrò duplicati?
Sì. Se un invio non riesce, viene ripetuto, e ogni nuovo tentativo ha un nuovo delivery_id, quindi il tuo endpoint può ricevere lo stesso articolo più di una volta. Usa article.id come chiave di idempotenza: aggiorna il post esistente invece di crearne un secondo, e i duplicati diventano impossibili.
I metadati SEO vengono trasmessi?
Sì — seo.title, seo.description e seo.keyword accompagnano ogni articolo, insieme all'estratto, ai tag, ai nomi delle categorie e alla lingua. Usa ciò che la tua piattaforma supporta e ignora il resto.

Vuoi pubblicare su WordPress invece? Leggi la guida di WordPress

Le tue tecnologie, i nostri articoli

Collega un endpoint una sola volta e ogni articolo — pubblicato manualmente, in blocco o automaticamente — arriva direttamente sul tuo sito.