/ Руководство для разработчиков
Публикация через API на любом сайте.
Как создать собственный endpoint, чтобы Orova публиковала готовые статьи на сайт, работающий не на WordPress.
Orova публикует статьи в WordPress «из коробки». Если ваш сайт не на WordPress — собственная CMS, headless-решение, статический сайт или свой backend, — вы все равно можете настроить автоматическую публикацию из Orova, принимая статьи через небольшой API. Вы пишете один HTTP endpoint; Orova вызывает его каждый раз, когда статья готова. Это руководство для разработчика, в нем описан полный контракт запроса.
/ Обзор
Как это работает
Когда Orova заканчивает статью под ключевое слово, она отправляет один HTTP-запрос POST на зарегистрированный вами endpoint URL. Тело запроса — JSON со всей статьей. Ваш endpoint создает запись на вашей стороне и возвращает итоговый публичный URL статьи. Orova сохраняет этот URL как ссылку на опубликованную статью — так же, как для записи в WordPress. Используйте этот способ, если нужна автоматическая публикация на сайте не на WordPress или вы хотите полностью контролировать хранение статей.
/ Настройка
Настройка — шесть шагов
- 1
Включите «Подключить через API» в проекте. Откройте проект (Проекты → ваш проект → Подключения). Рядом с карточкой WordPress есть карточка Подключить через API. Введите в нее URL вашего endpoint. Поле секретного ключа необязательно: оставьте его пустым — Orova сгенерирует ключ, или вставьте ключ, который уже использует ваш сайт. После сохранения ключ отображается на карточке с кнопкой «Копировать». Каждый проект публикует только в одно место: подключение через API блокирует кнопку WordPress, и наоборот.
- 2
Создайте endpoint, принимающий POST. На своем сайте или сервере создайте маршрут, который принимает HTTP POST с телом JSON. Именно этот URL вы вставляете на экране «Подключения» проекта. Orova вызывает его один раз для каждой готовой статьи.
- 3
Проверяйте Bearer token в каждом запросе. Каждый запрос от Orova содержит заголовок Authorization вида «Bearer <secret>» с точным секретным ключом из вашего проекта. Сравнивайте его с сохраненной копией и отклоняйте все, что не совпадает, — только это не дает посторонним публиковать на вашем сайте.
- 4
Создайте статью из payload. Прочитайте тело JSON и создайте запись в своей CMS или базе данных: title, slug и content_html — для самой статьи, excerpt — как meta description, featured_image_url — как обложку, а keyword / lang / published_at — как метаданные.
- 5
Верните реальный URL статьи. Ответьте HTTP 200 или 201 с телом JSON { "url": "https://yoursite.com/the-new-article" }. Orova сохранит этот URL как ссылку на опубликованную статью. Если вы вернете код не из группы 2xx или не укажете поле url, Orova сочтет публикацию неудачной.
- 6
Обрабатывайте запросы на обновление от движка Оптимизации. Если в теле есть action = "update", найдите существующую запись по target_url (или slug) и перезапишите ее title, content_html и excerpt на месте — URL не меняйте. Ответьте 200 с { "url": ... }, указывающим на ту же запись. Если endpoint пока это не обрабатывает, Orova просто сообщит о неудачной оптимизации; публикация новых статей работает как прежде.
/ Контракт
Запрос, который отправляет Orova
Каждая готовая статья приходит одним запросом такого вида:
POST <your endpoint URL>
Headers:
Content-Type: application/json
Authorization: Bearer <secret> # the secret key shown in Project -> Connections
User-Agent: Orova-SEO
Body (JSON):
{
"title": "...", # article headline
"slug": "...", # URL-friendly identifier
"content_html": "...", # full article HTML
"excerpt": "...", # meta description
"featured_image_url": "..." | null,
"keyword": "...", # target keyword
"lang": "en", # ISO language code
"published_at": "2026-05-17T09:00:00.482913", # ISO 8601, UTC, no offset suffix
"status": "draft" # optional: "draft" keeps it unpublished
}
# Optional fields are left out of the body when empty — never sent as null./ Справочник
Поля payload
Поля запроса на публикацию новой статьи. Запросы на удаление и обновление используют тот же endpoint с более короткими телами — см. соответствующие разделы ниже.
| Поле | Тип | Описание |
|---|---|---|
| title | string | Заголовок статьи. |
| slug | string | URL-friendly identifier suggested for the post. |
| content_html | string | Полный текст статьи в виде готового к публикации HTML. |
| excerpt | string | Краткое описание для meta description. |
| featured_image_url | string | null | URL обложки или null, если ее нет. |
| keyword | string | Целевое SEO-ключевое слово, под которое написана статья. |
| lang | string | Language as an ISO code, e.g. "en" or "vi". |
| published_at | string | Момент отправки статьи Orova, ISO 8601 в UTC, без суффикса часового пояса. |
| status | string | undefined | Необязательно. "draft" оставляет статью черновиком; "publish" или отсутствие значения — публикует ее. |
| action | string | undefined | Отсутствует у новых статей. "delete" — просьба удалить запись; "update" (отправляет движок Оптимизации) — просьба перезаписать существующую запись на месте. |
| target_url | string | Только при action = "update": реальный URL записи для перезаписи. URL не меняйте. |
| id | number | undefined | Only sent when Orova knows your post id (from `list` or `get`): match on it before `target_url`. |
Необязательные поля полностью исключаются из тела, если отправлять нечего — Orova никогда не отправляет null, поэтому читайте их с проверкой. status тоже необязателен: "draft" создает статью как черновик, а "publish" или отсутствие status публикует ее сразу.
/ Контракт
Ответ, который нужно вернуть
Создав запись, ответьте с HTTP-статусом 200 или 201 и таким телом JSON:
HTTP 200 (or 201)
Content-Type: application/json
{
"url": "https://yoursite.com/published-article"
}Orova сохраняет этот url как ссылку на опубликованную статью. Если endpoint вернет любой статус не из группы 2xx или тело, которое не является JSON-объектом с полем url, Orova сочтет публикацию неудачной и пометит статью, чтобы вы могли повторить попытку. Отвечайте в течение 60 секунд — после этого Orova прерывает запрос (это касается публикации, обновления и удаления).
/ Пример
Пример кода
Минимальный принимающий endpoint на Node.js с Express. Те же шаги — проверить токен, ветвиться по action (update / delete), создать или изменить запись, вернуть URL — подходят для любого языка и фреймворка.
// Node.js / Express — a minimal receiving endpoint
import express from "express";
const app = express();
app.use(express.json({ limit: "5mb" }));
// The secret Orova generated for this project.
const OROVA_SECRET = process.env.OROVA_SECRET;
app.post("/orova/publish", async (req, res) => {
// 1. Verify the Bearer token.
const auth = req.get("authorization") || "";
if (auth !== "Bearer " + OROVA_SECRET) {
return res.status(401).json({ error: "unauthorized" });
}
// 2. Required: list your articles (duplicate check, internal links). action = "list".
if (req.body.action === "list") {
const posts = await listPosts(req.body.limit || 500); // newest first, 500 max
// each post: { id, title, slug, link, date, modified, status, author, excerpt? }
return res.status(200).json({ ok: true, posts });
}
// 3. Required: read one article in full (used when optimizing it). action = "get".
if (req.body.action === "get") {
const post = await findPost(req.body.id, req.body.target_url, req.body.slug);
if (!post) return res.status(404).json({ ok: false, err: "not_found" });
return res.status(200).json({ ok: true, post });
}
// 4. Visual Editor: store an image, answer with its absolute URL.
if (req.body.action === "upload_image") {
const { filename, mime, data_base64 } = req.body;
if (!String(mime || "").startsWith("image/")) {
return res.status(422).json({ error: "image files only" });
}
const bytes = Buffer.from(data_base64, "base64");
if (bytes.length > 15 * 1024 * 1024) {
return res.status(413).json({ error: "image too large" });
}
return res.status(200).json({ ok: true, url: await saveImage(filename, bytes) });
}
// 5. Delete requests: body carries action = "delete".
if (req.body.action === "delete") {
const removed = await deletePostByUrl(req.body.url, req.body.slug);
if (!removed) return res.status(404).json({ deleted: true }); // already gone
return res.status(200).json({ deleted: true });
}
// 6. Update requests: action = "update". Optional fields may be absent.
if (req.body.action === "update") {
const updated = await updatePost(req.body.id, req.body.target_url, {
title: req.body.title,
html: req.body.content_html,
metaDescription: req.body.excerpt,
coverImage: req.body.featured_image_url, // optional
status: req.body.status, // optional: "publish" | "draft"
});
if (!updated) return res.status(404).json({ error: "post not found" });
return res.status(200).json({ url: updated.url });
}
// 7. New article: read the payload Orova sent.
const {
title, slug, content_html, excerpt,
featured_image_url, keyword, lang, published_at, status,
} = req.body;
// 8. Create the article in your own CMS or database.
const post = await createPost({
title,
slug,
html: content_html,
metaDescription: excerpt,
coverImage: featured_image_url,
keyword,
lang,
publishedAt: published_at,
draft: status === "draft", // status is optional; absent means publish now
});
// 9. Return 200/201 with the live URL.
return res.status(201).json({
url: "https://yoursite.com/" + post.slug,
});
});
app.listen(3000);/ Контракт — обновление
Обновление статей (Оптимизация)
Движок Оптимизации Orova переписывает уже опубликованные статьи — обновляет факты, улучшает заголовки, объединяет контент — и перезаписывает существующую запись на месте, сохраняя URL. Запрос идет на тот же endpoint URL как POST с тем же Bearer token:
POST <your endpoint URL>
Headers:
Content-Type: application/json
Authorization: Bearer <secret>
User-Agent: Orova-SEO
Body (JSON):
{
"action": "update", # always the literal string "update"
"id": 123, # optional: post id, match on this first
"target_url": "...", # public URL of the post to overwrite (match next)
"title": "...", # new headline
"slug": "...", # slug fallback for matching, may be empty
"content_html": "...", # full new article body as HTML
"excerpt": "...", # new meta description
"featured_image_url": "...", # optional: new cover image
"status": "publish" # optional: "publish" or "draft"
}
# Optional fields are left out of the body when empty — never sent as null.Найдите запись по target_url (если нет — по slug), замените заголовок, текст и meta description, оставьте URL как есть и ответьте 200 с { "url": "<same post URL>" }. Любой статус не из группы 2xx или тело без url заставят Orova пометить оптимизацию как неудачную (дубликат записи она никогда не создает). Endpoint без этой обработки продолжает публиковать новые статьи как раньше — поддержка обновления нужна, только когда вы используете экран Оптимизации.
С 29 августа тело обновления может также содержать id (сопоставляйте по нему раньше, чем по target_url), featured_image_url для новой обложки и status со значением "publish" или "draft". Все три поля необязательны; пустые исключаются из тела и никогда не отправляются как null.
/ Контракт — Visual Editor
Чтение статей (обязательно)
Orova должна видеть статьи, уже размещенные на вашем сайте, — чтобы не дублировать темы, добавлять внутренние ссылки и оптимизировать старые записи. Добавьте еще два action на тот же endpoint URL с тем же Bearer token: list возвращает список статей, get — одну статью целиком.
POST <your endpoint URL> # same endpoint, same Bearer secret
Body (JSON):
{
"action": "list", # always the literal string "list"
"limit": 500 # newest article first, 500 maximum
}
Response — HTTP 200:
{
"ok": true,
"posts": [
{
"id": 123, # your own post id, reused by "get" and "update"
"title": "...",
"slug": "...",
"link": "https://yoursite.com/the-article",
"date": "2026-08-29", # published date, YYYY-MM-DD
"modified": "2026-08-29",
"status": "publish", # "publish" or "draft"
"author": "...",
"excerpt": "..." # optional: short summary, 300 characters max
}
]
}
# Required: Orova reads this list to avoid duplicate topics,
# add internal links and analyze the whole site.POST <your endpoint URL>
Body (JSON):
{
"action": "get", # always the literal string "get"
"id": 123, # match on this first
"target_url": "...", # then the public URL
"slug": "..." # then the slug
}
Response — HTTP 200:
{
"ok": true,
"post": {
"id": 123,
"title": "...",
"slug": "...",
"link": "https://yoursite.com/the-article",
"content_html": "...", # full article body as HTML
"excerpt": "...",
"featured_image_url": "...",
"status": "publish" # "publish" or "draft"
}
}
No such post — HTTP 404:
{ "ok": false, "err": "not_found" }limit — не более 500, самая новая статья идет первой. Для get Orova отправляет id, target_url и slug вместе — сопоставляйте по тому, что узнаете, в этом порядке. Если ничего не найдено, ответьте 404 с { "ok": false, "err": "not_found" }. excerpt в list необязателен (до 300 символов). Обязательно: Orova вызывает list, чтобы узнать, какие статьи уже есть (избежать дублей, добавить внутренние ссылки и проанализировать весь сайт), и get, чтобы прочитать статью при оптимизации. Если сайт не отвечает на эти два action, новые статьи останутся без внутренних ссылок, а старые нельзя будет оптимизировать.
/ Контракт — Visual Editor
Загрузка изображений (Visual Editor)
Когда вы перетаскиваете изображение в Visual Editor, Orova отправляет его на тот же endpoint URL в виде base64 внутри тела JSON — без multipart и без второго URL, который нужно защищать.
POST <your endpoint URL>
Body (JSON):
{
"action": "upload_image", # always the literal string "upload_image"
"filename": "cover.png", # original file name, extension included
"mime": "image/png", # image/* only
"data_base64": "iVBORw0KGgo..." # file bytes, base64, 15 MB maximum
}
Response — HTTP 200 (or 201):
{
"ok": true,
"url": "https://yoursite.com/uploads/cover.png" # absolute URL
}Принимайте только image/* и отклоняйте файлы больше 15 MB. Сохраните файл и верните абсолютный URL изображения: относительный путь сломает статью, как только она отобразится в другом месте. Этот action нужен только для Visual Editor.
/ Контракт — удаление
Удаление статей
Когда пользователь удаляет опубликованную статью внутри Orova (на экране «Написать» или «Отчеты»), Orova просит ваш сайт тоже ее удалить. Запрос идет на тот же endpoint URL как POST с тем же Bearer token — отличается только тело:
POST <your endpoint URL>
Headers:
Content-Type: application/json
Authorization: Bearer <secret>
User-Agent: Orova-SEO
Body (JSON):
{
"action": "delete", # always the literal string "delete"
"url": "...", # public URL of the article to remove (match on this first)
"slug": "...", # slug fallback, may be empty
"keyword": "..." # the article's target keyword, for your logs
}Найдите запись по url (если нет — по slug), удалите ее или снимите с публикации и ответьте 200 с { "deleted": true } — подойдет и пустой 204. Если записи уже нет, ответьте 404, и Orova сочтет ее удаленной. Ответ { "deleted": false } сообщает Orova, что запись не удалена. Обычные запросы на публикацию никогда не содержат поля action, поэтому существующие endpoint продолжают работать без изменений — поддержка удаления необязательна, но рекомендуется.
/ Безопасность
Рекомендации по безопасности
- Всегда проверяйте Bearer token. Ваш endpoint — публичный URL, вызвать его может кто угодно. Заголовок Authorization — единственное, что доказывает, что запрос действительно пришел от Orova, поэтому отклоняйте каждый запрос, токен которого не совпадает в точности с сохраненным секретом.
- Используйте HTTPS для endpoint. Секрет передается в заголовке. HTTPS защищает его от перехвата при передаче.
- Не храните секрет в коде. Храните его в переменной окружения или менеджере секретов, никогда не прописывайте в файле, который коммитите. Если ключ утек, сгенерируйте новый в разделе Проект → Подключения.
- Считайте content_html контентом, а не доверенной разметкой. Храните и отображайте его так же аккуратно, как любой текст статьи в вашей CMS.
/ Итог
Вот и весь контракт
Когда endpoint работает и зарегистрирован в проекте, Orova публикует статьи на ваш сайт автоматически — точно так же, как для пользователей WordPress. Каждая готовая статья попадает на ваш сервер, становится записью, а ее публичный URL возвращается в Отчеты и Анализ, чтобы вы могли отслеживать результаты.
/ Нужна помощь?
Не получается подключить endpoint? Откройте раздел «Поддержка» в своем рабочем пространстве или напишите нам.
