
## Опубликовать приложение

`POST /v1/apps/:id/publish`

Переводит приложение в `PUBLISHED`, привязывает места встраивания к порталу и делает приложение видимым всем сотрудникам.

## Параметры

| Параметр | Тип | Обяз. | Описание |
|----------|-----|:-----:|---------|
| `id` (path) | string | да | Идентификатор приложения. Список: `GET /v1/apps` |

## Поля запроса (body)

Тело необязательное. При пустом теле берутся значения из записи приложения — это основной сценарий повторной публикации без изменений. Если передаёте тело, добавьте заголовок `Content-Type: application/json` — без него запрос с телом вернёт `413 PAYLOAD_TOO_LARGE`.

| Поле | Тип | Обяз. | Описание |
|------|-----|:-----:|----------|
| `catalogTitle` | string | нет | Название в каталоге портала. Если не передано — текущее значение приложения |
| `catalogDescription` | string | нет | Описание в каталоге |
| `catalogIcon` | string | нет | Код иконки в каталоге |
| `appUrl` | string | нет | Адрес приложения: сервер Black Hole, контейнер в галактике или внешний https-адрес. Если не передан — текущий `appUrl` приложения |
| `placements` | string[] | нет | Коды мест встраивания. Список допустимых кодов — [Доступные места](/docs/apps/placements/available). Если не передан — текущий набор приложения |
| `sourceVersionId` | string | нет | Конкретный снапшот исходников для публикации, формат `v<номер>`. Применимо при включённом [хранилище исходников](/docs/source-storage) |
| `sourceServerId` | string | нет | Сервер, исходники которого публикуются, — идентификатор из `POST /v1/infra/servers/:id/deploy`. Нужен, когда исходники сохранил автосейв деплоя на сервере, который не принадлежит OAuth-ключу этого приложения: такой снапшот хранится у сервера, а не у приложения. Тот же смысл у заголовка `X-Source-Server`. Платформа сервер не подбирает — без этого поля проверка ищет снапшот приложения. Применимо при включённом [хранилище исходников](/docs/source-storage) |

## Примеры

### curl — личный ключ

```bash
curl -X POST "https://vibecode.bitrix24.tech/v1/apps/YOUR_APP_ID/publish" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "placements": ["CRM_DEAL_DETAIL_TAB"] }'
```

### curl — OAuth-приложение

```bash
curl -X POST "https://vibecode.bitrix24.tech/v1/apps/YOUR_APP_ID/publish" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "placements": ["CRM_DEAL_DETAIL_TAB"] }'
```

### JavaScript — личный ключ

```javascript
const res = await fetch(
  'https://vibecode.bitrix24.tech/v1/apps/YOUR_APP_ID/publish',
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ placements: ['CRM_DEAL_DETAIL_TAB'] }),
  }
)
const body = await res.json()
if (!body.success) throw new Error(body.error.code)
// body.warnings — массив, если часть мест встраивания не привязалась
console.log(body.data.placements)
```

### JavaScript — OAuth-приложение

```javascript
const res = await fetch(
  'https://vibecode.bitrix24.tech/v1/apps/YOUR_APP_ID/publish',
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ placements: ['CRM_DEAL_DETAIL_TAB'] }),
  }
)
```

## Поля ответа

| Поле | Тип | Описание |
|------|-----|----------|
| `success` | boolean | `true` при успешной публикации |
| `data` | object | Приложение после публикации — те же 17 полей, что отдаёт [данные приложения](/docs/apps/get) |
| `data.placements` | string[] | Привязанные места встраивания |
| `data.catalogStatus` | string | После публикации — `PUBLISHED` |
| `data.publishedAt` | string \| null | Дата публикации, ISO 8601 |
| `warnings` | string[] | Предупреждения, не отменяющие публикацию. Часть мест встраивания не удалось привязать — строка перечисляет коды. Про снятие лишних мест строка приходит по-разному в зависимости от раскатки — разбор во врезке под таблицей ошибок. Переданный `sourceServerId` не был использован, потому что хранилище исходников для портала отключено. Опубликованная версия сервера осталась без бессрочного хранения — тогда строка несёт готовый `PATCH`, которым её можно пометить. Название или описание карточки потеряли не-ASCII символы по дороге — строка называет поле. Набор строк открытый: незнакомую строку показывайте как есть, а не отбрасывайте |

## Пример ответа

Поле `data` повторяет объект приложения с заполненным массивом `placements`:

```json
{
  "success": true,
  "data": {
    "id": "33c4d5e6-f7a8-49b0-1234-5c6d7e8f9012",
    "title": "Дашборд продаж",
    "description": null,
    "scopes": ["crm", "user", "placement"],
    "handlerUrl": "https://vibecode.bitrix24.tech/v1/bitrix-handler",
    "appUrl": "https://app-abc12345.vibecode.bitrix24.tech",
    "redirectUris": [
      "https://vibecode.bitrix24.tech/oauth/complete",
      "http://localhost"
    ],
    "bitrixClientId": "local.7c3d4e5f6a7b80.55556666",
    "prefix": "vibe_app_local_7c3",
    "suffix": "6666",
    "authorId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "portalId": "8b1f0e2a-3c4d-5e6f-7a8b-9c0d1e2f3a4b",
    "createdAt": "2026-06-24T09:12:45.781Z",
    "updatedAt": "2026-06-24T10:03:18.204Z",
    "placements": ["CRM_DEAL_DETAIL_TAB"],
    "catalogStatus": "PUBLISHED",
    "publishedAt": "2026-06-24T10:03:18.204Z"
  }
}
```

## Пример ответа при ошибке

400 — у OAuth-ключа нет скоупа `placement`:

```json
{
  "success": false,
  "error": {
    "code": "MISSING_SCOPE",
    "message": "OAuth API key must have the placement scope to publish (catalog publishing binds placements on Bitrix24)."
  }
}
```

## Ошибки

| HTTP | Код | Описание |
|------|-----|----------|
| 403 | `INFRA_FORBIDDEN_FOR_COWORK_KEY` | Вызов сделан ключом Cowork/Code — такой ключ работает только с данными, изменяющие операции ему закрыты. Что делать — [Проектный ключ для деплоя](/docs/cowork/deploy-key) |
| 404 | `NOT_FOUND` | Приложение не найдено или принадлежит другому порталу |
| 403 | `FORBIDDEN` | Запрос не от автора приложения и не от администратора портала |
| 409 | `ALREADY_PUBLISHED` | Приложение уже в статусе `PUBLISHED` |
| 400 | `NO_OAUTH_KEY` | У приложения нет связанного OAuth-ключа |
| 400 | `MISSING_SCOPE` | У OAuth-ключа нет скоупа `placement` |
| 409 | `SNAPSHOT_REQUIRED` | Включено хранилище исходников, но сохранённого снапшота нет. Возраст снапшота на исход проверки не влияет. Тело несёт `hint`: без `sourceServerId` он ведёт на `POST /v1/apps/:id/sources` и объясняет, как опубликовать исходники с сервера. С `sourceServerId` — на `POST /v1/infra/servers/:id/sources`. Поле `hint.reason` различает две причины отказа — снапшота нет у приложения или у названного сервера. Ещё два значения приходят, только когда платформа проверяет возраст снапшота. Состав полей подсказки описан в [Хранилище исходников](/docs/source-storage) |
| 404 | `SERVER_NOT_FOUND` | Передан `sourceServerId` сервера, которого нет, который принадлежит другому порталу или который удалён |
| 403 | `NOT_AUTHORIZED` | Передан `sourceServerId` сервера, исходники которого запрос не вправе использовать. Права на приложение сюда не распространяются — нужен ключ-владелец сервера, личный ключ того же пользователя или администратор портала |
| 400 | `NO_USER_TOKEN` | Приложение не авторизовано на портале по OAuth. Пройдите авторизацию и повторите публикацию — [Авторизация пользователей приложения](/docs/keys-auth/oauth). Тело несёт `hint` из трёх полей: `requiredAction` — что сделать, `docsUrl` — эта страница, `oauthDocsUrl` — порядок авторизации. Набор полей у этой подсказки свой, он не совпадает с подсказкой `SNAPSHOT_REQUIRED` |
| 400 | `TITLE_TOO_LONG_FOR_CATALOG` | Итоговое название длиннее 100 символов. Так бывает, когда `catalogTitle` не передан, а `title` приложения длиннее предела каталога |
| 502 | `PLACEMENT_UNBIND_FAILED` | Аккаунт не подтвердил снятие лишних мест встраивания. Приложение НЕ опубликовано, метаданные каталога из этого запроса не сохранены. Коды, которые остались на аккаунте, перечислены в `error.placements`. Поле `placements` приложения при этом уже перезаписано — перечитайте приложение. Приходит только после включения проверки — см. врезку ниже |
| 503 | `NETWORK_DEVKEY_REQUIRED` | Аккаунт переведён на транспорт ключа разработчика, а у автора приложения такого ключа нет. Повтор не поможет — попросите автора переподключить аккаунт. Приходит только после включения проверки — см. врезку ниже |

Полный список общих ошибок API — [Ошибки](/docs/errors).

**Проверка снятия лишних мест встраивания на стороне аккаунта сейчас раскатывается по аккаунтам, поэтому вариантов два:**

- **Пока возможность не включена на аккаунте** — публикация отвечает `200`, даже если снять лишние места на аккаунте не удалось. Неснятый код при этом ОСТАЁТСЯ в `placements` приложения, а причина приходит строкой `Failed to unbind <код>` в `warnings`. Это единственный признак расхождения списка с аккаунтом, поэтому на сегодняшнем проде `warnings` надо разбирать. Кодов `PLACEMENT_UNBIND_FAILED` и `NETWORK_DEVKEY_REQUIRED` у этого эндпоинта не бывает.
- **После включения** — неподтверждённое снятие отвечает `502 PLACEMENT_UNBIND_FAILED` со списком кодов в `error.placements`, а приложение не публикуется вовсе. Ответа `200` с неснятым местом на этом пути не бывает — про снятие в `warnings` успешного ответа остаются пояснения о подтверждении: каким транспортом снятие подтверждено и отказало ли при этом второе плечо. Остальные строки набора включение не меняет — отказы привязки и прочие поводы из таблицы `warnings` выше приходят и на этом пути.

**Отказ `502` здесь не означает «ничего не изменилось».** Поле `placements` приложения записывается ДО того, как выносится отказ, и уже отражает фактическое состояние аккаунта: успешно снятые коды из него ушли, а неподтверждённые остались. Поэтому перед повтором перечитайте приложение через [данные приложения](/docs/apps/get). Повторный запрос без поля `placements` опубликует ровно тот набор, который лежит в приложении сейчас, — вместе с местом, которое вы считали снятым. Чтобы снять его, повторите запрос с явным `placements` без этого кода.

## Известные особенности

- **Публикация требует авторизации приложения на портале.** Привязку мест встраивания выполняет OAuth-токен приложения, поэтому перед первой публикацией приложение проходит авторизацию на портале хотя бы один раз. Без неё публикация возвращает `NO_USER_TOKEN`. Порядок авторизации — [Авторизация пользователей приложения](/docs/keys-auth/oauth). Для публикации достаточно пройти согласие на портале. Забирать токен сессии не нужно — платформа сохраняет токен приложения на возврате.
- **Вход на странице самого приложения токен приложения не создаёт.** Кнопка «Войти через Bitrix24» на адресе вашего приложения выдаёт доступ к приложению — это другая поверхность, и на `NO_USER_TOKEN` при публикации она не влияет. Токен появляется только на согласии OAuth **на стороне портала**: авторизация по ссылке выше, установка приложения на портал или первое открытие встроенного места. Если после входа на странице приложения публикация всё ещё отвечает `NO_USER_TOKEN` — авторизации на портале не было.
- **Токен живёт долго и продлевается сам.** Авторизация нужна один раз, а не перед каждой публикацией: платформа обновляет токен по refresh-токену. Повторная авторизация требуется, только если доступ отозвали на портале или OAuth-приложение пересоздали.
- **`autoSaved: true` у деплоя не всегда закрывает проверку публикации.** Автосейв деплоя сохраняет исходники у того сервера, на который вы деплоили. Если этот сервер принадлежит не OAuth-ключу публикуемого приложения (например, вы деплоили личным ключом `vibe_api_`), снапшот хранится у сервера — проверка публикации по умолчанию смотрит снапшоты приложения и его не видит. Передайте `sourceServerId` того же сервера, и публикация возьмёт исходники оттуда — повторно сохранять их через `POST /v1/apps/:id/sources` не нужно. Идентификатор сервера — тот же, что в адресе деплоя. Список снапшотов сервера — `GET /v1/infra/servers/:serverId/sources`.
- **Заголовок места встраивания — это название приложения.** Пункт левого меню и вкладки CRM подписываются итоговым `catalogTitle`, отдельного названия для меню нет. Переименование приложения через [обновление](/docs/apps/update) меняет и подписи мест встраивания.
- **Снятые с публикации приложения публикуются повторно.** Публикация принимается и в статусе `UNPUBLISHED` — приложение возвращается в `PUBLISHED` с теми же или новыми местами встраивания. Метаданные каталога при снятии с публикации сохранялись, поэтому при пустом теле каталог восстанавливается без повторного заполнения.
- **Часть мест встраивания может не привязаться.** Привязка идёт по каждому коду отдельно. Коды, которые не удалось привязать, не прерывают публикацию — приложение становится `PUBLISHED`, а несработавшие коды перечисляются в `warnings`.

## Смотрите также

- [Приложения](/docs/apps)
- [Авторизация пользователей приложения](/docs/keys-auth/oauth)
- [Снять с публикации](/docs/apps/unpublish)
- [Обновить приложение](/docs/apps/update)
- [Данные приложения](/docs/apps/get)
- [Хранилище исходников](/docs/source-storage)
- [Подписки на события портала](/docs/infra/event-subscriptions)
