Зачем
Публичный API — для тех, кому нужно завести генерацию и публикацию из своей системы: CRM, скрипт по расписанию, внутренний дашборд агентства. Он даёт то же самое, что кабинет: создание креатива по теме или по чужому посту, свои файлы, постановку публикации и календарь — но без браузера и без сессии.
Открытая спецификация лежит по адресу
/api/v1/openapi.json в формате OpenAPI 3.1 — из неё клиенты и
SDK генерируются любым генератором OpenAPI (openapi-generator, Postman, Kiota и так
далее), без ручного описания эндпоинтов.
Ключ
Ключ выпускается в кабинете, в разделе «API и интеграции» (пункт меню в нижней части
сайдбара, страница /app/api-keys).
При выпуске указываешь:
- Область —
read(только чтение: бренды, каналы, статус креативов и публикаций) илиwrite(то же плюс создание креативов, загрузка файлов, постановка и отмена публикаций). - Бренды — список брендов, которые ключ открывает. Бренд не из списка закрыт даже тому, кто им владеет: у ключа нет доступа «по умолчанию ко всему».
Секрет ключа (ohv_…) показывается один раз, в момент выпуска — сохрани его сразу,
второй раз кабинет его не покажет. Забыл сохранить — выпускай новый и отзывай старый.
Отзыв ключа в кабинете необратим: все интеграции, которые его использовали, начинают
получать invalid_key.
Заголовок
Ключ передаётся заголовком Authorization, схема Bearer:
curl https://ohvat.top/api/v1/me \
-H "Authorization: Bearer ohv_5f2b9c1a7e4d3f80b6c2a91d5e7f3c40"
Cookie-сессия кабинета здесь не принимается — только этот заголовок.
Лимиты
| Что | Потолок |
|---|---|
Чтение (GET) | 300 запросов в минуту на ключ |
Изменения (POST/DELETE) | по тарифу владельца бренда, в сутки |
Суточный потолок изменений:
| Тариф | Изменений в сутки |
|---|---|
| Бесплатный | 30 |
| Старт | 300 |
| Рост | 1 000 |
| Бизнес | 5 000 |
Ответ, упёршийся в лимит, — 429 rate_limited с заголовками Retry-After,
X-RateLimit-Limit и X-RateLimit-Remaining: по ним понятно, сколько осталось и когда
пробовать снова, без угадывания.
Ошибки
Любой отказ — одна форма:
{ "error": { "code": "not_found", "message": "Креатив не найден" } }
message — по-русски, для человека, который смотрит логи интеграции. Разбирать код —
только по error.code: набор кодов закрыт, внутренние коды модулей наружу не
просачиваются.
| Код | Статус | Когда |
|---|---|---|
invalid_key | 401 | заголовка нет, ключ не найден или отозван |
read_only_key | 403 | ключ только для чтения, а вызов меняет данные |
forbidden_brand | 403 | бренд не в списке ключа или недоступен владельцу |
demo_mode | 403 | ключ выпущен в демо-кабинете — он ничего не меняет |
no_brands | 400 | у ключа нет ни одного доступного бренда |
not_found | 404 | сущности не существует или она закрыта ключом (чужая — тот же ответ, что несуществующая) |
rate_limited | 429 | исчерпан бакет чтения или суточный потолок изменений |
bad_url | 400 | адрес картинки не http/https, приватный или локальный адрес, битая ссылка или редирект в приватную сеть |
bad_request | 400 | тело не прошло разбор — конкретное поле в message |
insufficient_credits | 402 | не хватает кредитов на генерацию или публикацию |
plan_locked | 402 | формат или функция закрыты тарифом владельца бренда |
language_locked | 403 | формат не поддерживает язык контента бренда — код появится после волны «Язык контента» |
creative_not_ready | 409 | публикуем креатив, который ещё не ready |
publication_not_cancelable | 409 | публикация уже вышла или публикуется — отменить нельзя |
busy | 409 | креатив занят другой операцией |
internal | 500 | внутренняя ошибка |
Долгие операции (генерация, скачивание картинки по адресу, публикация) отвечают
202 с идентификатором — результат спрашивают опросом GET-роута. Вебхуков в v1 нет.
Пример 1: пост по теме → публикация
Тема поста → ждём готовности креатива → ставим публикацию в канал.
KEY="Authorization: Bearer ohv_5f2b9c1a7e4d3f80b6c2a91d5e7f3c40"
BRAND="cljk3x9p10000qzrmn8g4d2yx"
# 1. Создать креатив и запустить генерацию
curl -s -X POST "https://ohvat.top/api/v1/brands/$BRAND/creatives" \
-H "$KEY" -H "Content-Type: application/json" \
-d '{
"type": "imagePost",
"source": { "kind": "idea", "text": "Открытие нового зала для йоги на Ленина, 12 — расписание и первое бесплатное занятие" }
}'
# → 202 { "creativeId": "clm...", "versionId": "clm..." }
# 2. Опрашивать статус, пока не ready или failed
curl -s "https://ohvat.top/api/v1/creatives/clm..." -H "$KEY"
# → 200 { "status": "generating", ... } — повторить через пару секунд
# → 200 { "status": "ready", "media": [{ "assetId": "...", "kind": "image", "url": "https://..." }], ... }
# 2a. Скачать картинку, если она нужна у себя. Ссылка НЕ публичная — тот же заголовок ключа.
curl -s -H "$KEY" "https://ohvat.top/api/generation/media/..." -o post.jpg
# 3. Поставить публикацию готовым креативом
curl -s -X POST "https://ohvat.top/api/v1/publications" \
-H "$KEY" -H "Content-Type: application/json" \
-d '{
"brandId": "'"$BRAND"'",
"channelIds": ["chn_telegram_1", "chn_vk_1"],
"creativeId": "clm...",
"scheduledAt": "2026-09-10T09:00:00+03:00"
}'
# → 202 { "publications": [{ "id": "pub...", "channelId": "chn_telegram_1" }, { "id": "pub...", "channelId": "chn_vk_1" }] }
Канал вне списка ключа или несуществующий бренд ответят 403/404; попытка
опубликовать креатив раньше готовности — 409 creative_not_ready.
Пример 2: свой файл → публикация «Своим контентом»
Загружаем готовую фотографию, пишем свой текст, публикуем без генерации.
KEY="Authorization: Bearer ohv_5f2b9c1a7e4d3f80b6c2a91d5e7f3c40"
BRAND="cljk3x9p10000qzrmn8g4d2yx"
# 1. Загрузить файл (jpeg/png/webp/heic/heif/avif до 20 МБ)
curl -s -X POST "https://ohvat.top/api/v1/brands/$BRAND/media" \
-H "$KEY" \
-F "file=@photo.jpg"
# → 201 { "assetId": "ast..." }
# 2. Поставить публикацию своим текстом и своим файлом
curl -s -X POST "https://ohvat.top/api/v1/publications" \
-H "$KEY" -H "Content-Type: application/json" \
-d '{
"brandId": "'"$BRAND"'",
"channelIds": ["chn_vk_1"],
"text": "Сегодня привезли свежую партию — заходи посмотреть на месте",
"mediaAssetIds": ["ast..."],
"scheduledAt": "2026-09-09T18:00:00+03:00"
}'
# → 202 { "publications": [{ "id": "pub...", "channelId": "chn_vk_1" }] }
Файла нет под рукой, а есть только ссылка на фото (частый случай для CRM) — вместо
multipart/form-data шлём JSON с адресом, и файл качает воркер:
curl -s -X POST "https://ohvat.top/api/v1/brands/$BRAND/media" \
-H "$KEY" -H "Content-Type: application/json" \
-d '{ "url": "https://example.com/photo.jpg" }'
# → 202 { "fetchId": "fch..." }
curl -s "https://ohvat.top/api/v1/media/fetch/fch..." -H "$KEY"
# → 200 { "state": "pending", "assetId": null, "error": null } — опрос
# → 200 { "state": "ready", "assetId": "ast...", "error": null }
Загрузка своих файлов — платная функция: тариф берётся у владельца бренда, бесплатному
она закрыта (402 plan_locked).
Пример 3: календарь публикаций за месяц
Окно from–to не шире 62 дней; отменённые публикации в выборку не попадают.
curl -s "https://ohvat.top/api/v1/publications?brandId=$BRAND&from=2026-09-01T00:00:00Z&to=2026-09-30T23:59:59Z" \
-H "$KEY"
# → 200 { "publications": [
# { "id": "pub...", "status": "published", "scheduledAt": "2026-09-03T09:00:00Z", "platform": "telegram", "channelTitle": "Йога на Ленина", "text": "...", "externalUrl": "https://t.me/..." },
# { "id": "pub...", "status": "scheduled", "scheduledAt": "2026-09-10T09:00:00Z", "platform": "vk", "channelTitle": "Йога на Ленина | ВКонтакте", "text": "..." }
# ] }
Одну публикацию с полным текстом, причиной отказа и ссылкой на пост площадки — отдельным запросом:
curl -s "https://ohvat.top/api/v1/publications/pub..." -H "$KEY"
Отменить можно только то, что ещё не ушло — DELETE на тот же адрес; публикация уже
публикуется или вышла отвечает 409 publication_not_cancelable.
Чего нет в v1
- Один тип креатива —
imagePost(пост-картинка). Каруселей, видео и других форматов генерации в API пока нет. - Источник поста — только текст: тема своими словами (
kind: "idea") или текст чужого поста для рерайта (kind: "donor"). Разбора сайта или ссылки на чужой пост в API нет. - Свои файлы — только картинки, до 20 МБ. Видео через
POST /brands/{id}/mediaне принимается. - Вебхуков нет: результат долгой операции (генерация, скачивание по адресу, публикация)
узнают опросом
GET-роута, а не пуш-уведомлением.
В v1 поля в схемах только добавляются — переименование и удаление возможны только в
v2, с отдельным анонсом.