Menú de la documentación

Desarrolladores

Webhooks

Recibe un POST firmado en tu servidor cada vez que Blogenia publica un artículo. Eventos, firma HMAC, ejemplos en Node, Python y PHP, y reintentos.

Ver como Markdown

Un webhook es una URL de tu servidor a la que Blogenia envía un POST con JSON cuando ocurre un evento. Así te enteras de cada publicación sin consultar la API una y otra vez.

Crear un webhook

  1. En el panel, abre API y pulsa Añadir webhook.
  2. Indica la URL que recibirá los avisos. Debe ser https, sin usuario ni contraseña, apuntar a una dirección pública y usar el puerto 80 o el 443.
  3. Copia el secreto (whsec_…). Solo se muestra una vez.
  4. Pulsa Enviar prueba para comprobar que tu servidor responde.

Puedes tener hasta 5 webhooks. Solo las organizaciones con plan Starter o Pro reciben avisos.

Eventos

EventoCuándo se envía
article.publishedSe publica un artículo: a mano, programado o por el piloto automático. Si un artículo se despublica y se vuelve a publicar, se envía otro aviso.
pingAl pulsar Enviar prueba en el panel. No se puede suscribir.

La petición

Blogenia envía un POST con Content-Type: application/json y estas cabeceras:

CabeceraContenido
Blogenia-Eventarticle.published o ping.
Blogenia-DeliveryIdentificador de la entrega. Es el mismo en todos los reintentos y coincide con id del cuerpo.
Blogenia-TimestampSegundos Unix en que se firmó. Cambia en cada reintento.
Blogenia-Signaturev1= seguido de la firma HMAC-SHA256 en hexadecimal.
User-AgentBlogenia-Webhooks/1.0

Campos del cuerpo

CampoTipoDescripción
idstringIdentificador de la entrega.
eventstringarticle.published o ping.
createdAtstringISO 8601 en UTC.
dataobjectEn article.published: { article, blogId }, donde article tiene los mismos campos que un artículo de la lista. En ping: { webhookId }.
Cuerpo de article.published
{
  "id": "66f1c0a2b3c4d5e6f7a8b9d0",
  "event": "article.published",
  "createdAt": "2026-10-01T09:00:03.120Z",
  "data": {
    "article": {
      "id": "66f1c0a2b3c4d5e6f7a8b9c1",
      "title": "¿Cuánto cuesta reformar una cocina?",
      "slug": "cuanto-cuesta-reformar-una-cocina",
      "url": "https://reformas-ejemplo.blogelia.com/cuanto-cuesta-reformar-una-cocina",
      "excerpt": "Precios orientativos y qué incluye cada partida.",
      "publishedAt": "2026-10-01T09:00:00.000Z",
      "updatedAt": "2026-10-02T10:30:00.000Z",
      "category": { "slug": "reformas", "name": "Reformas" },
      "coverImage": { "url": "https://cdn.example.com/cover.webp", "alt": "Cocina reformada" },
      "readingMinutes": 5,
      "faqs": [{ "q": "¿Cuánto tarda una reforma?", "a": "Entre dos y cuatro semanas." }]
    },
    "blogId": "66f1c0a2b3c4d5e6f7a8b9c0"
  }
}
Cuerpo de ping
{
  "id": "66f1c0a2b3c4d5e6f7a8b9d1",
  "event": "ping",
  "createdAt": "2026-10-01T09:00:03.120Z",
  "data": { "webhookId": "66f1c0a2b3c4d5e6f7a8b9d2" }
}

Verificar la firma

Comprueba siempre la firma: así sabes que el aviso lo envía Blogenia y que nadie lo ha modificado.

  1. Lee el cuerpo en bruto, tal como llega, sin volver a serializar el JSON.
  2. Calcula HMAC-SHA256 con tu secreto completo (incluido el prefijo whsec_) sobre el texto <timestamp>.<cuerpo>, donde <timestamp> es el valor de Blogenia-Timestamp. Pásalo a hexadecimal y antepón v1=.
  3. Compáralo con Blogenia-Signature en tiempo constante.
  4. Rechaza la petición si el timestamp tiene más de 5 minutos de antigüedad.
Receptor con verificación
import crypto from 'node:crypto';
import express from 'express';

const SECRET = process.env.BLOGENIA_WEBHOOK_SECRET; // whsec_…
const app = express();

function isValid(rawBody, headers) {
  const timestamp = headers['blogenia-timestamp'];
  const received = headers['blogenia-signature'] || '';
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > 300) return false;
  const expected =
    'v1=' + crypto.createHmac('sha256', SECRET).update(timestamp + '.' + rawBody).digest('hex');
  return (
    received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected))
  );
}

// express.raw conserva el cuerpo en bruto.
app.post('/webhooks/blogenia', express.raw({ type: 'application/json' }), (req, res) => {
  const rawBody = req.body.toString('utf8');
  if (!isValid(rawBody, req.headers)) return res.sendStatus(400);

  const event = JSON.parse(rawBody);
  if (event.event === 'article.published') {
    console.log('Publicado:', event.data.article.title);
  }
  res.sendStatus(200);
});

app.listen(3000);
import hashlib
import hmac
import json
import os
import time

from flask import Flask, abort, request

SECRET = os.environ["BLOGENIA_WEBHOOK_SECRET"]  # whsec_…
app = Flask(__name__)


def is_valid(raw_body: bytes, headers) -> bool:
    timestamp = headers.get("Blogenia-Timestamp", "")
    received = headers.get("Blogenia-Signature", "")
    try:
        age = abs(time.time() - int(timestamp))
    except ValueError:
        return False
    if age > 300:
        return False
    signed = timestamp.encode() + b"." + raw_body
    expected = "v1=" + hmac.new(SECRET.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(received, expected)


@app.post("/webhooks/blogenia")
def blogenia():
    raw_body = request.get_data()  # cuerpo en bruto
    if not is_valid(raw_body, request.headers):
        abort(400)
    event = json.loads(raw_body)
    if event["event"] == "article.published":
        print("Publicado:", event["data"]["article"]["title"])
    return "", 200
<?php
$secret = getenv('BLOGENIA_WEBHOOK_SECRET'); // whsec_…

$rawBody = file_get_contents('php://input'); // cuerpo en bruto
$timestamp = $_SERVER['HTTP_BLOGENIA_TIMESTAMP'] ?? '';
$received = $_SERVER['HTTP_BLOGENIA_SIGNATURE'] ?? '';

if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {
    http_response_code(400);
    exit;
}

$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
if (!hash_equals($expected, $received)) {
    http_response_code(400);
    exit;
}

$event = json_decode($rawBody, true);
if ($event['event'] === 'article.published') {
    error_log('Publicado: ' . $event['data']['article']['title']);
}
http_response_code(200);

Tu respuesta

  • Responde con cualquier código 2xx en menos de 10 segundos. Es la única respuesta que cuenta como entrega correcta.
  • Cualquier otro código, un error de red o superar los 10 segundos cuenta como fallo y se reintenta. No seguimos redirecciones: un 3xx es un fallo.
  • Si respondes 410 Gone, desactivamos el webhook y no reintentamos. Puedes reactivarlo en el panel.
  • Leemos como máximo los primeros 4096 bytes de tu respuesta.

Reintentos

Cada entrega tiene hasta 5 intentos: el primero inmediato y los demás tras la espera indicada desde el fallo anterior.

IntentoCuándo
1Al publicarse el artículo
21 minuto después del fallo
35 minutos después
430 minutos después
52 horas después

Si fallan los cinco, la entrega queda como fallida. Puedes ver el estado de las entregas y reintentar una fallida desde API en el panel.

Buenas prácticas

  • Responde rápido y procesa después: guarda el aviso, devuelve 200 y haz el trabajo pesado en segundo plano.
  • Ignora duplicados con Blogenia-Delivery: el mismo aviso puede llegar más de una vez.
  • No te fíes del orden: dos publicaciones seguidas pueden llegar cambiadas, sobre todo con reintentos.
  • Verifica siempre la firma y el timestamp antes de hacer nada con el cuerpo.
  • Guarda el secreto en una variable de entorno, nunca en el código.
  • Para el contenido, pide el artículo con la API de lectura en lugar de confiar solo en el aviso.