Guide
Connectez n’importe quel site web via un webhook
Vous n’utilisez pas WordPress ? Aucun problème. BlogTend envoie chaque article terminé par POST au format JSON signé vers un point de terminaison que vous contrôlez — quelle que soit votre pile technique ou votre CMS, avec une vingtaine de lignes de code de votre côté.
Présentation du connecteur webhook
Lorsque la génération d’un article est terminée — ou que l’heure prévue pour sa publication arrive — BlogTend l’envoie à votre point de terminaison en une seule requête POST au format JSON : titre, contenu HTML complet, extrait, métadonnées SEO, noms des catégories et des étiquettes, langue, sources, et URL publique de l’image à la une. Votre point de terminaison l’enregistre selon le mode de stockage du contenu de votre site, et tout le reste dans BlogTend fonctionne exactement comme pour les sites WordPress : automatisations, planification, brouillons, tableau de bord, indexation Google.
Chaque requête est signée à l'aide d'un secret connu uniquement de vous et de BlogTend, afin que votre point de terminaison puisse vérifier que l'article provient bien de nous et n'a pas été altéré.
Ce que votre point de terminaison doit faire
- Acceptez une requête POST HTTPS avec un corps JSON à une URL publique.
- Répondez dans les 15 secondes avec un code de statut 2xx. Effectuez les opérations lentes (téléchargements d’images, reconstructions du cache) après avoir répondu, pas avant.
- Répondez à chaque ping avec un code 2xx. Un ping ne contient aucun article et ne doit entraîner aucune écriture, vous pouvez donc y répondre avant de le vérifier (ou sans le vérifier) : cela permet à Tester & connecter de réussir alors que votre secret est encore en cours de déploiement.
- Vérifiez X-BlogTend-Signature à chaque événement lié à un article (code ci-dessous), et renvoyez une réponse 401 si la signature ne correspond pas. Nous ne l’imposons pas, mais sans cette vérification, toute personne qui découvre l’URL pourrait envoyer de faux articles à votre site.
- Utilisez article.id comme clé d’idempotence : si un article portant cet identifiant existe déjà, mettez-le à jour au lieu de créer un doublon.
- Lorsque vous refusez une requête, indiquez la raison dans le corps de la réponse (par exemple {"error": "invalid signature"}). Nous affichons les 300 premiers caractères dans votre journal de livraison.
Connexion
- Déployez votre point de terminaison de réception (l’exemple en bas est complet).
- Dans BlogTend, allez dans Sites → Connecter un site → Webhook, saisissez un nom et l'URL du point de terminaison, puis appuyez sur Suivant.
- Nous affichons le secret de signature whsec_…. Copiez-le maintenant dans l’environnement de votre serveur et déployez : cet écran est le seul endroit où il est affiché.
- Cliquez sur Tester & connecter. Nous envoyons une requête de test signée avec ce secret ; une réponse 2xx enregistre la connexion. Si votre point de terminaison la refuse, nous affichons son code de statut et son propre message d’erreur pour que vous puissiez corriger le problème et cliquer à nouveau sur Tester & connecter.
Reconnecter la même URL (pour renommer le site, ou après une erreur) ne modifie jamais le secret : la boîte de dialogue affiche le secret actuel et la requête de test est signée avec celui-ci. Pour obtenir un nouveau secret, effectuez une rotation.
En-têtes envoyés
Chaque requête, ping ou article, contient ces en-têtes :
| En-tête | Valeur | Notes |
|---|---|---|
| X-BlogTend-Signature | t=<unix seconds>,v1=<hex>[,v1=<hex>] | Signature HMAC-SHA256. Une v1 par secret actif : deux uniquement pendant le délai de grâce de 24 heures lors d'une rotation, la plus récente en premier. |
| X-BlogTend-Event | ping | article.published | article.updated | Identique au champ event dans le corps. |
| X-BlogTend-Delivery | whd_… | Identique à delivery_id dans le corps. Renouvelé à chaque tentative, y compris lors des nouvelles tentatives. |
| X-BlogTend-Test | 1 | Uniquement lors des requêtes de test (Tester & connecter, Revérifier, actualisation des catégories). Une requête de test ne contient aucun article et ne doit rien écrire. |
| Content-Type | application/json | Le corps est au format JSON encodé en UTF-8. |
| User-Agent | BlogTend-Webhook/1 | S’appelait YoDon-Webhook/1 avant le changement de nom. |
| X-Yodon-Signature, X-Yodon-Event, X-Yodon-Delivery | deprecated | Envoyés pour les récepteurs développés avant le changement de nom, avec les mêmes valeurs, sauf que X-Yodon-Signature contient toujours exactement une signature v1, générée avec le secret le plus récent. Les nouveaux récepteurs doivent lire les en-têtes X-BlogTend-*. |
Les en-têtes X-Yodon-* sont obsolètes. Ils restent fonctionnels pour les récepteurs développés avant le changement de nom, mais seul X-BlogTend-Signature contient les deux signatures lors d’une rotation, alors adoptez les nouveaux noms lors de votre prochaine modification du récepteur.
La charge utile, champ par champ
Trois types d’événements sont reçus, identifiés par le champ event et l’en-tête X-BlogTend-Event : ping (test de connexion, sans article), article.published (première livraison d’un article) et article.updated (le même article à nouveau : après une nouvelle tentative que vous avez déclenchée, la publication d’un brouillon, ou l’actualisation d’un article déjà en ligne, auquel cas url désigne l’article en ligne à remplacer et id est l’identifiant que vous avez enregistré pour celui-ci). La structure complète :
{
"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 correspond à l’état demandé : "publish" ou "draft". Les articles programmés arrivent à l’heure prévue avec "publish".
- article.html contient le corps complet de l’article. Les références aux images qu’il contient pointent déjà vers des URL publiques
- category et categories sont des noms, pas des identifiants — associez-les à votre propre taxonomie, ou ignorez-les.
- featured_image vaut null lorsque l'article n'a pas d'image. L'URL est signée et stable — vous pouvez y accéder à tout moment
- delivery_id identifie cette tentative précise et change à chaque nouvelle tentative ; article.id identifie l’article et ne change jamais. Éliminez les doublons à l’aide de article.id.
- Le corps d’un ping contient uniquement event, delivery_id, site_id et sent_at ; aucun article.
Vérification de la signature
Chaque requête contient X-BlogTend-Signature: t=<unix seconds>,v1=<hex>. Chaque valeur v1 est un HMAC-SHA256 de la chaîne "<t>.<raw body>" calculé avec un secret whsec_. Vérifiez la signature à partir du corps brut de la requête, avant toute analyse JSON, rejetez les horodatages présentant un écart de plus de 5 minutes, utilisez une fonction de comparaison à temps constant et acceptez la requête si au moins une valeur v1 correspond à votre secret : lors d'une rotation, il y en a deux.
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 ancien récepteur qui lit X-Yodon-Signature avec un motif exact n’acceptant qu’un seul v1 continue de fonctionner : cet en-tête contient toujours exactement un v1, généré avec le secret le plus récent.
Rotation du secret
Vous avez perdu le secret ou vous le remplacez à intervalles réguliers ? Sur la page Sites, ouvrez la fiche du site utilisant un webhook et cliquez sur Renouveler le secret. Le nouveau secret n'est affiché qu'une seule fois.
- Pendant les prochaines 24 heures, chaque requête contient deux signatures dans X-BlogTend-Signature : l'une générée avec le nouveau secret, l'autre avec l'ancien. Un destinataire disposant de l'un ou l'autre secret peut vérifier la signature, ce qui évite tout échec pendant la mise à jour de votre serveur.
- Après 24 heures, seul le nouveau secret est utilisé pour signer. La carte indique quand l’ancien cesse d’être utilisé.
- Si l'ancien secret a été divulgué, cochez "le désactiver maintenant" lors de son renouvellement : aucun délai de grâce n'est accordé, et les envois échouent avec une erreur 401 tant que votre serveur ne dispose pas du nouveau secret.
- Les récepteurs qui lisent encore l’en-tête obsolète X-Yodon-Signature ne voient que la signature du nouveau secret, donc pour eux le changement est immédiat.
Réponse à fournir
Tout code 2xx confirme la livraison ; le corps de la réponse est facultatif et peut être vide. Répondez en JSON pour rendre BlogTend plus intelligent :
{ "ok": true, "id": "123", "url": "https://example.com/blog/my-post" }- url — enregistrée comme lien vers l'article en ligne : le bouton Voir du tableau de bord l'ouvre, et elle est soumise à Google pour indexation
- id — l'identifiant de votre publication, enregistré pour que les futurs événements article.updated puissent aussi être associés à cette publication de votre côté
- held — une courte explication lorsque vous avez accepté l'article mais ne l'avez pas publié sur votre site (une file d'attente éditoriale, une règle interne). BlogTend l'affiche alors comme un brouillon accompagné de cette explication plutôt que comme un article publié, et n'envoie pas d'e-mail "published". Répondre status: "draft" produit le même résultat sans explication
Pour un ping, tout code 2xx suffit également. Le seul champ que nous lisons dans une réponse au ping est categories : répondez avec { ok: true, categories: ["Guides", "Actualités"] } (un tableau contenant jusqu'à 100 noms) et ces noms apparaîtront comme catégories disponibles dans les formulaires de création de BlogTend. Ces catégories sont actualisées à chaque nouvelle vérification et via le bouton d'actualisation du sélecteur de catégories. Si vous omettez ce champ, l'IA propose simplement des noms de catégories à la place.
Échecs, nouvelles tentatives et doublons
Comment BlogTend interprète chaque réponse à la livraison d'un article :
| Votre réponse | Ce que cela signifie pour nous | Renvoyé ? |
|---|---|---|
| 2xx | Livré. Le corps est facultatif ; un corps JSON est lu comme décrit dans Que répondre. | Terminé. |
| 3xx | Échec. Les redirections ne sont pas suivies : connectez plutôt l’URL finale. | Pas renvoyé immédiatement. |
| 401 / 403 | Échec : affiché comme "signature rejetée". Le secret sur votre serveur ne correspond pas. Collez-le à nouveau ou remplacez-le. | Pas renvoyé immédiatement. |
| 503 | Échec : affiché comme "pas encore configuré", la réponse habituelle d’un récepteur dont le secret n’est pas défini. | Renvoyé une seule fois, immédiatement. |
| Autres 4xx | Échec : vous avez compris la requête et l'avez refusée. | Pas renvoyé immédiatement. |
| Autres 5xx | Échec : votre point de terminaison a planté ou est indisponible. | Renvoyé une seule fois, immédiatement. |
| Aucune réponse sous 15 s, ou une erreur réseau | Échec : affiché comme "impossible de joindre votre point de terminaison". | Renvoyé une seule fois, immédiatement. |
- Articles que BlogTend publie de façon autonome (automatisations et publications programmées) : si la livraison échoue encore, l'envoi de l'article entier est retenté lors des deux exécutions suivantes, espacées d'environ une minute, quelle que soit la réponse reçue. Après la troisième tentative infructueuse, BlogTend s'arrête, conserve l'article terminé et vous envoie la raison de l'échec par e-mail. Publiez-le à nouveau une fois votre point de terminaison corrigé.
- Articles que vous publiez manuellement : aucune nouvelle tentative automatique. La raison s’affiche immédiatement sur l’article ; cliquez à nouveau sur Publier, ou sur Réessayer dans Envois récents.
- Un article n'est jamais perdu : si sa publication échoue, il reste dans BlogTend avec la raison de l'échec, prêt à être publié à nouveau.
- Où consulter les résultats : sur la page Sites, la fiche d'un site utilisant un webhook comporte une section Envois récents. Chaque envoi affiche le code de statut, sa signification habituelle et les 300 premiers caractères de votre message d'erreur ; les envois ayant échoué disposent d'un bouton Réessayer. Le premier kilooctet de chaque réponse est conservé.
- En raison des nouvelles tentatives, votre point de terminaison peut recevoir deux fois le même article, chaque fois avec un nouveau delivery_id. C’est pourquoi il est important d’insérer ou de mettre à jour les données en se basant sur article.id.
Un exemple complet de récepteur
Une route de l’App Router de Next.js qui vérifie, déduplique et stocke les données. Remplacez savePost par votre propre mécanisme de stockage et elle sera prête pour la production :
// 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 même logique s’adapte à n’importe quel framework en quelques minutes — la FAQ ci-dessous répond aux questions courantes sur les plateformes.
Questions
Avec quelles plateformes cela fonctionne-t-il ?
J’ai perdu le secret de signature. Où puis-je le retrouver ?
Les articles programmés fonctionnent-ils ?
Qu’en est-il des brouillons ?
Comment les images sont-elles transmises ?
L'envoi des données est-il parfois retenté ? Vais-je recevoir des doublons ?
Les métadonnées SEO sont-elles transmises ?
Vous souhaitez plutôt publier sur WordPress ? Lire le guide WordPress
Vos technologies, nos articles
Connectez un point de terminaison une seule fois et chaque article — publié manuellement, en masse ou automatiquement — est envoyé directement sur votre site.