# Изменения API: 23 июля 2026

[← Журнал изменений](/docs/changelog) · [Июль 2026](/docs/changelog/2026-07)

### NEW-0723-1: библиотека чертежей приложений — ТЗ по API-ключу

Готовые технические задания популярных приложений теперь доступны по ключу: `GET /v1/app/blueprints/:slug?locale=ru|en` возвращает сырой markdown ТЗ (`Content-Type: text/markdown`). Эндпоинт требует `Authorization: Bearer <ключ>`; неизвестный или скрытый чертёж — `404 BLUEPRINT_NOT_FOUND`. Ссылку на ТЗ несёт копируемый «Промт для AI» при создании ключа — AI-агент скачивает ТЗ тем же ключом. Прежний анонимный путь `/api/public/blueprints/:slug.md` удалён.

### NEW-0723-2: исходники удалённого сервера: доступ, уборка и честный ответ на вычищенные байты

Исходники переживают сервер — это давняя гарантия платформы, но добраться до них через API было нельзя: весь набор `/v1/infra/servers/:id/sources*` отвечал `404` на удалённый сервер, поэтому владелец не мог ни посмотреть свои версии, ни снять с них тег, ни удалить. Для версии с тегом `published` или `manual` это был тупик: снятие тега — единственный разрешённый способ обойти `409 PROTECTED_BY_TAG`, а именно оно и было недоступно.

Теперь на удалённом сервере работают чтение и уборка: список версий, метаданные версии, скачивание, `tag`, `PATCH`, `DELETE` и `cleanup`. Сохранение новых версий (`POST /sources`) по-прежнему отвечает `404` — мёртвый сервер новых депозитов не принимает.

Чтобы удалённый сервер вообще можно было найти, [GET /v1/infra/servers](/docs/infra/servers/list) принимает `?includeDeleted=true`. По умолчанию выдача не меняется. В каждой строке появилось поле `deletedAt` (`null` у живых серверов).

Отдельно: версия, чьи байты уже вычищены из хранилища, теперь отвечает `410` с кодом `SOURCE_VERSION_BYTES_PURGED` вместо `404`. Разница существенная — `404` утверждал, что версии нет, тогда как запись о ней жива, а восстановление другое: перезалить архив, а не искать его в другом месте. Код приходит на скачивании и на деплое по `{"source": {"versionId": "vN"}}`.

**Затронутые эндпоинты:** [GET /v1/infra/servers](/docs/infra/servers/list), [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy), `GET /v1/infra/servers/:id/sources`, `GET /v1/infra/servers/:id/sources/:versionId/download` — контракт исходников описан на странице [Хранилище исходников](/docs/source-storage)

### FIX-0723-3: деплой galaxy-приложения: обрыв туннеля при сборке больше не маскируется под «host unreachable»

**Было**

Если во время сборки на galaxy-хосте моргал туннель и здорового контейнера этого деплоя в итоге не оказывалось (сборку прервало, контейнер не поднялся, канал exec был занят или приложение крашилось), деплой (`POST /v1/infra/servers/:id/deploy`) возвращал `502 GALAXY_HOST_UNREACHABLE` с советом «повторите, когда хост переподключится». Хост при этом часто был доступен — совет вводил в заблуждение, а вызывающая сторона не видела реальную причину (например, что её приложение падает при старте).

**Стало**

Если после моргания туннеля хост доступен, но деплой не довёл приложение до рабочего состояния, деплой возвращает новый код `502 GALAXY_DEPLOY_INTERRUPTED` — «хост доступен, но деплой прервался до старта приложения: отправьте тот же деплой повторно; если приложение раз за разом не стартует, сначала почините его (команду запуска, порт, зависимости, переменные окружения или лимит памяти)». Это по-прежнему повторяемая ошибка — слот цел, удалять и пересоздавать его не нужно. Код `GALAXY_HOST_UNREACHABLE` теперь остаётся только для действительно недоступного хоста (ни одна попытка проверки до него не достучалась). Если приложение действительно крашится в цикле, это надёжно выявляется уже на повторном деплое обычной проверкой живости (код `GALAXY_APP_START_FAILED`).

### NEW-0723-4: подсказка в ошибке таймаута шага деплоя

Ошибка `DEPLOY_TIMEOUT` у [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy) теперь несёт структурированное поле `error.hint` (`reason`, `recovery`, `recoveryAction`), привязанное к зависшему шагу (`error.step`). Для команд пользователя (`install`, `preStart`) подсказка объясняет, что команда не завершилась в отведённое время, и советует сделать её неинтерактивной и завершающейся, а долгоживущие сервисы запускать из команды `start` или в фоне (`docker compose up -d`). Для шага установки рантайма (`runtime` — платформенный шаг, а не команда пользователя) и прочих служебных шагов подсказка указывает на возможный обрыв туннеля и на `POST /v1/infra/servers/:id/repair`. Поле аддитивное: прежние `error.code`, `error.message` и `error.step` не изменились, менять интеграцию не нужно; подсказка приходит и в JSON-режиме, и в SSE-событии `error`.

### FIX-0723-5: ошибка шага деплоя показывает реальную причину, а не безобидное предупреждение

**Было**

При падении шага деплоя поле `data.steps[].stderr` (и, как следствие, `error.message`) могло нести только безобидное предупреждение из одного потока, теряя реальную причину сбоя.

**Стало**

Оба потока возвращаются вместе, с метками `stderr:` и `stdout:`; реальная причина больше не скрывается. Форма ответа и имя поля не изменились.

### FIX-0723-6: сортировка списка по неуникальному полю больше не теряет записи на второй странице

**Было**

Запрос списка или поиск с сортировкой по неуникальному полю (например по датовому `begindate`) при выборке больше 50 записей мог молча вернуть меньше записей, чем есть: на границе страницы часть записей с одинаковым значением поля сортировки терялась. Ответ приходил с кодом 200, без признака неполноты. Затронуты сущности на основе CRM smart-process — сделки, лиды, контакты, компании, предложения, счета и элементы смарт-процессов `/v1/items/{entityTypeId}` — на путях `GET /v1/{entity}`, `POST /v1/{entity}/search`, в подвызовах `POST /v1/batch` и per-entity `POST /v1/{entity}/batch`. Тот же класс нестабильности затрагивал и числовую агрегацию (`POST /v1/{entity}/aggregate` с `sum`/`avg`/`min`/`max`/`groupBy`): выборка записей для агрегата шла без порядка, поэтому на объёмах больше 50 записей часть строк могла теряться и искажать результат.

**Стало**

К сортировке автоматически добавляется вторичный ключ по `id` — порядок становится строго определённым, и постраничная выборка не теряет и не дублирует записи независимо от поля сортировки. Пользовательская сортировка остаётся основным ключом; записи с одинаковым значением поля упорядочиваются по `id` по возрастанию. Менять запросы не нужно.

### FIX-0723-7: переоткрытие тикета комментарием больше не оставляет штамп решения

**Было**

Комментарий команды через `POST /v1/feedback/:id/comments`, возвращающий тикет из `RESOLVED` или `WITHDRAWN` обратно в активный статус (`NEW`, `REVIEWING`, `AWAITING_USER`, `NEEDS_REVIEW`), не сбрасывал `resolvedAt` и `resolvedBy`. Они «зависали» от прошлого закрытия, и на чтении (`GET /v1/feedback/:id`, `GET /v1/feedback`) переоткрытый тикет выглядел одновременно активным и решённым.

**Стало**

Такой комментарий очищает `resolvedAt` и `resolvedBy` — на чтении активный тикет больше не несёт даты решения. `resolution` при этом не очищается: он отражает текст самого комментария. Комментарий, ставящий `RESOLVED`, по-прежнему проставляет штамп; переход в `ARCHIVED` и обычный переход между активными статусами штамп не трогают. Поведение согласовано с уже действовавшим сбросом на `PATCH /v1/feedback/:id`.

### FIX-0723-8: фильтр списков statuses / departments / storages / currencies / products больше не игнорируется молча

**Было**

`GET /v1/statuses`, `/v1/departments`, `/v1/storages`, `/v1/currencies`, `/v1/products` работают через legacy-методы Битрикс24 (`crm.status.list`, `department.get`, `disk.storage.getlist`, `crm.currency.list`, `crm.product.list`), которые молча игнорируют нефильтруемые ключи и операторы. Неизвестное или неподдерживаемое поле фильтра — например `filter[system]` у статусов, `filter[module]` у хранилищ или `filter[price]` у товаров — а также операторы `$gt` / `$contains` / `$ne` возвращали `200` со всей таблицей. Клиент получал полный набор вместо ожидаемого подмножества — тихий отказ с неверными данными.

**Стало**

Для этих сущностей фильтр проверяется до вызова Битрикс24: разрешены только те поля, которые метод действительно фильтрует (проверено вживую). Остальные поля, операторы и пустые множества возвращают `400 UNSUPPORTED_FILTER` с перечнем фильтруемых полей. Разрешённые поля по сущностям: `statuses` — id, entityId, statusId, name, sort, semantics, categoryId; `departments` — id, name, parentId, headId; `storages` — id, name, code, entityType, entityId; `products` — id, name, code, xmlId, active, sectionId, sort. `crm.currency.list` не фильтрует ничего — любой `filter` у `/v1/currencies` возвращает `400` с подсказкой отфильтровать на стороне клиента.

### BC-0723-9: POST /v1/apps больше не возвращает поля prefix и suffix в ответе на создание

> Поддержка старого формата до: 21.07.2026

**Было**

Ответ POST /v1/apps на создание приложения содержал два значения на `vibe_app_` — короткий prefix и полный rawKey. Короткий prefix ошибочно принимали за ключ, и запрос с ним возвращал 401.

**Стало**

Ответ на создание содержит одно значение `vibe_app_` — рабочий rawKey. Поля prefix и suffix остаются в GET /v1/apps и GET /v1/apps/:id для отображения маскированного ключа.

**Что делать интеграторам**

Используйте поле rawKey из ответа на создание как X-Api-Key. Маскированный префикс, если он нужен, берите из GET /v1/apps или GET /v1/apps/:id вместо ответа на создание.

### FIX-0723-10: POST /v1/batch сохраняет телефон и почту при создании и обновлении лидов и контактов

**Было**

Через общий POST /v1/batch значения телефона и почты (мультиполя) при создании или обновлении лида либо контакта терялись. Вызов возвращал успех, но поле не сохранялось. Те же данные через одиночный POST /v1/leads или PATCH /v1/contacts/:id и через POST /v1/{entity}/batch сохранялись корректно.

**Стало**

Общий POST /v1/batch сериализует мультиполя так же, как одиночные вызовы. Телефон и почта сохраняются при создании и обновлении.

### FIX-0723-11: Поиск и research больше не отвечают 402 INSUFFICIENT_BALANCE при положительном балансе

**Было**

`POST /v1/search` и `POST /v1/research` с платформенным движком (`bitrix-search`) на персональном ключе могли вернуть `402 INSUFFICIENT_BALANCE` даже при достаточном балансе Vibe на счёте портала. Предварительная проверка баланса искала счёт по владельцу ключа, а счёт с недавних пор один на весь портал — и не находился.

**Стало**

Проверка и списание баланса всегда идут по счёту портала. При положительном балансе запрос выполняется и списывается корректно; `402` возвращается только при реальной нехватке средств. Форма запроса и ответа не изменилась.

### FIX-0723-12: POST /v1/apps честнее сообщает о необходимости подписки Маркетплейса на cloud-shared пути

**Было**

При создании приложения на портале без активной подписки «BitrixGPT + Маркетплейс» [POST /v1/apps](/docs/apps/create) возвращал непрозрачный `502 CONNECTOR_APP_INSTALL_FAILED` без указания причины.

**Стало**

Отказ по подписке теперь классифицируется заранее: до обращения к коннектору `POST /v1/apps` проверяет авторитетное состояние подписки портала и, если оно отсутствует, сразу возвращает `403` с кодом `B24_MARKET_SUBSCRIPTION_REQUIRED` (или `B24_MARKET_TRIAL_USED`, если демо уже использован), понятным сообщением и ссылкой на оформление в `error.details.upgradeUrl`. Если состояние не авторитетно, тот же результат срабатывает при явном отказе коннектора по подписке. Прочие отказы cloud-shared выпуска (модуль не установлен, доступ запрещён портал-админом, прочие ошибки) классифицируются как прежде.

**Влияние на интеграторов**

Менять ничего не нужно, успешные вызовы не затронуты. Тем, кто обрабатывал `502 CONNECTOR_APP_INSTALL_FAILED` при создании приложения, стоит дополнительно ловить `403 B24_MARKET_SUBSCRIPTION_REQUIRED` / `B24_MARKET_TRIAL_USED` и подсказывать пользователю оформить подписку на портале.

### FIX-0723-13: запуск сервера не отвечает ошибкой, если машина уже работает

**Было**

[POST /v1/infra/servers/:id/start](/docs/infra/lifecycle/start) для сервера в состоянии `error`
вызывал запуск машины у облака и любой отказ отдавал как `502 PROVIDER_ERROR`. Если машина к этому
моменту уже работала — например, её подняло автоматическое восстановление после вытеснения, — облако
отвечало отказом «машина уже в состоянии RUNNING», и вызов возвращал ошибку на операции, которая
фактически удалась. Клиент видел `502` и не мог отличить это от настоящего сбоя.

**Стало**

Такой отказ распознаётся как идемпотентный успех: если машина уже работает или находится в
переходном состоянии, вызов возвращает `200` и сервер переходит в `provisioning`, как при обычном
запуске. Настоящие отказы — недостаточно прав, исчерпана квота, машина не найдена — по-прежнему
возвращают `502 PROVIDER_ERROR`.

### BC-0723-14: форма deployment.standalone.requiredFields.create в /v1/me стала объектом + документирует slug поля name

> Поддержка старого формата до: 22.01.2027

**Было**

В ответе `GET /v1/me` per-kind под-блок `deployment.standalone.requiredFields.create` был массивом `["provider", "name", "plan", "region"]` — формат `name` не указывался; у соседнего `deployment.galaxyApp.requiredFields.create` поле `name` говорило лишь «required». Имя с кириллицей или заглавными буквами при [POST /v1/infra/servers](/docs/infra/servers/create) отклонялось с `400 INVALID_REQUEST`, но self-discovery об этом ограничении молчал.

**Стало**

`deployment.standalone.requiredFields.create` теперь объект (как соседний `deployment.galaxyApp.requiredFields.create`), и в обоих под-блоках `name` несёт формат: slug из строчных латинских букв по маске `^[a-z][a-z0-9-]*$`, длина 2–63 символа. Человекочитаемую подпись кладите в необязательное поле `displayName`.

**Что делать интеграторам**

Плоский `deployment.requiredFields["POST /v1/infra/servers"]` (массив `["provider","name","plan","region"]`) НЕ изменился — если вы читаете его, делать ничего не нужно, набор обязательных полей тот же. Если же ваш код парсил per-kind под-блок `deployment.standalone.requiredFields.create` как массив (`.forEach` / `.includes("name")` / `.length` / `[0]`), перейдите на чтение объекта: ключи — имена полей (`provider`/`name`/`plan`/`region`), значения — их описания.

### NEW-0723-15: GET /v1/contacts/fields получил label и description для всех полей

В ответе [GET /v1/contacts/fields](/docs/entities/contacts/fields) теперь у всех 28 статических полей контакта есть человекочитаемые `label` и `description`. Раньше базовые поля (`name`, `lastName`, `typeId` и другие) приходили только с `type` и `readonly`, без описания смысла. Метки приходят по-русски. Семантику поля можно получить программно из ответа, без сверки со статической документацией. Кроме того, `GET /v1/openapi.json` публикует эти метки и описания (по-английски) как `title` и `description` в схемах `Contact` и `ContactInput`.

### NEW-0723-16: smart-processes: поля relations и linkedUserFields во входной схеме

Поля `relations` (связи с сущностями CRM — parent/child, например привязка смарт-процесса к сделкам) и `linkedUserFields` теперь объявлены во входной схеме и видны в `GET /v1/smart-processes/fields`. Их можно передавать в `POST /v1/smart-processes` и `PATCH /v1/smart-processes/:entityTypeId`, чтобы связать смарт-процесс с другими сущностями CRM и вывести его в пользовательских полях. Фильтрация и сортировка по этим полям не поддерживаются — это вложенные структуры записи, а не поля выборки.

### FIX-0723-17: bizproc-activities и bizproc-robots: тип documentType в /fields исправлен на array

**Было**

`GET /v1/bizproc-activities/fields` и `GET /v1/bizproc-robots/fields` показывали для `documentType` тип `object`, тогда как поле — массив из трёх элементов (`[moduleId, entity, documentType]`), как уже было объявлено у `bizproc-templates`.

**Стало**

Тип `documentType` в `/fields` теперь `array` у всех трёх сущностей — согласованно с реальным контрактом.

### FIX-0723-18: PATCH /v1/bizproc-templates возвращает id числом

**Было**

[PATCH /v1/bizproc-templates/:id](/docs/entities/bizproc-templates/update) возвращал `data.id` строкой (`"1215"`), тогда как `POST` возвращает число (`1215`). Клиент, сравнивавший id из ответа создания с ответом обновления, получал ложное несовпадение.

**Стало**

Ответ `PATCH` возвращает `data.id` числом (`1215`) — так же, как `POST`.

### FIX-0723-19: userfields: тип label в схеме создания исправлен на string

**Было**

OpenAPI-схема `POST /v1/userfields/{entity}` объявляла `label` как `object`. B24 `crm.<entity>.userfield.add` принимает `LABEL` только строкой, поэтому SDK, сгенерированный по спеке (где `label` — объект), отправлял неверный тип и получал ошибку. OpenAPI-спека — публичный контракт: клиенты генерируют по ней SDK, и у тех, у кого тип был «объект», клиент был сломан.

**Стало**

`label` в схеме создания объявлен как `string` (подпись на языке портала по умолчанию). Мультиязычные подписи задаются через `editFormLabel` / `listColumnLabel` / `listFilterLabel` (PATCH после создания). Рантайм не менялся — правка только генерируемой спеки.

### FIX-0723-20: sleep-now для galaxy-приложения теперь отвечает 400 — управляйте им со страницы «Галактики»

**Было**

`POST /v1/infra/servers/:id/sleep-now` для приложения, размещённого в галактике (`GALAXY_APP`), усыплял контейнер и отвечал `200`. Это расходилось с сессионным маршрутом, который такое приложение уже отклонял, и могло рассинхронизировать состояние контейнера с хостом.

**Стало**

Тот же вызов для galaxy-приложения отвечает `400` с `error.code = "GALAXY_APP_USE_GALAXY_ROUTE"` и не меняет состояние: контейнер остаётся `RUNNING`. Управляйте жизненным циклом приложения через маршруты галактики. Для обычных (standalone) серверов поведение sleep-now не изменилось.

### FIX-0723-21: комментарий задачи больше не отдаётся под чужой задачей

**Было**

На старых порталах `GET /v1/tasks/:taskId/comments/:id` возвращал `200` и сам комментарий, даже когда комментарий не принадлежал задаче `:taskId`: один и тот же комментарий отдавался под любой задачей, а поле `taskId` в ответе было простым эхом пути.

**Стало**

Перед выдачей комментарий сверяется с задачей из пути. Если комментарий не принадлежит `:taskId`, эндпоинт отвечает `404` с кодом `NOT_FOUND` и сообщением «Comment not found». Поле `taskId` в ответе теперь совпадает с реальной родительской задачей. Запросы комментария по его настоящей задаче работают как прежде.

### FIX-0723-22: тип компании — единое поле typeId, не companyType

**Было**

Имя поля «тип компании» различалось на слоях. [POST /v1/companies](/docs/entities/companies/create) с полем `companyType` молча игнорировал тип — компания создавалась с типом по умолчанию; сохранить тип можно было только полем `typeId`. Чтение (`GET`, поиск) всегда возвращало тип в поле `typeId`. Фильтр же принимал `companyType`, но не `typeId`.

**Стало**

Тип компании — единое поле `typeId` во всех операциях: создание и изменение, чтение и поиск, фильтр (`filter[typeId]`) и группировка (`groupBy: typeId`). Значения прежние — `CUSTOMER`, `SUPPLIER`, `COMPETITOR` (список: `GET /v1/statuses?filter[entityId]=COMPANY_TYPE`). Поле теперь описано в [GET /v1/companies/fields](/docs/entities/companies/fields).

**Влияние на интеграторов**

Указывайте тип полем `typeId`. Чтение не меняется — тип всегда приходил в `typeId`. На создании и изменении `companyType` больше не описан (он и раньше не сохранял значение). В фильтре и группировке теперь работает `typeId`, а `companyType` возвращает `400` (`UNKNOWN_FILTER_FIELD` в фильтре, `INVALID_AGGREGATION_FIELD` в группировке) — замените имя на `typeId`.
