#Генерация статей по 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:generate | 403 scope_missing |
Коды и формат ошибок — Соглашения.
#Как это работает
POST /articles/generation-jobsсоздаёт задание и резервирует стоимость на кошельке. Ответ —202с конвертомoperation(type: article.generation,resource: {type: generation_job, id}).- Платформа ставит задание в очередь и обрабатывает его воркером. Генерация одной статьи занимает минуты (во внутренней массовой генерации платформы закладывают 3–5 минут на статью; для API срок не обещается).
- Когда статья готова, создаётся статья со статусом
draft, приходят событияarticle.readyиoperation.completed. Если генерация не удалась —article.generation.failedиoperation.completedсо статусомfailed, резерв возвращается. - Вы читаете статью (
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.
| Параметр | Тип | Обязательно | Ограничения | Пример |
|---|---|---|---|---|
topic | string | нет | Тема статьи. Если не указана ни topic, ни topic_id, тема выбирается по профилю редакции из его источников тем | "Как выбрать увлажняющий крем" |
topic_id | string (uuid) | нет | Утверждённая тема из очереди стратега (состояние approved); взаимоисключающе с topic. Иначе 409 invalid_state | |
keyword | string | нет | Основной поисковый запрос | "увлажняющий крем" |
locale | string | нет | Язык статьи; по умолчанию язык профиля | "ru" |
max_cost | object | нет | Потолок стоимости: {"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 из конверта operation | e4c1a7b2-… |
Ответ 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. Списание происходит с кошелька арендатора; при ошибке резерв возвращается. Тестовые ключи не списывают деньги. Общие правила — Платные операции.
#Заметки
- Генерация идёт в очереди воркера;
?waitждёт не более 20 секунд, не рассчитывайте на готовую статью в этом окне. - Утверждение темы стратега само по себе генерацию не запускает; её заказывают этим запросом с
topic_idлибо флагомgenerate: trueв утверждении темы. - «Глубокий» режим и массовая генерация по расписанию доступны только на площадке Sapport и в публичный API не входят.
- Тема, которая уже есть у вас в очереди, может быть отклонена планировщиком как дубликат; поведение API в этом случае не определено.
#Открытые вопросы
- Набор параметров задания (техническое). Тема, ключевой запрос, количество, режим, длина; имена
topic_idиmax_costпредварительные. - Тариф и точность
estimated_cost(к владельцу). Каким будет тариф генерации и насколько точной должна быть оценка. - Тема-дубликат (техническое). Как платформа поступает с темой, которая уже есть в очереди: ошибка, отказ задания или тихая замена.
- Ошибка профиля в кабинете (техническое). Кабинет возвращает
409с кодомEDITORIAL_PROFILE_REQUIREDне в форматеproblem+json; публичный API возвращаетeditorial_profile_requiredиз каталога.