# Изменения API: 16 сентября 2026

[← Журнал изменений](/docs/changelog) · [Сентябрь 2026](/docs/changelog/2026-09)

### FIX-0916-1: партнёрским аккаунтам подняли бесплатные лимиты Коворка

**Было**

Бесплатное место Коворка считалось по общим окнам независимо от того, чей это
аккаунт. `GET /v1/cowork/me` и `GET /v1/cowork/state` отдавали доли, посчитанные
от общей сетки, а на исчерпании окна запрос к AI получал `402`.

**Стало**

У партнёрского (NFR) аккаунта бесплатное место считается по своим окнам — они
задаются на платформе и накладываются только вверх. Форма ответа прежняя:
`quotaPct` и `windows` те же поля, но доли теперь считаются от партнёрского
потолка, и `402` по исчерпанию приходит позже. Платное место партнёра не
затронуто — оно считается по своему тарифу, и цена места не меняется.

Признак партнёрства платформа читает из лицензии аккаунта. У коробки он
появляется после перерегистрации модуля Вайбкод на портале.

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

Делать ничего не нужно: поля и статусы прежние, успешный ответ остаётся
успешным. Если клиент держит у себя копию потолка, взятую из доли и расхода, её
стоит пересчитывать по свежему ответу, а не кэшировать между сессиями.

### FIX-0916-3: одновременная замена ключа приложения выдаёт ровно один ключ

**Было**

Ограничение «одна замена на приложение» держалось не всё время работы запроса, а лишь до того, как соседняя замена финишировала. Два одновременных вызова `POST /v1/cowork/applications/{id}/key` с РАЗНЫМИ значениями `Idempotency-Key` могли попасть в это окно и выдать по ключу: документированный отказ `APPLICATION_KEY_REPLACE_IN_PROGRESS` второму не приходил, слот карточки доставался тому, кто финишировал вторым, а ключ, выданный первому, молча переводился на суточный срок как заменённый — его владелец об этом не узнавал.

**Стало**

Ограничение действует всё время работы запроса. Тот, чья карточка успела уйти под чужую замену, получает `409 APPLICATION_KEY_REPLACE_IN_PROGRESS` и ничего не чеканит — ровно тот отказ, который у метода уже описан. Ключ выдаётся один, и он же лежит в слоте.

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

Менять ничего не нужно: код `APPLICATION_KEY_REPLACE_IN_PROGRESS` и совет по нему у метода прежние, новых кодов не добавилось. Изменилась частота: отказ приходит теперь и в той гонке, где раньше проскакивала вторая выдача. Обрабатывайте его как штатный исход параллельного вызова — прочитайте карточку приложения и посмотрите, нужен ли ещё один ключ. Если вы шлёте замену параллельно сами себе, надёжнее сериализовать вызовы у себя.

### NEW-0916-4: детальная карточка сервера показывает порт, найденный агентом туннеля

[GET /v1/infra/servers/:id](/docs/infra/servers/get) в полной карточке сервера дополнительно возвращает `detectedPort` и `detectedPortObservedAt`: последний достоверно наблюдавшийся целевой порт туннеля и время наблюдения. До первого наблюдения оба поля равны `null`; неуспешное наблюдение не стирает прежнее. Сам GET не опрашивает машину, поэтому актуальность значения нужно оценивать вместе со временем и статусом подключения.

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

Действий не требуется: новые поля аддитивны. Для диагностики фактической цели туннеля используйте `detectedPort`, а `localPort` продолжайте трактовать как настройку или fallback.

### FIX-0916-5: код COWORK_HARNESS_DISABLED больше не приходит на ключах подписки Коворка

**Было**

[PATCH /v1/keys/:id](/docs/management-keys) и [POST /v1/keys/:id/rotate](/docs/management-keys)
отвечали `403` с кодом `COWORK_HARNESS_DISABLED`, пока выдача ключей подписки для сторонних
агентов была закрыта на платформе.

**Стало**

Этот код не приходит. Остальные проверки выдачи прежние: доступ владельца ключа к Коворку,
разрешение администратора портала на сторонние клиенты и состояние подписки — каждая по-прежнему
отвечает `403` со своим кодом, успешный ответ `200` остаётся успешным. Менять в интеграции ничего
не нужно, обработку кода `COWORK_HARNESS_DISABLED` можно снять.

### FIX-0916-6: Чтение объектов ограничивается отдельно для каждого портала

**Было**

[GET /v1/storage/objects/:key](/docs/storage/objects/get) и
[HEAD /v1/storage/objects/:key](/docs/storage/objects/head) не ограничивали поток
повторных запросов.

**Стало**

Каждый метод получил собственный бюджет 600 запросов в минуту на портал, общий
для всех API-ключей этого портала. Обычные успешные ответы не изменились. После
исчерпания бюджета API отвечает `429 RATE_LIMITED`: действующее значение лимита
приходит в `x-ratelimit-limit`, задержка перед повтором — в `Retry-After`.

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

Менять обычные запросы не требуется. При `429 RATE_LIMITED` выдерживайте
`Retry-After` перед повтором.

### FIX-0916-7: API показывает фактическое состояние туннеля

**Было**

После потери туннеля сервер мог оставаться `CONNECTED` в ответах GET и `/refresh`, поэтому клиент не видел действие `repair`, а `/exec`, `/upload` и `/logs` доходили до ошибки отсутствующего туннеля.

**Стало**

Список, карточка и refresh показывают фактическое состояние туннеля, включая `DISCONNECTED` и действие `repair`. Перед exec, upload, logs и deploy API проверяет доступность сервера. Отсутствие туннеля считается подтверждённым только по непустому актуальному снимку соединений; пустой или недоступный снимок сохраняет записанный статус. Exec, upload и deploy могут восстановить подтверждённо отсутствующий туннель, а GET logs возвращает `409 SERVER_NOT_READY` с указанием явного `POST /repair`, не запуская ремонт. Успешные ответы и их HTTP 200 статус сохраняются.

**Затронутые эндпоинты:** [GET /v1/infra/servers](/docs/infra/servers/list), [GET /v1/infra/servers/:id](/docs/infra/servers/get), [POST /v1/infra/servers/:id/refresh](/docs/infra/lifecycle/refresh), [POST /v1/infra/servers/:id/exec](/docs/infra/deploy/exec), [POST /v1/infra/servers/:id/upload](/docs/infra/deploy/upload), [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy), [GET /v1/infra/servers/:id/logs](/docs/infra/deploy/logs).

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

Считайте `DISCONNECTED` и `repair` актуальным состоянием. После `409 SERVER_NOT_READY` повторяйте операцию с обычным backoff и учитывайте `Retry-After`, когда заголовок присутствует. Для GET logs сначала вызовите указанный в `hint` `POST /repair`: сам GET ремонт не запускает. Форматы ответов и успешный HTTP 200 не меняются.

### NEW-0916-8: ключи выгодных часов и компенсаций Коворка приходят всегда

В [GET /v1/cowork/me](/docs/cowork/me) и [GET /v1/cowork/state](/docs/cowork/state) ключи приходят
в каждом успешном ответе: `offPeak` (оба ответа), `touSavedPct` (state), `relief` (оба ответа),
`boostPct` и `boostExpiresAt` (me). Проверку наличия этих ключей можно снять — ответ `200`
по-прежнему успешен, значения внутри блоков прежние, и `null` внутри блока сохраняет свой отдельный
смысл. `currentWindowEndsInHours` в [GET /v1/off-peak](/docs/ai/consumption/off-peak) приходит в
каждом успешном ответе, как и прежде: от включения по аккаунтам он не зависел никогда.

`offPeakHint` в отказе `402 cowork_quota_exhausted` на
[POST /v1/chat/completions](/docs/ai/chat/completions) больше не зависит от включения возможности
для аккаунта, но по-прежнему приходит, только когда момент разблокировки попадает в час со скидкой.
Проверка наличия этого ключа остаётся.

### FIX-0916-9: хранилище: файл, загруженный повторно под тем же ключом, больше не числится несколькими объектами

**Было**

До 04.09.2026 повторная загрузка файла под тем же ключом через личный ключ разработчика или от имени сервера создавала ещё один объект вместо замены. В ответе [GET /v1/storage/objects](/docs/storage/objects/list) один файл повторялся несколько раз под разными идентификаторами, а [GET /v1/storage/objects/{key}](/docs/storage/objects/get) и [DELETE /v1/storage/objects/{key}](/docs/storage/objects/delete) работали с одной из копий, поэтому после удаления файл продолжал числиться в списке.

**Стало**

Копии приватных файлов свёрнуты: у ключа остаётся один объект с идентификатором самой ранней неудалённой копии и метаданными последней загрузки (`sizeBytes`, `sha256`, `contentType`, `contentUpdatedAt`). Удаление по ключу срабатывает с первого раза. Байты файлов не менялись. Копии публичных файлов пока остаются на месте, чтобы выданные на них ссылки [GET /v1/public-storage/{portalId}/{objectId}](/docs/storage/objects/public-get) продолжали работать. Объекты, загруженные ключом приложения, не затронуты.

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

Менять ничего не нужно. Приватный файл читается и удаляется по ключу, а ключ не изменился. Идентификаторы свёрнутых копий пропадают из листинга.

### FIX-0916-10: будильник на сервере с режимом работы больше не отбивается как конфликт с круглосуточным

**Было**

`POST /v1/infra/servers/{id}/wake-schedules` отвечал `400 ALWAYS_ON_CONFLICT` любому серверу, у которого не задан порог засыпания по простою, — в том числе машине, которой владелец уже назначил режим работы по расписанию. Добавить такой машине дополнительное пробуждение было невозможно.

**Стало**

Сервер в режиме работы «по расписанию» (`SCHEDULE`) круглосуточным не считается: сон у него объявлен владельцем явно, окнами расписания. Дополнительный будильник на такой машине создаётся обычным порядком, ответ `201` не изменился. Для машины без расписания — в том числе в режиме `ALWAYS` — поведение прежнее: `400 ALWAYS_ON_CONFLICT`, как и раньше.

### BC-0916-11: Однозначный тип сущности при получении CRM-документов

> Поддержка старого формата до: не предусмотрена

**Было**

[GET /v1/crm-documents](/docs/entities/documents/crm-list) принимал повторённый `entityTypeId` с ответом HTTP 200 и мог вернуть документы другой сущности. Одновременная передача `entityTypeId` и `entityTypeID` также принималась.

**Стало**

Повтор параметра, оба имени вместе и скобочные формы возвращают HTTP 400 `INVALID_ENTITY_TYPE`, даже если значения совпадают. Для одиночной скобочной формы код ошибки изменён с `MISSING_PARAMS` на `INVALID_ENTITY_TYPE`. Одиночное положительное целое под именем `entityTypeId` или `entityTypeID` работает как прежде.

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

Передавайте ровно один параметр `entityTypeId` или `entityTypeID` с одним положительным целым значением. Не используйте массивы или объекты для типа сущности. Кодируйте пользовательский ввод при построении строки запроса.

### NEW-0916-12: у событий календаря появился include: owner, host, attendee

Событие календаря теперь объявляет три связи на сотрудников, и их можно затребовать параметром `include` у `GET /v1/calendar-events` и `GET /v1/calendar-events/{id}`: `owner` — владелец календаря (по полю `ownerId`), `host` — организатор встречи (по `meetingHost`), `attendee` — приглашённые (по `attendeeList`). Раньше связей у сущности не было объявлено ни одной, поэтому любое имя отвергалось с `400 INVALID_INCLUDE`, а список допустимых имён в тексте отказа был пустым.

Запрос вида `GET /v1/calendar-events/{id}?include=owner,host` отвечает `200` и кладёт карточки сотрудников в `_included`. Запрос без `include` работает как прежде. Связь читает карточку сотрудника, поэтому ключ обязан нести право `user` — иначе запрос отвечает `403 SCOPE_DENIED`.

Имя вне этого списка отвергается по-прежнему, но отказ теперь называет доступные связи: `Unknown include 'section'. Available: owner, host, attendee`. Секции календаря связью не объявлены намеренно: в Битрикс24 нет чтения секции по идентификатору, она доступна только списком.

### NEW-0916-13: пользовательские поля сотрудников через /v1/userfields/users

Пользовательские поля сотрудника (сущность `users`, префикс имени `UF_USR_`) теперь заводятся и правятся через API Вайбкод. Раньше платформа умела только читать их: поле сотрудника уже приезжало в `GET /v1/users/fields` со своим типом и списком значений, а на попытку создать такое же поле `POST /v1/userfields/users` отвечал `400 UNKNOWN_ENTITY` со списком из шести сущностей CRM.

Появились шесть маршрутов: `GET /v1/userfields/users` (список), `GET /v1/userfields/users/{id}` (одно поле), `POST /v1/userfields/users` (создание), `PATCH /v1/userfields/users/{id}` (правка), `DELETE /v1/userfields/users/{id}` (удаление). Нужен скоуп `user.userfield` — голого `user` для этой сущности недостаточно. Тело создания то же, что у полей CRM: `fieldName`, `userTypeId`, `label` и остальные свойства поля. Имя поля сотрудника несёт префикс `UF_USR_` и передаётся целиком — платформа его не дописывает.

`GET /v1/userfields/users/types` отвечает `400 UNSUPPORTED_ACTION`: у Битрикс24 нет метода, перечисляющего типы полей сотрудника, поэтому `userTypeId` при создании указывается явно. Прежнее поведение шести сущностей CRM на `/v1/userfields/{entity}` не изменилось ни в одном ответе.

Та же сущность появилась в MCP: тула `manage_userfield` принимает `entity: "users"`.

### FIX-0916-14: отказ агрегации объясняет ограничение чтения без пагинации

**Было**

В `POST /v1/{entity}/aggregate` сообщение `AGGREGATION_LIMIT_EXCEEDED` предлагало постраничную выгрузку даже для сущностей, которые этот путь агрегации читает без пагинации.

**Стало**

Для таких сущностей сообщение объясняет защиту от неограниченного чтения при числовой агрегации и группировке. Оно предлагает `count` без `groupBy`, не обещая отсутствие чтения данных при подсчёте. Статус HTTP 422, код ошибки и лимит 5000 записей сохранены.

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

Изменился только текст `error.message` для этого случая. Обрабатывайте отказ по коду `AGGREGATION_LIMIT_EXCEEDED`.

### BC-0916-15: previousKey в ответе замены ключа приложения

> Поддержка старого формата до: не предусмотрена

**Было**

`POST /v1/cowork/applications/{id}/key` на ветке `rotated` возвращал `previousKey` с обязательной датой `graceUntil` — моментом, до которого прежний ключ ещё аутентифицируется.

**Стало**

`previousKey` несёт `{ id, graceUntil, status }`. Поле `graceUntil` теперь может быть `null`, и ровно тогда `status` равен `BLOCKED`: прежний ключ был заблокирован, не работал ещё до замены, и отсрочки ему не выдавалось. У живого прежнего ключа всё как раньше — дата и `status` `ACTIVE`. Ответ остаётся HTTP 201.

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

Допустите `null` в `graceUntil` и печатайте текст по `status`: при `BLOCKED` прежний ключ уже не работает, ждать нечего, и приложению нужен новый секрет прямо сейчас. Код, читающий `graceUntil` как всегда присутствующую дату, на таком ответе сломается.

### FIX-0916-16: приложение с заблокированным ключом чинится заменой

**Было**

`POST /v1/cowork/applications/{id}/key` отвечал 409 `KEY_ROTATE_NOT_ACTIVE`, если личный ключ приложения был заблокирован. Освободить место под ключ было нечем: удаление отбивалось живым сервером, а включить ключ обратно нельзя — блокировка терминальна. Приложение чинилось только пересозданием.

**Стало**

Замена проходит. Блокировка при этом не отменяется: прежняя строка остаётся заблокированной, отсрочки на сутки ей не выдаётся, а выписанные ею живые токены доступа гасятся, а не переезжают на новый ключ. Отказ `KEY_ROTATE_NOT_ACTIVE` теперь означает только отозванный или просроченный ключ. Тексты отказов замены переписаны: три из четырёх называют конкретный раздел кабинета и действие, а отказ по системному ключу раздела не называет — его в кабинете нет, — и вместо этого говорит, что делать: обратиться в поддержку. Если живые токены заблокированного ключа погасить не удалось, замена не выдаётся за успех: ответ — 500 `KEY_ROTATE_REVOKE_FAILED` с `details.orphanKeyId` (id выписанного, но никому не отданного ключа), секрет не выдан. Дальнейшее решает одно поле. `details.strandedSlots` нет — слоты вернулись на прежний ключ целиком: отзовите ключ из `orphanKeyId` и повторите с НОВЫМ `Idempotency-Key`. Поле есть — откат удался не целиком, перечисленные категории всё ещё указывают на осиротевший ключ: повторять НЕЛЬЗЯ, отзывать его тоже нельзя — нужна поддержка. Одна запись там не категория слота: `previousKeyGrace` означает, что прежний ключ, заблокированный уже посреди замены, сохранил выданный ротацией суточный срок и перестанет работать на этом дедлайне. Повтор здесь хуже, чем бесполезен: он заменит тот ключ, на который карточка указывает СЕЙЧАС, и может ответить 201 с рабочим секретом, а живые токены утёкшего ключа так и не будут погашены — инцидент останется открытым за экраном, который говорит «готово».

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

Ответ на успешную замену не изменился, кроме `previousKey` (отдельная запись). Клиент, ветвившийся на 409 `KEY_ROTATE_NOT_ACTIVE` как на «ключ заблокирован», должен убрать эту ветку: заблокированный ключ теперь заменяется, а этот код означает только отозванный или просроченный.

### FIX-0916-17: POST /v1/keys/{id}/rotate принимает заблокированный ключ

**Было**

Ротация заблокированного ключа отвечала 403 `KEY_BLOCKED`. Ключ, который удерживал живой сервер, не чинился ничем: удалить его не давал сервер, перевыпустить — эта проверка.

**Стало**

Ротация проходит на тех же условиях, что и замена ключа приложения: блокировка остаётся, прежней строке не выдаётся отсрочка, её живые токены доступа гасятся. Если погасить их не удалось, ротация не выдаётся за успех: эндпоинт отвечает 500 `KEY_ROTATE_REVOKE_FAILED` с `details.orphanKeyId` (id уже выписанного, но не отданного ключа), а сырой секрет в таком ответе не возвращается. Если откат ротации сам прошёл не до конца, к ответу добавляется `details.strandedSlots` — что осталось указывать на осиротевший ключ; при чистом откате этого поля в ответе нет. Одна запись там не слот: `previousKeyGrace` означает, что прежний ключ сохранил суточный срок, выданный ротацией (так бывает, когда ключ заблокировали уже посреди неё и вернуть прежний срок не удалось), — на этом дедлайне он перестанет работать. Разблокировка по-прежнему невозможна — `PATCH` заблокированного ключа отвечает 403 `KEY_BLOCKED`, как и раньше.

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

Клиент, который раньше получал на заблокированном ключе только 403, теперь получает 201 либо 500 `KEY_ROTATE_REVOKE_FAILED`. Ветвитесь по `details.strandedSlots`: поля нет — откат прошёл целиком, отзовите осиротевший ключ из `details.orphanKeyId` и повторите вызов — `Idempotency-Key` эндпоинт не поддерживает, так что повтор это обычный новый запрос, и он попробует погасить токены заново. Поле есть — повторять НЕЛЬЗЯ: часть хозяйства осталась на осиротевшем ключе, и повтор заменит уже его, а живые токены утёкшего ключа так и не будут погашены — ответ может оказаться даже успешным, но компрометация останется. Отзывать осиротевший ключ тоже нельзя, пока на него что-то указывает, — нужна поддержка. Успешные ответы не изменились.

### NEW-0916-19: каталог моделей: фильтр по возможности для ключей Коворка/Кода

[GET /v1/models](/docs/ai/models) принимает необязательный параметр `capability`
— явная выборка моделей по одной объявленной возможности вместо перебора
полного списка на стороне клиента.

Параметр читается только у ключа со скоупом `vibe:cowork`. У остальных ключей он
**игнорируется**: ответ остаётся прежним при любом значении, включая неизвестное.
Набор допустимых значений закрыт и расширяется вместе с возможностями, которые
платформа обслуживает для Коворка/Кода.

Ответ без параметра не изменился ни для одного ключа.

### BC-0916-20: название приложения из одних пробелов отклоняется на всех путях записи

> Поддержка старого формата до: не предусмотрена

**Было**

Название, состоящее только из пробелов, табуляций или переводов строки (например `"   "`), проходило проверку «не короче одного символа». Приложение создавалось или переименовывалось, а его пункт меню в Битрикс24 получал пустое название.

**Стало**

Такое значение отклоняется с `400 VALIDATION_ERROR` до обращения к Битрикс24 — во всех запросах, где задаётся отображаемое имя приложения или подписи встройки:

- [POST /v1/apps](/docs/apps/create) — `title` (приложение и ключ не создаются);
- [PATCH /v1/apps/{id}](/docs/apps/update) — `title`;
- [POST /v1/apps/{id}/publish](/docs/apps/publish) — `catalogTitle`, а также публикация из мастера и изменение карточки каталога;
- `POST /v1/placements/bind` — `title` встройки (см. [встройки приложения](/docs/apps/placements)).

Название, в котором есть хотя бы один видимый символ, принимается как раньше и сохраняется без изменений, включая пробелы по краям. На уже сохранённые значения правило не распространяется: публикация приложения со старым пустым названием проходит как прежде.

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

Передавайте в `title` и `catalogTitle` название хотя бы с одним видимым символом. Если название формируется автоматически и может оказаться пустым, подставьте осмысленное значение по умолчанию до отправки запроса. Особое внимание — на сценарии переименования и повторной публикации: раньше они пустое название принимали.

### FIX-0916-21: вызов на прежний адрес переехавшего портала отбивается названным отказом

**Было**

Портал коробки переехал на новый адрес, а вебхук ключа остался выписан на прежний. Вызов отбивался защитой (секрет не уходит туда, где портала больше нет), но клиент получал `500` с телом `{"success":false,"error":{"code":"INTERNAL_ERROR","message":"Internal server error"}}` — ни причины, ни подсказки, что делать.

**Стало**

Тот же вызов отвечает `409`:

```json
{"success":false,"error":{"code":"PORTAL_ADDRESS_CHANGED","message":"Portal address changed: this credential is issued for the portal's previous address. Re-issue the key webhook to continue"}}
```

Действие то же, что и раньше: перевыпустить вебхук ключа («Ключи» → «Перевыпустить»). Ключ, его строка и права не меняются.
