#API статей
Статус: чтение — Доступно; правка, утверждение, публикация и генерация — Планируется.
GET /articlesиGET /articles/{id}работают.PATCH,POST …/approveиPOST …/publishв публичный API не выпущены: правка, утверждение и публикация доступны в кабинете Sapport. Описание этих точек — замысел, поля и коды предварительные. Сроки не обещаются.
API статей позволяет читать очередь статей, забирать текст в нужном формате, править и утверждать статьи, а также публиковать их на ваш сайт через модуль или отдавать вашей системе для самостоятельной публикации.
Доступ: articles:read — чтение; articles:write — правка, утверждение, публикация. Генерация (articles:generate) вынесена в отдельную страницу. Платно: нет. ПДн: нет.
#Базовый адрес
| Ячейка | Адрес |
|---|---|
| RU | https://formula-cream.pro/api/public/v1 |
| EN | https://wfacademy.org/api/public/v1 |
| ID | https://wfacademy.id/api/public/v1 |
Ошибки, пагинация, идемпотентность, пределы — Соглашения. Ключи и области — Аутентификация. Во всех примерах ключ заведомо ненастоящий.
#Точки
| Метод и путь | Область | Что делает |
|---|---|---|
GET /articles | articles:read | Список статей (Доступно) |
GET /articles/{id} | articles:read | Статья; формат содержимого html, markdown или blocks (Доступно) |
PATCH /articles/{id} | articles:write | Правка полей (Планируется) |
POST /articles/{id}/approve | articles:write | Утвердить (Планируется) |
POST /articles/{id}/publish | articles:write | Опубликовать через модуль или выдать вашей системе (Планируется) |
POST /articles/generation-jobs | articles:generate | Заказать генерацию (платно) — описание |
#Объект статьи
| Поле | Тип | Описание |
|---|---|---|
id | string (uuid) | Идентификатор |
title | string | Заголовок |
slug | string | null | Адрес страницы |
status | string | Статус статьи (см. ниже) |
locale | string | null | Язык: ru, en, id |
keyword | string | null | Основной поисковый запрос |
meta_title, meta_description | string | null | SEO-заголовок и описание |
cover_image_url | string | null | Обложка |
word_count | integer | Число слов (0, если не посчитано) |
content | string | array | Содержимое в запрошенном формате: html и markdown — строка, blocks — массив блоков. Только в GET /articles/{id}; в списке не возвращается. Пусто у статьи, чьё тело переехало в блог площадки |
publication | object | null | Состояние публикации (ниже). Только в GET /articles/{id}; null — статью ещё не выкладывали через модуль сайта |
created_at, updated_at, published_at | string (ISO 8601) | null | Даты, UTC |
#Статусы
Статусы, которые отдаёт API: draft (черновик), review (на проверке), approved (одобрена), published (опубликована), rejected (отклонена), transferred (передана в блог площадки), external (внешняя). Статус handed_off («выдана вам») относится к будущей выдаче статьи вашей системе и сейчас не возвращается.
#Состояние публикации (publication)
| Поле | Тип | Описание |
|---|---|---|
target | "connector" | Как публикуется: сейчас всегда через модуль на вашем сайте (none появится вместе с выдачей — Планируется) |
state | string | queued (в очереди или отдано модулю), applied, skipped, conflict, failed |
published_url | string | null | Адрес опубликованной страницы |
error | string | null | Причина конфликта или сбоя, до 200 знаков |
#GET /articles
Список статей арендатора, без содержимого и без publication. Порядок: updated_at по убыванию (статьи без даты в конце), затем id по убыванию. Читаются только собственные статьи арендатора (sp_seo_articles); статьи общего блога площадки в список не входят.
| Параметр | Тип | Обязательно | Ограничения | Пример |
|---|---|---|---|---|
limit | integer | нет | 1–100, по умолчанию 25 | 50 |
cursor | string | нет | Значение next_cursor; живёт 24 часа | |
status | string | нет | Один из статусов выше; иное значение — 422 | approved |
updated_after | string | нет | ISO 8601; только статьи с updated_at строго позже | 2026-10-01T00:00:00Z |
include_deleted | boolean | нет | Не поддерживается: удалённых статей API не хранит, true даёт 422 validation_failed (not_supported) | false |
{
"data": [
{
"id": "b9f27c14-0a3d-4e6b-8c51-7d2a9e4f1c03",
"title": "Как выбрать увлажняющий крем",
"slug": "kak-vybrat-uvlazhnyayushchij-krem",
"status": "approved",
"locale": "ru",
"word_count": 1850,
"updated_at": "2026-10-11T10:19:12Z"
}
],
"next_cursor": null
}
curl "https://formula-cream.pro/api/public/v1/articles?status=approved&limit=50" \
-H "Authorization: Bearer $SAPPORT_API_KEY"
# SAPPORT_API_KEY=sap_ru_test_EXAMPLE0KEYID_… (пример, не настоящий ключ)
const res = await fetch(
"https://formula-cream.pro/api/public/v1/articles?status=approved&limit=50",
{ headers: { Authorization: `Bearer ${process.env.SAPPORT_API_KEY}` } },
);
const { data, next_cursor } = await res.json();
r = httpx.get(
"https://formula-cream.pro/api/public/v1/articles",
params={"status": "approved", "limit": 50},
headers={"Authorization": f"Bearer {os.environ['SAPPORT_API_KEY']}"},
)
page = r.json()
$ch = curl_init("https://formula-cream.pro/api/public/v1/articles?status=approved&limit=50");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("SAPPORT_API_KEY")],
]);
$page = json_decode(curl_exec($ch), true);
#GET /articles/{id}
| Параметр | Тип | Обязательно | Ограничения | Пример |
|---|---|---|---|---|
id | string (uuid) | да | Статья вашего арендатора | |
format | string | нет | html, markdown или blocks; по умолчанию html | markdown |
Чужая или несуществующая статья — 404 not_found.
Содержимое статьи не больше 2 000 000 байт HTML — те же пределы, что у задания модуля. Больше — 422 validation_failed на поле /content. Неизвестный format — 422 на поле /format.
Форматы:
| Формат | Что в content |
|---|---|
html | Готовый HTML, очищенный от скриптов, обработчиков on* и ссылок javascript: |
markdown | Текст в Markdown |
blocks | Массив блоков {type, html}; других полей у блока нет, HTML очищен (скрипты, обработчики событий, ссылки и адреса картинок с опасной схемой убраны). Документ редактора (TipTap) преобразуется в HTML и отдаётся одним блоком {type: "html", html}; внутренний JSON редактора наружу не уходит |
Markdown строится из уже очищенного HTML; сложные конструкции (таблицы, вложенные списки) могут упроститься. Ответ не кэшируется общими кэшами (Cache-Control: no-store).
curl "https://formula-cream.pro/api/public/v1/articles/$ARTICLE_ID?format=html" \
-H "Authorization: Bearer $SAPPORT_API_KEY"
const res = await fetch(
`https://formula-cream.pro/api/public/v1/articles/${id}?format=html`,
{ headers: { Authorization: `Bearer ${process.env.SAPPORT_API_KEY}` } },
);
const article = await res.json();
r = httpx.get(
f"https://formula-cream.pro/api/public/v1/articles/{article_id}",
params={"format": "html"},
headers={"Authorization": f"Bearer {os.environ['SAPPORT_API_KEY']}"},
)
article = r.json()
$ch = curl_init("https://formula-cream.pro/api/public/v1/articles/$articleId?format=html");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("SAPPORT_API_KEY")],
]);
$article = json_decode(curl_exec($ch), true);
#PATCH /articles/{id}
Меняет поля статьи. Передавайте только то, что меняете.
| Параметр | Тип | Обязательно | Ограничения | Пример |
|---|---|---|---|---|
title | string | нет | "Новый заголовок" | |
slug | string | нет | Допустимый адрес страницы | |
content | string | нет | HTML до 2 000 000 байт; очищается при записи. Тело запроса — не более 256 КБ, иначе 413 payload_too_large; большие тексты правьте через кабинет | |
meta_title, meta_description | string | нет | ||
published_url | string | нет | Адрес страницы на вашем сайте; подтверждает публикацию при target: none | "https://example.com/blog/krem" |
Неверные значения — 422 validation_failed с errors[] (например, /slug). После правки опубликованной статьи следующая отправка через модуль создаёт новую версию задания (см. публикация через модули).
#POST /articles/{id}/approve
Переводит статью в статус approved. Тело не нужно. Утверждение бесплатно. Из состояния, где утверждение недопустимо (например, статья уже опубликована), — 409 invalid_state.
curl -X POST https://formula-cream.pro/api/public/v1/articles/$ARTICLE_ID/approve \
-H "Authorization: Bearer $SAPPORT_API_KEY" \
-H "Idempotency-Key: 5a3c8e21-9f4d-4b76-a1e0-2d7f6c9b8e53"
#POST /articles/{id}/publish
Запускает публикацию. Параметр target определяет путь.
| Параметр | Тип | Обязательно | Ограничения | Пример |
|---|---|---|---|---|
target | string | да | connector или none | "connector" |
site_id | string (uuid) | при connector | Привязанный сайт арендатора | |
publish_mode | string | при connector | draft или publish | "draft" |
#target = connector
Платформа создаёт задание модуля (см. публикация через модули). Итоговый режим всё равно ограничит сайт: если владелец включил потолок «только черновики», статья окажется черновиком.
| Ответ | Смысл |
|---|---|
200 + объект publication | Создана новая версия публикации (state: queued) либо источник не менялся и возвращается существующее задание |
404 not_found | Сайт или статья чужие либо не существуют |
409 invalid_state | Сайт не работает или не привязан, статья не утверждена, нечего публиковать (пустая статья) |
422 validation_failed | Не указан обязательный параметр; статья больше пределов (HTML 2 000 000 байт, payload 3 000 000 байт) — errors[].field = /content |
Публикация через модуль — не операция в смысле конверта operation: модуль забирает задание при очередном опросе. Результат (адрес страницы или код конфликта) приходит событием article.published или виден по GET /articles/{id} в поле publication.
#target = none
«Только отметить»: платформа ничего не публикует, статья получает состояние handed_off. Вы публикуете сами по событию article.ready или по данным GET /articles/{id}.
Статья считается опубликованной только после того, как вы подтвердите адрес страницы: PATCH /articles/{id} с published_url. До этого момента она остаётся «выданной».
# 1. Забрать текст
curl "https://formula-cream.pro/api/public/v1/articles/$ARTICLE_ID?format=html" \
-H "Authorization: Bearer $SAPPORT_API_KEY"
# 2. Объявить, что публикуете сами
curl -X POST https://formula-cream.pro/api/public/v1/articles/$ARTICLE_ID/publish \
-H "Authorization: Bearer $SAPPORT_API_KEY" \
-H "Idempotency-Key: 8d1e6f40-3b2a-4c97-b5f8-0a4e7c9d2b61" \
-H "Content-Type: application/json" \
-d '{"target":"none"}'
# 3. Подтвердить адрес
curl -X PATCH https://formula-cream.pro/api/public/v1/articles/$ARTICLE_ID \
-H "Authorization: Bearer $SAPPORT_API_KEY" \
-H "Idempotency-Key: 2c9a7d35-6e1b-4f08-8a3d-5b0c1e7f9a24" \
-H "Content-Type: application/json" \
-d '{"published_url":"https://example.com/blog/krem"}'
const base = "https://formula-cream.pro/api/public/v1";
const headers = {
Authorization: `Bearer ${process.env.SAPPORT_API_KEY}`,
"Content-Type": "application/json",
};
await fetch(`${base}/articles/${id}/publish`, {
method: "POST",
headers: { ...headers, "Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify({ target: "none" }),
});
// ...публикация на своём сайте...
await fetch(`${base}/articles/${id}`, {
method: "PATCH",
headers: { ...headers, "Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify({ published_url: "https://example.com/blog/krem" }),
});
base = "https://formula-cream.pro/api/public/v1"
h = {"Authorization": f"Bearer {os.environ['SAPPORT_API_KEY']}"}
httpx.post(f"{base}/articles/{article_id}/publish",
headers={**h, "Idempotency-Key": str(uuid.uuid4())},
json={"target": "none"})
# ...публикация на своём сайте...
httpx.patch(f"{base}/articles/{article_id}",
headers={**h, "Idempotency-Key": str(uuid.uuid4())},
json={"published_url": "https://example.com/blog/krem"})
$base = "https://formula-cream.pro/api/public/v1";
$call = function (string $method, string $path, array $body) use ($base) {
$ch = curl_init($base . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("SAPPORT_API_KEY"),
"Idempotency-Key: " . bin2hex(random_bytes(16)),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode($body),
]);
return json_decode(curl_exec($ch), true);
};
$call("POST", "/articles/$articleId/publish", ["target" => "none"]);
// ...публикация на своём сайте...
$call("PATCH", "/articles/$articleId", ["published_url" => "https://example.com/blog/krem"]);
#События
| Событие | Когда |
|---|---|
article.ready | Статья сгенерирована и готова (см. генерацию) |
article.published | Публикация подтверждена: модуль вернул applied с адресом либо вы передали published_url |
article.generation.failed | Генерация не удалась |
operation.completed | Завершилась операция article.generation (генерация) |
Нагрузка тонкая: тип, article_id, при публикации — published_url. Подпись и повторы — Вебхуки.
#Заметки
- Правка статьи после публикации не меняет страницу на сайте, пока вы не отправите её снова.
- Все
POSTиPATCHпринимаютIdempotency-Key; повтор с тем же телом безопасен (Идемпотентность). - Конфликты публикации через модуль (
edited_on_site,deleted_on_siteи другие) описаны в публикации через модули. - Пределы чтения и записи — в Соглашениях; стартовые значения 600 / 120 запросов в минуту.
#Ошибки
Общий формат и каталог кодов — Соглашения. Для этих точек:
| Код | HTTP | Когда |
|---|---|---|
invalid_api_key | 401 | Ключ не принят |
scope_missing | 403 | Нет articles:read / articles:write |
not_found | 404 | Статьи или сайта нет, либо они чужие |
invalid_state | 409 | Действие недопустимо в текущем состоянии статьи или сайта |
idempotency_conflict | 409 | Idempotency-Key уже использован с другим телом |
payload_too_large | 413 | Тело запроса больше 256 КБ |
validation_failed | 422 | Неверные значения; статья больше пределов |
rate_limited | 429 | Превышен предел частоты |
#Открытые вопросы
- Имя статуса выдачи (техническое).
handed_offне утверждён и пока не возвращается. - Состав блоков
blocks(техническое). Формат блоков редактора может расшириться. - Снятие с сайта (
unpublish) по API (техническое). Возможность есть в модуле и кабинете, в проекте API не названа. - Ручная правка опубликованной статьи (техническое). Как пользоваться API при конфликте
edited_on_site. - Тела ответов
publish(техническое). Точные тела при разных исходах, включая «слишком велика» и «пустая», уточнятся в OpenAPI. - Расписание публикации (техническое).
scheduled_atне реализовано.