Руководство
Подключите любой сайт через вебхук
Не используете 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 символов в вашем журнале доставки.
Подключение
- Разверните свою конечную точку для приёма запросов (внизу приведён полный пример).
- В BlogTend перейдите в Сайты → Подключить сайт → Вебхук, введите название и URL конечной точки, затем нажмите Далее.
- Мы показываем секретный ключ подписи whsec_…. Скопируйте его в переменные окружения вашего сервера сейчас и выполните развёртывание: ключ отображается только на этом экране.
- Нажмите Проверить & подключить. Мы отправим проверочный запрос, подписанный этим секретом; ответ 2xx сохранит подключение. Если ваша конечная точка отклонит запрос, мы покажем её код состояния и сообщение об ошибке, чтобы вы могли устранить проблему и снова нажать Проверить & подключить.
Повторное подключение того же URL (для переименования сайта или после ошибки) никогда не меняет секретный ключ: в диалоговом окне отображается текущий ключ, и тестовый запрос подписывается им. Чтобы получить новый секретный ключ, выполните его ротацию.
Отправляемые заголовки
Каждый запрос, проверочный или со статьёй, содержит эти заголовки:
| Заголовок | Значение | Примечания |
|---|---|---|
| X-BlogTend-Signature | t=<unix seconds>,v1=<hex>[,v1=<hex>] | Подпись HMAC-SHA256. Одна v1 на каждый действующий секретный ключ: две только в течение 24-часового переходного периода при замене ключа, сначала самая новая. |
| X-BlogTend-Event | ping | article.published | article.updated | Совпадает с полем event в теле запроса. |
| X-BlogTend-Delivery | whd_… | Совпадает с delivery_id в теле запроса. Меняется при каждой попытке, включая повторные. |
| X-BlogTend-Test | 1 | Только при проверочных запросах (Проверить & подключить, Повторная проверка, обновление категорий). Проверочный запрос не содержит статьи и не должен ничего записывать. |
| Content-Type | application/json | Тело запроса содержит JSON в кодировке UTF-8. |
| User-Agent | BlogTend-Webhook/1 | До переименования назывался YoDon-Webhook/1. |
| X-Yodon-Signature, X-Yodon-Event, X-Yodon-Delivery | deprecated | Отправляются для обработчиков, созданных до переименования, с теми же значениями, но 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 });
}Эту же логику можно перенести на любой фреймворк за несколько минут — в разделе часто задаваемых вопросов ниже разобраны типичные вопросы о платформах.
Вопросы
С какими платформами это работает?
Я потерял секретный ключ подписи. Где его снова найти?
Работает ли публикация статей по расписанию?
А что с черновиками?
Как передаются изображения?
Отправляются ли данные повторно? Будут ли дубликаты?
Передаются ли метаданные SEO?
Хотите вместо этого публиковать в WordPress? Прочитайте руководство по WordPress
Ваши технологии, наши статьи
Подключите конечную точку один раз, и каждая статья — созданная вручную, в рамках массового создания или автоматически — будет поступать прямо на ваш сайт.