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
- Metti in produzione il tuo endpoint di ricezione (l'esempio in fondo è completo).
- In BlogTend, vai su Siti → Collega un sito → Webhook, inserisci un nome e l'URL dell'endpoint, e premi Avanti.
- 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.
- 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:
| Intestazione | Valore | Note |
|---|---|---|
| X-BlogTend-Signature | t=<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-Event | ping | article.published | article.updated | Uguale al campo event nel corpo. |
| X-BlogTend-Delivery | whd_… | Uguale a delivery_id nel corpo. Nuovo a ogni tentativo, inclusi i tentativi successivi. |
| X-BlogTend-Test | 1 | Solo per i ping (Testa & connetti, Verifica di nuovo, aggiornamento delle categorie). Un ping non contiene articoli e non deve scrivere nulla. |
| Content-Type | application/json | Il corpo è in formato JSON con codifica UTF-8. |
| User-Agent | BlogTend-Webhook/1 | Si chiamava YoDon-Webhook/1 prima del cambio di nome. |
| X-Yodon-Signature, X-Yodon-Event, X-Yodon-Delivery | deprecated | Inviati 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 risposta | Cosa significa per noi | Reinviato? |
|---|---|---|
| 2xx | Consegnato. Il corpo è facoltativo; un corpo JSON viene letto come descritto in Cosa rispondere. | Fatto. |
| 3xx | Operazione non riuscita. I reindirizzamenti non vengono seguiti: collega invece l'URL finale. | Non reinviato immediatamente. |
| 401 / 403 | Non riuscito: indicato come "firma rifiutata". Il segreto sul tuo server non corrisponde. Incollalo di nuovo o ruotalo. | Non reinviato immediatamente. |
| 503 | Operazione 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 4xx | Non riuscito: hai compreso la richiesta e l'hai rifiutata. | Non reinviato immediatamente. |
| Altri 5xx | Non 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 rete | Operazione 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?
Ho perso la chiave segreta di firma. Dove posso ritrovarla?
Gli articoli programmati funzionano?
E le bozze?
Come vengono ricevute le immagini?
L'invio del payload viene ritentato? Vedrò duplicati?
I metadati SEO vengono trasmessi?
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.