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

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

### BC-0922-1: пакет одной сущности листает списки: offset в подвызове наконец читается

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

**Было**

Списочный подвызов [POST /v1/{entity}/batch](/docs/batch) игнорировал `offset`: параметр не извлекался
из `params`, на сторону Битрикс24 не уезжал и клиентским окном не применялся. Два подвызова,
отличающиеся только смещением, возвращали HTTP 200 и одни и те же записи — листать пакетом
одной сущности было невозможно, тогда как общий [POST /v1/batch](/docs/batch) и одиночные
[GET /v1/{entity}](/docs/entity-api) и [POST /v1/{entity}/search](/docs/entity-api) смещение
читали. У сущностей, чей метод Битрикс24 навигацию не
поддерживает, тот же подвызов тоже отвечал HTTP 200 — первой порцией записей при любом
смещении, то есть молча не тем окном, которое просил клиент.

**Стало**

Подвызов читает `offset` и отдаёт честное окно `[offset, offset + limit)` — тем же разбором,
что общая дверь: смещение выравнивается под страницу Битрикс24, остаток снимается с головы на
нашей стороне, а у сущностей, чей метод навигацию не применяет (`statuses`, `deal-categories`,
`bizproc-activities`, `bizproc-robots`) и у тех, что отдают весь набор одним ответом, окно
режется по полной коллекции. Ответ подвызова теперь несёт `hasMore` рядом с `total`, а
вырожденное окно приезжает предупреждением `OFFSET_BEYOND_FETCHED_PAGE` в `meta` этого же
подвызова.

Ломающая часть одна: сущность, чей метод Битрикс24 смещение принять не может (сегодня это
`telephony-lines`), на подвызов с `offset` больше нуля отвечает ошибкой `UNSUPPORTED_OFFSET`
вместо прежних HTTP 200 и первой порции записей. Отказ пер-вызовный — соседние подвызовы
пакета выполняются, и сам пакет по-прежнему отвечает HTTP 200. Вместе со смещением подвызов
начал соблюдать и `limit`: страница, которую Битрикс24 вернул длиннее запрошенной, режется по
`limit` (по умолчанию 50), как на остальных дверях. Раньше такой подвызов отдавал всё, что
прислал портал, — с окнами это означало бы, что страницы разных смещений перекрываются и
клиент читает одни записи дважды.

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

1. Уберите `offset` из подвызовов к `telephony-lines`: смещение этой сущности недоступно, и
   ответ `UNSUPPORTED_OFFSET` называет это прямо. Нужна выборка — сужайте её фильтром.
2. Если код рассчитывал на страницу длиннее запрошенной — передавайте нужное число в `limit`
   явно, до 5000 записей.
3. Листайте по `hasMore` из ответа подвызова, а не по арифметике от `total`.

### NEW-0922-2: два новых кода отказа для ключей рантайма агента

Ключ рантайма агента теперь получает два кода отказа, которых раньше не было. Пока ход агента ждёт подтверждения пользователя, [POST /v1/chat/completions](/docs/ai/chat/completions) отвечает 409 с кодом `agent_turn_awaiting_confirmation`: обращения к модели по такому ходу не выполняются, поэтому ожидание ответа человека больше не расходует лимит. Код снимается, как только пользователь подтвердил или отменил операцию, а также по истечении срока ожидания.

Вызов `DELETE` к V1 от того же ключа, совпавший с предыдущим по методу и адресу в пределах окна в 30 секунд, отвечает 409 с кодом `AGENT_WRITE_ALREADY_EXECUTED`. Такой вызов на портал не уходит, а в поле `error.firstStatus` возвращается код ответа первого удаления и в `error.firstExecutedAt` — его время. Неуспешное первое удаление повтор не запирает: ретрай проходит обычным порядком. Создание и обновление дедупликации не подлежат.

Ключей, не принадлежащих рантайму агента, ни один из двух кодов не касается, и прежние успешные ответы не изменились.

### NEW-0922-3: приём результата долгого метода решения

Появился публичный маршрут `POST /v1/solution-calls/:callId/result`, куда решение присылает результат асинхронного вызова метода по контракту (`solution.invoke` с `mode: async`). Адрес, одноразовый reply-токен и срок приходят решению вместе с самим вызовом в заголовках `X-Vibe-Reply-Url`, `X-Vibe-Reply-Token` и `X-Vibe-Reply-Deadline`; платформа шлёт вызов с `Prefer: respond-async` — решение отвечает `202` и присылает результат сюда либо отвечает `200` с тем же телом сразу. Аутентификация — `Authorization: Bearer vcr_…`, ключ платформы на этом маршруте не принимается. Тело — `{"outcome":"completed","result":{…}}` либо `{"outcome":"failed","error":{"code","message","problem"}}`; ответ `200` с `data.state`. Ошибки: `401 REPLY_TOKEN_INVALID` (токен не подошёл ни к одному открытому вызову; существование `callId` не раскрывается), `409 SOLUTION_CALL_CLOSED` (вызов уже закрыт, `error.state` несёт его состояние), `422 RESULT_INVALID` (тело больше 512 КиБ, не JSON нужной формы или `result` не по схеме того снимка контракта, по которому вызов принят). Ответ `422` закрывает вызов как `failed` — повторять с тем же телом бессмысленно. Принятый исход платформа доставляет на портал Битрикс24 сама. Прежние вызовы и ответы не меняются: до этого релиза `mode: async` отвечал `FEATURE_DISABLED`, и на порталах без включённой возможности ответ остаётся прежним.

### FIX-0922-4: отказ Битрикс24 «не передан параметр» отвечает 400, а не 422

**Было**

Когда Битрикс24 отклонял вызов до запуска метода — потому что вызов не принёс параметр, который метод объявляет обязательным, — портал отвечал `Could not find value for parameter {…}`, а платформа Вайбкод передавала это наружу как `422 BITRIX_ERROR`, то есть как бизнес-ошибку портала. Имя недостающего параметра лежало только в тексте сообщения, подсказки не было, и повтор того же запроса возвращал тот же отказ.

**Стало**

Тот же отказ приходит как `400 INVALID_PARAMS` — код той же группы, что и остальные отклонения параметров со стороны Битрикс24, — а `error.hint` называет параметр, значения которого не хватило, и говорит, что повтор без изменения вызова не поможет. Ответ на любой запрос, который Битрикс24 принимает, не изменился.

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

Клиент, который различал этот случай по `422`, увидит `400 INVALID_PARAMS`. Ответ по-прежнему несёт текст Битрикс24 в `error.message`, а имя параметра теперь ещё и в `error.hint`. Отказы, отклонённые проверками самой платформы до обращения к порталу, как и раньше отвечают `400 MISSING_REQUIRED_PARAMS`.

### FIX-0922-5: смена адреса портала: вызов отвечает 409 `PORTAL_ADDRESS_CHANGED` вместо 500

**Было**

После смены адреса портала в Битрикс24 вебхук выданного ключа оставался выписан на прежний
адрес. Платформа отбивала такой вызов до отправки — секрет не уходит туда, где портала больше
нет, — но отказ был безымянным: любой проксируемый запрос отвечал `500 INTERNAL_ERROR` без кода
и без подсказки, а в батч-конверте подвызов уезжал в общий бакет `CALL_FAILED`. Причину и путь
восстановления клиент из ответа узнать не мог.

**Стало**

Отказ называет и причину, и действие: `409` с кодом `PORTAL_ADDRESS_CHANGED`, подсказка ведёт на
переподключение ключа — в разделе «Ключи» кнопка «Переподключить»; строка ключа и привязанные к
нему боты при этом сохраняются. Тот же код с той же подсказкой едет в подошибке батч-конверта.
Срока ожидания отказ не несёт намеренно: повтор не поможет, пока вебхук ключа не перевыпущен.

### FIX-0922-6: удаление приложения освобождает ключ авторизации в карточке

**Было**

`DELETE /v1/apps/{id}` снимал приложение и отзывал парные ключи, но карточка приложения
продолжала считать ключ авторизации выданным. Выдать новый ключ было нельзя — дверь выдачи
отвечала `409 APPLICATION_HAS_AUTH_KEY`, а перевыпуск — `409 APPLICATION_NO_AUTH_KEY`. Тем же
следствием `POST /v1/cowork/applications/{id}/key` возвращал `warningCodes` со значением
`APPLICATION_HAS_AUTH_KEY` для приложения, у которого ключа авторизации уже не было.

**Стало**

Удаление снимает и привязку: после него приложение считается без ключа авторизации, ложный
`APPLICATION_HAS_AUTH_KEY` в `warningCodes` по такому приложению больше не приходит, а выдача
нового ключа проходит штатно. Успешный ответ самого удаления не изменился — это по-прежнему `HTTP 204`.

### FIX-0922-7: схема агрегации в OpenAPI описывает поля так же, как их принимает API

Правка касается ОПИСАНИЯ в `GET /v1/openapi.json`; поведение `POST /v1/{entity}/aggregate` не меняется, ответы прежние.

**Было**

Поле `aggregate[].field` публиковалось закрытым списком из `"*"` и полей группировки сущности. Список группировки и поля, по которым считаются `sum`/`avg`/`min`/`max`, — разные множества, поэтому клиент, проверяющий тело запроса по спеке, не отправлял вызовы, которые API выполняет: агрегацию по числовому полю сущности вне списка группировки (у сделок это `probability`, `taxValue`, `receivedAmount` и другие) и по пользовательскому полю портала типа `integer`, `double` или `money`. Одновременно спека разрешала пары, на которые API всегда отвечает `400 INVALID_PARAMS`: `count` по именованному полю и числовую функцию по `"*"` или пустому имени. Поле `groupBy` публиковалось таким же закрытым списком и не допускало пользовательских полей, которые API принимает.

**Стало**

`aggregate[].field` описан в паре с `function`: `count` считается по `"*"`, а `sum`/`avg`/`min`/`max` принимают имя числового поля — как объявленного в сущности, так и пользовательского поля портала. Числовые поля сущности перечислены в `examples` как подсказка, а не как ограничение: набор пользовательских полей зависит от портала и в общей спеке не публикуется, актуальный список отдаёт `GET /v1/{entity}/fields`. Пары, на которые API всегда отвечал отказом, схема теперь тоже отвергает — включая агрегацию по полю, которое сущность объявляет нечисловым: такое имя API отклоняет независимо от того, что настроено на портале. `groupBy` допускает пользовательские поля и отвергает имена, совпадающие с ключами ответа (`count`, `aggregates`, `meta`, `groups`), — их API не принимал никогда. Текстовое описание операции больше не выдаёт список группировки за список полей агрегации: теперь оно называет обе роли раздельно.

### FIX-0922-8: подняты дефолтные лимиты на пользователя — ключи, серверы, боты

**Было**

На портале, где эти лимиты ни разу не настраивали вручную, по умолчанию
действовали ограничения на пользователя: 10 API-ключей, 5 серверов, 5 ботов.

**Стало**

Дефолты подняты до 100 ключей, 50 серверов и 50 ботов на пользователя. Изменение
затрагивает только порталы, которые не задавали свои значения; портал с
собственной настройкой сохраняет её без изменений. Существующие запросы работают
по-прежнему — доступного объёма стало больше.

### BC-0922-9: Повтор незавершённой ротации ключа требует восстановления

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

**Было**

После неполного отката замены ключа запрос с новым `Idempotency-Key` мог ответить
201 для осиротевшего кандидата, хотя отзыв токенов исходного ключа не был подтверждён.

**Стало**

Повторная ротация исходного или нового ключа отвечает `409 KEY_ROTATION_RECOVERY_REQUIRED`.
Замена ключа приложения и назначение такого ключа серверу отвечают
`409 APPLICATION_KEY_RECOVERY_REQUIRED`. Новый `Idempotency-Key` не обходит отказ.
Выдача токена доступа при незавершённой ротации также отвечает
`409 KEY_ROTATION_RECOVERY_REQUIRED`, а если сервер уже сменил ключ —
`403 SERVER_KEY_MISMATCH`, без нового токена.
Передача бота с исходным или целевым ключом в состоянии незавершённой ротации отвечает
`400 TARGET_KEY_INVALID` с `error.reason: recovery_required`, без изменения владельца.
Параллельная передача того же портала отвечает существующим
`409 BOT_TRANSFER_CONFLICT` вместо непрозрачного 500; привязка не меняется.

Затронутые эндпоинты: [POST /v1/keys/{id}/rotate](/docs/management-keys),
[DELETE /v1/keys/{id}](/docs/management-keys),
[POST /v1/cowork/applications/{id}/key](/docs/cowork/applications-create),
[POST /v1/infra/servers](/docs/infra/servers/create),
[POST /v1/infra/servers/{id}/access-tokens](/docs/infra/access-tokens/create),
[POST /v1/bots/{botId}/transfer](/docs/bots/management/transfer).

Неподтверждённый отзыв отвечает `500 KEY_ROTATE_REVOKE_FAILED` без сырого секрета.
Кандидат указан в `error.details.orphanKeyId`, остаток отката — в
`error.details.strandedSlots`. Непустой остаток требует поддержки.
Отсутствующий или пустой остаток сам по себе не разрешает повтор.
Ничем не используемый кандидат удаляется только штатным DELETE-эндпоинтом после
подтверждения, что на него больше не ссылаются другие ресурсы.

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

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

### FIX-0922-10: служебные имена JavaScript больше не подменяют значение параметра

**Было**

Имена, которые есть у любого объекта JavaScript — `constructor`, `toString`, `valueOf`,
`hasOwnProperty`, — при сверке со списком допустимых значений считались найденными в этом
списке. Значением оказывалась служебная функция, и дальше поведение расходилось по ручкам:

- `GET /v1/userfields/constructor` — отказ `400 UNKNOWN_ENTITY` не приходил, вместо него
  возвращалась ошибка Битрикс24;
- `GET /v1/requisite-links?filter[constructor]=…` и `?sort=constructor` — вместо
  `400 UNKNOWN_FILTER_FIELD` / `400 UNKNOWN_SORT_FIELD` в Битрикс24 уходило мусорное имя
  поля;
- `GET /v1/openline-configs?filter[constructor]=…` и `?sort=constructor` — мусорное имя поля
  вместо ожидаемого `CONSTRUCTOR`; отказа по неизвестному полю у этой ручки нет и не было;
- `POST`/`PATCH /v1/userfields/:entity` — поле с таким именем уезжало в Битрикс24 под
  подменённым именем вместо передачи как есть; то же — у внутренних ключей элементов
  списка (`list`);
- `POST /v1/users/invite` — то же подменённое имя вместо передачи как есть;
- `model: "constructor"` в запросах к AI подменял имя модели служебным значением.

**Стало**

Список допустимых значений проверяется по собственным ключам, поэтому служебные имена в нём
не находятся. Там, где ручка отвечает отказом на неизвестное значение, этот отказ приходит
до обращения к Битрикс24; там, где отказа нет, имя поля передаётся как есть.

### NEW-0922-11: билет десктопа Коворк/Код для телефонного доступа

Новый метод `POST /v1/cowork/relay-ticket` выдаёт десктопному приложению Коворк/Код короткоживущий подписанный билет, по которому relay телефонного доступа пускает десктоп. В теле передаётся `pubkey` — публичный ключ Ed25519 десктопа (32 байта, base64url без паддинга). Ответ `200` содержит `ticket` (JWT с алгоритмом `EdDSA`, живёт 5 минут) и `expiresAt`. Метод доступен только ключу десктопного приложения со скоупом `vibe:cowork` и только при активном месте Коворк/Код.

Коды отказов: `403 INSUFFICIENT_SCOPE` (нет скоупа `vibe:cowork`), `403 COWORK_DESKTOP_KEY_REQUIRED` (ключ не десктопного приложения), `404 COWORK_NOT_ACTIVATED` (нет места), `403 COWORK_SEAT_INACTIVE` (место не активно), `400 INVALID_PUBKEY` (неверный ключ), `429` (не больше 30 билетов в час на человека), `503 COWORK_FEATURE_DISABLED` и `503 COWORK_RELAY_NOT_CONFIGURED` (выдача билетов временно недоступна).

### FIX-0922-12: политика «только владелец» работает одинаково на всех входах в приложение

**Было**

При политике `OWNER_ONLY` пользователь с записью в списке доступа открывал приложение через
плейсмент внутри Битрикс24, но получал отказ по прямой ссылке. Документация при этом обещает,
что при `OWNER_ONLY` записи списка доступа не применяются: сценарий «вернуть приватность» —
перевести сервер обратно в `OWNER_ONLY` и оставить записи в базе — на деле закрывал доступ не
для всех.

**Стало**

Вход один и тот же на всех путях: при `OWNER_ONLY` приложение открывает только владелец, а
записи списка доступа не применяются, как и написано в документации. Чтобы снова открыть
доступ конкретным людям, переключите политику на `NAMED_USERS` — записи уже в базе и начнут
действовать сразу.

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

Если ваш сценарий опирался на то, что человек из списка доступа входит через плейсмент при
`OWNER_ONLY`, переключите сервер на `NAMED_USERS`: набор людей останется прежним.

### FIX-0922-13: приложению в галактике снова можно задать расписание пробуждения

**Было**

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

**Стало**

Тариф, унаследованный от галактики, больше не считается выбором круглосуточного режима: окна
расписания приложению в галактике задаются обычным порядком. У отдельного сервера, которому
владелец сам выбрал тариф без прерываний и отключил сон, поведение прежнее — `400
ALWAYS_ON_CONFLICT`.

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

Действий не требуется. Запрос, который раньше отклонялся для приложения в галактике, теперь
создаёт окно; ответ `GET /v1/infra/servers/{id}/logs` у спящего приложения больше не сообщает
об отказе окна.

### NEW-0922-14: в библиотеке расписаний работы появились три новые готовые заготовки

`GET /v1/work-schedules` отдаёт три дополнительных готовых расписания — `weekdays-9-19`,
`weekdays-9-20` и `weekdays-8-19` (будни, понедельник–пятница). Раньше готовых было три, и, если
рабочая неделя аккаунта в них не попадала, оставалось заводить своё расписание через
`POST /v1/work-schedules`, даже когда отличие было в одном часе.

Новые заготовки заводятся в аккаунте при первом чтении библиотеки, как и прежние, и так же не
редактируются: у заготовки `canEdit: false`, а попытка правки отвечает
`WORK_SCHEDULE_PRESET_READONLY`. Назначать их машинам платформа сама не станет — они лишь
появляются в списке.

Прежние ключи и их окна не менялись, уже назначенные машины остались на своих расписаниях.

Набор заготовок может пополняться и дальше, поэтому незнакомый `presetKey` стоит считать заготовкой
без локальной подписи, а не ошибкой. Различать заготовки надёжнее по `presetKey`, а не по `name`:
имя зависит от языка ключа.
