# API changes: August 5, 2026

[← Changelog](/docs/changelog) · [August 2026](/docs/changelog/2026-08)

### FIX-0805-1: key rotation no longer strands the bot registered with it

**Before**

A bot registered via [POST /v1/bots](/docs/bots) remembers the key it was created with. After [POST /v1/keys/:id/rotate](/docs/management-keys) that binding stayed on the old key: the new key got `403 BOT_ACCESS_DENIED` on every call for that bot, and once the old key expired after its grace period the bot went silent entirely — incoming events kept queueing, but nothing could fetch them (`GET /v1/bots/:id/events`). The only recovery was `POST /v1/bots/:botId/transfer`.

**After**

Rotation moves the bot to the new key along with the application: calls for the bot and event polling with the new key keep working with no manual step.

**What is still not ideal**

Bots owned by an AI agent or a managed bot are not repointed by this change — they have their own key-recovery path, and moving them from here would desynchronize their own fields. Their behavior is unchanged.

**Impact on integrators**

No action needed. A manual `POST /v1/bots/:botId/transfer` after rotation is no longer required — it remains only for handing a bot to a different key.

### FIX-0805-2: a container on a shared host is no longer lost after key rotation

**Before**

The repoint done by [POST /v1/keys/:id/rotate](/docs/management-keys) skipped servers on a shared host (`kind=GALAXY_APP`): such a container stayed on the old key, and once the grace period ended, deploy, exec, file upload, and log calls made with the new key stopped finding it.

**After**

A container on a shared host switches to the new key together with the other servers. Exactly one narrow exception remains: the container is never moved onto an application authorization key (`vibe_app_`) — that binding is irreversible and breaks deployment.

**What is still not ideal**

The platform does not replace the key stored in the container's own environment variables: their values are set when the container starts. Update the variable and deploy the application again — the key's grace period gives you time.

**Impact on integrators**

No action needed. Calls made with the new key against a container on a shared host now keep working after the grace period ends.

### FIX-0805-3: key rotation no longer disconnects the server and app bound to it

**Before**

After [POST /v1/keys/:id/rotate](/docs/management-keys) the server and app created with that key kept referencing the old key internally. Once the old key expired after its grace period, deploy, exec, upload, and log calls made against that server with the NEW key stopped finding the server.

**After**

After rotation the server, the app, and its live access tokens (`api-bearer`, minted via `POST /v1/infra/servers/:id/access-tokens`) switch to the new key along with it — deploy/exec/upload/logs calls made with the new key keep finding the server, and refreshing such a token (`POST .../access-tokens/:tokenId/refresh`) no longer fails on a key mismatch.

**What is still not ideal**

The old key loses access to the server and app IMMEDIATELY, at the moment of rotation — not once its grace period ends. The key itself stays formally active for those hours (`KEY_GRACE_PERIOD_HOURS`), but the server and app have already moved to the new key, so calls made with the old key against that server stop finding it right away.

**Impact on integrators**

No action needed. A client that treated the post-rotation disappearance as a persistent failure now sees continuous access — except for the old key itself, which loses visibility of the server sooner than its formal expiry.

### BC-0805-4: the task field reference now matches the task response, and numbers are numbers

> Old format supported until: 04.02.2027

**Before**

`GET /v1/tasks/fields` described 92 fields, 66 of them under names such as
`MARK`, `NOT_VIEWED`, `STAGE_ID`, `CHAT_ID` — keys that `GET /v1/tasks` and
`GET /v1/tasks/:id` never return. Most of the keys the response does carry were
missing from the reference. Values disagreed with the declared type as well:
`id`, `status`, `priority`, `groupId`, `chatId`, `responsibleId`, `createdBy`,
`changedBy`, `closedBy`, `statusChangedBy`, `timeEstimate` and
`timeSpentInLogs` were declared numbers but arrived as strings (`"289"`, `"2"`).
Yes/no flags arrived as the strings `"Y"` and `"N"`, and `"N"` is truthy in every
language. Empty `tags`, `group`, `accomplicesData` and `auditorsData` arrived as
an empty array while the reference declared an object. Fields that really do come
back empty were not marked as nullable. `chatId` also changed type between
surfaces: a string in the list, a number in the card.

A client generated from that reference did not work.

**After**

The reference and the response name the same fields. 69 fields are declared, and
the raw upper-case names are gone from the reference (only the account's custom
fields and `CHECKLIST` remain — the latter is served by the task checklist
endpoints). Fields declared as numbers arrive as numbers, yes/no flags arrive as
`true` and `false`, and empty `tags`, `group`, `accomplicesData` and
`auditorsData` arrive as an empty object. 27 fields are marked nullable.
`chatId` is a number on both surfaces.

**What integrators should do.** Check your code for: comparisons against strings (`status === "2"`,
`id === "289"`), flag checks that test for a non-empty string, and code that
treats empty `tags` / `group` / `accomplicesData` / `auditorsData` as an array.

`subStatus` (list only) and `action`, `checklist`, `checkListTree`,
`checkListCanAdd` (card only) are still returned and are deliberately not in the
reference — task lists and task cards differ in key set on the Bitrix24 side.
`realStatus` is filter- and sort-only and never appears in the response — the reference now says so machine-readably, via the `notReturned` marker.

### FIX-0805-5: the model-unavailable pause now lengthens until the cluster recovers

**Before**

The pause behind a `429 ai_provider_cooldown` refusal always lasted about a minute. When it expired the platform let the full load back into the model cluster, and if the cluster had not recovered the cycle repeated: a minute of waiting, a burst of retries, more failures. `Retry-After` always carried the same value, so a client that hard-coded one minute behaved exactly like one that read the header.

**After**

The first pause is still about a minute, but if the cluster is still failing when it expires, the next pause doubles — up to a four-minute ceiling. As soon as a call succeeds the count resets and the next pause starts from a minute again. The `Retry-After` header (and the `retryAfter` field in the terminal streaming frame) carries the current remainder, so take the wait from the response rather than from a constant in your own code.

### NEW-0805-6: deploy warns when the verified app path disagrees with the address Bitrix24 opens

**Before**

A deploy with a `healthPath` other than `/` verified the app on a subpath and answered `200`, saying nothing about the address Bitrix24 opens the app at. When the app answered only on that subpath while `appUrl` stayed the bare server address, the placement iframe opened the root: a green deploy, an app that does not open inside Bitrix24, and not a word about it in the response.

**After**

`POST /v1/infra/servers/{id}/deploy` now adds an entry to the optional `warnings` array for that combination (plain JSON response and the SSE `done` event, exactly like the existing `displayName`/`description` and `changelog` nudges). The entry names both halves of the contradiction — the verified path and the framed address — and both ways out: serve the bundle at `/`, or move the subpath into the application's `appUrl` via `PATCH /v1/apps/{id}`. A subpath in `appUrl` is supported, not rejected.

The entry is absent when `healthPath` is unset or `/`, when `appUrl` already carries a path, when the app address is outside the platform domain, and when no application is linked to the server yet. The response shape is unchanged: `warnings` was optional before and stays optional.

### FIX-0805-7: Placement binding names the reason it was refused

Binding a placement with an application key no longer answers with an unnamed `502 BITRIX_UNAVAILABLE` when Bitrix24 rejects the registration.

**Before**

[POST /v1/placements/bind](/docs/apps/placements/bind) returned the same answer for every Bitrix24-side refusal — `502 BITRIX_UNAVAILABLE` with "Failed to register placement on Bitrix24 via dev key". There was no way to tell "the application's grant lacks the required scope" from "a required placement option is missing": the account returns both as one opaque code. For the chat widgets `IM_SIDEBAR`, `IM_NAVIGATION`, `IM_TEXTAREA` a call without `options.iconName` landed in that same unnamed refusal.

**After**

The reason is named:

- `403 PLACEMENT_APP_GRANT_MISSING` — the placement is not available to the Bitrix24 application. `details` carries the required `requiredScope`, the placements the application CAN bind (`availablePlacements`, `availablePlacementsTotal`) and the way to widen the grant in `remediation`.
- `400 PLACEMENT_OPTIONS_REQUIRED` — a required placement option is missing and the platform could not fill it in (`missing` in `details`).
- `400 PLACEMENT_NOT_REST_BINDABLE` — the code cannot be bound over the API at all.
- `502 BITRIX_UNAVAILABLE` remains for everything else and now carries `placementInAppList` in `details`, plus `diagnostics` (`"placement_list_skipped"` or `"placement_list_empty"`) when the diagnosis was unavailable.

A chat widget icon is no longer mandatory: when `options.iconName` is absent the platform fills one in and reports it in the success response as `optionsDefaulted`. Your own value always wins over the filled-in one.

The reference [GET /v1/placements/available](/docs/apps/placements/available) returns three new fields per code — `requiredScope`, `requiresIconName`, `restBindable` — and now lists ten codes that were bindable but missing from it, including the task card tabs and panels. The `placements.bindPrerequisite` block in [key data](/docs/keys-auth/me) states the scope requirement up front.

**Impact on integrators**

No call changes are needed. If you branch on placement-bind refusal codes, add the three new ones; if you relied on `options.iconName` being mandatory, it is now optional and behaviour with a supplied value is unchanged.

### FIX-0805-8: the tunnel survives an app restart, and port auto-detection no longer loses the target

**Before**

An agent in port auto-detect mode dropped its detected target after a single failed
observation. An app restart (1-3 s), or a first response slower than 1.5 s, made the
tunnel serve the stub page for about 5 more seconds **after** the app was answering
again. Separately, the target was re-elected on every successful scan, so a sidecar
appearing on a lower port took the tunnel away from a perfectly healthy application —
HTTP 200 with the wrong content and no error anywhere.

`data.warning` on `PATCH /v1/infra/servers/:id/port` and the `tunnel_routing` step of
`POST /v1/infra/servers/:id/deploy` promised that auto-detection converges "within ~30s".

**After**

The agent now separates two signals. While the app's port is present among the listening
ones, the target is held; it is released only after several consecutive observations that
the port is gone (~15 s), or — if the port listens but never answers — after about two
minutes. A responding target is no longer re-elected, except when the current target is
80/443 and a real application port answered.

The `data.warning` and `tunnel_routing` texts were rewritten honestly. Auto-detection
picks up the new port within about a minute **if the previous port was released**; if a
live process still answers on the previous port, the scanner deliberately keeps it and
will not switch on its own — set the port explicitly with `PATCH /v1/infra/servers/:id/port`
or stop that process. `POST /v1/infra/servers/:id/repair` is not the tool for this case:
it reinstalls the agent with auto-detection, so the election simply runs again — with the
same outcome while the old process keeps answering.

The change reaches a server together with the agent update to 1.3.7.

### FIX-0805-9: the MISSING_FIELDS hint for requisite links names the field names instead of sending you to another endpoint

**Before**

The `400 MISSING_FIELDS` refusal of [POST /v1/requisite-links](/docs/entities/requisite-links/register) stated that raw UPPER_SNAKE names are accepted alongside camelCase, and suggested taking them from `GET /v1/requisite-links/fields`. They are not there: the `/fields` response returns names in camelCase. Whoever read the refusal went looking for the list where the list is in the other notation, and came back with nothing.

**After**

The message lists all six names inline: `ENTITY_TYPE_ID`, `ENTITY_ID`, `REQUISITE_ID`, `BANK_DETAIL_ID`, `MC_REQUISITE_ID`, `MC_BANK_DETAIL_ID`. Both notations are still accepted on write; the refusal code and its condition are unchanged.

### FIX-0805-10: the API schema now covers the session exchange and embed slots, and states its own coverage honestly

**Before**

`GET /v1/openapi.json` was described as complete, and `GET /v1/guide` recommended slicing it by scope so it fits an AI agent's context window. Some live methods were nevertheless absent from it, so an agent that followed that advice concluded the method did not exist. The concrete case was the embed-context-to-session exchange: the method worked and was covered in the documentation, but the schema listed only authorization start, callback, code exchange and revocation under `/v1/oauth/` — so we received a request to add something that had shipped long before.

**After**

The schema now covers the embed-context-to-session exchange, the authorization-result poll for environments that cannot receive a redirect, and all four embed-slot methods: list registered, reference of available codes, register and remove. The mutating ones declare the access scope their handler actually enforces.

More importantly, the schema no longer promises completeness it does not have. Entity paths are generated from the live registry and are complete, while the hand-written sections are still being backfilled — so both the schema description and `GET /v1/guide` now say it plainly: a missing path does not mean a missing method, and they explain how to settle it in one call (a genuinely absent path answers `ROUTE_NOT_FOUND`, a live one answers a validation error). Both also name the sections that will never appear there: inbound handlers the platform receives rather than exposes, wrong-path hints, and sections available only to a management key.

### FIX-0805-11: a client on a smart process item is written, and a disabled client block answers with a refusal instead of a false success

**Before**

The `contactIds` field on smart process items ([PATCH /v1/items/:entityTypeId/:id](/docs/entities/items/update)) and on quotes ([PATCH /v1/quotes/:id](/docs/entities/quotes/update)) was marked read-only, so a write was rejected with `400 READONLY_FIELD`. The stated reason was that the contact binding is not managed through `crm.item.update`; a check against a real account did not confirm it — the method does change the binding set.

The second half of the same story: when a smart process has the client block disabled, Bitrix24 accepts `contactId`, `contactIds` and `companyId`, answers with success, and does not store the value. The platform passed that success through as is — the caller received a `200` for a write that never happened, and the only way to learn about it was to read the item back.

**After**

`contactIds` is writable on smart process items and on quotes. Send the full list: the binding set is replaced rather than merged, and the first contact in the list becomes the primary one. The `contacts` field (expanded objects rather than identifiers) stays read-only.

Writing a client to a smart process whose client block is disabled is now rejected before any Bitrix24 call — `400` with code `CLIENT_BLOCK_DISABLED`. The message names the offending field and `GET /v1/smart-processes/:entityTypeId`, whose `isClientEnabled` field shows the state of the block. The rule covers all three client fields — `contactId`, `contactIds`, `companyId` — because the block gates them identically.

The rule holds on every write surface, batch included: both `POST /v1/items/:entityTypeId/batch` and `POST /v1/batch`. On the per-entity batch the refusal applies to the whole batch and names the offending item index; on the global batch it arrives per sub-call and leaves the other sub-calls alone.

Empty values are not covered by the rule: `0`, `''`, `[]` and `null` mean "no client", not a client write. That matters for the read-modify-write pattern: on an item with the block disabled the client fields read back exactly like that and are echoed in every update. If the type metadata cannot be fetched, the write proceeds — a failure to read settings does not block an update.

**Impact on integrators**

A request that wrote a client to a smart process with the block disabled previously received `200` and will now receive `400 CLIENT_BLOCK_DISABLED`. That is the fix: nothing was stored before either, but now it is visible immediately. Either enable the client block on the smart process type, or stop sending client fields. Requests against types with the block enabled are unaffected.

### FIX-0805-12: a JSON-object filter is applied, and an OR attempt gets its own error code

**Before**

The `filter` parameter on list requests had two spellings, and the second silently did nothing. The bracket form (`?filter[id]=3`) was applied. The JSON-object form (`?filter={"id":3}`) — the one the documentation examples show — was not recognised: the parameter was discarded, the request answered `200` and returned the entire collection. Nothing in the response distinguished a working filter from a discarded one.

A separate problem was expressing OR. The query-string parser supports two levels of bracket nesting, so `?filter[$or][0][id]=1` never reached the filter at all and was read as a field name. On deals that produced `UNKNOWN_FILTER_FIELD` naming the "field" `filter[$or][0][id]` — an answer that sent the caller off to check field names instead of saying that OR cannot be expressed in one filter. Shorter spellings meanwhile answered the correct `INVALID_FILTER_OPERATOR`, so one mistake got two different answers.

**After**

Both `filter` spellings are equal: bracket notation and a JSON object. A value that is neither (a string that does not parse as JSON, or parses to a number, an array or `null`, or a parameter that arrived as an array — the `?filter[]=` form) is rejected with `400` and code `INVALID_FILTER` — the refusal happens before any Bitrix24 call. An empty `?filter=` still means "no filter". A repeated `?filter=a&filter=b` does not arrive as an array: the parser keeps the last value, which is then rejected for not being JSON.

The same `INVALID_FILTER` code rejects a request that mixes both forms — `?filter={"id":3}&filter[amount]=5`. The query-string parser writes them into the same place, so the second form replaces the first and half of the conditions are lost while the response looks correctly filtered. The lost half cannot be recovered, so the request is rejected.

The logic keys `$or`, `$and`, `$not` and `logic` are now rejected with `400 INVALID_FILTER_OPERATOR` at any nesting depth and on any entity, with one shared message: it names `$in` for same-field OR, a batch request for cross-field OR, and reminds that AND is the default. A field whose name merely starts with such a key (`logicGroup`, for instance) is not caught by the rule.

**Impact on integrators**

A request that sent `filter` in an unrecognised shape previously received `200` and the whole collection, and will now receive `400 INVALID_FILTER`. That is the fix: the old response looked successful while the data came back unfiltered. The same applies to a request that mixes both forms: half of the conditions used to be applied, and now the request is rejected — put the whole filter in one form. Working requests — entirely bracket or entirely JSON — are unaffected.

### FIX-0805-13: a galaxy build failure is no longer reported as npm help text

**Before**

An app shipped without a dependency lock file goes through the platform's automatic dependency install: it tries `npm ci` first and, when that refuses, installs the usual way. The step itself succeeds, but the `npm ci` refusal stays in the build log, and its last line is advisory text along the lines of "Run `npm help ci` for more info".

If the build then failed for an entirely different reason — in the interface bundler, say, or in the type checker — the short `provisionError` field showed that advisory line. It reads like dependency-install guidance, so the real cause never surfaced: the developer rebuilt again and again, chasing a step that had actually passed.

**After**

npm's boilerplate and advisory lines ("Run `npm help …` for more info", "command failed", "command sh -c …") can no longer become the error headline — they are dropped the same way log-file pointers already were.

The platform also learned to recognise bundler and type-checker failures: a failed file transform, an "expected one thing, found another" syntax line, a type-checker diagnostic, a failed import resolution. When no specific line exists, the build tool's own failure message is used — it at least names what broke. The full log remains available in `buildLog`.

### FIX-0805-14: employee directory resolves through the server owner's personal key

**Before**

`GET /v1/infra/servers/:id/b24-users` returned an empty list with a hint for a server bound to an application authorization key until the application was authorized on the account — even when the server owner had a working personal key.

**After**

When neither the server key nor the linked application resolves account access, the directory is read through the server owner's active personal key. The response shape is unchanged; the hint is returned only when no source works.

### FIX-0805-15: on a self-hosted account a module refusal ends the issuance again

**Before**

The change published on 4 August made a module refusal non-final: installing an
app (`POST /v1/apps`) on a self-hosted account was retried through the developer
key instead of returning `403`.

**After**

That change is withdrawn. The refusal is final again: the request answers `403`
with code `INT_TARIFF_REQUIRED`, and the second route is not attempted. This is
the same behaviour that applied before 4 August. Cloud accounts were affected
neither by that change nor by its withdrawal.

**Impact on integrators**

If you relied on the note published on 4 August, the retry through the developer
key no longer happens, and the response follows what the account is entitled to.
Accounts that see this `403` need their Bitrix24 plan to cover the feature.

### NEW-0805-16: region is now optional when creating a server

**Before**

Creating a standalone server required the full `provider` + `plan` + `region` triple. A request without a region was rejected with `400 INVALID_REQUEST` stating that all three fields are required. The same applied to galaxy creation.

**After**

`region` may be omitted — the platform resolves the provider default itself (its preferred region first, otherwise the first one in the catalog). `provider` and `plan` stay required. An explicitly passed region is still honoured exactly as before. If the provider exposes no regions at all, the response is `400 INVALID_REGION` naming that provider.

This covers `POST /v1/infra/servers` and galaxy creation.

### NEW-0805-17: fields of six directories now come with a name and a description

The field directory is the `/fields` response a client or an AI agent reads to understand what a field actually holds. For six entities it did not answer that question: a field was described by its type and a read-only flag, and what it contained had to be looked up in the documentation.

Now a name (`label`) and a description (`description`) are present for every declared field: [GET /v1/payments/fields](/docs/entities/payments/fields) — 44 fields, [GET /v1/basket-items/fields](/docs/entities/basket-items/fields) — 27, [GET /v1/pages/fields](/docs/entities/pages/fields) — 27, [GET /v1/catalog-sections/fields](/docs/entities/catalog-sections/fields) — 10, [GET /v1/items/:entityTypeId/fields](/docs/entities/items/fields) — 34, and in [GET /v1/statuses/fields](/docs/entities/statuses/fields) the service field `extra` gained a label — the only one of the eleven that lacked it.

The descriptions name the things that are easy to get wrong. For payments: Bitrix24 marks `datePayBefore` deprecated, accepts `companyId` without using it, `psStatus` is a `Y`/`N` flag rather than status text, and `priceCod` and `externalPayment` belong to the self-hosted edition. For pages it is now stated outright which fields arrive as the string `"Y"`/`"N"` (`deleted`, `public`, `sys`, `sitemap`, `folder`) — unlike the boolean `active`. For basket items the measurement-unit codes are named, along with the fact that `properties` and `reservations` come back only from the single-item endpoint and are absent from the list.

Along the way, fields the API already returned in its data but never described in the directory are now declared: for smart-process items — `entityTypeId` and the UTM block (`utmSource`, `utmMedium`, `utmCampaign`, `utmContent`, `utmTerm`); for companies — eleven fields: the phone numbers and e-mail addresses split by type (`phoneWork`, `phoneMobile`, `phoneMailing`, `emailWork`, `emailHome`, `emailMailing`), the Open Channels contact `imol`, the actual and legal addresses, `entityTypeId`, and the service search string `searchContent` — whose description says outright that its composition can change without notice and should not be relied on.

The keys are added to the field description and the existing `type` and `readonly` of previously described fields are unchanged — the labels themselves require no action. This release also carries FIX entries where writes did tighten: for companies, for smart-process items and for the `active` field of site pages, writing a value Bitrix24 never stored is now refused instead of falsely succeeding. If your code sends those fields in a body, read those entries — they say what to drop.

### FIX-0805-18: an empty value of the declared company fields now arrives as null, and a write to them is no longer silently ignored

Eleven company fields and six smart-process item fields used to arrive in the data while the `/fields` directory did not describe them. As long as a field is undeclared, the platform passes its value through as is and does not validate a write — hence two consequences a client can observe.

**Before**

For the company fields `emailWork`, `emailHome`, `emailMailing`, `phoneWork`, `phoneMobile`, `phoneMailing`, `imol`, `address`, `addressLegal` and `searchContent` an unfilled value arrived as the empty string `""`. Writing any of them — like `entityTypeId`, like the UTM tags of smart-process items — was accepted with a success and silently changed nothing: Bitrix24 does not store those fields from a body. That held for create, for update and for both batch surfaces: `POST /v1/companies` carrying `address` returned `201`, the company was created, and the address was lost.

**After**

An unfilled value of those fields arrives as `null`, the same as for every other string field of the platform, so an `if (value)` check behaves uniformly. Writing any of them in a body is refused with `400 READONLY_FIELD` — on create, on update and inside a batch sub-call: there is no more silent success without a result. A filter and a sort over those company fields now work as well — `filter[phoneWork]`, for instance — where the request used to be refused as a reference to an unknown field. That follows from describing the field rather than being a feature of its own: a filter over the service string `searchContent` is now accepted too, but its composition can change without notice, so do not build on it. The values are still written the same way: phone numbers and e-mail addresses through the `phone` and `email` multifields, addresses in the requisites of the company, and the entity type through the request path.

**Impact on integrators**

If your code reads those company fields and expects a string (taking its length or calling `trim`, for example), add a `null` check. If your code passed any of those fields in a create or update body, drop it: the value was never stored anyway, and now the whole request is refused, so the rest of the body is not applied either. There is no parallel support for the previous behaviour: the previous behaviour was that the value was silently lost, so there is nothing to keep. Writes to every other field and the `list`/`get` response shape of the previously described fields are unchanged.

### FIX-0805-19: the active field of a site page is read-only now — publishing goes through its own call

**Before**

`GET /v1/pages/:id/fields` described `active` as an ordinary writable field, and a request carrying it went through: `POST /v1/pages` and `PATCH /v1/pages/:id` answered with a success. The value was dropped. Bitrix24 accepts `ACTIVE` neither in `landing.landing.add` nor in `landing.landing.update` — their contract does not declare the field, and a new page is always created inactive. The client got a "done" and an unpublished page, and found the discrepancy when looking at the site.

**After**

`active` is marked `readonly`. Passing it in a create or update body is refused with `400 READONLY_FIELD`. The field stays in the `list`/`get` response and in the `/fields` directory — reading it is unchanged.

Publishing and unpublishing run through their own calls, which do work: `POST /v1/pages/:id/publication` and `POST /v1/pages/:id/unpublish`.

**Impact on integrators**

If your code passed `active` in a page create or update body, drop it and call publication separately. The value was never stored anyway, but now the whole request is refused, so the rest of the body is not applied either — title, code, description. There is no parallel support for the previous behaviour: the previous behaviour was that the value was silently lost, so there is nothing to keep.

### NEW-0805-20: creating a server with inline code joined the shared queue for heavy requests

A server-creation request may carry a code archive in the body itself — the `source.content` field. Such a request is expensive in memory, and until now it was the only heavy one running unqueued: [`POST /:id/deploy`](/docs/infra/deploy/deploy) and [`POST /:id/upload`](/docs/infra/deploy/upload) already bounded their concurrency, creation did not.

**Before**

Concurrent server creations carrying an inline archive were unbounded. The response was always on the merits — either success or a field-validation error.

**After**

Such a creation now shares the counter with code uploads. Once the cap is taken, the call returns `429` with the `DEPLOY_BACKEND_BUSY` code and a `Retry-After: 30` header. Requests without `source` (plain server creation) and requests carrying a link instead of an archive are unaffected.

To stay out of the queue entirely, create the server without `source` and upload the code in a separate request with a link — `{source: {url: ...}}`.

### FIX-0805-21: repair restores the tunnel even when inbound SSH is unavailable

**Before**

`POST /v1/infra/servers/:id/repair` reported the `serial_console` step as successful, then failed
`ssh_install` with `SSH install failed (exit 255)` after roughly 10 seconds, leaving the server
`DISCONNECTED` — the documented recovery to `CONNECTED` did not happen, and `deploy` / `exec` on
that server stayed blocked. Separately, agent installation over the serial console never worked on
a server without a public IP.

**After**

The `serial_console` step no longer confirms success when opening the firewall failed. Agent
installation over the serial console is fixed and now works both for a server without a public IP
and as a fallback: when the inbound SSH attempt fails, `repair` installs the agent out-of-band over
the serial console (the agent only needs an outbound connection). Step names in
[`GET /repair-status`](/docs/infra/lifecycle/repair-status) are unchanged; if both paths fail, the
`error` field carries both reasons joined by `; serial fallback:`. The fallback adds up to two
minutes to an already-failing call.

### FIX-0805-22: placement binding: when Bitrix24 names the reason, so do we

**Before**

When Bitrix24 refused a placement binding with "application not found" or "access denied", [POST /v1/placements/bind](/docs/apps/placements/bind) answered `502 BITRIX_UNAVAILABLE`. The stated reason was visible only in the diagnostic fields, and the status code could not tell "the application is not on the account" from "no rights to install it".

**After**

Two refusals now carry their own code:

- `404 B24_EMBEDDING_APP_NOT_FOUND` — Bitrix24 does not know the application id: the local application was deleted or reinstalled. Remedy: create the local application again and call `POST /v1/apps/:id/relink-oauth` with the new `bitrixClientId` and `bitrixClientSecret`.
- `403 B24_EMBEDDING_INSTALL_DENIED` — an access denial that survived a confirmed-active subscription check: the user whose developer key makes the call may not install local applications and/or has no access to the application itself.

The second code is emitted only where the subscription state could be confirmed as active. Where it could not, the denial stays `502`: an unknown cause is never dressed up as a specific one.

A third refusal — the developer key lacking the required scope — used to be reported on a self-hosted account as an administrator-rights requirement, even though granting an admin role changes nothing: a key's scope is fixed when it is issued. It now arrives as `403 BOX_WEBHOOK_NOT_DEVELOPER_KEY` — the same code the dashboard sections already return — and before the subscription check.

**Integrator impact**

A client that branched on `502` for these causes now receives `4xx` — switch the error handling to the codes. The new codes are listed in `placements.bindPrerequisite.errorCodes` on `GET /v1/me`, limited to the ones the account can actually receive.

**Affected endpoints:** [POST /v1/placements/bind](/docs/apps/placements/bind), `GET /v1/me`

### FIX-0805-23: a personal key with no Bitrix24 webhook now says what it is missing

**Before**

A personal key (`vibe_api_*`) with no Bitrix24 webhook answered every entity call with
`401 TOKEN_MISSING` and the text "API key has no OAuth tokens configured. Key may need
re-authorization." Such a key has no OAuth at all — it reaches the Bitrix24 account through
a webhook — so the re-authorization advice pointed the wrong way. Meanwhile `GET /v1/me`
answered `200` and looked healthy, and the key list did not tell a working key from a dead one.

**After**

The personal-key text names the missing webhook and points at `/v1/me` for the reason. The
response code is unchanged (`TOKEN_MISSING`); an optional `error.details` now carries a
machine-readable reason — `INT_TARIFF_REQUIRED` when the Bitrix24 account has no paid plan,
`VIBE_SCOPES_ONLY` when the key requests no Bitrix24 scope at all, `WEBHOOK_NOT_CONFIGURED`
otherwise — plus `paywallCode` and `upgradeUrl` where an upgrade resolves it. `details` is
returned on `/v1/{entity}` and `POST /v1/batch`.

`GET /v1/me` for a personal key carries a `b24Credentials` block — `ready`, and when
`ready: false` also `reason`, `paywallCode`, `upgradeUrl` and a `hint` when the access state is
worth re-reading. The key list and single-key read (`GET /v1/keys`, `GET /v1/keys/{id}`) return
a `b24Ready` flag: `true` — the key carries credentials for account calls, `false` — it does
not, `null` — not applicable to this key (an authorization key or a management key). No secrets
were added to any response.

### FIX-0805-24: the app subdomain answers machines with JSON instead of a page, and survives a brief tunnel drop

**Before**

While a server was waking up or its tunnel was reconnecting, every request to the app subdomain got the HTML wake page with status `503`. A browser polled it and eventually reached the app, but a webhook or an integration got markup instead of a response: the body, method and path were dropped, and the response could not tell the caller whether the action had been applied. A Bitrix24 event that arrived in that window was lost entirely.

**After**

A caller that is not a browser (it carries `Authorization`, `X-Api-Key`, `Accept: application/json`, `X-Requested-With`, `Sec-Fetch-Dest: empty`, or it is a `POST`/`PUT`/`PATCH`/`DELETE`) gets the ordinary error envelope with a code and a `Retry-After` header: `BH_SERVER_WAKING` (503), `BH_TUNNEL_CONNECTING` (503), `BH_TUNNEL_DISCONNECTED` (502), `BH_APP_STARTING` (503), `BH_SERVER_ERROR` (500), `BH_SERVER_NOT_FOUND` (404), `BH_WAKE_BLOCKED` (402). For the first four the `Retry-After` header and the `error.retryAfter` field agree.

On top of that, a brief tunnel drop on an already-running server is now absorbed silently: the request is held for up to 15 seconds, and if the tunnel returns in time it is delivered to the app and the caller gets the real response. A cold start does not fit that window — there the caller still has to repeat the request.

The browser wake, startup and error pages are unchanged, including their polling. The page poll (`?_bh_poll=`) is untouched.

### FIX-0805-25: the feedback quota is counted per key, not shared across all callers

**Before**

[POST /v1/feedback](/docs/feedback) answered `429 RATE_LIMITED` even when your key had sent fewer than five reports a minute: the counter was shared by every caller at once, so someone else's traffic drained your quota. It showed up as a rare unexplained refusal on the very first call.

**After**

The counter is kept per authorization key: someone else's traffic no longer spends your quota.

**Impact on integrators**

No action required. Refusals caused by another caller's traffic go away on this method. Keep your threshold as it is: handling `429 RATE_LIMITED` and retrying on the `Retry-After` header is still the only reliable way to learn your own limit.

### FIX-0805-26: Open Channels configuration paging: the window is no longer shifted twice

**Before**

[GET /v1/openline-configs](/docs/openlines/config/list) and [POST /v1/openline-configs/search](/docs/openlines/config/search) applied `limit` and `offset` twice: Bitrix24 applied them first, then the wrapper cut the window out of the already-prepared page a second time. The client received an empty or shifted result with no error: `limit=3&offset=3` came back empty, and with `offset=2` the first record was the fourth rather than the third. The `hasMore` field was computed over that same trimmed page, so a full page always reported `false` and a page walk stopped after the first request.

Separately, a fractional `limit` below 1 (for example `limit=0.5`) floored to zero, and the underlying method reads a zero limit as "no limit". The response came back empty with `hasMore: true`, so a walk driven by that flag never finished.

**After**

Bitrix24 cuts the window and the wrapper no longer moves it. The request asks for one record beyond the requested limit, and `hasMore` is derived from whether that record arrived; at the `limit=200` ceiling the flag is derived from the page coming back full, so the last full page may be followed by one empty response. A fractional `limit` below 1 falls back to the default of `50`, the same way `limit=0` and non-numeric values already did.

**Impact on integrators**

No code changes are required. A page walk over `offset` and `hasMore` now returns the complete result — records were previously lost silently. The meaning of `total` is unchanged: it is still the number of records in the current window rather than in the whole result, and a paging loop is bounded by `hasMore`.

### BC-0805-27: aggregate over a large pipeline: per-stage counts without reading deals, refusal instead of truncation

> Old format supported until: 05.02.2027

Both changes ship disabled and are switched on per account by a platform administrator.

**Before**

`POST /v1/{entity}/aggregate` that needs rows to answer (numeric operations and/or
`groupBy`) still fetched the first 5000 records when `total > 5000` and flagged the answer
`meta.truncated: true`. On a large pipeline that fetch did not finish — the client waited
twenty seconds and got a dropped connection instead of an answer.

**After**

With the refusal mode on, such a request answers `422 AGGREGATION_LIMIT_EXCEEDED`
immediately and fetches no rows at all. The error text says what to do: narrow the filter,
ask for the count only, or (for deals) ask for the count grouped by stage, which is
answered without reading rows.

With stage grouping on, `POST /v1/deals/aggregate` with `groupBy: ["stageId"]` or
`["stageSemanticId"]` and a scalar `categoryId` in the filter answers **on a pipeline of any
size**: each stage count comes from a separate cheap count on the account side. The
response carries `meta.recordsProcessed: 0`, `meta.truncated: false`,
`meta.aggregatePath: "fanout"` and `meta.stageCountDelta` — the difference between the
overall total and the sum of the stage counts (0 when the split is complete). Numeric
operations per stage stay available while the combined group size fits in 5000.

Unchanged: a count-only request without grouping (`aggregate: [{"function": "count",
"field": "*"}]`) answers as before — one count, at any volume; selections up to 5000
records behave exactly as they did.
