{"openapi":"3.1.0","info":{"title":"Охват API","version":"1.0.0","description":"Внешний API Охвата: бренды, каналы, генерация постов-картинок, медиа и публикации.\n\n**Ключ.** Выпускается в кабинете, раздел «API и интеграции» (`/app/api-keys`). Секрет\nпоказывается один раз при выпуске — сохрани его сразу. Передаётся заголовком\n`Authorization: Bearer <ключ>`; cookie-сессия здесь не принимается. У ключа есть область\n(`read` — только чтение, `write` — чтение и изменения) и список брендов: бренд вне списка\nзакрыт, даже если человек им владеет.\n\n**Лимиты.** Чтение — 300 запросов в минуту на ключ. Изменения — суточный потолок по тарифу\nВЛАДЕЛЬЦА бренда: 30 на «Бесплатном», 300 на «Старте», 1000 на «Росте», 5000 на «Бизнесе».\nОтвет бакета несёт `Retry-After` и `X-RateLimit-*`.\n\n**Долгие операции** (генерация, скачивание картинки по адресу, публикация) отвечают `202` и\nидентификатором: результат спрашивают опросом GET-роута, вебхуков в v1 нет.\n\n**Версия в пути.** В `v1` поля только добавляются; переименование и удаление — только в `v2`."},"servers":[{"url":"https://ohvat.top"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Ключ","description":"Кто стоит за ключом"},{"name":"Бренды","description":"Бренды ключа и их каналы"},{"name":"Креативы","description":"Генерация и чтение креативов"},{"name":"Медиа","description":"Свои картинки для публикаций"},{"name":"Публикации","description":"Календарь, постановка и отмена"}],"paths":{"/api/v1/me":{"get":{"operationId":"getMe","tags":["Ключ"],"summary":"Владелец ключа","description":"Первый вызов любой интеграции: им проверяют, что ключ вставлен верно. `brandIds` — уже пересечение списка ключа с тем, что доступно владельцу сейчас: бренд мог уйти из команды или подписка владельца протухнуть.","responses":{"200":{"description":"Ключ действителен","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"401":{"description":"`invalid_key` — заголовка нет, ключ не найден или отозван","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — исчерпан бакет: 300 чтений в минуту или суточный потолок мутаций по тарифу. Ответ несёт `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal` — внутренняя ошибка","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/brands":{"get":{"operationId":"listBrands","tags":["Бренды"],"summary":"Бренды ключа","description":"Бренды, которые открывает ключ, с тарифом владельца и списком открытых тарифом форматов. Тариф протух — `plan: null` и пустой `formats`: генерация такому бренду откажет.","responses":{"200":{"description":"Список брендов","content":{"application/json":{"schema":{"type":"object","required":["brands"],"properties":{"brands":{"type":"array","items":{"$ref":"#/components/schemas/Brand"}}}}}}},"401":{"description":"`invalid_key` — заголовка нет, ключ не найден или отозван","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — исчерпан бакет: 300 чтений в минуту или суточный потолок мутаций по тарифу. Ответ несёт `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal` — внутренняя ошибка","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/brands/{id}/channels":{"get":{"operationId":"listBrandChannels","tags":["Бренды"],"summary":"Подключённые каналы бренда","description":"Наружу отдаётся только то, что нужно для публикации: токенов, прав и внутренних идентификаторов площадки в ответе нет. Канал со статусом `needsReconnect` публиковать не станет — человеку надо переподключить его в кабинете.","parameters":[{"name":"id","in":"path","required":true,"description":"Идентификатор бренда (cuid) из `GET /api/v1/brands`","schema":{"type":"string"}}],"responses":{"200":{"description":"Список каналов","content":{"application/json":{"schema":{"type":"object","required":["channels"],"properties":{"channels":{"type":"array","items":{"$ref":"#/components/schemas/Channel"}}}}}}},"401":{"description":"`invalid_key` — заголовка нет, ключ не найден или отозван","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden_brand` — бренд не в списке ключа или недоступен владельцу","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — исчерпан бакет: 300 чтений в минуту или суточный потолок мутаций по тарифу. Ответ несёт `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal` — внутренняя ошибка","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/brands/{id}/creatives":{"post":{"operationId":"createCreative","tags":["Креативы"],"summary":"Создать креатив и запустить генерацию","description":"Черновик и запуск одним вызовом: у интеграции мастера нет. Ответ `202` — генерация долгая, статус спрашивают `GET /api/v1/creatives/{id}` до `ready` или `failed`.\n\nВ v1 доступен один тип — `imagePost` (пост-картинка). Тон и рубрика необязательны: без них работают дефолты бренда.","parameters":[{"name":"id","in":"path","required":true,"description":"Идентификатор бренда (cuid) из `GET /api/v1/brands`","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCreativeRequest"}}}},"responses":{"202":{"description":"Генерация поставлена в очередь","content":{"application/json":{"schema":{"type":"object","required":["creativeId","versionId"],"properties":{"creativeId":{"type":"string","description":"Идентификатор креатива для опроса"},"versionId":{"type":"string","description":"Версия, которую сейчас генерируем"}}}}}},"400":{"description":"`bad_request` — тело не прошло разбор; в `message` указано поле","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_key` — заголовка нет, ключ не найден или отозван","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"`plan_locked` — формат закрыт тарифом владельца бренда; `insufficient_credits` — не хватает кредитов","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`read_only_key` — ключ только для чтения; `forbidden_brand` — бренд не в списке ключа; `demo_mode` — демо-кабинет ничего не меняет","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — бренд не найден","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — исчерпан бакет: 300 чтений в минуту или суточный потолок мутаций по тарифу. Ответ несёт `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal` — внутренняя ошибка","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/creatives/{id}":{"get":{"operationId":"getCreative","tags":["Креативы"],"summary":"Статус и содержимое креатива","description":"Этим роутом опрашивают долгую генерацию. `status: ready` — можно публиковать; `failed` — причина в `failure`. Медиа отдаются абсолютными ссылками: ролик идёт первым, картинки за ним.\n\nЧужой креатив и креатив бренда вне списка ключа дают тот же `404`, что и несуществующий: существование чужих строк не раскрываем.","parameters":[{"name":"id","in":"path","required":true,"description":"Идентификатор креатива","schema":{"type":"string"}}],"responses":{"200":{"description":"Креатив","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Creative"}}}},"401":{"description":"`invalid_key` — заголовка нет, ключ не найден или отозван","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — креатив не найден или закрыт ключом","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`busy` — креатив занят другой операцией","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — исчерпан бакет: 300 чтений в минуту или суточный потолок мутаций по тарифу. Ответ несёт `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal` — внутренняя ошибка","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/brands/{id}/media":{"post":{"operationId":"uploadBrandMedia","tags":["Медиа"],"summary":"Загрузить картинку бренда","description":"Два пути, различаются типом тела.\n\n**Файлом** — `multipart/form-data`, поле `file`: `201 { assetId }`, ассет готов сразу. Принимаются jpeg, png, webp, heic, heif, avif до 20 МБ.\n\n**По адресу** — JSON `{ \"url\": \"https://…\" }`: `202 { fetchId }`, файл качает воркер (у CRM часто есть только ссылка на фото). Итог спрашивают `GET /api/v1/media/fetch/{fetchId}`.\n\nСвои файлы — платная функция: тариф берётся у владельца бренда. Видео в v1 не принимается.","parameters":[{"name":"id","in":"path","required":true,"description":"Идентификатор бренда (cuid) из `GET /api/v1/brands`","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary","description":"Картинка до 20 МБ"}}}},"application/json":{"schema":{"$ref":"#/components/schemas/MediaByUrlRequest"}}}},"responses":{"201":{"description":"Файл принят, ассет готов","content":{"application/json":{"schema":{"type":"object","required":["assetId"],"properties":{"assetId":{"type":"string","description":"Идентификатор для `mediaAssetIds`"}}}}}},"202":{"description":"Адрес принят, картинку качает воркер","content":{"application/json":{"schema":{"type":"object","required":["fetchId"],"properties":{"fetchId":{"type":"string","description":"Идентификатор для опроса статуса"}}}}}},"400":{"description":"`bad_request` — нет поля `file`, не картинка или тяжелее 20 МБ; `bad_url` — адрес не http(s)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_key` — заголовка нет, ключ не найден или отозван","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"`plan_locked` — «Свой контент» закрыт тарифом владельца бренда","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`read_only_key` — ключ только для чтения; `forbidden_brand` — бренд не в списке ключа; `demo_mode` — демо-кабинет ничего не меняет","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — бренд не найден","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — исчерпан бакет: 300 чтений в минуту или суточный потолок мутаций по тарифу. Ответ несёт `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal` — внутренняя ошибка","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/media/fetch/{fetchId}":{"get":{"operationId":"getMediaFetch","tags":["Медиа"],"summary":"Статус скачивания картинки по адресу","description":"Опрос до `ready` (тогда в `assetId` лежит готовый ассет) или `failed` (причина в `error`). Чужое скачивание даёт `404`, как и несуществующее.","parameters":[{"name":"fetchId","in":"path","required":true,"description":"Идентификатор из ответа `POST /api/v1/brands/{id}/media`","schema":{"type":"string"}}],"responses":{"200":{"description":"Состояние скачивания","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MediaFetch"}}}},"401":{"description":"`invalid_key` — заголовка нет, ключ не найден или отозван","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — такого скачивания нет или оно чужое","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — исчерпан бакет: 300 чтений в минуту или суточный потолок мутаций по тарифу. Ответ несёт `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal` — внутренняя ошибка","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/publications":{"get":{"operationId":"listPublications","tags":["Публикации"],"summary":"Календарь публикаций бренда","description":"Та же выборка, что рисует календарь кабинета. Окно `from`–`to` — не больше 62 дней. Отменённые публикации выборка не отдаёт: это ни план, ни история.","parameters":[{"name":"brandId","in":"query","required":true,"description":"Бренд из списка ключа","schema":{"type":"string"}},{"name":"from","in":"query","required":true,"description":"Начало окна, ISO 8601","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":true,"description":"Конец окна, ISO 8601; не дальше 62 дней от `from`","schema":{"type":"string","format":"date-time"}},{"name":"status","in":"query","required":false,"description":"Фильтр по статусу","schema":{"type":"string","enum":["scheduled","publishing","published","failed"]}}],"responses":{"200":{"description":"Публикации окна","content":{"application/json":{"schema":{"type":"object","required":["publications"],"properties":{"publications":{"type":"array","items":{"$ref":"#/components/schemas/PublicationListItem"}}}}}}},"400":{"description":"`bad_request` — нет обязательного параметра либо окно шире 62 дней","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_key` — заголовка нет, ключ не найден или отозван","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`forbidden_brand` — бренд не в списке ключа или недоступен владельцу","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — бренд не найден","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — исчерпан бакет: 300 чтений в минуту или суточный потолок мутаций по тарифу. Ответ несёт `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal` — внутренняя ошибка","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"createPublications","tags":["Публикации"],"summary":"Запланировать публикацию","description":"По одной строке на канал, как в кабинете. Ровно один из двух путей: `creativeId` — публикуем готовый креатив (его текст, заголовок и медиа), либо `text` — «Свой контент» со своими файлами в `mediaAssetIds`.\n\nОтвет `202`: роут только ставит задачи, в соцсети ходит воркер. Порядок `publications` совпадает с порядком `channelIds`. Прошедшее время в `scheduledAt` означает «в очередь сразу».","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePublicationRequest"}}}},"responses":{"202":{"description":"Публикации поставлены","content":{"application/json":{"schema":{"type":"object","required":["publications"],"properties":{"publications":{"type":"array","items":{"type":"object","required":["id","channelId"],"properties":{"id":{"type":"string"},"channelId":{"type":"string"}}}}}}}}},"400":{"description":"`bad_request` — тело не прошло разбор либо канал отклонён площадкой","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"`invalid_key` — заголовка нет, ключ не найден или отозван","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"`plan_locked` — функция закрыта тарифом; `insufficient_credits` — не хватает кредитов","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`read_only_key` — ключ только для чтения; `forbidden_brand` — бренд не в списке ключа; `demo_mode` — демо-кабинет ничего не меняет","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — креатив, бренд или канал не найден","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`creative_not_ready` — креатив ещё не готов, дождись статуса `ready`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — исчерпан бакет: 300 чтений в минуту или суточный потолок мутаций по тарифу. Ответ несёт `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal` — внутренняя ошибка","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/publications/{id}":{"get":{"operationId":"getPublication","tags":["Публикации"],"summary":"Одна публикация","description":"Статус, канал, время, текст, ссылка на пост на площадке и причина отказа. Чужая публикация и публикация бренда вне списка ключа дают `404`.","parameters":[{"name":"id","in":"path","required":true,"description":"Идентификатор публикации","schema":{"type":"string"}}],"responses":{"200":{"description":"Публикация","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Publication"}}}},"401":{"description":"`invalid_key` — заголовка нет, ключ не найден или отозван","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — публикация не найдена или закрыта ключом","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — исчерпан бакет: 300 чтений в минуту или суточный потолок мутаций по тарифу. Ответ несёт `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal` — внутренняя ошибка","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"cancelPublication","tags":["Публикации"],"summary":"Отменить запланированную публикацию","description":"Отменить можно только то, что ещё не ушло. Публикация уже публикуется или вышла — `409`: молча отвечать «ок» на такое запрещено.","parameters":[{"name":"id","in":"path","required":true,"description":"Идентификатор публикации","schema":{"type":"string"}}],"responses":{"200":{"description":"Публикация отменена","content":{"application/json":{"schema":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","const":true}}}}}},"401":{"description":"`invalid_key` — заголовка нет, ключ не найден или отозван","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"`read_only_key` — ключ только для чтения; `forbidden_brand` — бренд не в списке ключа; `demo_mode` — демо-кабинет ничего не меняет","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"`not_found` — публикация не найдена или закрыта ключом","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"`publication_not_cancelable` — публикацию уже нельзя отменить","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"`rate_limited` — исчерпан бакет: 300 чтений в минуту или суточный потолок мутаций по тарифу. Ответ несёт `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"`internal` — внутренняя ошибка","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Ключ из кабинета: `Authorization: Bearer ohv_…`"}},"schemas":{"Error":{"type":"object","description":"Единая форма отказа. `message` — по-русски, для человека в логах интеграции.","required":["error"],"properties":{"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","description":"Набор кодов закрыт: внутренние коды модулей наружу не просачиваются.","enum":["invalid_key","read_only_key","forbidden_brand","demo_mode","no_brands","not_found","rate_limited","bad_url","bad_request","creative_not_ready","publication_not_cancelable","insufficient_credits","plan_locked","language_locked","busy","internal"]},"message":{"type":"string"}}}}},"Me":{"type":"object","required":["userId","email","scope","brandIds"],"properties":{"userId":{"type":"string"},"email":{"type":"string","description":"Почта владельца ключа"},"scope":{"type":"string","enum":["read","write"],"description":"Область ключа"},"brandIds":{"type":"array","items":{"type":"string"},"description":"Бренды, доступные ключу прямо сейчас"}}},"Brand":{"type":"object","required":["id","name","plan","formats"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"plan":{"type":["string","null"],"enum":["free","start","rise","biz",null],"description":"Тариф владельца бренда; `null` — подписка неактивна"},"formats":{"type":"array","items":{"type":"string"},"description":"Типы креативов, открытые тарифом. Пустой список — генерация откажет."}}},"Channel":{"type":"object","required":["id","platform","title","status"],"properties":{"id":{"type":"string","description":"Идентификатор для `channelIds` при постановке публикации"},"platform":{"$ref":"#/components/schemas/Platform"},"title":{"type":"string","description":"Название сообщества или аккаунта"},"status":{"type":"string","enum":["active","needsReconnect"],"description":"`needsReconnect` — доступ протух, публикация не пройдёт"}}},"Platform":{"type":"string","description":"Площадка канала","enum":["vk","telegram","max","youtube","ok","instagram","tiktok","facebook","x","threads","pinterest","linkedin","reddit","bluesky","wordpress"]},"CreateCreativeRequest":{"type":"object","required":["type","source"],"additionalProperties":false,"properties":{"type":{"type":"string","enum":["imagePost"],"description":"В v1 доступен только пост-картинка"},"source":{"oneOf":[{"type":"object","required":["kind","text"],"properties":{"kind":{"type":"string","enum":["idea"]},"text":{"type":"string","minLength":20,"maxLength":1500,"description":"Тема поста своими словами"}}},{"type":"object","required":["kind","text"],"properties":{"kind":{"type":"string","enum":["donor"]},"text":{"type":"string","minLength":200,"maxLength":8000,"description":"Текст чужого поста целиком — его перепишем голосом бренда"}}}]},"tone":{"type":"string","description":"Тон из реестра; не передан — дефолт бренда"},"rubric":{"type":"string","description":"Рубрика из реестра; не передана — дефолт бренда"}}},"Creative":{"type":"object","required":["id","brandId","type","status","media","hashtags","creditsSpent","createdAt"],"properties":{"id":{"type":"string"},"brandId":{"type":"string"},"type":{"type":"string","description":"Тип креатива, например `imagePost`"},"status":{"type":"string","enum":["draft","generating","ready","failed"],"description":"Публиковать можно только `ready`"},"title":{"type":["string","null"],"description":"Заголовок поста для площадок, где он отдельный"},"text":{"type":["string","null"],"description":"Подпись поста"},"hashtags":{"type":"array","items":{"type":"string"}},"media":{"type":"array","items":{"$ref":"#/components/schemas/CreativeMedia"}},"failure":{"type":["string","null"],"description":"Причина отказа при `status: failed`"},"creditsSpent":{"type":"integer","description":"Списано кредитов"},"createdAt":{"type":"string","format":"date-time"}}},"CreativeMedia":{"type":"object","required":["assetId","kind","url"],"properties":{"assetId":{"type":"string","description":"Годится для `mediaAssetIds` при публикации"},"kind":{"type":"string","enum":["image","video"]},"url":{"type":"string","format":"uri","description":"Абсолютная ссылка на отдачу файла. Скачивать с тем же заголовком `Authorization: Bearer <ключ>` — ссылка не публичная."}}},"MediaByUrlRequest":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"Полный адрес картинки, http или https"}}},"MediaFetch":{"type":"object","required":["state","assetId","error"],"properties":{"state":{"type":"string","enum":["pending","ready","failed"]},"assetId":{"type":["string","null"],"description":"Заполнен при `state: ready`"},"error":{"type":["string","null"],"description":"Причина при `state: failed`"}}},"CreatePublicationRequest":{"type":"object","required":["brandId","channelIds","scheduledAt"],"additionalProperties":false,"description":"Ровно один из `creativeId` и `text`: с обоими непонятно, чей текст главнее.","properties":{"brandId":{"type":"string"},"channelIds":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":20,"description":"Каналы из `GET /api/v1/brands/{id}/channels`"},"scheduledAt":{"type":"string","format":"date-time","description":"Время публикации, ISO 8601"},"creativeId":{"type":"string","description":"Публикуем готовый креатив (`status: ready`)"},"text":{"type":"string","maxLength":20000,"description":"Свой текст поста"},"title":{"type":"string","maxLength":100,"description":"Заголовок; сильнее заголовка креатива"},"mediaAssetIds":{"type":"array","items":{"type":"string"},"maxItems":10,"description":"Свои файлы; при публикации креатива не читается — берутся медиа версии"}}},"PublicationListItem":{"type":"object","description":"Строка календаря: проекция уже, чем `Publication`.","required":["id","status","scheduledAt","platform","channelTitle","text"],"properties":{"id":{"type":"string"},"status":{"$ref":"#/components/schemas/PublicationStatus"},"scheduledAt":{"type":"string","format":"date-time"},"publishedAt":{"type":["string","null"],"format":"date-time"},"platform":{"$ref":"#/components/schemas/Platform"},"channelTitle":{"type":"string"},"text":{"type":"string"},"externalUrl":{"type":["string","null"],"description":"Ссылка на пост на площадке"},"creativeId":{"type":["string","null"]},"failureCode":{"type":["string","null"]},"failureMessage":{"type":["string","null"]}}},"Publication":{"type":"object","required":["id","brandId","status","scheduledAt","platform","channelTitle","text"],"properties":{"id":{"type":"string"},"brandId":{"type":"string"},"status":{"$ref":"#/components/schemas/PublicationStatus"},"scheduledAt":{"type":"string","format":"date-time"},"publishedAt":{"type":["string","null"],"format":"date-time"},"channelId":{"type":["string","null"],"description":"`null` — канал отключили после постановки, публикация осталась историей"},"platform":{"$ref":"#/components/schemas/Platform"},"channelTitle":{"type":"string"},"text":{"type":"string"},"title":{"type":["string","null"]},"externalUrl":{"type":["string","null"]},"creativeId":{"type":["string","null"]},"failureCode":{"type":["string","null"]},"failureMessage":{"type":["string","null"]}}},"PublicationStatus":{"type":"string","enum":["scheduled","publishing","published","failed","canceled"],"description":"`canceled` в выборке календаря не появляется"}}}}