Перейти к содержимому

Руководство

Подключите любой сайт через вебхук

Не используете WordPress? Не проблема. BlogTend отправляет каждую готовую статью POST-запросом в виде подписанного JSON на указанный вами адрес — любой стек, любая CMS, около двадцати строк кода с вашей стороны.

Что такое коннектор вебхуков

Когда генерация статьи завершается — или наступает запланированное время публикации — BlogTend отправляет её на вашу конечную точку одним POST-запросом в формате JSON: заголовок, полный HTML, краткое описание, метаданные SEO, названия категорий и тегов, язык, источники, а также общедоступный URL основного изображения. Ваша конечная точка сохраняет статью так, как ваш сайт хранит контент, а всё остальное в BlogTend работает точно так же, как для сайтов на WordPress: автоматизации, публикация по расписанию, черновики, панель управления, индексация в Google.

Каждый запрос подписывается секретным ключом, известным только вам и BlogTend, чтобы ваша конечная точка могла убедиться, что статья действительно пришла от нас и не была изменена.

Что должна делать ваша конечная точка

  • Принимайте POST-запросы по HTTPS с телом в формате JSON по общедоступному URL.
  • Отвечайте в течение 15 секунд кодом состояния 2xx. Длительные операции (скачивание изображений, перестроение кеша) выполняйте после отправки ответа, а не до.
  • Отвечайте на каждый проверочный запрос кодом 2xx. Такой запрос не содержит статьи и не должен приводить к записи данных, поэтому на него можно ответить до проверки (или без неё): именно это позволяет выполнить проверку и подключение, пока ваш секретный ключ ещё внедряется.
  • Проверяйте X-BlogTend-Signature при каждом событии статьи (код ниже), и возвращайте код 401 при несовпадении. Мы не требуем этой проверки, но без неё любой, кто найдёт URL, сможет отправлять на ваш сайт поддельные статьи.
  • Используйте article.id как ключ идемпотентности: если запись с таким идентификатором уже существует, обновите её вместо создания дубликата.
  • При отклонении запроса укажите причину в теле ответа (например {"error": "invalid signature"}). Мы показываем первые 300 символов в вашем журнале доставки.

Подключение

  1. Разверните свою конечную точку для приёма запросов (внизу приведён полный пример).
  2. В BlogTend перейдите в Сайты → Подключить сайт → Вебхук, введите название и URL конечной точки, затем нажмите Далее.
  3. Мы показываем секретный ключ подписи whsec_…. Скопируйте его в переменные окружения вашего сервера сейчас и выполните развёртывание: ключ отображается только на этом экране.
  4. Нажмите Проверить & подключить. Мы отправим проверочный запрос, подписанный этим секретом; ответ 2xx сохранит подключение. Если ваша конечная точка отклонит запрос, мы покажем её код состояния и сообщение об ошибке, чтобы вы могли устранить проблему и снова нажать Проверить & подключить.

Повторное подключение того же URL (для переименования сайта или после ошибки) никогда не меняет секретный ключ: в диалоговом окне отображается текущий ключ, и тестовый запрос подписывается им. Чтобы получить новый секретный ключ, выполните его ротацию.

Отправляемые заголовки

Каждый запрос, проверочный или со статьёй, содержит эти заголовки:

ЗаголовокЗначениеПримечания
X-BlogTend-Signaturet=<unix seconds>,v1=<hex>[,v1=<hex>]Подпись HMAC-SHA256. Одна v1 на каждый действующий секретный ключ: две только в течение 24-часового переходного периода при замене ключа, сначала самая новая.
X-BlogTend-Eventping | article.published | article.updatedСовпадает с полем event в теле запроса.
X-BlogTend-Deliverywhd_…Совпадает с delivery_id в теле запроса. Меняется при каждой попытке, включая повторные.
X-BlogTend-Test1Только при проверочных запросах (Проверить & подключить, Повторная проверка, обновление категорий). Проверочный запрос не содержит статьи и не должен ничего записывать.
Content-Typeapplication/jsonТело запроса содержит JSON в кодировке UTF-8.
User-AgentBlogTend-Webhook/1До переименования назывался YoDon-Webhook/1.
X-Yodon-Signature, X-Yodon-Event, X-Yodon-DeliverydeprecatedОтправляются для обработчиков, созданных до переименования, с теми же значениями, но X-Yodon-Signature всегда содержит ровно одну подпись v1, созданную с использованием новейшего секретного ключа. Новые обработчики должны читать заголовки X-BlogTend-*.

Заголовки X-Yodon-* устарели. Они продолжают работать для обработчиков, созданных до переименования, но только X-BlogTend-Signature содержит обе подписи при ротации ключей, поэтому перейдите на новые имена, когда в следующий раз будете дорабатывать обработчик.

Содержимое запроса, поле за полем

Поступают события трёх типов, которые различаются по полю event и заголовку X-BlogTend-Event: ping (проверка соединения, без статьи), article.published (первая доставка статьи) и article.updated (та же статья повторно: после запущенной вами повторной попытки, публикации черновика или обновления уже опубликованной записи, при котором url указывает на опубликованную запись для замены, а id — это сохранённый вами идентификатор этой записи). Полная структура:

{
  "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 — это запрашиваемый нами статус: "publish" или "draft". Статьи с отложенной публикацией поступают в назначенное время со статусом "publish".
  • article.html содержит полный текст статьи. Ссылки на изображения в нём уже указывают на общедоступные URL.
  • category и categories содержат названия, а не идентификаторы — сопоставьте их с вашей системой категорий, либо игнорируйте их.
  • featured_image имеет значение null, если у статьи нет изображения. URL подписан и не меняется — загружайте изображение по нему в любое время.
  • delivery_id идентифицирует конкретную попытку и меняется при каждой повторной попытке; article.id идентифицирует статью и никогда не меняется. Исключайте дубликаты по article.id.
  • Тело проверочного запроса содержит только event, delivery_id, site_id и sent_at; статьи в нём нет.

Проверка подписи

Каждый запрос содержит X-BlogTend-Signature: t=<unix seconds>,v1=<hex>. Каждое значение v1 — это HMAC-SHA256 строки "<t>.<raw body>" с секретным ключом whsec_. Проверяйте подпись по исходному телу запроса до разбора JSON, отклоняйте временные метки с отклонением более 5 минут, сравнивайте значения функцией с постоянным временем выполнения и принимайте запрос, если хотя бы одно значение v1 соответствует вашему секретному ключу: при смене ключа их два.

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

Старый обработчик, который читает X-Yodon-Signature по строгому шаблону с единственным v1, продолжит работать: этот заголовок по-прежнему содержит ровно один v1, созданный с использованием самого нового секретного ключа.

Смена секретного ключа

Потеряли секретный ключ или меняете его по расписанию? На странице Сайты откройте карточку сайта с вебхуком и нажмите Заменить секретный ключ. Новый секретный ключ показывается только один раз.

  • В течение следующих 24 часов каждый запрос содержит две подписи в X-BlogTend-Signature: одну с новым секретным ключом, другую со старым. Получатель с любым из этих ключей сможет проверить подпись, поэтому при обновлении вашего сервера сбоев не будет.
  • Через 24 часа для подписи используется только новый секретный ключ. В карточке указано, когда старый ключ перестанет действовать.
  • Если старый секретный ключ скомпрометирован, при замене отметьте "отключить сейчас": переходного периода не будет, и отправки будут завершаться ошибкой 401, пока на вашем сервере не будет установлен новый секретный ключ.
  • Получатели, которые всё ещё считывают устаревший X-Yodon-Signature, видят только подпись, созданную с новым секретным ключом, поэтому для них переход происходит немедленно.

Содержание ответа

Любой код 2xx означает успешную доставку; тело ответа необязательно и может быть пустым. Отправьте ответ в формате JSON, и BlogTend станет умнее:

{ "ok": true, "id": "123", "url": "https://example.com/blog/my-post" }
  • url — сохраняется как ссылка на опубликованную статью: кнопка «Просмотр» на панели управления открывает её, а функция индексации Google отправляет её на индексацию.
  • id — идентификатор вашей публикации, который сохраняется, чтобы вы тоже могли сопоставлять последующие события article.updated на своей стороне.
  • held — краткая причина, по которой вы приняли статью, но не разместили её на своём сайте (редакционная очередь, внутреннее правило). BlogTend затем отображает её как черновик с указанием этой причины, а не как опубликованную запись, и не отправляет уведомление "published" по электронной почте. Ответ status: "draft" даёт тот же результат, но без указания причины.

Для проверочного запроса тоже достаточно любого кода 2xx. Единственное поле, которое мы считываем из ответа на проверочный запрос, — categories: ответьте { ok: true, categories: ["Руководства", "Новости"] } (массив до 100 названий), и эти названия появятся как доступные категории в формах создания BlogTend. Список обновляется при каждой повторной проверке и при нажатии кнопки обновления в меню выбора категории. Если не передавать это поле, ИИ будет просто предлагать названия категорий.

Ошибки, повторные попытки и дубликаты

Как BlogTend интерпретирует каждый ответ на отправку статьи:

Ваш ответЧто это значит для насОтправлено повторно?
2xxДоставлено. Тело ответа необязательно; тело в формате JSON обрабатывается так, как описано в разделе Что отвечать.Готово.
3xxОшибка. Переходы по перенаправлениям не выполняются: подключите конечный URL.Не отправляется повторно сразу.
401 / 403Ошибка: отображается как "подпись отклонена". Секретный ключ на вашем сервере не совпадает. Вставьте его повторно или замените.Не отправляется повторно сразу.
503Ошибка: отображается как "ещё не настроено", обычный ответ обработчика, для которого не задан секретный ключ.Отправлено повторно один раз, сразу же.
Другие 4xxОшибка: вы распознали запрос и отклонили его.Не отправляется повторно сразу.
Другие 5xxОшибка: ваша конечная точка завершила работу со сбоем или недоступна.Отправлено повторно один раз, сразу же.
Нет ответа в течение 15 с, или сетевая ошибкаОшибка: отображается как "не удалось связаться с вашей конечной точкой".Отправлено повторно один раз, сразу же.
  • Статьи, которые BlogTend публикует самостоятельно (автоматизации и публикации по расписанию): если доставка всё же не удалась, отправка всей статьи повторяется при следующих двух запусках с интервалом около минуты, независимо от полученного ответа. После третьей неудачной попытки BlogTend прекращает отправку, сохраняет готовую статью и сообщает вам причину по электронной почте. Опубликуйте её снова после устранения неполадок на вашей конечной точке.
  • Для статей, которые вы публикуете вручную: автоматических повторных попыток нет. Причина сразу отображается в статье; нажмите «Опубликовать» ещё раз или «Повторить» в разделе «Последние доставки».
  • Статья никогда не теряется: если публикация не удалась, она остаётся в BlogTend с указанием причины, готовая к повторной публикации.
  • Где искать: на странице Сайты в карточке сайта с вебхуком есть раздел Последние отправки. Для каждой отправки показаны код состояния, его обычное значение и первые 300 символов вашего сообщения об ошибке; для неудачных отправок доступна кнопка Повторить. Сохраняется первый килобайт каждого ответа.
  • Из-за повторных попыток ваша конечная точка может получить одну и ту же статью дважды, каждый раз с новым delivery_id. Поэтому важно выполнять вставку или обновление по article.id.

Полный пример обработчика входящих запросов

Обработчик маршрута Next.js App Router, который проверяет данные, устраняет дубликаты и сохраняет их. Замените savePost своей реализацией сохранения данных, и он готов к использованию в рабочей среде:

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

Эту же логику можно перенести на любой фреймворк за несколько минут — в разделе часто задаваемых вопросов ниже разобраны типичные вопросы о платформах.

Вопросы

С какими платформами это работает?
Любой набор технологий, способный принимать HTTPS POST: Next.js, Laravel, Django, Rails, Express, Cloudflare Worker, инструмент без программирования вроде Make или n8n, собственная CMS — если у него есть URL, он может получать статьи.
Я потерял секретный ключ подписи. Где его снова найти?
Нигде: мы храним его в зашифрованном виде и показываем только при подключении. Нажмите «Сменить секрет» на карточке сайта в разделе «Сайты». Вы получите новый секрет, а старый продолжит использоваться для подписи наряду с новым в течение 24 часов, чтобы доставка продолжалась, пока вы обновляете свой сервер.
Работает ли публикация статей по расписанию?
Да. За публикацию по расписанию отвечает BlogTend: в запланированное время мы отправляем статью со статусом "publish". Вашей конечной точке не нужно реализовывать публикацию по расписанию.
А что с черновиками?
Статья, отправленная как черновик, поступает со статусом "draft". Что это означает на вашем сайте, решаете вы — большинство получателей сохраняют запись без публикации. Когда вы позже нажмёте Опубликовать в BlogTend, та же статья поступит снова как событие article.updated со статусом "publish".
Как передаются изображения?
Главное изображение доступно на наших серверах по подписанному публичному URL (featured_image.url), и HTML статьи ссылается на тот же URL. Скачайте его и разместите у себя или загружайте напрямую с наших серверов — оба варианта работают.
Отправляются ли данные повторно? Будут ли дубликаты?
Да. При сбое отправка повторяется, и каждая повторная попытка получает новый delivery_id, поэтому ваша конечная точка может получить одну и ту же статью несколько раз. Используйте article.id как ключ идемпотентности: обновляйте существующую публикацию вместо создания новой, и дубликаты станут невозможны.
Передаются ли метаданные SEO?
Да — seo.title, seo.description и seo.keyword передаются вместе с каждой статьёй, а также с кратким описанием, тегами, названиями категорий и языком. Используйте то, что поддерживает ваша платформа, и игнорируйте остальное.

Хотите вместо этого публиковать в WordPress? Прочитайте руководство по WordPress

Ваши технологии, наши статьи

Подключите конечную точку один раз, и каждая статья — созданная вручную, в рамках массового создания или автоматически — будет поступать прямо на ваш сайт.