Aller au contenu

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

  1. Déployez votre point de terminaison de réception (l’exemple en bas est complet).
  2. Dans BlogTend, allez dans Sites → Connecter un site → Webhook, saisissez un nom et l'URL du point de terminaison, puis appuyez sur Suivant.
  3. 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é.
  4. 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êteValeurNotes
X-BlogTend-Signaturet=<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-Eventping | article.published | article.updatedIdentique au champ event dans le corps.
X-BlogTend-Deliverywhd_…Identique à delivery_id dans le corps. Renouvelé à chaque tentative, y compris lors des nouvelles tentatives.
X-BlogTend-Test1Uniquement 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-Typeapplication/jsonLe corps est au format JSON encodé en UTF-8.
User-AgentBlogTend-Webhook/1S’appelait YoDon-Webhook/1 avant le changement de nom.
X-Yodon-Signature, X-Yodon-Event, X-Yodon-DeliverydeprecatedEnvoyé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éponseCe que cela signifie pour nousRenvoyé ?
2xxLivré. 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 ?
Toute pile technologique capable de recevoir une requête POST HTTPS : Next.js, Laravel, Django, Rails, Express, un Cloudflare Worker, un outil sans code comme Make ou n8n, un CMS personnalisé — si elle dispose d’une URL, elle peut recevoir des articles.
J’ai perdu le secret de signature. Où puis-je le retrouver ?
Nulle part : nous le stockons sous forme chiffrée et ne l’affichons que pendant la connexion. Utilisez Renouveler le secret sur la fiche du site dans Sites. Vous obtenez un nouveau secret, et l’ancien continue de signer en parallèle pendant 24 heures, pour que les envois continuent de fonctionner pendant la mise à jour de votre serveur.
Les articles programmés fonctionnent-ils ?
Oui. La planification reste gérée par BlogTend : à l’heure prévue, nous envoyons l’article avec le statut "publish". Votre point de terminaison n’a jamais à gérer la planification.
Qu’en est-il des brouillons ?
Un article envoyé comme brouillon arrive avec le statut "draft". C'est vous qui décidez de ce que cela signifie sur votre site — la plupart des destinataires conservent l'article sans le publier. Lorsque vous cliquez ensuite sur Publier dans BlogTend, le même article arrive à nouveau sous la forme d'un événement article.updated avec le statut "publish".
Comment les images sont-elles transmises ?
L’image à la une est fournie sous forme d’URL publique signée sur nos serveurs (featured_image.url), et le code HTML de l’article utilise la même URL. Téléchargez-la et hébergez-la vous-même, ou diffusez-la directement depuis nos serveurs — les deux options fonctionnent.
L'envoi des données est-il parfois retenté ? Vais-je recevoir des doublons ?
Oui. En cas d’échec, l’envoi est retenté, et chaque nouvelle tentative possède un nouveau delivery_id, votre point de terminaison peut donc recevoir le même article plusieurs fois. Utilisez article.id comme clé d’idempotence : mettez à jour l’article existant au lieu d’en créer un second, et les doublons deviennent impossibles.
Les métadonnées SEO sont-elles transmises ?
Oui — seo.title, seo.description et seo.keyword accompagnent chaque article, ainsi que l’extrait, les étiquettes, les noms des catégories et la langue. Utilisez ce que votre plateforme prend en charge et ignorez le reste.

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.