Ga naar inhoud

Handleiding

Koppel elke website via een webhook

Gebruik je geen WordPress? Geen probleem. BlogTend verstuurt elk voltooid artikel via POST als ondertekende JSON naar een endpoint dat je zelf beheert — elke technologiestack, elk CMS, ongeveer twintig regels code aan jouw kant.

Wat de webhookkoppeling is

Wanneer een artikel klaar is met genereren — of het geplande tijdstip aanbreekt — stuurt BlogTend het naar je endpoint als één JSON POST: titel, volledige HTML, samenvatting, SEO-metadata, categorie- en tagnamen, taal, bronnen, en een openbare URL voor de uitgelichte afbeelding. Je endpoint slaat het op zoals je website inhoud opslaat, en al het andere in BlogTend werkt precies zoals voor WordPress-websites: automatiseringen, planning, concepten, het dashboard, Google-indexering.

Elk verzoek wordt ondertekend met een geheim dat alleen jij en BlogTend kennen, zodat je eindpunt kan aantonen dat het artikel echt van ons afkomstig is en niet is gewijzigd.

Wat je eindpunt moet doen

  • Accepteer een HTTPS POST met een JSON-body op een openbare URL.
  • Antwoord binnen 15 seconden met een 2xx-status. Voer tijdrovende taken (afbeeldingen downloaden, caches opnieuw opbouwen) uit nadat je hebt geantwoord, niet ervoor.
  • Beantwoord elke ping met een 2xx. Een ping bevat geen artikel en mag niets wegschrijven, dus je mag deze beantwoorden voordat (of zonder dat) je deze verifieert: daardoor kan Testen & verbinden slagen terwijl je geheime sleutel nog wordt uitgerold.
  • Controleer X-BlogTend-Signature bij elke artikelgebeurtenis (code hieronder), en antwoord met 401 als deze niet overeenkomt. We dwingen dit niet af, maar zonder deze controle kan iedereen die de URL vindt nepartikelen naar je website sturen.
  • Gebruik article.id als idempotentiesleutel: als er al een bericht met die id bestaat, werk het dan bij in plaats van een duplicaat te maken.
  • Als je een verzoek weigert, vermeld dan de reden in de berichtinhoud (bijvoorbeeld {"error": "invalid signature"}). We tonen de eerste 300 tekens in je afleverlogboek.

Koppelen

  1. Stel je ontvangende eindpunt beschikbaar (het voorbeeld onderaan is een volledig eindpunt).
  2. Ga in BlogTend naar Websites → Een website koppelen → Webhook, voer een naam en de URL van het eindpunt in, en druk op Volgende.
  3. We tonen de geheime ondertekeningssleutel whsec_…. Kopieer deze nu naar de omgeving van je server en rol de configuratie uit: dit scherm is de enige plek waar de sleutel wordt getoond.
  4. Druk op Testen & verbinden. We sturen een ping die met die geheime sleutel is ondertekend; bij een 2xx wordt de verbinding opgeslagen. Als je eindpunt de ping weigert, tonen we de status en de eigen foutmelding van het eindpunt zodat je het probleem kunt oplossen en opnieuw op Testen & verbinden kunt drukken.

Dezelfde URL opnieuw verbinden (om de website te hernoemen, of na een fout) verandert de geheime sleutel nooit: het dialoogvenster toont de huidige sleutel en de testping wordt ermee ondertekend. Om een nieuwe geheime sleutel te krijgen, moet je deze vernieuwen.

Verzonden headers

Elk verzoek, ping of artikel, bevat deze headers:

HeaderWaardeNotities
X-BlogTend-Signaturet=<unix seconds>,v1=<hex>[,v1=<hex>]HMAC-SHA256-handtekening. Eén v1 per actieve geheime sleutel: alleen twee tijdens de overgangsperiode van 24 uur bij sleutelvervanging, de nieuwste eerst.
X-BlogTend-Eventping | article.published | article.updatedGelijk aan het veld event in de body.
X-BlogTend-Deliverywhd_…Gelijk aan delivery_id in de body. Nieuw bij elke poging, ook bij herhaalde pogingen.
X-BlogTend-Test1Alleen bij pings (Testen & koppelen, Opnieuw verifiëren, categorieën vernieuwen). Een ping bevat geen artikel en mag niets schrijven.
Content-Typeapplication/jsonDe body is JSON in UTF-8.
User-AgentBlogTend-Webhook/1Heette YoDon-Webhook/1 vóór de naamswijziging.
X-Yodon-Signature, X-Yodon-Event, X-Yodon-DeliverydeprecatedVerzonden voor ontvangers die vóór de naamswijziging zijn gebouwd, met dezelfde waarden, behalve dat X-Yodon-Signature altijd precies één v1 bevat, aangemaakt met de nieuwste geheime sleutel. Nieuwe ontvangers moeten de X-BlogTend-* headers lezen.

De X-Yodon-* headers zijn verouderd. Ze blijven werken voor ontvangers die vóór de naamswijziging zijn gebouwd, maar alleen X-BlogTend-Signature bevat beide handtekeningen tijdens een sleutelrotatie, dus stap over op de nieuwe namen wanneer je je ontvanger weer aanpast.

De payload, veld voor veld

Er komen drie gebeurtenistypen binnen, te onderscheiden aan het veld event en de header X-BlogTend-Event: ping (verbindingstest, geen artikel), article.published (eerste levering van een artikel) en article.updated (hetzelfde artikel opnieuw: na een nieuwe poging die je hebt gestart, het publiceren van een concept, of het vernieuwen van een bericht dat al online staat, waarbij url verwijst naar het te vervangen gepubliceerde bericht en id het id is dat je daarvoor hebt opgeslagen). De volledige structuur:

{
  "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 is de status die we aanvragen: "publish" of "draft". Ingeplande artikelen komen op het geplande tijdstip binnen met "publish".
  • article.html bevat de volledige artikeltekst. Afbeeldingsverwijzingen daarin verwijzen al naar openbare URL's.
  • category en categories zijn namen, geen ID's — koppel ze aan je eigen taxonomie, of negeer ze.
  • featured_image is null als het artikel geen afbeelding heeft. De URL is ondertekend en stabiel — je kunt de afbeelding op elk moment ophalen.
  • delivery_id identificeert deze specifieke poging en verandert bij elke nieuwe poging; article.id identificeert het artikel en verandert nooit. Verwijder duplicaten op basis van article.id.
  • De body van een ping bevat alleen event, delivery_id, site_id en sent_at; geen artikel.

De handtekening verifiëren

Elk verzoek bevat X-BlogTend-Signature: t=<unix seconds>,v1=<hex>. Elke v1-waarde is een HMAC-SHA256 van de tekenreeks "<t>.<raw body>" met een whsec_-geheim als sleutel. Verifieer aan de hand van de onbewerkte verzoekinhoud, voordat je JSON verwerkt, wijs tijdstempels af die meer dan 5 minuten afwijken, vergelijk met een timingveilige functie, en accepteer het verzoek als een van de v1-waarden overeenkomt met je geheim: tijdens een rotatie zijn er twee.

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")

Een oudere ontvanger die X-Yodon-Signature leest met een exact patroon voor één v1 blijft werken: die header bevat nog steeds precies één v1, aangemaakt met de nieuwste geheime sleutel.

Geheime sleutel vervangen

Ben je de geheime sleutel kwijt, of vervang je die periodiek? Open op de pagina Websites de kaart van de webhookwebsite en klik op Geheime sleutel vernieuwen. De nieuwe geheime sleutel wordt eenmalig getoond.

  • De komende 24 uur bevat elk verzoek twee handtekeningen in X-BlogTend-Signature: één gemaakt met het nieuwe geheim, één met het oude. Een ontvanger met een van beide geheimen kan het verzoek verifiëren, zodat er niets misgaat terwijl je je server bijwerkt.
  • Na 24 uur wordt alleen de nieuwe geheime sleutel gebruikt voor ondertekening. De kaart toont wanneer de oude niet meer wordt gebruikt.
  • Als de oude geheime sleutel is uitgelekt, vink dan "nu stoppen" aan bij het vernieuwen: er is geen overgangsperiode, en afleveringen mislukken met 401 totdat je server de nieuwe geheime sleutel heeft.
  • Ontvangers die nog de verouderde X-Yodon-Signature uitlezen, zien alleen de handtekening van de nieuwe geheime sleutel, dus voor hen gaat de omschakeling direct in.

Antwoordinhoud

Elke 2xx telt als afgeleverd; de body is optioneel en mag leeg zijn. Antwoord met JSON en BlogTend wordt slimmer:

{ "ok": true, "id": "123", "url": "https://example.com/blog/my-post" }
  • url — opgeslagen als de link naar het gepubliceerde artikel: de knop Bekijken in het dashboard opent deze, en voor Google-indexering wordt deze ingediend.
  • id — de identificatie van je bericht, opgeslagen zodat toekomstige article.updated-gebeurtenissen ook aan jouw kant kunnen worden gekoppeld.
  • held — een korte reden waarom je het artikel hebt geaccepteerd maar niet op je website hebt geplaatst (een redactionele wachtrij, een huisregel). BlogTend toont het dan als concept met die reden in plaats van als gepubliceerd bericht, en stuurt geen e-mail met "gepubliceerd". Antwoorden met status: "draft" doet hetzelfde zonder reden.

Voor een ping is elke 2xx ook voldoende. Het enige veld dat we uit een pingantwoord lezen is categories: antwoord met { ok: true, categories: ["Guides", "News"] } (een array van maximaal 100 namen) en die namen verschijnen als daadwerkelijke categoriekeuzes in de aanmaakformulieren van BlogTend. Wordt vernieuwd bij elke klik op Opnieuw verifiëren en op de vernieuwknop van de categoriekiezer. Laat het veld weg en de AI stelt in plaats daarvan gewoon categorienamen voor.

Fouten, nieuwe pogingen en duplicaten

Hoe BlogTend elke reactie op de levering van een artikel interpreteert:

Je antwoordWat het voor ons betekentOpnieuw verzonden?
2xxAfgeleverd. De inhoud is optioneel; JSON-inhoud wordt gelezen zoals beschreven in Wat je moet antwoorden.Klaar.
3xxMislukt. Omleidingen worden niet gevolgd: koppel in plaats daarvan de uiteindelijke URL.Niet meteen opnieuw verzonden.
401 / 403Mislukt: weergegeven als "handtekening afgewezen". De geheime sleutel op je server komt niet overeen. Plak deze opnieuw of vervang deze.Niet meteen opnieuw verzonden.
503Mislukt: weergegeven als "nog niet geconfigureerd", het gebruikelijke antwoord van een ontvanger waarvoor geen geheime sleutel is ingesteld.Eenmaal opnieuw verzonden, meteen.
Overige 4xxMislukt: je hebt het verzoek begrepen en geweigerd.Niet meteen opnieuw verzonden.
Overige 5xxMislukt: je eindpunt is gecrasht of niet bereikbaar.Eenmaal opnieuw verzonden, meteen.
Geen antwoord binnen 15 s, of een netwerkfoutMislukt: weergegeven als "kon je eindpunt niet bereiken".Eenmaal opnieuw verzonden, meteen.
  • Artikelen die BlogTend zelfstandig publiceert (automatiseringen en geplande berichten): als de aflevering nog steeds mislukt, wordt het hele artikel tijdens de volgende twee runs opnieuw verstuurd, met ongeveer een minuut ertussen, ongeacht het antwoord. Na de derde mislukte poging stopt BlogTend, bewaart het voltooide artikel en mailt je de reden. Publiceer het opnieuw zodra je endpoint is hersteld.
  • Artikelen die je handmatig publiceert: geen automatische nieuwe poging. De reden verschijnt meteen bij het artikel; druk opnieuw op Publiceren, of op Opnieuw proberen bij Recente leveringen.
  • Een artikel gaat nooit verloren: bij een mislukte publicatie blijft het in BlogTend staan met de reden, klaar om opnieuw te publiceren.
  • Waar je moet kijken: op de pagina Websites staat op de kaart van een webhookwebsite Recente afleveringen. Elke aflevering toont de statuscode, de gebruikelijke betekenis ervan en de eerste 300 tekens van je foutmelding; mislukte afleveringen hebben een knop Opnieuw proberen. De eerste kilobyte van elke reactie wordt opgeslagen.
  • Door nieuwe pogingen kan je endpoint hetzelfde artikel twee keer ontvangen, telkens met een nieuwe delivery_id. Daarom is het belangrijk om artikelen op basis van article.id toe te voegen of bij te werken.

Een volledig voorbeeld van een ontvanger

Een Next.js App Router-route die verifieert, duplicaten verwijdert en opslaat. Vervang savePost door je eigen opslaglogica en de route is klaar voor productie:

// 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 });
}

Dezelfde logica is in een paar minuten naar elk framework over te zetten — de veelgestelde vragen hieronder behandelen de gangbare vragen over platforms.

Vragen

Met welke platforms werkt dit?
Elke technologiestack die een HTTPS POST kan ontvangen: Next.js, Laravel, Django, Rails, Express, een Cloudflare Worker, een tool zonder code zoals Make of n8n, een CMS op maat — als het een URL heeft, kan het artikelen ontvangen.
Ik ben de ondertekeningssleutel kwijt. Waar vind ik die terug?
Nergens: we slaan de geheime sleutel versleuteld op en tonen deze alleen tijdens het koppelen. Gebruik Geheime sleutel vernieuwen op de websitekaart in Websites. Je krijgt een nieuwe geheime sleutel, en de oude blijft daarnaast nog 24 uur ondertekenen, zodat leveringen blijven werken terwijl je je server bijwerkt.
Werken geplande artikelen?
Ja. De planning blijft bij BlogTend: op het geplande tijdstip sturen we het artikel met de status "publish". Je endpoint hoeft nooit zelf de planning te implementeren.
Hoe zit het met concepten?
Een artikel dat als concept wordt verstuurd, komt aan met de status "draft". Wat dat op je website betekent, bepaal je zelf — de meeste ontvangers slaan het bericht ongepubliceerd op. Wanneer je later in BlogTend op Publiceren klikt, komt hetzelfde artikel opnieuw aan als een article.updated-gebeurtenis met de status "publish".
Hoe worden afbeeldingen aangeleverd?
De uitgelichte afbeelding wordt geleverd via een ondertekende openbare URL op onze servers (featured_image.url), en de HTML van het artikel verwijst naar dezelfde URL. Download de afbeelding en host deze zelf, of laad deze rechtstreeks vanaf onze servers — beide werken.
Wordt de payload ooit opnieuw verzonden? Krijg ik dubbele berichten?
Ja. Een mislukte levering wordt opnieuw geprobeerd, en elke nieuwe poging krijgt een nieuwe delivery_id, waardoor je eindpunt hetzelfde artikel meerdere keren kan ontvangen. Gebruik article.id als idempotentiesleutel: werk het bestaande bericht bij in plaats van een tweede aan te maken, zodat duplicaten onmogelijk worden.
Worden SEO-metagegevens doorgegeven?
Ja — seo.title, seo.description en seo.keyword worden met elk artikel meegestuurd, samen met de samenvatting, tags, categorienamen en taal. Gebruik wat je platform ondersteunt en negeer de rest.

Wil je liever op WordPress publiceren? Lees de WordPress-handleiding

Jouw technologie, onze artikelen

Koppel eenmalig een endpoint en elk artikel — handmatig, in bulk of automatisch — komt rechtstreeks op je website terecht.

Koppel elke website via een webhook · BlogTend