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

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

### NEW-0714-1: OpenAPI-спека: валидность 3.1, семантика полей и срез по скоупу

**Было**

Машинная спека [GET /v1/openapi.json](/docs/cli) содержала 3.0-ключ `nullable` (невалиден в 3.1), не объявляла path-параметр `{entityTypeId}` на batch/aggregate/fields/products, не несла описаний/допустимых значений/примеров полей, описывала только ключи `vibe_app_` и отдавалась одним монолитом.

**Стало**

Спека валидна по OpenAPI 3.1: nullable-поля используют union-тип `["<тип>","null"]`, все path-параметры объявлены. У свойств появились `title`/`description`, допустимые значения (`x-enumValues` + расшифровка в описании) и примеры. Секьюрити-схемы называют все три вида ключей (`vibe_api_`/`vibe_app_`/`vibe_live_`), у каждой операции есть `x-required-scope`, а в корне — каталог `x-scopes`. Добавлены корневые `tags`/`externalDocs`, блок `webhooks` для бот-событий и `anyOf` для мультиполей. `GET /v1/openapi.json?scope=<scope>` (например `?scope=crm`) отдаёт срез спеки по одному скоупу, чтобы он помещался в контекст агента. Указатели на спеку добавлены в `/v1/me` и `/v1/guide`.

### FIX-0714-2: Общая 5xx-ошибка на AI-эндпоинтах теперь в OpenAI-совместимом конверте

[AI-эндпоинты](/docs/ai) (`/v1/ai/*`, `/v1/models`, `/v1/chat/*`, `/v1/audio/*`) документированы с OpenAI-совместимым форматом ошибок. При исчерпании пула соединений это уже соблюдалось, но общая (непредвиденная) `5xx`-ошибка на этих эндпоинтах отдавала обычный V1-конверт.

**Было**

Общая `5xx`-ошибка на AI-эндпоинте: `{"success": false, "error": {"code": "...", "message": "..."}}` — не тот формат, что SDK-клиенты OpenAI ожидают для этих путей.

**Стало**

Тот же случай теперь отдаёт `{"error": {"message": "...", "type": "...", "code": "..."}}` — единый конверт для всех ошибок AI-эндпоинтов, включая общие `5xx`.

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

Клиенты на OpenAI SDK, уже читающие `error.type`/`error.code` (штатный путь для этих эндпоинтов), не заметят изменения. Код, который на AI-эндпоинтах ожидал `success`/`error.code` в верхнем уровне именно на общих `5xx`, должен переключиться на `error.type`/`error.code`.

### FIX-0714-3: `versions` в списке версий исходников приложения ограничен 500 записями

`GET /v1/apps/:id/sources` возвращал весь список версий без ограничения — для приложений с очень длинной историей это была неограниченная выборка.

**Было**

`data.versions` — весь список версий без ограничения по размеру; `data.totalVersions` всегда равнялся `data.versions.length`.

**Стало**

`data.versions` содержит не более 500 последних версий (по `savedAt`, убывание). `data.totalVersions` и `data.totalSizeBytes` по-прежнему считаются по полной выборке — точность агрегатов не зависит от кэпа.

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

Для приложений с историей до 500 версий поведение не меняется. Для приложений с историей больше 500 версий `data.versions.length` теперь может быть меньше `data.totalVersions` — код, полагавшийся на их равенство, должен ориентироваться на `data.totalVersions`/`data.totalSizeBytes` для агрегатов и не считать `data.versions` полным списком.

### FIX-0714-4: Телефония: `userId`/`duration` теперь по-настоящему валидируются, а не только на truthy

`POST /v1/calls/register`, `/v1/calls/:callId/show`, `/v1/calls/:callId/hide`, `/v1/calls/:callId/finish` принимали `userId` (и `duration` у `finish`) без проверки типа/формы — любое truthy-значение (например, строка `"abc"` или объект) проходило в Битрикс24 и падало там уже непрозрачной ошибкой апстрима.

**Было**

`{"userId": "abc"}` (или любое другое truthy, не являющееся положительным целым) проходил валидацию и уходил в Битрикс24; ошибка `Required: userId (number)` появлялась только на полностью пустом/falsy значении.

**Стало**

`userId` принимается как положительное целое число либо как числовая строка (`"42"`), иначе — чистый `400 MISSING_PARAMS` с уточнённым текстом `Required: userId (positive integer)` (для `register` — плюс `phoneNumber`). `duration` у `finish` аналогично — неотрицательное число либо числовая строка, иначе `400`.

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

Корректные вызовы (`userId` — число или числовая строка) не меняются. Вызовы с ранее «проходившим» некорректным `userId`/`duration` (не число, не числовая строка) теперь получают явный `400` вместо непрозрачной ошибки со стороны Битрикс24.

### FIX-0714-5: Создание комментария к несуществующей задаче — понятная ошибка вместо ложного успеха

[POST /v1/tasks/:taskId/comments](/docs/entities/task-comments/create) и его пакетный вариант [POST /v1/tasks/:taskId/comments/batch](/docs/entities/task-comments/comments-batch) (`action: create`) обращаются к Битрикс24 для создания комментария на старой карточке задачи. Если задача не существует или недоступна ключу, Битрикс24 не создаёт комментарий и не возвращает идентификатор.

**Было**

Оба эндпоинта отвечали успехом с пустым идентификатором — одиночный вызов `201 {"success": true, "data": {"id": null}}`, пакетный — элементом `{"success": true, "id": null}`. Комментарий не создавался, но интегратор не мог отличить это от штатного случая.

**Стало**

Одиночный вызов возвращает `404 TASK_NOT_FOUND`. Пакетный вариант помечает соответствующий элемент как `{"success": false, "error": "TASK_NOT_FOUND"}`, не отменяя обработку остальных элементов пакета. Отдельный, не связанный с этой ошибкой случай сохраняется: на новой карточке задачи комментарий уходит в чат, и если система не смогла восстановить его id отдельным поиском, ответ по-прежнему `success: true` с `id: null` — комментарий в этом случае реально создан.

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

Код, проверяющий `data.id` / `data[i].id` на `null` как признак ошибки, продолжит работать без изменений и получит более точный код ошибки. Код, полагавшийся на молчаливый успех с `id: null` при недоступной задаче, должен обрабатывать `404` / `TASK_NOT_FOUND` явно.

### FIX-0714-6: Лимит запросов `/v1/search`, `/v1/research`, `/v1/batch` теперь считается на портал

**Было**

Лимиты (`/v1/search` 60/мин, `/v1/research` 20/мин, `/v1/batch` 30/мин) фактически считались по IP-адресу, а не по порталу. Портал с несколькими API-ключами (или несколько порталов за одним общим egress-IP) мог получить больше документированного лимита, а само ограничение обходилось ротацией IP.

**Стало**

Лимит считается **на портал**: все API-ключи одного портала делят единый бакет (60 / 20 / 30 запросов в минуту соответственно). Документированный лимит на тенант теперь применяется корректно и не обходится ни числом ключей, ни сменой IP.

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

Если ваш портал распределял нагрузку на `/v1/search` / `/v1/research` / `/v1/batch` между несколькими API-ключами, суммарный предел теперь — единый лимит портала, а не сумма по ключам. При достижении предела возвращается `429` с заголовком `Retry-After` (как и раньше).

### FIX-0714-7: Календарь: рабочий batch-delete секций, чистые ошибки update и без утечки sync-полей

**Было**

- `POST /v1/calendar-sections/batch` с `action: "delete"` не имел канала для `type`/`ownerId`, которые требует `calendar.section.delete` — каждый элемент падал на обеих платформах.
- `PATCH /v1/calendar-sections/{id}` без `type`/`ownerId`/`name` уходил в Битрикс24 и возвращал сырой `422` с именем внутреннего метода.
- `GET /v1/calendar-sections` отдавал недокументированные сырые поля Битрикс24 `GAPI_CALENDAR_ID`, `CAL_DAV_CON`, `SYNC_TOKEN`, `PAGE_TOKEN`, `EXTERNAL_TYPE` (три из них — токены синхронизации).
- `GET /v1/calendar-events` и `GET /v1/calendar-events/{id}` отдавали внутреннее поле `attendeesEntityList` — схема пыталась его вырезать, но не срабатывала из-за несовпадения регистра ключа.

**Стало**

- Batch-delete секций читает `type`/`ownerId` из тела рядом с `ids` и прокидывает их в каждую команду удаления. Отсутствие любого — чистый `400 MISSING_REQUIRED_PARAMS` до вызова Битрикс24.
- Partial-update секций требует якорные `type`/`ownerId`/`name` (у секций нет get-by-id для подстановки) — чистый `400`, без сырого `422` и без утечки имени метода.
- Оба календарных read-пути убирают перечисленные внутренние/sync-поля из ответа.

**Затронутые эндпоинты:**

- [`POST /v1/calendar-sections/batch`](/docs/entities/calendar-sections/create)
- [`PATCH /v1/calendar-sections/{id}`](/docs/entities/calendar-sections/update)
- [`GET /v1/calendar-sections`](/docs/entities/calendar-sections/list)
- [`GET /v1/calendar-events`](/docs/entities/calendar-events/list)

### FIX-0714-8: PATCH catalog-product-properties снова работает (был неустранимый catch-22)

**Было**

Обновить свойство товара было невозможно ни при каком теле: `PATCH` без `iblockId` → `422` («Required fields: iblockId» — B24 требует его на каждом update), а `PATCH` **с** `iblockId` → `400 READONLY_FIELD` (поле только для создания). Итог — весь глагол UPDATE мёртв: ни одно поле нельзя было изменить после создания.

**Стало**

`iblockId` теперь подставляется автоматически из существующей записи (pre-fetch, как у `catalog-sections`), так что `PATCH {name:"…"}` доходит до B24 с нужным `iblockId` и возвращает `200`. `iblockId` в теле по-прежнему не нужен (и по-прежнему отклоняется как read-only, если его прислать) — его сохраняет сам сервис.

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

Если раньше ваш `PATCH` свойства товара всегда падал `422`/`400` — теперь шлите только изменяемые поля (`PATCH {name:"…"}`), `iblockId` передавать не надо.

### FIX-0714-9: Валидация entityTypeId: мусорные формы → 400 вместо тихого усечения

**Было**

Пять поверхностей парсили `entityTypeId` (селектор типа smart-process / динамической сущности) снисходительно — неякорным `parseInt` или коэрсирующим `Number()`, — и мусорная форма молча превращалась в ДРУГОЙ (в рамках портала) тип сущности:

- Путевой `entityTypeId` (`/v1/items/:entityTypeId/...`, `/v1/categories/:entityTypeId/...` — CRUD и `/aggregate`): `GET /v1/items/1058abc` усекался до `1058`, `1e3` → `1`, `1.5` → `1`.
- Глобальный `POST /v1/batch`: `params.entityTypeId` через `Number()` принимал дробные (`1.5`), hex (`'0x10'` → 16), переполнение до `Infinity`, а также массив `[1058]` → 1058.
- `/v1/items/:entityTypeId/userfields/*`: собственный парсер — `2abc` резолвил пользовательские поля типа `2`.
- `POST /v1/smart-processes/batch`: `ids: ['1030abc']` усекался до `1030` — delete/update молча выполнялся против ЧУЖОГО реального типа; дробное число (`1030.5`) тоже проходило.
- `POST /v1/triggers/fire` (`entityType="item"`): `entityTypeId: '1038abc'` → триггер автоматизации выстреливал по типу `1038`.

**Стало**

Все пять поверхностей требуют каноничную форму положительного целого (`/^[1-9]\d*$/` для строк, `Number.isInteger` для чисел): любая иная форма → `400` с прежним кодом ошибки поверхности (`INVALID_DYNAMIC_PARAM` / `INVALID_ENTITY_TYPE_ID` / `BATCH_ITEM_VALIDATION` / `MISSING_PARAMS`) ДО вызова Битрикс24. Aggregate использует общий с CRUD-маршрутами валидатор вместо инлайн-копии.

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

Формы, которые раньше коэрсились в корректное значение и обслуживались — `007` → 7, `%20`-пробелы, `+2` → 2, массив `[1058]` в batch — теперь отклоняются с `400`: значение должно быть каноничным целым без префиксов, хвостов и ведущих нулей. Boolean в batch отклонялся и раньше (коэрсился в reserved-тип); меняется только код ошибки — теперь `INVALID_DYNAMIC_PARAM`. Корректные вызовы не меняются.

### FIX-0714-10: Переоткрытие фидбека очищает поля решения

**Было**

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

**Стало**

Возврат тикета **из** `RESOLVED`/`WITHDRAWN` в активный статус очищает `resolvedAt`, `resolvedBy` и `resolution`. `RESOLVED`/`WITHDRAWN` по-прежнему проставляют штамп решения; `ARCHIVED` не трогает поля (архивирование сохраняет историю решения). Обычный переход между активными статусами (например `AWAITING_USER → REVIEWING`) поля НЕ трогает — на активных тикетах `resolution` отражает последний комментарий команды. Явно переданный в том же запросе `resolution` имеет приоритет над очисткой.

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

Если вы читали `resolvedAt`/`resolution` переоткрытого тикета и получали значения от прошлого закрытия — теперь они `null` для активного тикета.

### FIX-0714-11: `INVALID_JSON_BODY` больше не цитирует внутренний текст парсера

**Было**

Пять маршрутных групп (`/v1/apps*`, `/v1/bots*`, `/v1/keys*`, `/v1/note*`, `/v1/infra/servers/*` deploy/exec/upload) на битый JSON отвечали `400` с сообщением вида `Invalid JSON: Unexpected token } in JSON at position 41` — сырой текст движка V8 (отпечаток рантайма, деталь реализации). Группа deploy/exec/upload при этом не ставила и код ошибки.

**Стало**

Все пять отвечают единым статическим сообщением `Request body is not valid JSON.` — как `/v1/<entities>` (тот же класс закрывается там отдельным исправлением). На V1-поверхностях код — `INVALID_JSON_BODY` (deploy/exec/upload теперь тоже его ставит); статус `400` не изменился.

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

Если ваш код парсил текст сообщения (например, вытаскивал позицию ошибки) — опирайтесь на код `INVALID_JSON_BODY`; позиция ошибки больше не сообщается.

### FIX-0714-12: Смета: amount, currency и даты в ответе GET снова заполнены (были null)

**Было**

Чтение сметы (`GET`/list/search `/v1/quotes`) отдавало `null` для `amount`, `currency`, `beginDate`, `closeDate` — значения «протекали» только под сырыми ключами Битрикс24 (`opportunity`, `currencyId`, `begindate`, `closedate`). Запись работала правильно, но READ-проекция теряла все поля-алиасы: **сумма сметы и валюта были 100% невидимы через задокументированный API**.

**Стало**

READ-ветка теперь реверс-мапит объявленные алиасы (зеркало write-маппинга): `opportunity → amount`, `currencyId → currency`, `begindate → beginDate`, `closedate → closeDate` — с приведением типов. Сырые ключи Битрикс24 в ответе больше не появляются.

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

Если вы читали `amount`/`currency` смет и получали `null` — теперь они заполнены. Код, читавший обходным путём сырой `opportunity`/`currencyId` из ответа, их там больше не найдёт — переключитесь на документированные `amount`/`currency`.

### FIX-0714-13: Валидация записи: фантомный чек-лист → 404, мусорные типы и `:id` → 400

**Было**

- `POST /v1/tasks/:taskId/checklist` на несуществующую задачу возвращал `201` с правдоподобным `id`, хотя пункт не создавался (его нельзя было получить GET-ом).
- `POST /v1/warehouses` принимал нестроковые `title`/`address` (число, объект) и пересылал их в Битрикс24 с непредсказуемым результатом; `POST /v1/doc-templates` так же пропускал нестроковые `name`/`region` и нечисловой `numeratorId`.
- Нечисловой `:id` в путях сущностей с типизированным числовым id (`GET/PATCH/DELETE /v1/quotes/abc`, `/v1/deals/1.5`, `/v1/leads/1e3`) уходил в Битрикс24 как есть — в ответ прилетала непрозрачная ошибка B24 вместо внятного кода. У smart-processes `12abc` усекался `parseInt`-ом до `12` и попадал в ЧУЖОЙ тип.

**Стало**

- Чек-лист: родительская задача проверяется до создания пункта; несуществующая (или недоступная ключу) задача → `404 TASK_NOT_FOUND`.
- Склады и шаблоны документов: значение не того типа → `400 INVALID_PARAMS` без вызова Битрикс24 (числовая строка в `numeratorId` по-прежнему принимается).
- Сущности с явно типизированным числовым id: не-канонично-целый `:id` → `400 INVALID_PARAMS` до вызова Битрикс24 (id `0` — основная воронка сделок `categories` — остаётся валидным). Smart-processes сохраняют `INVALID_ENTITY_TYPE_ID` и теперь отклоняют `12abc` на GET/PATCH/DELETE, а не усекают до `12`. Сущности, у которых тип id не задекларирован в схеме, сохраняют прежнее сквозное поведение.

Затронутые эндпоинты: [POST /v1/tasks/:taskId/checklist](/docs/entities/tasks/checklist), [POST /v1/warehouses](/docs/entities/warehouses/create), [POST /v1/doc-templates](/docs/entities/doc-templates/create) + GET/PATCH/DELETE по сущностям с типизированным числовым id.

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

Если ваш код опирался на фантомный `201` от чек-листа или отправлял мусорные значения «на авось» — теперь придёт явный `4xx` с кодом. Корректные вызовы не меняются.

### FIX-0714-14: Пять тихих false-success/hint-дефектов: честный ответ вместо мнимого успеха

**Было**

- `PATCH /v1/userfields/{entity}/{id}` c `label` — тихий no-op на обновлении: ответ `200`, но подпись поля не менялась (Битрикс24 `crm.*.userfield.update` игнорирует `LABEL`).
- `POST /v1/humanresources/nodes/{id}` c `parentId` — ложный «перенос удался»: `name` применялся, `parentId` молча игнорировался, а в ответе эхом возвращался старый родитель.
- `POST /v1/chats/messages/bulk` — псевдоним `dialogId: "me"` не разрешался в bulk-цикле (одиночные роуты его разрешают) → сообщение уходило не туда.
- `POST /v1/bots/{botId}/chats/{dialogId}/users` — предохранитель `USERS_NOT_ADDED` был мёртвым кодом (не совпадал с формой ответа метода v2) → неудачное добавление проходило как успех.
- Ошибки `crm.item.list` получали нерелевантную подсказку «Maximum 50 records…» даже когда причина была иной (например «entity type does not exist»).

**Стало**

- `label` на обновлении разворачивается в реальные параметры `EDIT_FORM_LABEL`/`LIST_COLUMN_LABEL`/`LIST_FILTER_LABEL` — подпись действительно меняется.
- `parentId` и `type` теперь только для создания: на `PATCH` они отклоняются с `400` (перенос — через `POST /v1/humanresources/nodes/{id}/move`), а не имитируют успех.
- Bulk-чтение сообщений разрешает `dialogId: "me"` для каждого элемента, как и одиночные роуты.
- Предохранитель добавления в бот-чат снова срабатывает: непринятые пользователи возвращаются в `warning.USERS_NOT_ADDED`.
- Подсказки об известных ограничениях привязаны к релевантности сообщения ошибки, а не только к имени метода.

**Затронутые эндпоинты:**

- [`PATCH /v1/userfields/{entity}/{id}`](/docs/userfields/crm/update)
- [`PATCH /v1/humanresources/nodes/{id}`](/docs/humanresources/nodes/update)
- [`POST /v1/chats/messages/bulk`](/docs/chats)
- [`POST /v1/bots/{botId}/chats/{dialogId}/users`](/docs/bots)

### FIX-0714-15: GET /v1/task-time теперь честно возвращает больше 50 записей при limit>50

**Было**

[GET /v1/task-time](/docs/entities/tasks/time) с `limit` больше 50 возвращал только 50 записей, хотя `meta.limit` повторял запрошенное значение, а `meta.hasMore` мог вводить в заблуждение. Клиент, обходивший данные постранично с шагом больше 50, молча терял часть записей.

**Стало**

Запрос отдаёт до `limit` записей (максимум 500), собирая их постранично на стороне бэкенда; `meta.total` и `meta.hasMore` соответствуют фактически возвращённому окну. При `limit` больше 50 значение `offset` теперь указывает на правильную позицию, а не сдвигается на первые страницы.

### NEW-0714-16: GET /v1/companies/fields и системные поля catalog-prices получили label и description

В ответе [GET /v1/companies/fields](/docs/entities/companies/fields) теперь у всех полей компании есть человекочитаемые `label` и `description`. В [GET /v1/catalog-prices/fields](/docs/entities/catalog-prices/fields) те же метки добавлены системным полям `extraId`, `priceScale` и `timestampX`. Метки приходят по-русски. Семантику поля можно получить программно из ответа, без сверки со статической документацией.

### FIX-0714-17: /stop и /reboot различают отсутствующий сервер и неподходящий статус

**Было**

[POST /v1/infra/servers/:id/stop](/docs/infra/lifecycle/stop) и [POST /v1/infra/servers/:id/reboot](/docs/infra/lifecycle/reboot) на сервере не в статусе `running` (например, спящем) отвечали плоским `404 NOT_FOUND` с текстом «Running server not found» — по нему нельзя было понять, что сервер существует, и агент решал, что он удалён.

**Стало**

Оба маршрута ведут себя как [/start](/docs/infra/lifecycle/start) и [/wake](/docs/infra/lifecycle/wake): `404 SERVER_NOT_FOUND` — только когда сервера с таким `id` действительно нет; `422 SERVER_WRONG_STATE` — когда сервер есть, но не в статусе `running`. В `error.currentState` возвращается текущее состояние, в `error.availableActions` — доступные действия (`wake`/`start`/`repair`/`delete`).

### FIX-0714-18: понятная ошибка на /fields у комментариев и учёта времени задач

**Было**

Запрос [GET /v1/tasks/:taskId/comments/fields](/docs/entities/task-comments) отвечал сбивающим с толку `400 INVALID_PARAMS` с текстом «taskId and id must be positive integers» (спрашивали про поля — ответ про идентификатор), а [GET /v1/tasks/:taskId/time/fields](/docs/entities/tasks/time) утекал сырой ошибкой Bitrix24 с внутренним PHP-методом и HTML-тегом.

**Стало**

Обе сущности распознают сегмент `fields` и возвращают понятный `400 WRONG_PATH`: у них нет метода `/fields` (состав полей описан в справочнике `/v1/guide`), в тексте перечислены доступные маршруты. Нечисловой или дробный идентификатор в маршрутах по id теперь отклоняется как `400 INVALID_PARAMS` до обращения к Bitrix24 — без утечки внутренней ошибки.

### FIX-0714-19: Лимиты `/v1/ai/credentials*` считаются на портал; `CREDENTIAL_NOT_FOUND` подсказывает решение

**Было**

- Лимиты BYOK-маршрутов (`POST /v1/ai/credentials`, `/:id/test`, `/:id/fetch-models` — 10/мин; `/:id/models` добавление/удаление — 30/мин) фактически считались по IP-адресу: перебор чужих ключей обходился ротацией IP, а арендаторы за одним общим egress-IP делили один бакет. Маршрут `PATCH /:id` (при передаче `credentials` он проверяет ключ у апстрима — тот же оракул, что `/:id/test`) не имел лимита вовсе.
- `404 CREDENTIAL_NOT_FOUND` от `/v1/search` и `/v1/research` содержал только слаг провайдера — без указания, как настроить ключ.

**Стало**

- Лимит считается **на портал**: все API-ключи одного портала делят единый бакет; ротация IP и число ключей на предел не влияют. При превышении — `429` с `Retry-After`. Дополнительно `PATCH /:id` (проверяет ключ у апстрима, как `/:id/test`) раньше вовсе не имел лимита — теперь тоже 10/мин на портал.
- В ответ `CREDENTIAL_NOT_FOUND` добавлено поле `hint` с точным рецептом: `POST /v1/search/credentials {provider, apiKey}`, список провайдеров — `GET /v1/search/providers`.

### FIX-0714-20: PATCH bizproc-шаблонов, роботов и активностей через /v1 применяет изменения

**Было**

`PATCH /v1/bizproc-templates/:id`, `/v1/bizproc-robots/:code` и `/v1/bizproc-activities/:code` с полями метаданных (`name`, `description`, `autoExecute`) возвращали `422 BITRIX_ERROR "No fields to update."` — обновить сущность было нельзя (то же и в пакетных запросах). Поле `autoExecute`, переданное числом, дополнительно отвергалось как `Incorrect field AUTO_EXECUTE!`.

**Стало**

Поля корректно применяются (в том числе `autoExecute`, переданное числом); эндпоинт подтверждает успех и возвращает `id` обновлённой сущности. Работают и одиночный `PATCH`, и пакетные запросы. Создание (`POST`) не изменилось.

### FIX-0714-21: Транскрипция: проверка кошелька до вызова распознавания

**Было**

`POST /v1/audio/transcriptions` (и `/v1/ai/audio/transcriptions`) не проверял состояние кошелька перед вызовом: PREPAY-счёт, ушедший за овердрафт (но ещё не замороженный фоновой проверкой), всё равно запускал распознавание и уводил баланс глубже в минус. Чат и эмбеддинги отклоняли такой вызов заранее; полностью замороженный счёт и раньше блокировался глобально (`ACCOUNT_FROZEN`).

**Стало**

Как у чата и эмбеддингов: денежная проверка выполняется **до** обращения к Whisper. Превышенный овердрафт → `402 insufficient_balance`, распознавание не запускается. BYOK-ключи (USER-scope) бесплатны — для них проверки нет и поведение не меняется.

### FIX-0714-22: удаление приложения больше не блокируется галактическим хостом на его ключе

**Было**

`DELETE /v1/apps/:id` возвращал `409 APP_HAS_ACTIVE_SERVERS`, если на ключе приложения оказывался галактический хост (общая инфраструктура портала). Снять хост через удаление приложения было нельзя, а объяснения в ответе не было.

**Стало**

Галактический хост исключён из проверки блокирующих серверов: он управляется на уровне портала, а не ключа, поэтому не должен мешать удалению приложения. Отдельные серверы приложения (в том числе контейнеры приложений) по-прежнему блокируют удаление с `409 APP_HAS_ACTIVE_SERVERS` — их сначала нужно перепривязать к другому ключу.

### FIX-0714-23: OpenAPI: тело per-entity батч-эндпоинтов теперь описано верно (action + items/ids/calls)

**Было**

Спека (`GET /v1/openapi.json`) описывала тело каждого пер-сущностного батча как `{create:[], update:[], delete:[]}`. Рантайм же (общий обработчик батчей) требует `{action, items|ids|calls}` и отвечает `400 INVALID_BATCH_ACTION` на документированную форму. Клиент, сгенерированный из спеки (codegen / AI-агент), получал **100% отказ batch-write** на всех ~45 пер-сущностных батч-эндпоинтах. Сама фича работает — врала только спека (глобальный `POST /v1/batch`, `/v1/tasks/{taskId}/comments/batch` и `/v1/guide` уже описывали правильную форму).

**Стало**

Генератор спеки выдаёт правильную форму: один `action` (`create`/`update`/`delete`/`list`/`get`/`fields`); `create`/`update` шлют `items`, `delete` — `ids`, чтения — `calls`. Совпадает с рантаймом и с глобальным `/v1/batch`.

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

Если вы генерировали клиент из `openapi.json` и batch-write падал `INVALID_BATCH_ACTION` — перегенерируйте: тело теперь `{action:"create", items:[…]}` вместо `{create:[…]}`. Handwritten-клиенты, уже славшие `{action,…}`, не затронуты.

### FIX-0714-24: Валюты: fullName, формат и число знаков теперь сохраняются при плоской записи

**Было**

`POST`/`PATCH /v1/currencies` с плоскими `fullName`, `formatString`, `decimals`, `decPoint`, `thousandsSep` возвращал успех, но значения **молча терялись** — Битрикс24 хранит их в структуре локализации по языкам (`LANG`), а API слал их плоско. Задокументированный обходной путь «передавайте сырой `LANG`» тоже не работал: `LANG` — поле только для чтения, запрос отклонялся.

**Стало**

API упаковывает плоские локализуемые поля в локализацию **вашего** языка (языка API-ключа) перед вызовом Битрикс24, поэтому плоская запись сохраняется и читается обратно (`POST {fullName:"…",decimals:3}` → `GET` вернёт их). Работает на всех путях записи: одиночном, `POST /v1/currencies/batch` и глобальном `POST /v1/batch`. Сырой `LANG` в теле по-прежнему отклоняется как read-only. Разные значения сразу для нескольких языков через API пока не задаются. Язык записи определяется локалью API-ключа (ru или en) и может не совпадать с языком отображения валюты на портале — на порталах с иной локалью (de/pl/ua…) правка ляжет под en.

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

Если вы обходили баг сырым `LANG` (и упирались в `400 READONLY_FIELD`) — уберите его и шлите плоские поля. Уже работавшие плоские запросы теперь ещё и сохраняют значения.

### FIX-0714-25: Aggregate уважает обязательные фильтры; infra-валидация не течёт сырым Zod

**Было**

- `POST /v1/<entity>/aggregate` с сущностью, требующей фильтр (напр. `catalog-products` требует `iblockId`), пропускал пустой запрос в Битрикс24 и возвращал сырой `422`, тогда как GET-list/search на том же условии отдают чистый `400`.
- `POST /v1/infra/servers/:id/{deploy,exec,upload,logs}` при ошибке валидации тела клал в `error.message` многострочный JSON-сериализованный массив Zod-issues (сырой отпечаток валидатора).

**Стало**

- Aggregate проверяет обязательные фильтры/параметры до вызова Битрикс24: нет обязательного фильтра — `400 MISSING_REQUIRED_FILTER` (напр. `catalog-products`→`iblockId`); нет обязательного list-параметра — `400 MISSING_REQUIRED_PARAMS` (напр. `calendar-events`→`type`,`ownerId`; `humanresources-nodes`→`type`). Как у list/search.
- Infra-валидация форматирует ошибку компактно (`поле: сообщение; …`). Код (`VALIDATION_ERROR`) и статус `400` не изменились.

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

Если вы ловили сырой `422` от aggregate без фильтра — теперь придёт `400 MISSING_REQUIRED_FILTER`. Если парсили infra-`error.message` как JSON — теперь это плоская строка `поле: сообщение`.

### FIX-0714-26: Batch: работает per-entity batch для items и создание папок через batch

**Было**

- `POST /v1/items/{entityTypeId}/batch` возвращал `404` — per-entity batch-роут для сущностей с динамическим параметром (`items`, `categories`) монтировался по адресу без сегмента параметра (`/v1/items/batch`), поэтому документированный путь не находился, а `entityTypeId` не доходил до команды Битрикс24. Обходной путь — глобальный `POST /v1/batch` — работал.
- `POST /v1/folders/batch` с `action: "create"` падал на каждом элементе с `ERROR_ARGUMENT`: batch-создание слало `fields[...]`, тогда как `disk.folder.addsubfolder` ожидает родительскую папку верхним параметром `id`, а остальные поля — под `data[...]`.

**Стало**

- Per-entity batch-роут для `items`/`categories` монтируется с сегментом `:{entityTypeId}` и прокидывает валидированный `entityTypeId` (положительное целое; id выделенных API вроде `deals=2` отклоняются с подсказкой — как в одиночных роутах) в каждую команду — для **всех** действий: `create`/`update`/`delete` и read-действий `list`/`get`/`fields`.
- Batch-создание папок повторяет форму одиночного роута: `id=<родитель>&data[...]`. Пропущенный `parentId` — чистый per-item `400` до вызова Битрикс24.

**Затронутые эндпоинты:**

- [`POST /v1/items/{entityTypeId}/batch`](/docs/entities/items/create)
- [`POST /v1/folders/batch`](/docs/entities/folders/create)

### NEW-0714-27: восстановление зависшего exec-канала сервера

Новый эндпоинт [POST /v1/infra/servers/:id/unstick](/docs/infra/deploy/exec) принудительно освобождает залипший канал команд Black Hole-сервера, когда деплой или exec продолжает отвечать `EXEC_BUSY` («Another command is running») даже после `DELETE /v1/infra/servers/:id/lock`. Эндпоинт снимает блокировку на стороне платформы и разрывает туннель агента — при переподключении агент завершает зависшую команду и освобождает свой мьютекс. Перезагрузка сервера не выполняется.

Если в момент вызова на сервере ещё выполняется легитимная операция (деплой, exec, харден), эндпоинт по умолчанию отвечает `409 OPERATION_IN_PROGRESS` и НЕ трогает её — расклинивать нужно только зависший канал. Повторите с `?force=true`, если уверены, что канал команд действительно завис.

Ответ: `{ success: true, data: { backendLockReleased, agentBounced, reconnected } }`. Коды ошибок: `404 SERVER_NOT_FOUND`, `409 CONFLICT` (восстановление уже идёт), `409 OPERATION_IN_PROGRESS` (на сервере выполняется операция — повторите с `?force=true`), `409 GALAXY_UNSTICK_UNSUPPORTED` (для galaxy-хостов и galaxy-приложений не поддерживается), `502 GATEWAY_ERROR`. Ошибка `/exec` (`EXEC_BUSY`) и провал `/deploy` (код `DEPLOY_FAILED`, сообщение «Another command is running») теперь несут подсказку `hint` с этим эндпоинтом.

### NEW-0714-28: описание сервера в `PATCH /v1/infra/servers/:id`

**Было**

`PATCH /v1/infra/servers/:id` принимал только `displayName`. Поле `description` в контракте отсутствовало, а `GET`-ответы его не отдавали.

**Стало**

`PATCH /v1/infra/servers/:id` принимает опциональное поле `description` (строка, до 500 символов; пустая строка или `null` очищает описание; отсутствие поля оставляет текущее значение без изменений). Значение синхронизируется с карточкой приложения в каталоге. Поле `description` теперь возвращается в `GET /v1/infra/servers`, `GET /v1/infra/servers/:id` и в ответе `PATCH`. Старые запросы без `description` продолжают работать без изменений.

### FIX-0714-29: деплой galaxy-приложения при обрыве связи во время сборки возвращает честную ошибку, а не ложный успех

**Было**

Если соединение с хостом обрывалось прямо во время сборки galaxy-приложения (частое явление под нагрузкой тяжёлой сборки), `POST /v1/infra/servers/:id/deploy` мог вернуть `200` со статусом `running` и пометкой `[recovered]` в `buildLog`, хотя новая версия так и не собралась и не поднялась — на хосте продолжал работать прежний контейнер. Повторный деплой упирался в тот же обрыв и снова рапортовал ложный успех.

**Стало**

Деплой считается восстановленным, только если он полностью завершился: под именем приложения запущен контейнер именно этой попытки, и деплой прошёл до конца. Если связь оборвалась во время сборки — или в любой момент до завершения деплоя — и новая версия не поднялась полностью, эндпоинт возвращает retryable-ошибку `502` с кодом `GALAXY_HOST_UNREACHABLE` и подсказкой «повторите тот же деплой, не удаляйте сервер» — вместо ложного `200`. Только если деплой завершился полностью и потерялся лишь финальный ответ, восстановление в `200` работает как прежде.

### BC-0714-30: переименование приложения в каталоге доходит до Битрикс24, publish потерял menuTitle

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

**Было**

[PATCH /v1/apps/:id](/docs/apps/update) с полем `title` у приложения, добавленного в каталог, возвращал `200`, но не менял ничего из того, что видит пользователь: карточка каталога и привязки мест встраивания на портале (пункт левого меню, вкладки CRM) оставались со старым именем. Отказа не было никогда — вызов всегда успешен.

У [POST /v1/apps/:id/publish](/docs/apps/publish) тело не проверялось, а заголовок места встраивания задавался отдельным полем `menuTitle`.

**Стало**

Для приложения в каталоге `title` — это одна операция над отображаемым именем: имя синхронизируется в карточку каталога (вместе с `title` пишется `catalogTitle`) и перепривязывается в места встраивания на портале. Поэтому вызов, который раньше всегда отдавал `200`, теперь может честно отказать:

- `400 NO_USER_TOKEN` — приложение не авторизовано на портале, перепривязать места встраивания нечем;
- `400 TITLE_TOO_LONG_FOR_CATALOG` — имя приложения в каталоге ограничено 100 символами, тогда как `title` допускает 255;
- `502 BITRIX_PARTIAL_REBIND` — Битрикс24 отклонил привязку. Имя в этом случае не записывается, повтор того же запроса чинит состояние.

Тело `POST /v1/apps/:id/publish` теперь валидируется, а параметр `menuTitle` удалён: заголовок места встраивания всегда равен отображаемому имени приложения. Пустое тело по-прежнему работает — публикация берёт значения из записи приложения.

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

- Убрать `menuTitle` из тела публикации: поле игнорируется, имя пункта меню задаётся через `title` и `catalogTitle`.
- Держать имя приложения в каталоге в пределах 100 символов.
- Обработать отказы при переименовании: на `NO_USER_TOKEN` авторизовать приложение на портале, на `BITRIX_PARTIAL_REBIND` повторить запрос.
- Учесть побочный эффект: переименование через `title` теперь пишет и `catalogTitle` — у приложения в каталоге эти поля держатся синхронными.

### NEW-0714-31: GET /v1/tasks/:taskId/comments/fields — схема полей комментариев задачи

У комментариев к задаче появился метод `/fields`, как у остальных сущностей: `GET /v1/tasks/:taskId/comments/fields` возвращает статическую схему из 5 полей (`id`, `taskId`, `authorId`, `message`, `createdAt`) с типом, признаком «только для чтения», названием и описанием по-русски. Метод не обращается к Битрикс24. Раньше этот путь возвращал `400 WRONG_PATH` — состав полей был доступен только из статических доков.

### FIX-0714-32: пер-сущностный batch с action list теперь применяет filter

**Было**

`POST /v1/{entity}/batch` с `action: "list"` игнорировал `filter`: имена полей не приводились к именам Битрикс24 (например `statusId` у лидов не превращался в `stageId`), а операторы `$gt` / `$contains` / `$in` и другие не работали. Вызов возвращал `200` со всей таблицей — тихий отказ с неверными данными. Глобальный `POST /v1/batch` и `POST /v1/{entity}/search` при этом фильтровали корректно.

**Стало**

Пер-сущностный batch пропускает `filter` через тот же транслятор, что `search` и глобальный `/v1/batch`. Псевдонимы полей и операторы (`$gt`, `$gte`, `$lt`, `$lte`, `$ne`, `$contains`, `$in`, `$nin`, префиксные `>=`, `<=`, `!` и т.д.) применяются. Некорректный `filter` (неизвестное поле у сущности с полной схемой, неподдерживаемый оператор, префиксы `@` / `!@`, логические токены `$or` / `$and`) теперь возвращает `400` с указанием индекса вызова, а не молча всю выборку — так же, как на одиночных эндпоинтах.

### FIX-0714-33: авто-пагинация больше не дублирует записи на границах страниц

**Было**

Для методов Битрикс24 без поддержки сортировки (например список объектов хранилища) запись на границе страниц могла сместиться между запросами соседних страниц и попасть в обе — при `limit > 50` в выдаче появлялся дубль, занимавший слот, и клиент обрабатывал одну и ту же запись дважды.

**Стало**

После склейки всех страниц выдача дедуплицируется по `id` (сохраняется первое вхождение). Для методов со стабильной сортировкой ничего не меняется (дублей нет — операция вхолостую).

### FIX-0714-34: список полей шаблона реквизитов больше не пустеет; создание не возвращает чужую запись

**Было**

`GET /v1/requisite-presets/:presetId/fields` мог вернуть `200` с пустым `data: []`, хотя в шаблоне есть поля: метод Битрикс24 отдаёт `result` то массивом `[{…}]`, то объектом-картой `{"0":{…},"1":{…}}`, а обработчик принимал только массив. При создании поля (`POST …/fields`) ответ мог содержать ЧУЖУЮ существующую запись — эхо созданной строки читалось по id из ответа `add`, а на некоторых порталах чтение по этому id возвращало другое поле.

**Стало**

Список нормализует обе формы ответа Битрикс24 (массив и объект-карту) — поля больше не теряются. Эхо созданной записи возвращается только если её `fieldName` совпадает с тем, что создавали; при любом расхождении ответ содержит `{ id }` (сам объект не подставляется), чтобы клиент не получил постороннюю строку.

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

Идентификатор поля шаблона (`id`) — это позиционный идентификатор Битрикс24: он может меняться после операций записи в шаблоне и на части порталов не является устойчивым ключом. Не кэшируйте `id` между изменениями шаблона — перечитывайте список полей перед `get`/`update`/`delete` по конкретному полю.

### FIX-0714-35: привязка плейсмента для ранее созданных приложений больше не отклоняется по обработчику

**Было**

[POST /v1/placements/bind](/docs/keys-auth) мог вернуть `400` с кодом `PLATFORM_HANDLER_UNRESOLVABLE` для приложения, созданного до перехода на единый платформенный обработчик (у такого приложения в качестве обработчика сохранился его собственный технический адрес). Привязка отклонялась даже тогда, когда платформенный обработчик `/v1/bitrix-handler` был доступен, — приложение нельзя было опубликовать заново через API.

**Стало**

Привязка проходит: обработчик плейсмента регистрируется на платформенный `/v1/bitrix-handler`, а в ответе появляются `handlerRewritten: true` и `requestedHandler` с исходным значением. Код `PLATFORM_HANDLER_UNRESOLVABLE` теперь возвращается только когда платформенный обработчик действительно недоступен. [POST /v1/placements/unbind](/docs/keys-auth) снимает такой плейсмент по тому же адресу.
