# API changes: August 28, 2026

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

### FIX-0828-1: stopping a server is no longer billed as a full running hour

**Before**

After a server was stopped via `POST /v1/infra/servers/{id}/stop`, the transition to
sleep was not recorded in the status journal. When the hourly charge for that hour ran
late, it relied on the last known record — running — and billed a full hour at the
running rate, including the time the machine was already stopped. The endpoint response
itself was correct; the discrepancy showed up only on the invoice.

**After**

The transition is recorded immediately, and the hour is billed as it actually happened:
running time up to the stop, sleeping time after it. The response shape and status codes
are unchanged, and the HTTP 200 response is preserved.

### FIX-0828-2: a late upload through a valid URL preserves the object

**Before**

If an upload through a signed URL started more than one hour after issuance, its reservation could be deleted before the URL expired. A later completion returned `404`, while the uploaded bytes remained without an API object.

**After**

An empty reservation remains until the maximum signed URL lifetime expires. The successful completion response remains unchanged; a late upload through a still-valid URL remains linked to its API object.

### FIX-0828-3: GET /v1/guide returns the paging rules as a single paginationCanon block

**Before**

The paging and record-counting rules were repeated inside every entity of the response. The `operations.search.paginationStability` fields and the `operations.search.params.withTotal` description carried the same text for every entity.

**After**

The same text arrives once, as a top-level `paginationCanon` block. The wording is carried over verbatim — no rule was rewritten or shortened. The same-named fields inside the entities are still present and carry a pointer to the matching `paginationCanon` entry.

The one exception is `operations.search.paginationStability.counting`. It still arrives per entity with its own text, because how to obtain an exact count depends on whether that entity offers the aggregate operation.

**Impact on integrators**

Calls work exactly as before and no response field was removed. If your code read the rule text out of the entity fields, read it from the `paginationCanon` block instead. On a full set of scopes the response is roughly 37 percent smaller.

### FIX-0828-4: task comment filter and sort accept camelCase field names

**Before**

[GET /v1/tasks/:taskId/comments](/docs/entities/task-comments/list) accepted only raw Bitrix24 field names in `filter` and `sort`. The names `authorId` and `createdAt`, in which the same fields arrive in the response, returned `400 UNKNOWN_FILTER_FIELD` or `400 INVALID_SORT_FIELD`.

**After**

The `filter` and `sort` parameters accept camelCase names alongside raw ones: `id`, `authorId`, `authorName`, `createdAt`, and `sort` additionally accepts `authorEmail`. Card-type limits are unchanged: filtering by `authorName` and sorting by `authorName` or `authorEmail` are still accepted only on the old card and return `400` on the new one. When `filter` names one field twice in different spellings of the name or of the equality sign, the response is `400 INVALID_FILTER`. Unknown names still return `400`.

**Impact on integrators**

Filtering and sorting can use `authorId` and `createdAt` — the same names in which these fields arrive in the response. Existing requests with raw field names require no changes.

### BC-0828-5: Deploy outcomes can be confirmed after a dropped connection

> Old format supported until: not provided

**Before**

If the gateway connection dropped during a deploy, the client received neither the outcome nor the operation identifier. A repeated deploy could encounter `EXEC_BUSY` without answering the main question: whether the first deploy completed.

**After**

`POST /v1/infra/servers/:id/deploy` stores an observable standalone or galaxy deploy outcome, while `GET /v1/infra/servers/:id/operations` returns recent operations started by the current API key even when the terminal response carrying `operationId` was lost. For `GATEWAY_UNREACHABLE`, `GATEWAY_CONNECTION_TERMINATED`, `GATEWAY_STREAM_ERROR*`, `GATEWAY_TIMEOUT*`, `TUNNEL_NOT_FOUND`, and a post-drop Galaxy interruption, the outcome is stored as `unknown`, and `error.hint` requires reconciling the operation, server, and logs first. On standalone this applies equally when the transport throws and when the gateway reports a timeout/error frame in-band during exec or upload/download; that unconfirmed outcome carries `error.retryable: false` and does not trigger an internal retry. After confirmed `EXEC_BUSY`, a standalone server may use `/unstick` only when no operation is still running; tenant recovery is unsupported for a shared Galaxy host, so wait and contact support if the refusal persists.

**What integrators should do**

When `error.retryable: false`, do not resend the same deploy: read `GET /v1/infra/servers/:id/operations`, the server state, and logs first. Retry only after a durable `failed` outcome when the response hint explicitly permits the same request. Do not delete or recreate the slot while the outcome is `unknown`.

### FIX-0828-6: task comment creation returns the correct numeric ID

**Before**

On the new task card, [POST /v1/tasks/:taskId/comments](/docs/entities/task-comments/create) and [POST /v1/tasks/:taskId/comments/batch](/docs/entities/task-comments/comments-batch) with `action: create` could return `id: null` for a found message when its numeric identifiers arrived as strings. With multiple matches, string comparison could select the wrong ID.

**After**

Both endpoints return the found message's numeric `id` and compare matching IDs as numbers. The single POST response remains HTTP 201. The batch response remains HTTP 200, and a successfully created item remains `success: true`.

If the search does not find the message or cannot identify it unambiguously, `id: null` remains a valid successful result: the comment has already been created, and retrying the request may create a duplicate.

**Impact on integrators**

No client changes are required. Continue supporting `number | null` and do not treat `null` as a failed creation.

### BC-0828-7: incomplete public uploads are no longer anonymously accessible

> Old format supported until: not provided

**Before**

For a `PUBLIC` object in the `PENDING` state, [GET /v1/public-storage/{portalId}/{objectId}](/docs/storage/objects/public-get) returned `302`, while [HEAD /v1/public-storage/{portalId}/{objectId}](/docs/storage/objects/public-head) returned `200`. The file became accessible without authentication before the upload was completed.

**After**

Both requests return `404` while the upload remains in the `PENDING` state. Anonymous access is available only for a `PUBLIC` object whose upload is complete.

**What integrators should do**

After uploading the file, call [POST /v1/storage/objects/complete](/docs/storage/upload/complete) and publish the anonymous link only after a successful response.

### FIX-0828-8: MCP returns the Drive file content instead of a network error

**Before**

The `download` action of `manage_file` parsed the [GET /v1/files/:fileId/download](/docs/entities/files/download) response as service JSON, although that request returns file bytes. On any real file the agent got `success: false` with code `NETWORK_ERROR` and an `Unexpected token …` message — the first bytes of the already downloaded file presented as a connection failure. The action had no working answer at all.

**After**

The action returns `content` with the file content in base64, plus `contentType`, `size` and `filename` — mirroring how `upload` accepts `content`. Files above 10 MiB are refused with code `FILE_TOO_LARGE`, and the message names the way to get such a file: a request to the same address with the same API key. Error responses stay the usual platform envelope: a missing file is `ENTITY_NOT_FOUND`, not `NETWORK_ERROR`. The generic `call_api`, which only reads JSON envelopes, now refuses known non-JSON routes up front with `NON_JSON_RESPONSE_UNSUPPORTED` instead of making a request it cannot parse. It refuses the browser entry points of the OAuth and connect flows the same way, with `REDIRECT_ROUTE_UNSUPPORTED`: they answer with a redirect, and the request used to travel there with the API key and then follow that redirect to an address the platform does not choose. The path is reduced to its canonical form before the check, so spelling it with `..` no longer slips past the refusal, and a full address in place of a path is refused with `INVALID_PATH`. The `download` action now requires the file id to be a positive whole number: a fractional value used to return another file's content as a successful answer. The REST request itself still returns binary data, so ordinary clients work as before.

### FIX-0828-9: concurrent direct uploads of one object are coordinated

**Before**

Concurrent [POST /v1/storage/objects/upload](/docs/storage/upload/direct) calls for the same physical app address could mix file content and its metadata.

**After**

Path A direct uploads with an app-bound key run one at a time. After a turn is acquired, the response remains HTTP 200. If safe write ownership is not confirmed within 60 seconds or the concurrency limit, the request receives `409 STORAGE_KEY_CONFLICT` before writing to object storage. Retry the entire request.

The guarantee applies only to Path A with an app-bound key. It does not extend the behaviour of personal keys (`appId = null`) or Paths B/C.

**Impact on integrators**

The successful response format is unchanged. On `STORAGE_KEY_CONFLICT`, retry the original request in full.

### FIX-0828-10: create and update response specifications reflect the identifier

**Before**

The OpenAPI specification and response examples for create and update operations promised a full entity where the API returns only `data.id`.

**After**

OpenAPI and the documentation describe the actual response with `data.id`. The HTTP 201 status for create, the HTTP 200 status for update, and API runtime behavior are unchanged.

**Affected endpoints:** [POST /v1/bizproc-activities](/docs/entities/bizproc-activities/create), [PATCH /v1/bizproc-activities/:code](/docs/entities/bizproc-activities/update), [POST /v1/bizproc-robots](/docs/entities/bizproc-robots/create), [PATCH /v1/bizproc-robots/:code](/docs/entities/bizproc-robots/update), [POST /v1/bizproc-templates](/docs/entities/bizproc-templates/create), [PATCH /v1/bizproc-templates/:id](/docs/entities/bizproc-templates/update), [POST /v1/calendar-sections](/docs/entities/calendar-sections/create), [PATCH /v1/calendar-sections/:id](/docs/entities/calendar-sections/update), [POST /v1/telephony-lines](/docs/telephony/lines/create), [PATCH /v1/telephony-lines/:number](/docs/telephony/lines/update).

**Impact on integrations**

Existing requests require no changes. Regenerated clients now see the actual successful response shape.

### FIX-0828-11: commands in a galaxy app are no longer refused before dispatch

**Before**

`POST /v1/infra/servers/{id}/exec` on a galaxy app answered `502` with code `EXEC_FAILED` and a message about safe container execution not being supported. The refusal hit every app and depended neither on the command nor on the state of the container: the request never reached the container at all.

**After**

The command runs in the app container, and the successful response remains `HTTP 200` with the `exitCode`, `stdout`, `stderr` and `duration` fields — the same shape as on other servers. The `CONTAINER_NOT_READY` code for a stopped or missing container is returned where the platform can confirm that state; where it cannot, the reason arrives in the `stderr` of the result and the response itself is still `HTTP 200`.

### FIX-0828-12: Node.js 20 runtime templates install more reliably

Node.js 20 in `node20*` templates is now installed more reliably when external network access is unavailable.

**Before**

The template performed an unbounded NodeSource installation. On a network failure, Node.js 18 could be installed, and the deploy then failed only with a generic version-check error.

**After**

The NodeSource installation uses bounded network attempts and an integrity check. If it fails, its package source is temporarily isolated, pre-existing configuration is preserved, and a verified Node.js 20 archive is used as a fallback; runtime identifiers and the deploy API stay unchanged.

### FIX-0828-13: A ZIP deploy no longer asks you to change the archive format when the server is busy with another command

**Before**

While another command was running on the server, [POST /v1/infra/servers/:id/upload](/docs/infra/deploy/upload) with a zip and `extract: true`, and a galaxy-app deploy from a zip, reported a failed `unzip` install (`UNZIP_PREFLIGHT_FAILED` / a build failure) and advised re-packing the archive as `.tar.gz`.

**After**

A short-lived busy command channel is retried by the server. If the channel is still busy, the response is `409` with code `EXEC_BUSY` (on a galaxy-app deploy — `GALAXY_APP_BUSY`), `retryable: true`, and a `Retry-After` header. Do not change the archive format. A real failure to install `unzip` is still `502 UNZIP_PREFLIGHT_FAILED` and the `.tar.gz` advice.

**Impact on integrators**

Retry using `Retry-After`. Do not re-pack the archive because of `EXEC_BUSY` / `GALAXY_APP_BUSY`. The `UNZIP_PREFLIGHT_FAILED` branch is unchanged.

### FIX-0828-14: a payment with an inflated vibe count is held instead of rejected

**Before**

A `payment.paid` event whose `metadata.tokens` was ABOVE the catalog count received
`HTTP 400` with code `TOKENS_MISMATCH`. The rejection was terminal: no retry follows
for that event, while the payer has already been charged — the credit never happened
and the payment surfaced in no review queue.

**After**

Such an event is accepted as held: `HTTP 200` with body
`{ "ok": true, "held": true, "reason": "TOKENS_ABOVE_CATALOG", "vendor_reason": "catalog-drift-over" }`.
No Vibe credits are added — the payment enters the review queue, the same way the
opposite-direction `TOKENS_BELOW_CATALOG` hold does. The `TOKENS_ABOVE_CATALOG` value
of `reason` is new; the two directions carry separate dictionary names because an
overpayment and an underpayment owe different amounts.

The downward direction (`TOKENS_BELOW_CATALOG`) is unchanged. An unreadable count is
still rejected with `BAD_TOKENS`.

### FIX-0828-15: a pending-deletion owner's key no longer calls Bitrix24 methods through V1

**Before**

A Bitrix24 account owner's key with scheduled self-deletion could still call some Bitrix24 methods through V1. The restriction did not cover every generated and handwritten wrapper, REST 3.0 method, direct call, and batch request.

**After**

All V1 methods that use an account-bound APP/OAuth key to send requests to Bitrix24 return `503 user_self_deletion_pending` with `Retry-After` before dispatch. This includes [GET /v1/lists](/docs/lists/lists/list), [GET /v1/calendar/settings](/docs/calendar/settings), [GET /v1/chats/recent](/docs/chats/discovery/recent), [GET /v1/workday/status](/docs/workday/status), `GET /v1/applications`, `POST /v1/apps`, tariff-refresh and trial-activation methods, and `POST /v1/batch`.

For a pending-deletion owner's READONLY key, this 503 takes precedence over `WRITE_BLOCKED_READONLY_KEY`. A fully authenticated active owner's key still receives the READONLY rejection, while earlier authentication and account-state errors keep their existing codes.

V1 metadata methods, including plain `GET /v1/me` without `refresh=tariff`, the MANAGEMENT control plane, and methods that operate only on Vibecode platform data are not frozen by this change.

**Impact on integrators**

No integration changes are required. On a 503 response, retry no earlier than the `Retry-After` value.

### BC-0828-16: include now requires the scope of the related entity

> Old format supported until: not provided

**Before**

The `include` parameter did not check whether the key held the scope of the entity it pulled in. Only the entity you were reading was checked. A key holding just `tasks` could therefore read a full employee record through `GET /v1/tasks/{id}?include=responsible`, while the same key was refused on `/v1/users`.

**After**

Both sides of the relation are checked. When the scope of the related entity is missing, the request is refused with code `SCOPE_DENIED` and HTTP 403, and the message names the missing scope and the relation that needed it.

This affects relations whose two sides require different scopes: `responsible` and `creator` on tasks and `owner` on workgroups require `user`, while `catalog` on product sections requires `catalog`. Relations that stay within one scope — `preset` on requisites or `parentSection` on product sections, for example — are unchanged.

If your integration uses such a relation, add the missing scope to the key: the scope set is editable on the key itself, there is no need to reissue it.

**Affected endpoints:** every endpoint that accepts `include` — read by identifier, list reads and `POST /search`. The mechanism itself — [including related records](/docs/includes).

### NEW-0828-17: include for requisites, workgroups and product sections

Three reference entities got their relations, so the `include` parameter is now declared and works on them. Requisites gained `preset`, the requisite preset that defines the field set. Workgroups gained `owner`, the record of the employee who owns the group. Product sections gained two at once: `catalog`, the trade catalog the section belongs to, and `parentSection`, the parent section, which comes back empty for a top-level section.

The related record still arrives in the `_included` field next to the record itself, on list reads, on read by identifier and in the `POST /search` body. The mechanism limits are unchanged: at most three relations per request, and on a selection of more than 200 records relations are not resolved and the answer is marked `includeSkipped`.

The other reference entities still do not declare `include`, and that is a decision rather than an omission. Bitrix24 returns deal pipeline stages as a string code instead of a reference-record identifier, so there is nothing to join them by. On a reference record the pipeline number is meaningful only together with the CRM object type, and one and the same number belongs to a deal pipeline and to a smart-process pipeline at the same time. On a document template the file identifier is not addressable through Drive methods, and the numerator is not exposed as a separate entity in the API. The workgroup member list is served by its own endpoint rather than through `include`.

**Affected endpoints:** [GET /v1/requisites](/docs/entities/requisites), [GET /v1/workgroups](/docs/entities/workgroups), [GET /v1/product-sections](/docs/entities/product-sections), their read by identifier and `POST /search`. The mechanism itself — [including related records](/docs/includes).

### FIX-0828-18: select combined with include no longer empties the relation on list and search

**Before**

When a list request or `POST /search` carried both `select` and `include` and the foreign key was not named in `select`, the related record came back empty. `GET /v1/deals?select=id,title&include=company` returned an empty `_included.company`, even though the same relation resolved without `select`. Read by identifier was unaffected.

**After**

The relation is resolved before the answer is narrowed to the requested fields, and the foreign key is added to the internal request on its own. The public answer is not widened by it: it still carries only the fields you named, plus the `_included` block.

**Affected endpoints:** list reads and `POST /search` on any entity that declares relations. The mechanism itself — [including related records](/docs/includes).

### NEW-0828-19: the current-user profile is now in the specification and the reference

The method [GET /v1/users/me](/docs/entities/users/me) already worked, but it was not declared in `GET /v1/openapi.json`, so it reached neither the API reference nor the endpoint map. A client checking against the specification did not find the method and concluded it did not exist.

The operation is now described: scope `user`, the same response shape as `GET /v1/users/:id` plus a tri-state `isAdmin` field. The behaviour of the method itself is unchanged.

`isAdmin` is `null` when the check could not be completed. The profile is still returned in full, so gate on `isAdmin === true` rather than on a negation.

### FIX-0828-20: `topUpAvailable` for a self-hosted account reflects the self-hosted checkout

**Before**

`GET /v1/cowork/subscription/preview` answered `topUpAvailable: true` for a self-hosted account whenever sales were open in its region at all. Self-hosted accounts buy through a separate checkout that opens separately, so the flag could promise a top-up that the very next order request refused.

**After**

The flag is computed from the availability of the self-hosted checkout in the account's region. The HTTP 200 response is unchanged and carries the same fields; for cloud accounts the value did not change. While the self-hosted checkout is closed, the answer is `topUpAvailable: false` with `currency: null`, and an attempt to start a top-up answers with code `BOX_TOPUP_NOT_AVAILABLE`.

### FIX-0828-21: the auth key now gets the scopes the Bitrix24 account actually grants

**Before**

When an application's personal API key carried no Bitrix24 scopes, the platform had
no credential to ask Bitrix24 with, and the auth key was issued with `crm` +
`placement` only. An app that needed tasks, chat or disk hit silent permission
errors, and an issued key cannot be widened — only re-issued.

**After**

Scopes are probed over the channel the platform manages the account with, so the set
no longer depends on whether a particular application key has a webhook. The auth key
receives the intersection of the account's available scopes with the supported ones,
as designed. When there is nothing to ask with, behaviour is unchanged and issuance
is not blocked.

### FIX-0828-22: an employee photo can be written through the API again

**Before**
The `personalPhoto` field is declared a string, so any array or object in it was refused with
`400 INVALID_PARAMS` — and the `[file name, base64]` array is the only shape Bitrix24 accepts.
On the entity routes no shape could write the photo: a base64 string and a URL answered `422`,
batch routes `400`. It only travelled through the `POST /v1/users/invite` wrapper.

**After**
On write, `personalPhoto` takes the file inline: an array of exactly two non-empty strings —
the file name and its base64 content — on `POST /v1/users` and `PATCH /v1/users/:id`. Reading
the field is unchanged, it still returns the photo URL. Other shapes stay refused on purpose:
the nested `{"personalPhoto": {"fileData": [...]}}` is accepted by Bitrix24 with success yet
clears the photo. Batch routes refuse the photo: a batch sub-call travels as a query string under the
global body cap, and past its length limit the value would be truncated while still
answering success. The `POST /v1/users/invite` wrapper accepts the same shape — it translates
the field name and validates the pair separately — but its body limit is still the global one
(1 MiB against the 40 MiB of the single routes), so send a real photo through
`POST /v1/users` or `PATCH /v1/users/:id`.
