# Изменения API: 12 августа 2026

[← Журнал изменений](/docs/changelog) · [Август 2026](/docs/changelog/2026-08)

### NEW-0812-1: nextPollAfterMs — платформа может попросить опрашивать события реже

[GET /v1/bots/{botId}/events](/docs/bots/events/polling) отдаёт новое необязательное поле `nextPollAfterMs` — сколько миллисекунд стоит подождать перед следующим запросом. Поле приходит только боту, которому события доставляются вебхуком (`eventMode` равен `webhook`): очередь `Event.get` у такого бота пуста по устройству Битрикс24, поэтому частый опрос не приносит ничего. Боту в режиме `fetch` поле не приходит вовсе — опрашивайте с прежней частотой.

Отсутствие поля означает «опрашивайте как раньше», поэтому пустым или нулевым оно не приходит. Поле необязательное: клиент, который его игнорирует, продолжает работать без изменений. Если в ответе есть непрочитанный остаток (`hasMore` равен `true`), дочитайте очередь не дожидаясь присланной паузы — она относится к следующему пустому опросу.

### FIX-0812-2: `touSavedPct` теперь доля месячного объёма подписки, а не доля расхода

**Было**

`GET /v1/cowork/state` считал `touSavedPct` как долю недисконтированного счёта — `сэкономлено / (сэкономлено + израсходовано)`. У такой дроби числитель и знаменатель обязаны покрывать одно и то же окно, а счётчик экономии начал работать посреди уже идущего расчётного периода. Поэтому поле намеренно возвращало `null` до конца первого периода — на практике почти у всех подписок.

**Стало**

Знаменатель — месячный объём подписки, тот же, что у полосы месячной квоты. Значение читается с первого дня и не подавляется: `null` приходит, только когда за текущий период ещё ничего не сэкономлено. Верхняя граница — 100.

Две доли теперь читаются рядом: «израсходовано N% месячного объёма» и «выгодные часы вернули M% месячного объёма». Тип и диапазон поля не изменились, действий на стороне клиента не требуется.

### NEW-0812-3: Порт приложения закрепляется за сервером и переживает пробуждение

**Было**
`GET /v1/infra/servers/:id` отдавал только `localPort`, а он равен 3000 у всех серверов по умолчанию — понять по ответу, ходит агент строго на этот порт или выбирает его сам, было нельзя. Порт, заданный через `PATCH /v1/infra/servers/:id/port`, жил только в памяти агента: после пробуждения сервера или починки туннеля выбор шёл заново, и публичный адрес мог начать отдавать соседний процесс.

**Стало**
Успешная выкладка, прошедшая проверку работоспособности, записывает порт в настройки агента на машине и помечает сервер закреплённым. Ответ `GET /v1/infra/servers/:id` несёт `portPinned`: при `true` агент проксирует строго на `localPort` и не переизбирает порт ни после пробуждения, ни после починки; при `false` порт по-прежнему определяется автоматически по слушающим сокетам.

У закреплённого сервера [`PATCH /v1/infra/servers/:id/port`](/docs/infra/deploy/port) применяет порт перезаписью настроек агента и его перезапуском, а не командой на лету. Следствия для клиента: `409 PORT_NOT_APPLIED` на таком сервере не приходит никогда; туннель на несколько секунд обрывается; `verified: true` означает, что настройки несут запрошенный порт и агент вернулся на связь, а `verified: false` — что порт записан, но возвращения агента дождаться не удалось (это не отказ, порт применится). `port: 0` снимает закрепление. Добавлены два кода отказа: `502 AGENT_CONFIG_WRITE_FAILED` — машина отклонила запись настроек, `502 GATEWAY_ERROR` — команда не дошла до машины.

В `data.steps[]` ответа [`POST /v1/infra/servers/:id/deploy`](/docs/infra/deploy/deploy) появился шаг `port_pin` — исход записи порта в настройки агента. Он не влияет на успех выкладки: `warning` на нём означает, что приложение выложено и работает, но порт не закрепился.

### BC-0812-4: context в обращениях должен быть JSON-объектом

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

**Было**

[POST /v1/feedback](/docs/feedback/submit) принимал `context` любого JSON-типа, включая строку, массив, число, boolean и `null`.

**Стало**

`context` остался необязательным. Если поле передано, его верхний уровень должен быть JSON-объектом, включая `{}`. Строка, массив, число, boolean и `null` получают `400 VALIDATION_ERROR`; обращение не создаётся.

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

Передавайте объект: `{"context":{"endpoint":"/v1/feedback"}}`. Отправители, которые передавали строку, массив, число, boolean или `null`, должны перейти на объект: старый формат перестаёт приниматься с этим релизом.

### NEW-0812-5: публикация приложения принимает исходники с указанного сервера

`POST /v1/apps/:id/publish` принимает необязательное поле `sourceServerId` (или заголовок `X-Source-Server`) — идентификатор сервера, исходники которого публикуются. Это закрывает случай, когда автосейв деплоя отвечал `autoSaved: true`, а следующая публикация всё равно возвращала `409 SNAPSHOT_REQUIRED`: если сервер принадлежит не OAuth-ключу публикуемого приложения (например, деплой шёл под личным ключом `vibe_api_`), снапшот сохраняется у сервера, а не у приложения, и проверка публикации его не видела. Теперь достаточно назвать тот же сервер, по которому вы вызывали деплой; повторно сохранять исходники через `POST /v1/apps/:id/sources` не нужно.

Сервер платформа не подбирает — идентификатор передаёте вы. Права на сервер проверяются отдельно от прав на приложение: подходит ключ-владелец сервера, личный ключ того же пользователя или администратор портала. Сервера нет, он из другого портала или удалён — `404 SERVER_NOT_FOUND`; прав нет — `403 NOT_AUTHORIZED`; заголовок `X-Source-Server` не в форме UUID или передан дважды — `400 VALIDATION_ERROR`. Поле тела приоритетнее заголовка: когда `sourceServerId` пришёл в теле, заголовок не читается и не проверяется. Вместе с `sourceServerId` поле `sourceVersionId` означает версию этого сервера: нумерация версий у сервера своя.

Тег `published` и отметка о публикации ставятся только на версию самого приложения. Версию сервера публикация не помечает: те же поля у неё хранят историю деплоя, которую отдаёт `GET /v1/infra/servers/:id/sources`, а тег означает бессрочное хранение и запрет на удаление. Отсюда следствие, о котором стоит знать: у опубликованной версии сервера бессрочного хранения нет — она живёт по обычным правилам очистки и может быть удалена, пока приложение остаётся опубликованным. Поэтому в `warnings` приходит строка с готовым `PATCH /v1/infra/servers/:id/sources/vN`, которым версию можно пометить самому; если версия уже помечена, строки не будет. Если хранилище исходников для портала отключено, переданный `sourceServerId` не используется вовсе — публикация проходит, а в `warnings` приходит строка о том, что селектор не пригодился.

Без `sourceServerId` поведение прежнее — проверка ищет свежий снапшот приложения. У отказа `409 SNAPSHOT_REQUIRED` появилось поле `hint.reason` с четырьмя значениями (`app_snapshot_missing`, `app_snapshot_stale`, `server_snapshot_missing`, `server_snapshot_stale`), а на пути без указанного сервера — блок `hint.serverKeyedSources`, который объясняет, как опубликовать исходники с сервера. Текст сообщения этого отказа переписан: раньше он начинался со слова «Deploy» и предлагал единственный выход через `POST /v1/apps/:id/sources`.

Подробности — [Опубликовать приложение](/docs/apps/publish) и [Хранилище исходников](/docs/source-storage).

### NEW-0812-6: товарные позиции сделки: в ответе появился признак усечения

**Было**

`GET /v1/{entity}/{id}/products` (сделки, лиды, счёта, предложения, элементы смарт-процессов) отдавал
только первую страницу товарных позиций — Bitrix24 отдаёт их порциями по 50 — и не сообщал об этом
никак. Ответ состоял из `success` и `data`, поля `meta` не было вовсе, поэтому у элемента с 125
позициями и у элемента с 50 позициями ответы выглядели одинаково, и приложение теряло строки, не имея
возможности это заметить. Параметры `limit` и `offset` на этом маршруте не действовали.

**Стало**

Ответ дополнен блоком `meta`: `meta.total` — сколько товарных позиций у элемента всего (по данным
Bitrix24), `meta.hasMore` — есть ли строки за пределами отданной страницы. Поля `success` и `data` не
изменились, поэтому клиент, который читает только их, продолжает работать как раньше.

Заодно на [GET /v1/deals/:id/products](/docs/entities/deals/products-get) и его аналогах у лидов,
счетов, предложений и элементов смарт-процессов начали работать `limit` и `offset`. `limit` до 5000
отдаёт запрошенное число позиций за один вызов, значения выше 5000 приводятся к 5000. `offset`
считается по строкам, а не по страницам: `offset=7` начинает выдачу с восьмой позиции. `meta.hasMore`
верна для любого запрошенного окна, а не только для первого. Вызов без `limit` и `offset` ведёт себя
ровно как раньше. Просьба `limit=0` размером страницы не считается: применяется значение по умолчанию,
а в `meta.warnings` появляется запись с кодом `LIMIT_ZERO_IGNORED` — так же, как на списочных
маршрутах. То же поведение описано в `knownIssues` ответа `GET /v1/guide`.

### NEW-0812-7: машинная схема описывает работающие методы приложений, ИИ и телефонии

Восемнадцать операций, которые работали и были описаны на страницах документации, отсутствовали в машинной схеме `GET /v1/openapi.json`. Клиент, который сверялся со схемой, не находил метод и делал вывод, что метода не существует. Теперь схема их описывает: семейство приложений ([GET /v1/apps](/docs/apps/list), создание, чтение, изменение, удаление, публикация и снятие с публикации, а также перепривязка OAuth), расход и квота ИИ ([GET /v1/ai/usage](/docs/ai/consumption/usage), [GET /v1/ai/quota](/docs/ai/consumption/quota)), расписание выгодных часов ([GET /v1/off-peak](/docs/ai/consumption/off-peak)), ИИ-итоги завершённых звонков ([POST /v1/calls/followups/list](/docs/calls/followup), [GET /v1/calls/followups/:callId](/docs/calls/followup)) и список линий ([GET /v1/voximplant-lines](/docs/telephony/lines/voximplant)).

Сами методы не новые и не менялись — ни адреса, ни параметры, ни формы ответов. Новое только описание, поэтому менять в интеграции нечего.

Четыре метода ИИ, совместимых с OpenAI, отвечают по двум адресам сразу: `/v1/models` и `/v1/ai/models`, и так же для завершений чата, векторных представлений и расшифровки аудио. В схеме были только короткие адреса. Теперь описаны оба, и у формы с приставкой `/v1/ai/` прямо указано, что она **устаревшая**: такие ответы несут заголовки `Deprecation: true` и `X-Deprecated-Use` с каноническим адресом. Вызовы по ним работают по-прежнему, но переходить стоит на короткий адрес. Исключение — `GET /v1/ai/usage` и `GET /v1/ai/quota`: у них короткой формы нет, они сами канонические и заголовков депрекации не несут.

Схема по-прежнему не описывает адреса-подсказки, которые существуют только чтобы вернуть ошибку «неверный путь» со списком настоящих адресов, и карточку одной модели: её идентификатор сам содержит косую черту, и параметр пути в OpenAPI такое значение описать не может.

### FIX-0812-8: список планов бесплатного тарифа отдаётся публичным идентификатором

**Было**

На международной поверхности `allowedPlans` в теле `402 PLAN_NOT_ALLOWED_ON_TRIAL` и в
`capabilities.servers.create.limits` (`GET /v1/me`) нёс внутренний идентификатор плана —
не тот, которым тот же план назван в каталоге `GET /v1/infra/providers/:providerId/plans`. Тот же
идентификатор подставлялся в кавычках в человекочитаемый `note`, поэтому ИИ-ассистент
показывал его пользователю дословно, а рядом стоящее `requestedPlan` могло вернуть план,
который выбрала сама платформа (создание агента), а не клиент.

**Стало**

Оба поля и текст `note` несут тот же нейтральный идентификатор, который отдаёт каталог
планов, поэтому ответ гейта и каталог наконец согласованы. Идентификаторы прежнего
каталога по-прежнему принимаются на входе создания сервера, так что клиент, отправляющий
ранее полученное значение, продолжает работать без изменений.

### FIX-0812-9: Исправлен отбор по пользовательским полям реквизитов

**Было**

[GET /v1/requisites](/docs/entities/requisites) с фильтром `filter[ufCrm_1698325419]` передавал имя пользовательского поля без изменения. Bitrix24 игнорировал такой ключ, поэтому успешный ответ не сужал список.

**Стало**

Оба написания одного поля — `filter[UF_CRM_1698325419]` и `filter[ufCrm_1698325419]` — принимаются и отбирают одинаково. Написание, которого Битрикс24 не даёт (например `filter[ufCrm_taxId]`), по-прежнему не применяется. Ошибка `UNKNOWN_FILTER_FIELD` для пользовательских полей не появилась.

### FIX-0812-10: неподтверждённая активация демо Маркетплейса отдаёт статус pending, а не activated

**Было**

[POST /v1/portals/{id}/activate-market-trial](/docs/activate-market-trial) присылал два успешных статуса: `activated` и `already_active`. Когда Битрикс24 включал демо, но подтверждения по нему не отдавал, вызов всё равно отвечал `activated` с полем `trialEndsAt` — то есть сообщал о завершённой активации, которую никто не подтвердил.

**Стало**

У неподтверждённого исхода появился свой статус — `pending`, без `trialEndsAt`. Он означает, что демо у Битрикс24 **уже включено и повторно включить его нельзя**, но подтверждения мы пока не получили. Статусы `activated` и `already_active` теперь приходят только на подтверждённые исходы.

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

Проверьте ветку `data.status === 'activated'`: часть удачных активаций приходит в неё больше не будет. Обработайте `pending` как выполненный вызов — перечитайте состояние аккаунта через минуту ([Самоописание ключа](/docs/keys-auth/me)) и не повторяйте запрос, он упрётся в ограничение частоты.

### FIX-0812-11: выкладка на отдельную виртуальную машину сохраняет внутренние ссылки сборки и проверяет корень приложения

**Было**

Перед запуском платформа удаляла из распакованного дерева все символические ссылки. Готовая сборка с серверным рендерингом связывает собственные файлы относительными ссылками внутри своего же каталога, поэтому приложение теряло часть зависимостей и на корневом адресе отвечало ошибкой 500. Проверка работоспособности опрашивала только путь из поля `healthPath`, так что лёгкая ручка вроде `/api/health` продолжала отвечать 200 и [POST /v1/infra/servers/{id}/deploy](/docs/infra/deploy/deploy) сообщал об успехе. В Битрикс24 приложение при этом не открывалось: ошибка приходила с собственным заголовком приложения `X-Frame-Options`, и вместо страницы ошибки пользователь видел сообщение браузера о невозможности соединения.

**Стало**

Ссылки внутри каталога выкладки сохраняются. Удаляются только небезопасные: абсолютные, ведущие в никуда и те, чей действительный путь выходит за пределы каталога выкладки.

Кроме того, когда `healthPath` отличается от `/`, выкладка после успешной проверки этого пути опрашивает ещё и корень приложения — адрес, который открывает Битрикс24. Ответ 5xx на корне останавливает выкладку ошибкой на шаге `healthcheck`. Ответ 4xx выкладку не останавливает и приходит отдельным шагом `app_root` со статусом `warning` и пояснением в `stdout`. С `healthPath` по умолчанию (`/`) не меняется ничего: дополнительный запрос не выполняется, шага `app_root` в ответе нет.

Разбор ссылок стоит дороже прежнего удаления, поэтому на очень большом дереве он может не уложиться в отведённое время. Раньше такой шаг всё равно отчитывался успехом — теперь шаги `normalize_windows_paths` и `cleanup_metadata` в этом случае приходят со статусом `warning` и объяснением в `stdout`, а выкладка продолжается.

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

Менять вызовы не нужно. Приложение, отвечающее на корне, ведёт себя как раньше. Сервер без страницы на корне — например, только с API — по-прежнему выкладывается успешно: его 404 приходит предупреждением, а чтобы предупреждения не было, укажите приложению тот путь, на котором оно действительно отвечает, через `PATCH /v1/apps/{id}`. Выкладка приложения, чей корень отвечает ошибкой 5xx, теперь завершается ошибкой, а не мнимым успехом — прежний зелёный ответ в этом случае был неверным.

### BC-0812-12: числовые поля учёта времени возвращаются числами, а не строками

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

**Было**

GET `/v1/task-time` и GET `/v1/tasks/:taskId/time` возвращали `id`, `taskId`, `userId`, `seconds` и `minutes` строками JSON. Справочник полей при этом объявлял их числовыми, то есть описание и ответ расходились.

**Стало**

Эти пять полей возвращаются числами JSON. Поле `source` остаётся строкой. Схема GET `/v1/task-time/fields` приведена в соответствие тем же изменением.

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

Обновите строгие схемы на своей стороне: там, где ожидалась строка, теперь придёт число. Старый формат не возвращается — сравнение вида `seconds === "900"` перестанет совпадать, нужно сравнивать с числом.

### FIX-0812-13: фильтр с зарезервированным именем поля отклоняется, а не отдаёт всю коллекцию

**Было**

Фильтр с зарезервированным именем поля — `filter[__proto__]`, `filter[constructor]`, `filter[prototype]` — принимался и молча отдавал ПОЛНУЮ, неотфильтрованную коллекцию (а на `/{entity}/aggregate` — счёт по всей коллекции). Клиент, собравший имя поля фильтра из пользовательского ввода, считал выборку отфильтрованной.

**Стало**

Такой фильтр отклоняется с `400 UNKNOWN_FILTER_FIELD` (как и любое другое неизвестное поле), на всех сущностях и на `/{entity}/aggregate`.

### FIX-0812-14: ключ, которому разрешена выкладка, может и поправить приложение на том же сервере

**Было**

Ключ, привязанный к серверу через приложение, успешно выкладывал код: [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy) отвечал успехом. Но [команда](/docs/infra/deploy/exec), [загрузка файла](/docs/infra/deploy/upload) и [чтение логов](/docs/infra/deploy/logs) тем же ключом отвечали `403 WRONG_KEY`, и программного способа получить это право не было — смена управляющего ключа выполняется только в личном кабинете. Мелкая правка рантайма на сервере, который принимает выкладку, оказывалась недостижимой. Отказ на команде приходил без объекта `hint`, поэтому подсказки о восстановлении в нём тоже не было.

**Стало**

Права на сервер делятся по тому, чего касается вызов. Операции над содержимым приложения — выкладка, команда, загрузка файла и чтение логов — принимают либо управляющий ключ сервера, либо ключ, приложение которого привязано к этому серверу. Управление самой машиной (запуск, остановка, пробуждение, перезагрузка, сон, удаление, починка, режим, политика доступа, порт, SSH), токены доступа и загрузка значка по-прежнему требуют управляющего ключа.

Отказ `403 WRONG_KEY` теперь на всех четырёх маршрутах несёт объект `hint` и называет оба шага восстановления: перепривязать сервер в кабинете и сменить ключ, которым обращается клиент — перепривязка не меняет секрет, который вы уже отправляете. Код ответа и его смысл не изменились: где отказывали раньше, отказывают и теперь.

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

Менять ничего не нужно. Вызовы, которые работали, продолжают работать; вызовы, которые отвечали `403 WRONG_KEY` из-за связи через приложение, теперь проходят. Подробности — [восстановление доступа к серверу](/docs/infra/server-access-recovery).

### NEW-0812-16: идентификатор операции выкладки и сверка исхода постфактум

Выкладка приложения на сервер теперь выдаёт идентификатор операции ДО начала работы, а не в конце. Он приходит заголовком ответа `X-Vibe-Operation-Id` в обоих режимах, в потоковом режиме дополнительно первым событием `operation` (браузерный `EventSource` заголовков не отдаёт), и дублируется полем `operationId` в конверте — и при успехе, и при отказе.

По этому идентификатору исход выкладки читается отдельным запросом: [GET /v1/infra/operations/:operationId](/docs/infra/deploy/operation-status). Он отвечает статусом `running`, `succeeded`, `failed` либо `unknown` (процесс прервался между стартом и записью исхода), шагом, временами старта и финиша и кодом ошибки. Ручка только на чтение, побочных действий у неё нет.

Зачем это нужно: раньше исход выкладки существовал только внутри самого ответа, поэтому при обрыве соединения узнать, чем всё кончилось, было нечем — оставалось запускать выкладку заново, не зная, сработала ли первая. Теперь идентификатор выдаётся до начала долгой работы, и оборванный вызов можно сверить.

Идентификатор выдаётся только при выкладке на отдельную виртуальную машину (`kind: "STANDALONE"`). У galaxy-приложения (`kind: "GALAXY_APP"`) его нет ни в заголовке, ни в кадре, ни в теле ответа, и сверка исхода постфактум для таких выкладок пока не поддержана.

Исходы хранятся 7 суток и доступны по идентификатору каждый в отдельности, включая предыдущие попытки на том же сервере. По истёкшему идентификатору ответ отличается от «не найдено» — приходит `410` с кодом `OPERATION_OUTCOME_EXPIRED`, то есть «операция была, исход больше не хранится». Списка операций пока нет.

Отсутствие заголовка НЕ означает, что выкладка не запускалась: если платформа не смогла завести запись, выкладка продолжается без идентификатора. Читать отсутствие заголовка как отказ и запускать вторую выкладку поверх первой нельзя.

### NEW-0812-17: Провалившийся деплой сообщает исход машинно, а не прозой

Деплой отдельной виртуальной машины мог закончиться так: приложение выложено и работает, проверка
работоспособности отвечает ошибкой, а платформа уже вернула приложение с ограниченных прав на
полные. Отличить это от «деплой не состоялся» можно было только по английскому тексту внутри
`error.message`, а машинный потребитель читает поля.

Теперь шаг `healthcheck` в `data.steps[]` несёт три факта о проверке: `httpCode` — код последней
попытки (поле отсутствует, если кода не было; ноль или `null` вместо него не приходят),
`healthPath` — путь, который фактически проверяли, и `portOwner` — `service`, `foreign` либо
`unknown`. Это факты о попытке, а не вердикт: шаг со статусом `error` законно может нести
`httpCode: 200`, потому что исход определяют `success` и статус шага.

Шаг `hardening` теперь приходит и при провальном деплое — раньше он был только при успешном.
Он несёт поле `hardeningRollback`: `completed` — возврат завершён, приложение работает с правами
администратора; `incomplete` — возврат начался и не завершился, приложение может по-прежнему
работать с ограниченными правами. То же значение продублировано в конверте ошибки как
`error.hardeningRollback`. Поле описывает возврат юнита и владения каталогом развёртывания, и
только их; его отсутствие означает «возврат не запускался», а не «ограничение прав не применялось»
— на второй вопрос отвечает шаг `service_user`.

Все поля необязательные и аддитивные: прежние вызовы работают без изменений.
