# API changes: September 17, 2026

[← Changelog](/docs/changelog) · [September 2026](/docs/changelog/2026-09)

### NEW-0917-1: Cowork plans read: quotas, prices and terms

A new `GET /v1/platform/cowork/plans` method joins the already working `GET /v1/platform/cowork/members` as the second read method for the Bitrix24 account checkout. It returns the list of Cowork plans with quotas, prices and sale terms. Same key, same scope — `cowork:read`.

Quota and price in the response are **different numbers** and must not be conflated. `quota` says how much a seat may spend: the monthly window plus the weekly and five-hour ones a person actually runs into mid-month. `price` says how much a seat costs.

The price of a term arrives **already computed**. Every sale term carries `vibesMonth` — the price of one month under that term, already discounted and already rounded — and `vibesTotal` for the whole term. Computing the total independently is unnecessary and inadvisable: the discount applies to the price of a single month, and the total is that discounted month multiplied by the number of months. The reverse order differs by one vibe on non-round percentages, and the charge follows the former. The `discountPercent` field travels alongside, but only for a label such as "−20 %".

The set of terms in the response is the set on sale **right now**: it changes by configuration without a deployment, so a term selector should be built from the response rather than from a list of one's own.

A plan that is not for sale comes with `purchasable: false` and `price: null` — it has no price, rather than a price of zero.

The account is required in the request (`portalNetworkId` or `portalDomain`), because the grid is not shared: some Bitrix24 accounts get wider windows on the free plan. An unknown account is not an error — the response carries `portal.known: false` and the shared grid.

### NEW-0917-2: servers now report a run mode: `runMode` and `workSchedule` in reads

`GET /v1/infra/servers` and `GET /v1/infra/servers/{id}` return two new fields.

`runMode` is `IDLE`, `SCHEDULE` or `ALWAYS`. It is derived from fields that were already public: the
assigned schedule, `createdVia` and `sleepAfterMinutes`. From `sleepAfterMinutes` alone those states
were indistinguishable: a machine that simply never sleeps and an agent that is forbidden to sleep
both carried `null`. For an agent or a bot `runMode` always reads `ALWAYS` — a number in
`sleepAfterMinutes` does not change that.

`workSchedule` is `null` until the server runs on a schedule. Otherwise it is an object:

```json
{
  "id": "…",
  "name": "Warehouse shifts",
  "presetKey": null,
  "timezone": "Europe/Berlin",
  "windows": [
    { "isoDay": 1, "start": "08:00", "end": "13:00" },
    { "isoDay": 1, "start": "14:00", "end": "20:00" }
  ]
}
```

`isoDay` is 1…7 counted from Monday; `start` and `end` are local HH:MM in the timezone of that
schedule. Windows arrive as a list rather than a single start/end pair: one day may hold two of
them — a shift with a break, for instance. A platform preset carries an empty `name` and is
identified by `presetKey`.

The fields are additive: existing requests and response parsing keep working unchanged.

### NEW-0917-3: account balance in the members read

The `GET /v1/platform/cowork/members` response carries a new `balance` block — the Bitrix24 account's vibe balance, the one seats are paid from, plus a flag saying charges for that account are blocked. A separate call for the balance when the page opens is no longer needed.

The block reaches **only a key that holds the `revenue:balances` scope** — the same one the balances export uses. The method itself still opens with `cowork:read`, but it exists for employee data, and money is not served through it: a key that was not granted the money scope gets a response without that field at all.

Hence three states, and they mean different things. The field is **absent** — the key holds no money scope. The field is present and **`null`** — the account has no billing record at all, it has never bought anything. The field is present and **filled** — here is the balance. Collapsing the first two into one value is not safe: "not permitted" and "no purchases" are different answers.

`balance.vibes` arrives as a string, like `usage.usedVibesMonth` next to it: the value is fractional, and a JSON number is a double, which silently loses fractions on a large balance.

`balance.blocked` says charges for the account are not going through right now. The flag is computed the way the charging gate itself computes it, not from a single column: under postpay, blocking is derived from the balance and the allowed overdraft, and no separate mark for it exists. So `blocked` can be true for an account that carries no "frozen at" stamp at all.

Existing fields are unchanged, and requests written before this entry keep working.

### FIX-0917-4: removed apps show a clear placement state

**Before**

A placement left in Bitrix24 for a locally removed app opened a raw `401 APP_RESOLVE_FAILED` response.

**After**

[POST /v1/bitrix-handler](/docs/infra/app-runtime) returns `410 Gone` with a localized app-unavailable page and a link back to Bitrix24. The response includes `Cache-Control: no-store`, `Pragma: no-cache`, and `Referrer-Policy: no-referrer`.

**Impact on integrators**

No integration changes are required. Users see a terminal state instead of an authorization error.

### FIX-0917-5: application external API returns 503 under overload

**Before**

Under high load, new application external API requests did not receive a dedicated response indicating temporary unavailability.

**After**

During temporary overload, the application external API rejects a new request with HTTP 503 `APP_API_UNAVAILABLE` and a `Retry-After` header. Successful application response formats remain unchanged.

**Affected endpoints:** [GET, POST, PUT, PATCH, DELETE, HEAD /v1/applications/:id/api/*](/docs/applications/external-api).

**Impact on integrators**

No changes are required. After HTTP 503 `APP_API_UNAVAILABLE`, a client can retry the request after the delay specified by `Retry-After`.

### FIX-0917-6: warnings from a successful runtime install are now visible in the deploy response

**Before**

The `runtime` step returned `status: "ok"` and a duration whenever the install succeeded, and anything
the runtime installer printed along the way never reached the caller — neither in the plain response nor
in the `?stream=true` stream. Yet the installer could be reporting that it had installed the runtime
along a different path than intended: for example it moved one of the package sources aside to keep
apt readable, or took the runtime from a fallback source. The deploy was green, and the only way to
find out was to log in.

**After**

When the install succeeds but the installer has something to report, the `runtime` step returns
`status: "warning"`, the text of those messages is in that step's `stdout`, and a pointer entry appears
in the response's `warnings[]`. The response itself stays successful: `success: true` and the HTTP 200
response are unchanged, the step keeps its `duration` field, and the response shape is the same. When
the installer has nothing to say, the step still returns `status: "ok"` with no `stdout`.

### NEW-0917-7: paid seat rights in the members read

The `GET /v1/platform/cowork/members` response carries a new `rights` block — seats that have been paid for but not handed to anyone yet. Each row is `plan`, `count` and `months`: "three Pro rights for twelve months".

A right is not a seat yet. It has no owner, no key, no quota accounting and **its term does not run**: the paid period is held by the company and starts the moment the seat is handed to an employee. Hence the row carries a length in months, and an end date is not there and cannot be.

For the same reason a right must not be added up with a seat that lost its owner: the latter's term is already running and will end on the calendar. Those are different messages to the buyer, and they are different fields in the response.

The block always arrives, an empty array included. An empty array means "no reserve" — not "unknown": the rows are read from their own table and do not depend on Bitrix24 being reachable, so in a response with `degraded: true` they are exactly as trustworthy as in an ordinary one.

The neighbouring `seats.unassigned` field is deprecated. It stays an empty array and keeps working, but the reserve moved to `rights` entirely: a right has a different shape and a different meaning, and filling the old field with it would silently change the meaning of a published contract. Removing the field will travel as a separate entry.

Existing fields are unchanged, and requests written before this entry keep working.

### FIX-0917-8: creating a folder through the global POST /v1/batch is no longer rejected by Bitrix24

**Before**

The sub-call `{ "entity": "folders", "action": "create" }` in [POST /v1/batch](/docs/batch) sent the folder fields in a single `fields[...]` envelope, while the Bitrix24 method expects the parent folder as the top-level `id` parameter and the remaining fields under `data[...]`. Every such create came back with the `ERROR_ARGUMENT` error, even though the single [POST /v1/folders](/docs/entities/folders/create) and the per-entity `POST /v1/folders/batch` worked with the same body.

**After**

The global batch builds a folder create in the same shape as the single route, so the sub-call really does create the folder. A missing `parentId` is refused with the `MISSING_PARENT_ID` code before Bitrix24 is called — the same code as on the single route, instead of a raw Bitrix24 error.

**Impact on integrators**

Nothing to change. If you worked around the defect with single folder-create calls, you can now create folders in bulk within one batch.

### FIX-0917-9: the time-entry list answers 404 for a missing task

**Before**

[GET /v1/tasks/:taskId/time](/docs/entities/tasks/time/list) for a task that does not exist, or is not visible to the key, answered `422 BITRIX_ERROR` and the `message` field carried the internal Bitrix24 exception text, such as `TASKS_ERROR_EXCEPTION_#1; Task not found or not accessible; 1/TE/TASK_NOT_FOUND_OR_NOT_ACCESSIBLE`. Sibling routes of the same section already answered `404` in that case.

**After**

The same request answers `404` with code `TASK_NOT_FOUND` and the short message `Task not found or not accessible.`; the internal exception text is no longer exposed. The successful response is unchanged: when the task is there, the HTTP 200 response is the same as before. Other read errors, including a rate-limit refusal, keep their previous statuses.

**Integrator impact**

No client changes are required. If your handler detected a missing task by the `422` status or by the message text, switch it to `404` and the `TASK_NOT_FOUND` code.

### FIX-0917-10: the "Time Management is not available on this portal" refusal has its own code

**Before**

The state in which Time Management is unavailable reached the Workday operations as two different responses, and neither named the cause. When the Time Management module is not installed or not included in the plan, Bitrix24 answers "method not found" for every `timeman.*` method — and the caller received `404 ENTITY_NOT_FOUND`, a message about a missing entity, while the entity was fine and it was the method that was unavailable. That code appeared in no error table of the section. When the module is installed but Time Management is switched off in portal settings, the same refusal arrived as the generic `422 BITRIX_ERROR` carrying the Bitrix24 text, indistinguishable from a data error.

**After**

Both states answer `409 TIMEMAN_MODULE_NOT_ENABLED` — the code the section already used in [GET /v1/workday/records](/docs/workday/records). The message names both possible causes and the action: ask a portal administrator to enable Time Management. The answer is the same across every operation of the section, so the advice in the workday history documentation — "call current status, the same refusal means Time Management is off" — now holds literally. This refusal is no longer counted by the error-loop protection, so it stays informative for any number of retries instead of turning into `429 ERROR_LOOP_DETECTED`.

**Integrator impact**

No action required: the refusal was and remains a 4xx response. An integration that told this state apart by the Bitrix24 message text can switch to the `409 TIMEMAN_MODULE_NOT_ENABLED` code. The former `404 ENTITY_NOT_FOUND` and `422 BITRIX_ERROR` were never promised by the section documentation for this state.

**Affected endpoints:** [POST /v1/workday/open](/docs/workday/open), [POST /v1/workday/close](/docs/workday/close), [POST /v1/workday/pause](/docs/workday/pause), [GET /v1/workday/status](/docs/workday/status), [GET /v1/workday/settings](/docs/workday/settings), [GET /v1/workday/schedule](/docs/workday/schedule), [GET /v1/workday/records](/docs/workday/records)

### BC-0917-11: a galaxy command with no exit status is no longer reported as successful

> Old format supported until: not provided

**Before**

[POST /v1/infra/servers/:id/exec](/docs/infra/deploy/exec) on an application inside a galaxy (`kind: "GALAXY_APP"`): when the host answered but sent no exit status, the call returned `success: true` with `exitCode: -1`. That claimed a completion the platform never observed, and a command that genuinely exited with code `-1` was indistinguishable from this case.

```json
{ "success": true, "data": { "exitCode": -1, "stdout": "", "stderr": "", "duration": 0, "truncated": false } }
```

**After**

The same case returns `EXEC_NO_EXIT` with `success: false`, exactly as it has long worked on a standalone virtual machine. The response stays HTTP 200. The `exitCode`, `duration` and `truncated` fields are absent — the platform does not know their values — and the output collected up to the break arrives in `data`.

```json
{ "success": false, "error": { "code": "EXEC_NO_EXIT", "message": "Exec stream ended without an exit status from the agent" }, "data": { "stdout": "", "stderr": "" } }
```

**What integrators should do**

No support window is provided for the previous format: it asserted an outcome that had not happened, and keeping it would mean continuing to return a false success.

Handle the `EXEC_NO_EXIT` code wherever you run a command on an application inside a galaxy: the «stream ended with no exit status» case now arrives that way instead of as a success with `exitCode: -1`.

⚠️ This does not make `exitCode: -1` unambiguous in every case. One situation still reports `-1`: the agent sent a terminal chunk but named no code in it. It is rare, this change does not alter its response shape, and in the body it remains indistinguishable from a genuine exit with code `-1`. Only the operation record tells them apart: there the code is stored solely when the agent named it, and `null` otherwise. So if you need to tell «exited -1» from «no code was named», read the outcome by its operation id through [GET /v1/infra/operations/:operationId](/docs/infra/deploy/operation-status) rather than from the response field.

### BC-0917-12: an empty query-parameter value in the operations list is no longer replaced by the default

> Old format supported until: not provided

**Before**

[GET /v1/infra/servers/:id/operations](/docs/infra/deploy/operations) with an empty parameter value — `?limit=` — answered `200` and returned the default feed, as if the parameter had not been sent at all.

```
GET /v1/infra/servers/srv_123/operations?limit=
→ 200, the 5 newest rows
```

**After**

The same call answers `400 INVALID_LIMIT`. An empty value is a value that was sent, not an omitted parameter: it comes from a template with an unfilled variable, so the caller meant to narrow the feed and failed. The `limit=5` and `kind=deploy` defaults apply only when the key is absent from the query string altogether.

```json
{ "success": false, "error": { "code": "INVALID_LIMIT", "message": "limit must be a non-negative safe integer" } }
```

**What integrators should do**

No support window is provided for the previous behaviour: it returned a set of rows the caller never asked for, and on a recovery channel that reads as «there are no records».

Check the places where the query string is assembled from a template. When the value can come out empty, leave the key out entirely — the default applies then. The same rule covers `?kind=`, but that parameter ships in this very release, so it had no previous behaviour.

### NEW-0917-13: the outcome of a server command is readable after an interrupted wait

The command [POST /v1/infra/servers/:id/exec](/docs/infra/deploy/exec) now has a durable run identifier that lets the outcome be read by a separate request. Previously the outcome existed only inside the current response, so a transport timeout or a dropped connection lost it for good, and there was no way to learn whether the command had run. Retrying is unsafe in that state: a transport timeout ends only the client-side wait, while the command on the server can run to completion after the client already received an error.

The identifier arrives in the body as `operationId` (`data.operationId` on success, `error.operationId` on failure) and in the `X-Vibe-Operation-Id` response header. On a standalone virtual machine the header is sent before the command starts executing, so it survives to the client even when the body is lost, and with `?stream=true` the same value arrives as the first `event: operation` frame. An application inside a galaxy has no early channel: that branch opens no stream and returns the header only with the terminal response, so an interrupted wait receives no identifier and the run is found in the server operation list instead.

The outcome is read through [GET /v1/infra/operations/:operationId](/docs/infra/deploy/operation-status) and is kept for 7 days. The `kind` field in that endpoint's response now also takes the value `exec` alongside `deploy`, and a command additionally carries `exitCode` — the exit status when the agent proved one, otherwise `null`. Status `succeeded` for a command means it ran to completion, and a non-zero exit status arrives with that same status. Status `failed` is set only when the platform can prove the command never ran, for example on `EXEC_BUSY`. While the command is still in flight the status is `running`. Every other interrupted case reports `unknown`: the command may keep running on the server, so a state-changing command must not be repeated on that status without reconciliation.

The list [GET /v1/infra/servers/:id/operations](/docs/infra/deploy/operations) gained a `kind` parameter defaulting to `deploy`. Without the parameter the same set of rows arrives as before — deploys only — and each row additionally carries a `kind` field. `kind=exec` shows commands and `kind=all` returns a mixed feed. An unsupported value returns `400 INVALID_KIND`.

The record carries the outcome of the attempt — status, exit status and the reason for a refusal. The command output is not stored in it: if you need the output after a drop, run the command as a background job and read the journal through [GET /v1/infra/servers/:id/logs](/docs/infra/deploy/logs).

### FIX-0917-14: on a busy server the machine-actionable recovery field names the guarded endpoint

**Before**

On `409 EXEC_BUSY` for a standalone virtual machine (`kind: "STANDALONE"`), `error.hint.recoveryAction` named [`DELETE /v1/infra/servers/:id/lock`](/docs/infra/deploy/lock) — a call that releases the lock unconditionally, without checking whether an operation is running. That field is the one part of the hint a client acts on without reading the prose, so an automated call could abort a live deploy.

```json
{ "error": { "code": "EXEC_BUSY", "hint": { "recoveryAction": "DELETE /v1/infra/servers/:id/lock" } } }
```

**After**

The same place now carries [`POST /v1/infra/servers/:id/unstick`](/docs/infra/servers/unstick), which answers `409 OPERATION_IN_PROGRESS` while a lock is still observed — both when the operation is provably live and when nothing can prove it dead — releasing a lock itself only when it is an exec mutex past its own TTL, while with no lock observed at all the call simply goes through, and additionally bounces the agent tunnel. The lock release has not left the contract: it remains in the `error.hint.recovery` text as a second step. Only an independently confirmed idle server — its state and logs — earns it. A missing `error.hint.holder` is not that confirmation: the holder read is fail-open, so «no holder» is also what arrives when it could not be read.

```json
{ "error": { "code": "EXEC_BUSY", "hint": { "recoveryAction": "POST /v1/infra/servers/:id/unstick" } } }
```

The hint text also stopped advising a check for «no running operation» through the operations list. That list returns only the runs of your own key, so an operation started by another key of the same Bitrix24 account is invisible there and an empty list proved nothing. The holder arrives in the same response, as `error.hint.holder`.

**Impact on integrators**

A client that executes `recoveryAction` as given needs no change — it will start calling the guarded endpoint on its own. If `DELETE /lock` is written into your code as a constant string, re-read `error.hint.recovery`: it states the condition under which that step belongs.

### FIX-0917-15: apps with an interface are no longer created in API-only mode

**Before**

[POST /v1/apps](/docs/apps/create) answered `201` and returned `mobile: true`, but the Vibecode platform did not tell Bitrix24 which install mode to use, so Bitrix24 fell back to its own default — API-only. An app with an interface was stored without one: REST worked, while the app itself appeared neither in the Bitrix24 menu nor in the mobile client.

**After**

The mode is now sent explicitly. An app with an interface is installed as an app with an interface; an app with no menu entries is still installed as API-only.

**Impact on integrators**

The `201` response and its payload are unchanged and no action is required. The platform does not convert apps created earlier — turn the API-only mode off in the application card in the Bitrix24 account, or create the app again.

### BC-0917-16: creating a Cowork/Code application: live paid seat and a scope check

> Old format supported until: not provided

**Before**

An account without platform access was refused on [POST /v1/cowork/applications](/docs/cowork/applications-create) before
Bitrix24 was contacted at all, even when the caller held a live paid Cowork/Code seat:
402 with the access reason, or 403 when the account plan could not be read.

A scope for a module the account does not have was accepted silently: the request answered
201 and the key was issued carrying a scope that the neighbouring key endpoints refuse to
the same account.

**After**

A live paid seat opens this endpoint: the application is created and the key is issued. Every
other key-ISSUANCE endpoint answers as before — the rule covers this one only.

⚠️ An application created here on a seat gains one restriction on a NEIGHBOURING operation:
its ownership can no longer be handed to a colleague who holds no right of their own — `403
RECIPIENT_LACKS_COWORK_SEAT`. The right required of the recipient is the same one that opened
this endpoint for you, and once the account passes access on its own the restriction is lifted.

**Affected endpoints:** [POST /v1/cowork/applications](/docs/cowork/applications-create) — the
seat relaxation; `POST /api/applications/{id}/transfer` — the new refusal above. The second one
is a cabinet call, absent from the Vibe API, so it has no page either.

A scope for a module the account does not have is now rejected explicitly: 403
`SCOPE_NOT_AVAILABLE_ON_PORTAL`, the same as on the neighbouring key endpoints. The refusal
arrives before any record is created, so no application is registered and no key is issued.

The seat relaxation arrives as a ROLLOUT and is switched on account by account. Until it
reaches your account the call answers with the previous refusal — 402 with the access reason,
or 403; that is a sign of the rollout, not an integration error. The scope check is not
gated by the rollout and takes effect at once.

A seat lifts ONLY the account's plan-access refusal, and nothing else. An exhausted balance is
not cured by it — the call still answers `ACCOUNT_FROZEN`, and that check runs earlier. Nor does
a seat lift an unsupported region or a self-hosted account with no confirmed partnership; such an
account has no rollout to wait for.

**What integrators need to do**

Send only the scopes available to the account in `b24Scopes`. The set of available modules
is read from the account card; keys issued earlier are unchanged. If a request answers 403
`SCOPE_NOT_AVAILABLE_ON_PORTAL`, drop the named scope from the body and repeat the request
with a fresh idempotency key.

### FIX-0917-17: the idle threshold on a scheduled server is no longer a silent no-op

**Before**

`PATCH /v1/infra/servers/{id}/sleep` on a server with a work schedule assigned answered `200`, stored
the threshold and changed nothing: the schedule outranks the threshold, so the machine kept running
during its windows. The bill stayed the same, and the response gave no way to tell that apart from an
applied setting. Wake rows generated by the schedule were also listed by `GET
/v1/infra/servers/{id}/wake-schedules` alongside ordinary ones, even though they could not be edited
or removed — the next schedule sync brought them back.

**After**

Writing the threshold clears the schedule: a number moves the server to "sleeps when idle", `null` to
"around the clock". The response is still a successful `200` and carries a new `workScheduleCleared`
field telling you the schedule was dropped. Wake rows generated by a schedule are no longer listed,
and addressing one by id in `PATCH`/`DELETE` answers `404 NOT_FOUND`: those rows are owned by the
server's schedule, not by the window list. Windows created by hand are untouched.

### BC-0917-18: the invoice schema is completed: eleven response fields declared, seven of them read-only, contacts marked as not returned

> Old format supported until: not provided

**Before**

[GET /v1/invoices/{id}](/docs/entities/invoices/get) (and the list, search and `include`) returned thirteen fields that were in neither the schema, nor the reference, nor the OpenAPI description: `contactIds`, `observers`, `taxValue`, `locationId`, `webformId`, `lastActivityBy`, `utmSource`, `utmMedium`, `utmCampaign`, `utmContent`, `utmTerm`, plus `parentId2` and `parentId7`. The fields page described eight of them with rows taken from the live Bitrix24 metadata — with Bitrix24 types (`user`, `crm_contact`, `double`…) and no read-only mark; the five `utm*` were nowhere. An explicit `select` of any of the first eleven answered `200` with an `UNKNOWN_SELECT_FIELD` warning although in `GET /v1/invoices/{id}` the value arrived. Seven of them are set by Bitrix24 itself and a sent value is never stored — `taxValue`, `lastActivityBy` and the five `utm*` — yet create, update and import answered success (`201`/`200`) while the value vanished. The fields page promised that `contacts` arrives in the invoice response and works in `select`; in fact the key never arrives, and a write of `contacts` answered a raw Bitrix24 ORM error `422 BITRIX_ERROR`. An empty `locationId` came back as an empty string.

**After**

Eleven fields are declared in the schema, the reference, the OpenAPI description and on the [fields page](/docs/entities/invoices/fields) with types from a live measurement: `contactIds`, `observers` (arrays; an update replaces the set, an empty list clears the bindings), `locationId` (string), `webformId` (number) — writable; `taxValue`, `lastActivityBy`, `utmSource`, `utmMedium`, `utmCampaign`, `utmContent`, `utmTerm` — read-only. An explicit `select` of these fields no longer raises the warning. A value sent to a read-only field — which Bitrix24 never stored — is now refused: create and update answer `400 READONLY_FIELD`, import `400 IMPORT_ITEM_VALIDATION`, the entity batch `400 BATCH_ITEM_VALIDATION`, and the global batch `READONLY_FIELD` under the call in `data.errors`. The `contacts` field is declared as not returned (`notReturned`) and read-only: the page and `GET /v1/invoices/fields` say honestly that the key never arrives, and a write is refused by the same guard with the same codes per door as the read-only fields (create and update — `400 READONLY_FIELD`, import — `400 IMPORT_ITEM_VALIDATION`, entity batch — `400 BATCH_ITEM_VALIDATION`, global batch — `READONLY_FIELD` under the call) instead of a raw Bitrix24 error; the bound contacts are in `contactIds`. An empty `locationId` comes back as `null`, like every other empty string of the record. As for any declared scalar field, an object or an array in `locationId` and `webformId` (and a non-numeric string in `webformId`) is now refused with `400 INVALID_PARAMS` before Bitrix24 is called — previously such a value travelled to Bitrix24 unchecked. An empty list in `contactIds` or `observers` clears the bindings on `PATCH /v1/invoices/{id}`; the entity batch and the global batch cannot carry an empty list (the sub-call travels as a query string, which has no encoding for an empty array — the key vanished and the response was a success) and now refuse it: `400 BATCH_ITEM_VALIDATION` and `INVALID_PARAMS` under the call in `data.errors`. List and search without `select` still carry the custom `ufCrm_*` fields and the `parentId2`/`parentId7` links; an explicit `select` of the five `utm*` on list or search is ignored by Bitrix24 — these fields arrive only in the full record. In `filter` and `order` these fields are, as before, not accepted (`400 UNKNOWN_FILTER_FIELD` / `400 UNKNOWN_SORT_FIELD`). The relation fields `parentId2` and `parentId7` stay dynamic: their type and labels still come from the live Bitrix24 metadata, a filter by them works, a sort does not.

**What integrators should do**

If you sent `taxValue`, `lastActivityBy` or `utm*` on invoice create, update or import — drop them from the body: Bitrix24 never stored them, the tax is set through the product rows, the activity author and the UTM tags are stamped by Bitrix24. If you sent `contacts` — switch to `contactIds` (the full list of bindings). If you checked `locationId` against an empty string — check for `null`. If your `select` named fields from the list above — the warning is gone, nothing to change; read `utm*` on list and search without `select`. If you cleared bindings or observers with an empty list through a batch request — that never worked; do it with a single `PATCH /v1/invoices/{id}`.

**Affected endpoints:** [GET /v1/invoices/{id}](/docs/entities/invoices/get), [GET /v1/invoices](/docs/entities/invoices/list), [POST /v1/invoices/search](/docs/entities/invoices/search), [GET /v1/invoices/fields](/docs/entities/invoices/fields), [POST /v1/invoices](/docs/entities/invoices/create), [PATCH /v1/invoices/{id}](/docs/entities/invoices/update), [POST /v1/invoices/import](/docs/import), [POST /v1/{entity}/batch](/docs/batch), [POST /v1/batch](/docs/batch).

### FIX-0917-19: Idempotency-Key protects galaxy app creation and reuse

**Before**

The first galaxy create did not reserve `Idempotency-Key`: a lost response and retry could create another resource. Slot reuse also did not record the key and could restart the build.

**After**

Once the new protection is activated, `POST /v1/infra/servers` and `POST /api/servers` reserve the key for galaxy app creation and reuse. The response remains HTTP 201: a retry within 15 minutes returns the same resource with `Idempotent-Replayed: true`, without restarting the build or exposing SSH secrets. Different keys can refer to one reused slot. Past the window a spent key receives `409 IDEMPOTENCY_KEY_ALREADY_USED` or an ordinary policy refusal. Recorded keys remain protected after the new protection is switched off; earlier galaxy creates without recorded keys cannot be recovered.

**Impact on integrators**

Use the same key after a lost response. After a build failure call explicit deploy. Until activation the first galaxy create retains its previous behavior; check the resource list before retrying.

### BC-0917-20: key mode errors point to an available switch

> Old format supported until: not provided

**Before**

Text guidance about `403 WRITE_BLOCKED_READONLY_KEY` in key self-description and operation descriptions sent every key to `/keys`. Application authorization keys are not shown there, so following the guidance could not change their mode.

**After**

The guidance now accounts for the calling key kind. The response shape is unchanged: the ready-to-use path remains in the required `error.details.switchUrl` field and points to `/keys` for a personal key, `/management-keys` for a management key, `/applications` for a key with an Application card, and `/apps` for a standalone OAuth application.

**Integrator impact**

No response-handling change is required for `WRITE_BLOCKED_READONLY_KEY`. If an interface offers an action, use `error.details.switchUrl` as the ready-to-use path.

**OAuth rotation migration:** a concurrent operation on the same application can now return `409 REISSUE_IN_PROGRESS` while a change is running, or `409 KEY_NOT_ACTIVE` if the source key has already changed. Re-read the key state and retry rotation only for a source row that is still active. The previous unsafe concurrent path without an explicit error is not supported.

**Affected endpoints:** [GET /v1/me](/docs/keys-auth/me), [GET /v1/openapi.json](/docs/api-reference), [POST /v1/placements/bind](/docs/apps/placements/bind), [POST /v1/placements/unbind](/docs/apps/placements/unbind), [PATCH /v1/apps/:id](/docs/apps/update), [POST /v1/apps/:id/relink-oauth](/docs/apps/relink-oauth), `POST /v1/keys/:id/rotate`.

### BC-0917-21: contact and deal responses drop the Bitrix24 search-index service string

> Old format supported until: not provided

**Before**

[GET /v1/contacts/{id}](/docs/entities/contacts/get) and [GET /v1/deals/{id}](/docs/entities/deals/get) (and the list, search, `include`, batch reads and the responses to create, update, moving a deal between stages and converting a lead) returned a `searchContent` field that is in neither the field description nor the reference: Bitrix24's full-text index service string — the record number, the transliterated name, the responsible's name, the stage and dates glued into one string. The field could not be requested through `select` or filtered by (`400 UNKNOWN_SELECT_FIELD`, `400 UNKNOWN_FILTER_FIELD`); sorting by it was refused on contacts (`400 UNKNOWN_SORT_FIELD`) and went through on deals. The field simply came along. [Leads](/docs/entities/leads/get) and [quotes](/docs/entities/quotes/get) already exclude the same string from their responses.

**After**

The `searchContent` field is excluded from contact and deal responses; the read response remains `200`, the other fields are unchanged. `select` and `filter` by this field are refused as before; sorting by it (`sort`, `order` in the list request and in the search body) on deals now answers `400 UNKNOWN_SORT_FIELD` too, as on contacts. The `order` parameter inside a sub-call of the [global batch request](/docs/batch) is still passed to Bitrix24 as is — use `sort` there. On [companies](/docs/entities/companies/get) the field stays declared and described as a service field — nothing changes there.

**What integrators should do**

If you read `searchContent` from contact or deal responses — the field is gone; run full-text search through filters on the regular fields (`name`, `lastName`, `title` with `$contains`); the responsible is `assignedById`, and their name comes by that id from [GET /v1/users/{id}](/docs/entities/users/get). If you sorted deals by `searchContent` — sort by a declared field such as `id`. If you never touched this field, nothing needs to change.

**Affected endpoints:** [GET /v1/contacts/{id}](/docs/entities/contacts/get), [GET /v1/contacts](/docs/entities/contacts/list), [POST /v1/contacts/search](/docs/entities/contacts/search), [POST /v1/contacts](/docs/entities/contacts/create), [PATCH /v1/contacts/{id}](/docs/entities/contacts/update), [GET /v1/deals/{id}](/docs/entities/deals/get), [GET /v1/deals](/docs/entities/deals/list), [POST /v1/deals/search](/docs/entities/deals/search), [POST /v1/deals](/docs/entities/deals/create), [PATCH /v1/deals/{id}](/docs/entities/deals/update), [POST /v1/deals/{id}/move](/docs/entities/deals/move), [POST /v1/leads/{id}/convert](/docs/entities/leads/convert), [POST /v1/{entity}/batch](/docs/batch), [POST /v1/batch](/docs/batch).

### FIX-0917-22: smart-processes batch updates use the documented identifier

**Before**

[POST /v1/batch](/docs/batch) could reject a valid `smart-processes` update because of an incorrect identifier even when the request supplied the documented public `entityId`.

**After**

[POST /v1/batch](/docs/batch) correctly updates `smart-processes` when the request supplies the same documented public `entityId`.

**Integrator impact**

No client changes are required.

### FIX-0917-23: search filter schemas now respect entity capabilities

**Before**

The generated `POST /v1/{entity}/search` schema advertised MongoDB-style filter operators for every entity, including entities without filter support and exact-match-only entities.

**After**

The OpenAPI schema derives allowed filter fields and forms from each entity's capabilities. Exact-match-only filters accept scalar values and, where supported, a non-empty array or `$in`. Unsupported fields and operators are no longer advertised as valid.

**Impact on integrations**

API behavior is unchanged. Client generators and request validators now represent the existing runtime restrictions more accurately.

### FIX-0917-24: hint for an unavailable Galaxy package registry

**Before**

When the Node.js registry or Debian source was unavailable during a Galaxy build, the API returned `502 GALAXY_APP_BUILD_FAILED` without a category or hint.

**After**

Those failures keep the `502 GALAXY_APP_BUILD_FAILED` response and return the `INSTALL_REGISTRY_UNAVAILABLE` category with a hint to retry the deploy.

### BC-0917-25: the seven client-requisite fields of a quote are returned again and declared read-only

> Old format supported until: not provided

**Before**

Since entry BC-0914-21 the seven client-requisite columns `clientTitle`, `clientAddr`, `clientContact`, `clientEmail`, `clientPhone`, `clientTpId`, `clientTpaId` were excluded from the response of [GET /v1/quotes/{id}](/docs/entities/quotes/get) (and the list, search and `include`) as service fields: through the API they were never filled. A measurement through the direct webhook on Bitrix24 test accounts showed otherwise: binding a company and a contact does not fill them, the item API the Vibecode platform writes through does not accept them — but the legacy `crm.quote.add` / `crm.quote.update` methods (the ones older integrations use) store them, and Bitrix24 returns those values in reads. On such accounts a reader lost the client requisites, and `select=clientTitle` answered `400 UNKNOWN_SELECT_FIELD`. A value sent on write was accepted with `201` / `200` and the `UNRECOGNIZED_WRITE_FIELD` hint, although Bitrix24 dropped it.

**After**

The seven `client*` fields are declared in the description and the reference as read-only fields: they are returned in read responses (the response remains `200`), accepted in `select`, `filter` and `order`, and a value sent on write is refused instead of being silently lost: create and update answer `400 READONLY_FIELD`, import `400 IMPORT_ITEM_VALIDATION`, entity batch `400 BATCH_ITEM_VALIDATION`, and the global batch `READONLY_FIELD` under the call in `data.errors`. On quotes where nothing wrote the snapshot (quotes created through the API included) these fields come as `null`, like every other unfilled string field.

**What integrators should do**

If you sent `client*` on quote create or update — remove them from the body (in batches and import too): Bitrix24 never stored them, and the request is now refused. Take the client requisites for new quotes from the bound [contact](/docs/entities/contacts/get) and [company](/docs/entities/companies/get) by `contactId` and `companyId`. If you read quotes created by older integrations — the requisite snapshot is available in these fields again.

**Affected endpoints:** [GET /v1/quotes/{id}](/docs/entities/quotes/get), [GET /v1/quotes](/docs/entities/quotes/list), [POST /v1/quotes/search](/docs/entities/quotes/search), [GET /v1/quotes/fields](/docs/entities/quotes/fields), [POST /v1/quotes](/docs/entities/quotes/create), [PATCH /v1/quotes/{id}](/docs/entities/quotes/update), [POST /v1/{entity}/import](/docs/import), [POST /v1/{entity}/batch](/docs/batch), [POST /v1/batch](/docs/batch).

### NEW-0917-26: a new PARKED value of the Cowork seat state

The seat state field can now carry the value `PARKED` in addition to the previous ones — both in `subscription.state` of the `GET /v1/cowork/state` response and in `state` of the `GET /v1/cowork/me` response. It is the same subscription column served by two operations. A seat gets it once its owner is no longer an active member of the account: such a seat is not charged, grants no access, keeps its paid term and plan, and the company administrator can hand it over to another employee.

Parking is not indefinite, which matters to anyone relying on it: once such a seat's paid term runs out, it moves to `PAUSED` and lives on by the ordinary rules of a paused seat. So `PARKED` means "there is still a paid remainder", not "the seat will wait forever".

The previous values and their meaning are unchanged, and existing requests keep working. A client only needs to treat an unknown value as "no access right now" — exactly like `PAUSED` and `CANCELLED`.

### BC-0917-27: Unambiguous record and page selection when listing CRM documents

> Old format supported until: not provided

**Before**

[GET /v1/crm-documents](/docs/entities/documents/crm-list) accepted a repeated `entityId` with HTTP 200 and returned documents of the record named last. Supplying both `entityId` and `entityID` was also accepted, and the outcome depended on the order of the names. Bracket forms — `entityId[]=7`, `entityId[0]=7`, `entityId[a][b][c]=7` — looked like an absent parameter: the record filter silently disappeared and the answer covered every record of the given type. The `start` parameter behaved the same way: a repeat produced the last value, and a bracket form silently returned the first page instead of the requested one.

**After**

Repeated parameters, both names together and bracket forms return HTTP 400 `INVALID_ENTITY_ID` for the record selector and HTTP 400 `INVALID_START` for the page selector, even when the values agree. The refusal arrives before Bitrix24 is called. A single positive integer supplied as `entityId` or `entityID`, a single `start`, and the empty value `entityId=` work as before. The `select` and `order` parameters are not changed by this release — their behaviour is the same as before.

**What integrators need to do**

Supply `entityId` (or `entityID`) and `start` at most once each, with one value each. Do not use arrays or objects for the record and page selectors. Encode user input when building query strings: an unencoded value can introduce any extra parameter into the request, including ones that pass the unambiguity check. If the record identifier may be empty, omit the parameter entirely instead of passing an empty value — an empty value means documents of every record of the type.

### BC-0917-28: document template card declares both scopes for the fileId example

> Old format supported until: not provided

**Before**

The [POST /v1/doc-templates](/docs/entities/doc-templates/create) card showed the Disk-file creation example with `fileId`, but its machine-readable `requiredScope` field named only `documentgenerator`. A client deriving key scopes from `api-reference.json` could issue a key without `disk` and receive `403 SCOPE_DENIED` when running the published example.

**After**

Runtime is unchanged: a body with the base64 `file` field still requires only `documentgenerator`, while the `fileId` form requires both `documentgenerator` and `disk`. For this card, `api-reference.json` no longer publishes the incomplete `requiredScope`; it publishes `requiredScopes: ["documentgenerator","disk"]` instead, and the card badges show both scopes.

**What integrators should do**

When reading `api-reference.json`, check `requiredScopes` first and request every scope from that list. Use `requiredScope` only when `requiredScopes` is absent.

### FIX-0917-29: a bracket-form parameter gets 400 instead of 500

**Before**

When a parameter arrived as an array or an object instead of a string (`?portalId[]=x` in the query or an array in the JSON body), `GET /v1/keys`, `POST /v1/keys`, `POST /v1/feedback` and `PATCH /v1/feedback/{id}` responded with `500` and an internal error code. `GET /v1/feedback` with a management key responded with `500` to `?portalId[]=x`.

**After**

Such a request gets a regular `400` with the same code as a missing or invalid field: `MISSING_PORTAL_ID` for `portalId`, `VALIDATION_ERROR` for `category` and `resolution`. The optional `portalId` filter of `GET /v1/feedback` in that form is not applied: the response is `200`, as without the filter. For valid string values the response is unchanged and the HTTP 200 response is preserved.
