◆ Sapport для разработчиков Справочник API Каталог ошибок OpenAPI
Разделы

#API статей

Статус: чтение — Доступно; правка, утверждение, публикация и генерация — Планируется. GET /articles и GET /articles/{id} работают. PATCH, POST …/approve и POST …/publish в публичный API не выпущены: правка, утверждение и публикация доступны в кабинете Sapport. Описание этих точек — замысел, поля и коды предварительные. Сроки не обещаются.

API статей позволяет читать очередь статей, забирать текст в нужном формате, править и утверждать статьи, а также публиковать их на ваш сайт через модуль или отдавать вашей системе для самостоятельной публикации.

Доступ: articles:read — чтение; articles:write — правка, утверждение, публикация. Генерация (articles:generate) вынесена в отдельную страницу. Платно: нет. ПДн: нет.

#Базовый адрес

ЯчейкаАдрес
RUhttps://formula-cream.pro/api/public/v1
ENhttps://wfacademy.org/api/public/v1
IDhttps://wfacademy.id/api/public/v1

Ошибки, пагинация, идемпотентность, пределы — Соглашения. Ключи и области — Аутентификация. Во всех примерах ключ заведомо ненастоящий.

#Точки

Метод и путьОбластьЧто делает
GET /articlesarticles:readСписок статей (Доступно)
GET /articles/{id}articles:readСтатья; формат содержимого html, markdown или blocks (Доступно)
PATCH /articles/{id}articles:writeПравка полей (Планируется)
POST /articles/{id}/approvearticles:writeУтвердить (Планируется)
POST /articles/{id}/publisharticles:writeОпубликовать через модуль или выдать вашей системе (Планируется)
POST /articles/generation-jobsarticles:generateЗаказать генерацию (платно) — описание

#Объект статьи

ПолеТипОписание
idstring (uuid)Идентификатор
titlestringЗаголовок
slugstring | nullАдрес страницы
statusstringСтатус статьи (см. ниже)
localestring | nullЯзык: ru, en, id
keywordstring | nullОсновной поисковый запрос
meta_title, meta_descriptionstring | nullSEO-заголовок и описание
cover_image_urlstring | nullОбложка
word_countintegerЧисло слов (0, если не посчитано)
contentstring | arrayСодержимое в запрошенном формате: html и markdown — строка, blocks — массив блоков. Только в GET /articles/{id}; в списке не возвращается. Пусто у статьи, чьё тело переехало в блог площадки
publicationobject | nullСостояние публикации (ниже). Только в GET /articles/{id}; null — статью ещё не выкладывали через модуль сайта
created_at, updated_at, published_atstring (ISO 8601) | nullДаты, UTC

#Статусы

Статусы, которые отдаёт API: draft (черновик), review (на проверке), approved (одобрена), published (опубликована), rejected (отклонена), transferred (передана в блог площадки), external (внешняя). Статус handed_off («выдана вам») относится к будущей выдаче статьи вашей системе и сейчас не возвращается.

#Состояние публикации (publication)

ПолеТипОписание
target"connector"Как публикуется: сейчас всегда через модуль на вашем сайте (none появится вместе с выдачей — Планируется)
statestringqueued (в очереди или отдано модулю), applied, skipped, conflict, failed
published_urlstring | nullАдрес опубликованной страницы
errorstring | nullПричина конфликта или сбоя, до 200 знаков

#GET /articles

Список статей арендатора, без содержимого и без publication. Порядок: updated_at по убыванию (статьи без даты в конце), затем id по убыванию. Читаются только собственные статьи арендатора (sp_seo_articles); статьи общего блога площадки в список не входят.

ПараметрТипОбязательноОграниченияПример
limitintegerнет1–100, по умолчанию 2550
cursorstringнетЗначение next_cursor; живёт 24 часа
statusstringнетОдин из статусов выше; иное значение — 422approved
updated_afterstringнетISO 8601; только статьи с updated_at строго позже2026-10-01T00:00:00Z
include_deletedbooleanнетНе поддерживается: удалённых статей 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}

ПараметрТипОбязательноОграниченияПример
idstring (uuid)даСтатья вашего арендатора
formatstringнетhtml, markdown или blocks; по умолчанию htmlmarkdown

Чужая или несуществующая статья — 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}

Меняет поля статьи. Передавайте только то, что меняете.

ПараметрТипОбязательноОграниченияПример
titlestringнет"Новый заголовок"
slugstringнетДопустимый адрес страницы
contentstringнетHTML до 2 000 000 байт; очищается при записи. Тело запроса — не более 256 КБ, иначе 413 payload_too_large; большие тексты правьте через кабинет
meta_title, meta_descriptionstringнет
published_urlstringнетАдрес страницы на вашем сайте; подтверждает публикацию при 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 определяет путь.

ПараметрТипОбязательноОграниченияПример
targetstringдаconnector или none"connector"
site_idstring (uuid)при connectorПривязанный сайт арендатора
publish_modestringпри connectordraft или 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. Подпись и повторы — Вебхуки.

#Заметки

#Ошибки

Общий формат и каталог кодов — Соглашения. Для этих точек:

КодHTTPКогда
invalid_api_key401Ключ не принят
scope_missing403Нет articles:read / articles:write
not_found404Статьи или сайта нет, либо они чужие
invalid_state409Действие недопустимо в текущем состоянии статьи или сайта
idempotency_conflict409Idempotency-Key уже использован с другим телом
payload_too_large413Тело запроса больше 256 КБ
validation_failed422Неверные значения; статья больше пределов
rate_limited429Превышен предел частоты

#Открытые вопросы