Guía
Conecta cualquier sitio web mediante un webhook
¿No usas WordPress? No hay problema. BlogTend envía cada artículo terminado mediante POST como JSON firmado a un punto de conexión que tú controlas — cualquier tecnología, cualquier CMS, unas veinte líneas de código de tu lado.
Qué es el conector de webhooks
Cuando termina de generarse un artículo — o llega el momento programado — BlogTend lo envía a tu punto de conexión en una única solicitud POST con JSON: título, HTML completo, extracto, metadatos SEO, nombres de categorías y etiquetas, idioma, fuentes, y una URL pública para la imagen destacada. Tu punto de conexión lo almacena del mismo modo que tu sitio almacena el contenido, y todo lo demás en BlogTend funciona exactamente igual que para los sitios de WordPress: automatizaciones, programación, borradores, el panel de control, indexación en Google.
Cada solicitud se firma con una clave secreta que solo tú y BlogTend conocen, para que tu punto de conexión pueda comprobar que el artículo realmente proviene de nosotros y no ha sido alterado.
Qué debe hacer tu punto de conexión
- Acepta una solicitud POST por HTTPS con un cuerpo JSON en una URL pública.
- Responde en un plazo de 15 segundos con un código de estado 2xx. Realiza las tareas lentas (descargas de imágenes, reconstrucciones de caché) después de responder, no antes.
- Responde a cada ping con un 2xx. Un ping no contiene ningún artículo y no debe escribir nada, así que puedes responder antes de verificarlo (o sin hacerlo): eso permite que Probar & conectar funcione mientras tu secreto aún se está desplegando.
- Verifica X-BlogTend-Signature en cada evento de artículo (código a continuación), y responde con 401 cuando no coincida. No exigimos esta verificación, pero sin ella cualquiera que encuentre la URL podría enviar artículos falsos a tu sitio.
- Usa article.id como clave de idempotencia: si ya existe una publicación con ese identificador, actualízala en lugar de crear un duplicado.
- Cuando rechaces una solicitud, indica el motivo en el cuerpo de la respuesta (por ejemplo {"error": "firma no válida"}). Mostramos los primeros 300 caracteres en tu registro de entregas.
Cómo conectarlo
- Despliega tu punto de conexión receptor (el ejemplo al final está completo).
- En BlogTend, ve a Sitios → Conectar un sitio → Webhook, introduce un nombre y la URL del punto de conexión, y pulsa Siguiente.
- Mostramos la clave secreta de firma whsec_…. Cópiala ahora en el entorno de tu servidor y realiza el despliegue: esta pantalla es el único lugar donde se muestra.
- Pulsa Probar & conectar. Enviamos una solicitud de prueba firmada con ese secreto; una respuesta 2xx guarda la conexión. Si tu punto de conexión la rechaza, mostramos su código de estado y su propio mensaje de error para que puedas corregirlo y pulsar Probar & conectar de nuevo.
Volver a conectar la misma URL (para cambiar el nombre del sitio, o después de un error) nunca cambia el secreto: el cuadro de diálogo muestra el actual y la solicitud de prueba se firma con él. Para obtener un nuevo secreto, rótalo.
Cabeceras que enviamos
Cada solicitud, de comprobación o de artículo, incluye estas cabeceras:
| Cabecera | Valor | Notas |
|---|---|---|
| X-BlogTend-Signature | t=<unix seconds>,v1=<hex>[,v1=<hex>] | Firma HMAC-SHA256. Un v1 por cada clave secreta activa: dos solo durante el período de gracia de 24 horas de una renovación, con la más reciente primero. |
| X-BlogTend-Event | ping | article.published | article.updated | Igual que el campo event en el cuerpo. |
| X-BlogTend-Delivery | whd_… | Igual que delivery_id en el cuerpo. Nuevo en cada intento, incluidos los reintentos. |
| X-BlogTend-Test | 1 | Solo en las comprobaciones de conexión (Probar & conectar, Volver a verificar, actualizar categorías). Una comprobación de conexión no contiene ningún artículo y no debe escribir nada. |
| Content-Type | application/json | El cuerpo es JSON en UTF-8. |
| User-Agent | BlogTend-Webhook/1 | Era YoDon-Webhook/1 antes del cambio de nombre. |
| X-Yodon-Signature, X-Yodon-Event, X-Yodon-Delivery | deprecated | Se envían para los receptores creados antes del cambio de nombre, con los mismos valores, salvo que X-Yodon-Signature siempre contiene exactamente una firma v1, generada con el secreto más reciente. Los nuevos receptores deben leer las cabeceras X-BlogTend-*. |
Las cabeceras X-Yodon-* están obsoletas. Siguen funcionando para los receptores creados antes del cambio de nombre, pero solo X-BlogTend-Signature contiene ambas firmas durante una rotación, así que adopta los nuevos nombres la próxima vez que modifiques tu receptor.
La carga útil, campo por campo
Llegan tres tipos de eventos, que se distinguen por el campo event y la cabecera X-BlogTend-Event: ping (prueba de conexión, sin artículo), article.published (primera entrega de un artículo) y article.updated (el mismo artículo de nuevo: tras un reintento que hayas iniciado, la publicación de un borrador, o la actualización de una entrada ya publicada, en cuyo caso url indica la entrada publicada que se debe reemplazar e id es el id que guardaste para ella). La estructura 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 es el estado que solicitamos: "publish" o "draft". Los artículos programados llegan a la hora prevista con "publish".
- article.html es el cuerpo completo del artículo. Las referencias a imágenes que contiene ya apuntan a URL públicas.
- category y categories son nombres, no identificadores — asígnalos a tu propia taxonomía, o ignóralos.
- featured_image es null cuando el artículo no tiene imagen. La URL está firmada y es estable — descarga la imagen en cualquier momento.
- delivery_id identifica este intento concreto y cambia en cada reintento; article.id identifica el artículo y nunca cambia. Elimina duplicados usando article.id.
- El cuerpo de un ping solo contiene event, delivery_id, site_id y sent_at; no incluye ningún artículo.
Verificación de la firma
Cada solicitud incluye X-BlogTend-Signature: t=<unix seconds>,v1=<hex>. Cada valor v1 es un HMAC-SHA256 de la cadena "<t>.<raw body>" calculado con una clave secreta whsec_. Verifica la firma con el cuerpo sin procesar de la solicitud, antes de analizar el JSON, rechaza las marcas de tiempo con más de 5 minutos de diferencia, compara con una función segura frente a ataques de temporización y acepta la solicitud si algún valor v1 coincide con tu clave secreta: durante una rotación hay dos.
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 receptor antiguo que lee X-Yodon-Signature con un patrón exacto de un único v1 sigue funcionando: esa cabecera sigue teniendo exactamente un v1, generado con el secreto más reciente.
Rotación del secreto
¿Perdiste el secreto o lo reemplazas periódicamente? En la página Sitios, abre la tarjeta del sitio con webhook y pulsa Rotar secreto. El nuevo secreto se muestra una sola vez.
- Durante las próximas 24 horas cada solicitud incluye dos firmas en X-BlogTend-Signature: una generada con la nueva clave secreta, otra con la anterior. Un receptor que tenga cualquiera de las dos claves secretas puede verificar la firma, así que nada falla mientras actualizas tu servidor.
- Después de 24 horas solo se usa la nueva clave secreta para firmar. La tarjeta muestra cuándo deja de usarse la anterior.
- Si se filtró el secreto anterior, marca "desactivarlo ahora" al rotarlo: no hay período de gracia, y las entregas fallan con un error 401 hasta que tu servidor tenga el nuevo secreto.
- Los receptores que aún leen el encabezado obsoleto X-Yodon-Signature solo ven la firma del nuevo secreto, por lo que para ellos el cambio es inmediato.
Contenido de la respuesta
Cualquier código 2xx cuenta como entrega realizada; el cuerpo es opcional y puede estar vacío. Responde con JSON y BlogTend se vuelve más inteligente:
{ "ok": true, "id": "123", "url": "https://example.com/blog/my-post" }- url — se guarda como enlace al artículo publicado: el botón Ver del panel lo abre y la función de indexación lo envía a Google.
- id — el identificador de tu publicación, que se guarda para que también puedas asociar futuros eventos article.updated en tu sistema.
- held — un motivo breve cuando aceptaste el artículo pero no lo publicaste en tu sitio (una cola editorial, una norma interna). BlogTend lo muestra entonces como borrador con ese motivo en lugar de como una publicación, y no envía el correo de "published". Responder con status: "draft" hace lo mismo sin indicar un motivo.
Para un ping, también basta con cualquier código 2xx. El único campo que leemos de la respuesta a un ping es categories: responde con { ok: true, categories: ["Guías", "Noticias"] } (un arreglo de hasta 100 nombres) y esos nombres aparecerán como opciones reales de categoría en los formularios de creación de BlogTend. Se actualizan cada vez que vuelves a verificar y al pulsar el botón de actualización del selector de categorías. Omite el campo y la IA simplemente propondrá nombres de categorías en su lugar.
Fallos, reintentos y duplicados
Cómo interpreta BlogTend cada respuesta a la entrega de un artículo:
| Tu respuesta | Qué significa para nosotros | ¿Reenviado? |
|---|---|---|
| 2xx | Entregado. El cuerpo es opcional; si el cuerpo es JSON, se lee como se describe en Qué responder. | Listo. |
| 3xx | Falló. No se siguen las redirecciones: conecta la URL final en su lugar. | No se reenvía de inmediato. |
| 401 / 403 | Fallido: se muestra como "firma rechazada". La clave secreta de tu servidor no coincide. Pégala de nuevo o renuévala. | No se reenvía de inmediato. |
| 503 | Falló: se muestra como "aún no configurado", la respuesta habitual de un receptor cuya clave secreta no está configurada. | Reenviado una vez, de inmediato. |
| Otros 4xx | Fallido: entendiste la solicitud y la rechazaste. | No se reenvía de inmediato. |
| Otros 5xx | Fallido: tu punto de conexión falló o no está disponible. | Reenviado una vez, de inmediato. |
| Sin respuesta en 15 s, o un error de red | Falló: se muestra como "no se pudo acceder a tu punto de conexión". | Reenviado una vez, de inmediato. |
- Artículos que BlogTend publica por su cuenta (automatizaciones y entradas programadas): si la entrega sigue fallando, se vuelve a intentar enviar el artículo completo en las dos ejecuciones siguientes, separadas por aproximadamente un minuto, independientemente de la respuesta recibida. Tras el tercer intento fallido BlogTend se detiene, conserva el artículo terminado y te envía el motivo por correo electrónico. Vuelve a publicarlo cuando hayas corregido tu punto de conexión.
- Artículos que publicas manualmente: sin reintentos automáticos. El motivo aparece en el artículo de inmediato; pulsa Publicar de nuevo, o Reintentar en Entregas recientes.
- Un artículo nunca se pierde: si falla la publicación, permanece en BlogTend con el motivo, listo para volver a publicarse.
- Dónde mirar: en la página Sitios, la tarjeta de un sitio conectado mediante webhook muestra Entregas recientes. Cada una muestra el código de estado, su significado habitual y los primeros 300 caracteres de tu mensaje de error; las fallidas tienen un botón Reintentar. Se almacena el primer kilobyte de cada respuesta.
- Debido a los reintentos, tu punto de conexión puede recibir el mismo artículo dos veces, cada vez con un nuevo delivery_id. Por eso es importante insertar o actualizar según article.id.
Un ejemplo completo de receptor
Una ruta de Next.js App Router que verifica, elimina duplicados y almacena. Sustituye savePost por tu propia lógica de persistencia y estará lista para producción:
// 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 misma lógica se puede adaptar a cualquier entorno de desarrollo en pocos minutos — las preguntas frecuentes de abajo responden a las dudas habituales sobre las plataformas.
Preguntas
¿Con qué plataformas funciona?
Perdí la clave secreta de firma. ¿Dónde puedo encontrarla de nuevo?
¿Funcionan los artículos programados?
¿Qué pasa con los borradores?
¿Cómo llegan las imágenes?
¿Se reintenta alguna vez el envío de los datos? ¿Veré duplicados?
¿Se reciben los metadatos SEO?
¿Vas a publicar en WordPress en su lugar? Lee la guía de WordPress
Tu entorno tecnológico, nuestros artículos
Conecta un punto de conexión una sola vez y todos los artículos — publicados manualmente, en lote o de forma automática — llegarán directamente a tu sitio.