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

#Генерация статей по API

Статус: Планируется. Публичной точки генерации нет. Сегодня статью заказывают в кабинете Sapport. Страница описывает замысел из проекта; названия полей предварительные. Сроки не обещаются.

Генерация — платная асинхронная операция: вы создаёте задание генерации, получаете 202 Accepted с конвертом operation (Асинхронные операции), а результат приходит событием, когда статья готова. Статья пишется по профилю вашей редакции (см. обзор) и попадает в очередь со статусом «черновик».

Доступ: область articles:generate. Платно: да, с кошелька арендатора. ПДн: нет.

#Требования

УсловиеЧто будет, если не выполнено
Заполнен профиль редакции арендатора409 editorial_profile_required
Есть доступ к модели (кошелёк с деньгами или собственный ключ модели)402 insufficient_funds или 503 upstream_unavailable
Не достигнут предел трат арендатора или интеграции, не заняты все 5 одновременных платных операций, оценка не выше максимума стоимости одной операции (по умолчанию эквивалент 100 ₽)402 spend_limit_reached
Хранилище пределов доступно503 service_unavailable
Ключ несёт область articles:generate403 scope_missing

Коды и формат ошибок — Соглашения.

#Как это работает

  1. POST /articles/generation-jobs создаёт задание и резервирует стоимость на кошельке. Ответ — 202 с конвертом operation (type: article.generation, resource: {type: generation_job, id}).
  2. Платформа ставит задание в очередь и обрабатывает его воркером. Генерация одной статьи занимает минуты (во внутренней массовой генерации платформы закладывают 3–5 минут на статью; для API срок не обещается).
  3. Когда статья готова, создаётся статья со статусом draft, приходят события article.ready и operation.completed. Если генерация не удалась — article.generation.failed и operation.completed со статусом failed, резерв возвращается.
  4. Вы читаете статью (GET /articles/{id}), правите, утверждаете и публикуете — см. API статей.

Результат можно получить тремя способами: параметр ?wait=N (до 20 секунд) на создающем запросе, опрос GET /operations/{id} или опрос ресурса GET /articles/generation-jobs/{id}.

#POST /articles/generation-jobs

Заголовки: Authorization: Bearer <ключ>, Idempotency-Key (обязателен для платных операций), Content-Type: application/json.

ПараметрТипОбязательноОграниченияПример
topicstringнетТема статьи. Если не указана ни topic, ни topic_id, тема выбирается по профилю редакции из его источников тем"Как выбрать увлажняющий крем"
topic_idstring (uuid)нетУтверждённая тема из очереди стратега (состояние approved); взаимоисключающе с topic. Иначе 409 invalid_state
keywordstringнетОсновной поисковый запрос"увлажняющий крем"
localestringнетЯзык статьи; по умолчанию язык профиля"ru"
max_costobjectнетПотолок стоимости: {"amount":"50.00","currency":"RUB"}; не выше максимума стоимости одной операции арендатора. Если оценка выше, задание не создаётся (402 spend_limit_reached)
wait (query)integerнет0–20 секунд: подождать завершения того же задания20

Набор параметров в проекте не определён подробно: известно, что генерация идёт по профилю редакции и может опираться на тему или ключевой запрос. Перечисленные параметры предварительные (см. «Открытые вопросы»).

Ответ 202 Accepted — конверт operation:

{
  "operation": {
    "id": "op_EXAMPLE02",
    "type": "article.generation",
    "status": "queued",
    "created_at": "2026-10-11T10:15:00Z",
    "resource": { "type": "generation_job", "id": "e4c1a7b2-5d3f-4a90-b8c6-1f2e3d4c5b6a" }
  }
}

Повтор запроса с тем же Idempotency-Key и тем же телом возвращает ту же операцию и не списывает деньги дважды; с другим телом — 409 idempotency_conflict. Если не дождались результата за wait, вернётся 202 с текущим состоянием операции.

#GET /articles/generation-jobs/{job_id}

ПараметрТипОбязательноОграниченияПример
job_id (путь)string (uuid)даresource.id из конверта operatione4c1a7b2-…

Ответ 200 OK:

{
  "job_id": "e4c1a7b2-5d3f-4a90-b8c6-1f2e3d4c5b6a",
  "status": "succeeded",
  "article_id": "b9f27c14-0a3d-4e6b-8c51-7d2a9e4f1c03",
  "estimated_cost": { "amount": "35.00", "currency": "RUB" },
  "usage": { "cost": { "amount": "34.20", "currency": "RUB" }, "model": "model-EXAMPLE" },
  "error": null,
  "created_at": "2026-10-11T10:15:00Z",
  "finished_at": "2026-10-11T10:19:12Z"
}

estimated_cost — оценка до запуска, usage — фактическая стоимость и модель (заполняется по завершении).

Статусы задания:

СтатусСмысл
queuedВ очереди
runningГенерируется
succeededГотово, article_id заполнен
failedНе удалось, error ({code, message}) описывает причину, резерв возвращён
canceledОтменено, резерв возвращён

Статусы совпадают со статусами конверта operation. Чужое или несуществующее задание — 404 not_found.

#Примеры

curl -X POST https://formula-cream.pro/api/public/v1/articles/generation-jobs \
  -H "Authorization: Bearer $SAPPORT_API_KEY" \
  -H "Idempotency-Key: 0e8a2f61-7c4b-4d19-a3f5-9b1c6d2e8f47" \
  -H "Content-Type: application/json" \
  -d '{"topic":"Как выбрать увлажняющий крем","locale":"ru"}'
# SAPPORT_API_KEY=sap_ru_test_EXAMPLE0KEYID_… (пример, не настоящий ключ)
const res = await fetch(
  "https://formula-cream.pro/api/public/v1/articles/generation-jobs?wait=20",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SAPPORT_API_KEY}`,
      "Idempotency-Key": crypto.randomUUID(),
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ topic: "Как выбрать увлажняющий крем", locale: "ru" }),
  },
);
if (res.status === 202) {
  const { operation } = await res.json();
  const jobId = operation.resource.id;
}
import os, uuid, httpx

r = httpx.post(
    "https://formula-cream.pro/api/public/v1/articles/generation-jobs",
    headers={"Authorization": f"Bearer {os.environ['SAPPORT_API_KEY']}",
             "Idempotency-Key": str(uuid.uuid4())},
    params={"wait": 20},
    json={"topic": "Как выбрать увлажняющий крем", "locale": "ru"},
    timeout=30,
)
assert r.status_code == 202
job_id = r.json()["operation"]["resource"]["id"]
$ch = curl_init("https://formula-cream.pro/api/public/v1/articles/generation-jobs");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    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(
        ["topic" => "Как выбрать увлажняющий крем", "locale" => "ru"],
        JSON_UNESCAPED_UNICODE
    ),
]);
$operation = json_decode(curl_exec($ch), true)["operation"];

Опрос статуса операции или задания:

curl https://formula-cream.pro/api/public/v1/operations/$OPERATION_ID \
  -H "Authorization: Bearer $SAPPORT_API_KEY"

curl https://formula-cream.pro/api/public/v1/articles/generation-jobs/$JOB_ID \
  -H "Authorization: Bearer $SAPPORT_API_KEY"

#События

СобытиеКогда
article.readyСтатья сгенерирована и лежит в очереди
article.generation.failedГенерация не удалась
operation.completedОперация article.generation завершилась (succeeded, failed, canceled); в data — конверт operation

Нагрузка событий статьи тонкая: тип, job_id, article_id. Подпись, повторы, проверка — Вебхуки.

#Стоимость

Фактическая стоимость возвращается в usage ({cost: {amount, currency}, model}) в конверте operation и в задании; оценка до запуска — estimated_cost. Списание происходит с кошелька арендатора; при ошибке резерв возвращается. Тестовые ключи не списывают деньги. Общие правила — Платные операции.

#Заметки

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