# Vibecode API Changelog

Vibecode API change history: new features, fixes, and breaking changes. Entries are ordered newest to oldest.

## Entry prefixes

- **NEW** — a new capability: a new endpoint, a new optional field or parameter, a new error code in a new scenario. Existing client requests keep working.
- **FIX** — corrected behavior. The response changes to the correct one, no client action required.
- **BC** — a breaking change. Requires client action. The old format is supported for the stated period, then discontinued.

Entry code format: `{TYPE}-{MMDD}-{N}`, where `MMDD` is the publication date and `N` is a running number within that date.

Some `NEW` entries are marked **rolling out**: the method shipped in a specific Bitrix24 update and is not yet available on every portal. Until the update reaches a portal, the call returns `422 METHOD_NOT_YET_AVAILABLE` with the target version — this is a sign of the rollout, not an integration error.

## 2026-07-28

### NEW-0728-1: an erased author's app no longer issues new user tokens

Once an application's author has erased their personal data, the application stops issuing tokens to new users. Such a request previously reached Bitrix24 and created a working token together with the new user's name and email, even though the author is already gone from the system.

The return from [GET /v1/oauth/callback](/docs/keys-auth/oauth) then arrives at your `redirect_uri` with `?error=app_unavailable` — the same shape `token_exchange_failed`, `invalid_domain`, and `profile_fetch_failed` already use. [POST /v1/oauth/placement-session](/docs/keys-auth/oauth) answers `403` with the `APP_UNAVAILABLE` code — so does the placement handler Bitrix24 opens the application widget through (it previously answered `401` with the generic `USER_AUTH_REQUIRED`, which read as a user-authorization problem).

Tokens and sessions already issued for that application are not renewed. Previously working calls are unaffected: while the author is active, both endpoints behave exactly as before.

### NEW-0728-2: stuck-lock release is now cross-replica; the DELETE /lock response carries broadcast and localLock

[DELETE /v1/infra/servers/:id/lock](/docs/infra/deploy/lock) now broadcasts the release to **all** platform replicas, so it releases a stuck lock even when it is held on a different replica (a common case under horizontal scaling). The response gains `broadcast` (the release was broadcast fleet-wide, best-effort) and `localLock` (whether the lock was held on this replica). The `released` field now describes only the current replica and is **not proof of a fleet-wide release** — when the stuck lock is on another replica, `released` can be `false` while the lock really was released; do not poll the endpoint until `released: true`, retry the operation instead. Existing calls keep working unchanged (the fields are additive). Additionally, a stuck `exec` lock is now guaranteed to be released by a server-side auto-sweep shortly after its TTL expires.

The [POST /v1/infra/servers/:id/exec](/docs/infra/deploy/exec) response on `502 EXEC_BUSY` for a galaxy app now carries an `error.hint` with an honest recovery path (the shared host exec channel; escalation to the platform team — `DELETE /lock` does not help there, as the agent mutex is the blocker). The [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy) response on `409 GALAXY_APP_BUSY` gains `error.hint`, `retryable: true`, `retryAfter`, and a `Retry-After` header.

## 2026-07-27

### NEW-0727-1: app favicon in one line — /_gw/icon

The browser-tab favicon is now set with a single static line that carries no server id:

```html
<link rel="icon" href="/_gw/icon">
```

`/_gw/icon` is a platform-served path on the app's own origin; it always returns the currently uploaded icon. One upload via [POST /v1/infra/servers/:id/icon](/docs/infra/app-icon) drives both the Bitrix24 catalog card and the favicon: re-upload the icon and the favicon refreshes on its own (~5 minutes), no rebuild needed. You no longer need to ship your own static icon file in the app. The line is compatible with one-shot app creation (no id required).

Additionally, the [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy) response and the one-shot create [POST /v1/infra/servers](/docs/infra/servers) response now return a `warnings[]` entry when no icon has been uploaded yet — with the exact upload endpoint (the icon is a separate request, since the id only exists after create). The same `warnings[]` still flags a missing `displayName`/`description`. A successful deploy that left the app icon-less or unnamed is no longer silently treated as finished.

### NEW-0727-2: source-at-create: SOURCE_AT_CREATE_GALAXY_ONLY now carries a hint with a path to a galaxy

A [POST /v1/infra/servers](/docs/infra/servers/create) refusal with `source` that could not be placed in a galaxy (a `both`-mode portal with no open galaxy host, or a standalone-only portal) now additionally carries `error.hint` — an actionable path: how to get a galaxy host and/or how to deploy to a dedicated server in two steps. The error code and message are unchanged.

### FIX-0727-3: galaxy apps: PATCH /sleep and PATCH /port now return 400 — manage them from the Galaxies page

**Before**

For an app hosted in a galaxy (`GALAXY_APP`), [PATCH /v1/infra/servers/:id/sleep](/docs/infra/lifecycle/sleep) returned `200` and wrote `sleepAfterMinutes`, and [PATCH /v1/infra/servers/:id/port](/docs/infra/deploy/port) returned 404/409 — contradicting the documented `/v1/me` (`deployment.galaxyApp`) contract, where no V1 lifecycle action applies to a galaxy app.

**After**

Both calls for a galaxy app return `400` with `error.code = "GALAXY_APP_USE_GALAXY_ROUTE"` and change nothing. Configure the app's auto-sleep via the galaxy route; a galaxy app's port is host-pinned and not settable. Standalone-server behavior for `/sleep` and `/port` is unchanged.

### FIX-0727-4: galaxy app recovers after its host tunnel drops

**Before**

When a galaxy host's secure tunnel dropped (the server still `RUNNING`, but the connection lost), deploying and running commands for a galaxy app returned `502 GALAXY_HOST_UNREACHABLE`, and a logs request returned an empty response with a hint. The host stayed unreachable until a manual repair: retrying the same request hit the same error indefinitely.

**After**

The platform now restores the host's tunnel in the background without holding up the response. Retrying the same request succeeds as soon as the host reconnects (usually within a minute). Parallel deploys/commands against one shared host do not start a duplicate recovery.

**Impact**

The error code and response shape are unchanged — `502 GALAXY_HOST_UNREACHABLE` (for deploy and exec) is still marked retryable, and logs still return an empty list with a hint. What changed is that a retry now succeeds instead of failing forever. Keep retrying on that code and on the empty logs response with your usual policy.

**Affected endpoints:** [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy), [POST /v1/infra/servers/:id/exec](/docs/infra/deploy/exec), [GET /v1/infra/servers/:id/logs](/docs/infra/deploy/logs)

### NEW-0727-5: warning when a deploy changelog has nowhere to be published

The [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy) response now carries an entry in `warnings[]` when the request included a `changelog` but the deploy produced no new source version. A release note is bound to a version, so without one the text is stored nowhere and never reaches the app's channel feed in the Bitrix24 messenger.

The reason is visible in the `source` field of the same response: source storage is disabled (`feature-disabled-platform` or `feature-disabled-portal`), the save failed (`save-failed`), or the uploaded bytes matched the previous version. Such a deploy used to answer with a plain success, leaving no way to learn the note was lost. The warning is delivered both in JSON mode and in the `done` event when `?stream=true` is used.

### FIX-0727-6: bizproc-templates list without an explicit select now returns every field, including id

**Before**

`GET /v1/bizproc-templates` and `POST /v1/bizproc-templates/search` without an explicit `select` returned only the `documentType` field. Without `id` a client could not issue a follow-up `update`/`delete` — the list was useless without a second request carrying an explicit `select`.

**After**

Both calls without a `select` return the full declared field set (`id`, `moduleId`, `entity`, `documentType`, `autoExecute`, `name`, `description`, `modified`, `isModified`, `userId`). An explicit `select` works as before. The request format is unchanged.

### NEW-0727-7: uploading a file to a knowledge-base document now returns assetMarkdown right away

An upload via [POST /v1/note/documents/{documentId}/files](/docs/note/files/upload) used to return only `{ id }`, so getting the ready-to-embed block meant a second call to `GET /v1/note/documents/{documentId}/files/{id}` — or assembling the `[[image fileId=N]]` markup by hand.

The response now carries the whole file object — `id`, `documentId`, `name`, `size`, `mimeType`, `assetType`, `assetMarkdown` — the same shape GET returns. Upload the image, take `assetMarkdown` from the response, put it into the document body and call PATCH: no second request needed.

The change is additive: `id` stays in place with the same value, so a client reading only that field keeps working unchanged. If a particular Bitrix24 account returns an object without `assetMarkdown`, the platform invents nothing — the response carries exactly what Bitrix24 sent.

### FIX-0727-8: `ttlSeconds` bounds for access tokens in the machine-readable schema now match the behaviour

**Before**

The OpenAPI schema for [POST /v1/infra/servers/{id}/access-tokens](/docs/infra/access-tokens/create) advertised `ttlSeconds` between 60 seconds and 30 days. The platform, from day one, accepted 300 seconds up to 315,360,000 (ten years) and rejected anything outside that with `400 INVALID_TTL`. So a client — or a client library generated from the schema — that took the minimum straight from the schema hit a hard error on a value the schema itself offered, while the "unlimited" option from the interface looked unavailable through the API. The prose documentation was correct all along; only the machine-readable schema disagreed.

**After**

The schema reads its bounds and default from the very constants the request is validated against, so the two can no longer drift: minimum 300, maximum 315,360,000, default 86,400.

**What this means for integrators**

The endpoint's behaviour did not change — only what the machine-readable schema says about it. If you generate a client from the OpenAPI document and it validates `ttlSeconds` on your side, regenerate it: the old client would reject valid values above 30 days and allow values below 300 seconds that the platform always refused.

### NEW-0727-9: A placement app can auto-resize its iframe

An app embedded in a placement can now report its content height to the platform, which grows the iframe to fit it — previously the height was fixed by Bitrix24 and tall content was clipped. The app posts a message to its parent window: `window.parent.postMessage({ type: 'vibe:resize', height: <pixels> }, '*')`. The accepted type is `vibe:resize` or `vibe:setHeight` with a numeric `height` field; `targetOrigin` must be `'*'` — the browser checks it against the window's immediate parent. Recompute the height when the content changes, for example with a `ResizeObserver`. A full description and recommendations are in the "iframe auto-height" section of the app runtime guide.

The feature is activated per Bitrix24 account on the platform side; if the resize does not take effect yet, it is not active for your account.

### FIX-0727-10: list offset now counts records, not pages

**Before**

Bitrix24 returns lists in pages of 50 and reads the offset as a page ordinal, not a record count. We forwarded `offset` verbatim, so it was silently floored to a multiple of 50: `offset=0`, `offset=7` and `offset=49` all returned the same first page — no error, no warning. Walking a selection in steps smaller than 50 records looped on the first page, while a step of exactly 50 worked and made the parameter look healthy.

**After**

`offset` counts records on the generic list endpoints: `GET /v1/{entity}`, `POST /v1/{entity}/search`, `POST /v1/batch` (action `list`) and `GET /v1/{entity}/{id}/activities`. `offset=7` starts at the 8th record. Offset and `limit` are independent: `?limit=2&offset=51` returns exactly two records starting at the 52nd. Bitrix24 still pages by 50 — Vibecode fetches the page covering the requested position and trims the head; the cost is at most one extra Bitrix24 page per request.

Two consequences are fixed alongside. `meta.hasMore` accounts for the offset: previously, for entities whose list Bitrix24 returns under a named key — deals, contacts, companies, leads, tasks, orders, products, invoices and others, two dozen in all — it compared only the page length against the total and stayed `true` on the last page whenever `offset` was non-zero. `GET /v1/{entity}/{id}/activities` now honours `limit`: the underlying Bitrix24 method accepts no size limit, so the whole page used to come back regardless of the requested value.

When the requested position lands past what Bitrix24 returned for the fetched page — because it filters after paginating — the response carries `meta.warnings` with code `OFFSET_BEYOND_FETCHED_PAGE`; this used to surface as an unexplained empty list.

For deep paging over large selections a keyset cursor (`filter[>id]` with `order[id]=asc`) is still more reliable than an offset: it is depth-independent and stable under concurrent writes.

**Impact on integrators**

Nothing to change: an offset that is a multiple of 50 behaves exactly as before, and code that already used it that way keeps working untouched.

Two things are worth checking. First, `GET /v1/{entity}/{id}/activities` with an explicit `limit`: it used to return the whole page (up to 50 records) regardless of the value, and now returns exactly what was asked for. If your code relied on getting more than it requested in one call, raise the `limit` or page through the selection. Second, walking a selection in steps smaller than 50: that used to loop on the first page and now moves forward, so a loop that leaned on an extra counter-based exit will start returning new records.

Residual exceptions where the offset is NOT row-exact. First, four generic-layer entities whose Bitrix24 method rounds the offset down to a page boundary and whose selection cannot be widened beyond the requested size without risking lost records: `calendar-events`, `calendar-sections`, `telephony-lines`, `workgroups`. The offset there is unchanged. Second, individual endpoints with their own handlers, which were out of scope for this change: `/v1/warehouses`, `/v1/bookings`, `/v1/posts`, `/v1/requisite-links`, `/v1/lists`, `/v1/timeline-logs` and the stage history under `/v1/crm-extras`. Their behaviour is unchanged.

The four generic-layer entities above keep their previous offset behaviour, but their `meta.hasMore` is now more accurate: on the last page with a non-zero offset it could previously say "no more" while records remained.

### FIX-0727-11: a closed feedback ticket no longer reports itself as "in progress"

**Before**

A ticket caught by probe-campaign detection reported `status: NEW` to its author regardless of what had actually happened to it. The list filter runs on the real status, so a closed ticket both passed the "Solved" tab filter and rendered an "in progress" badge — the same ticket contradicting itself. Affected [GET /v1/feedback](/docs/feedback) and `GET /v1/feedback/{id}`.

**After**

Only the state the mask exists for is masked: the auto-archive. A closed ticket returns `RESOLVED`, a ticket in progress returns its real status, and an auto-archived one still arrives as `NEW`. Keys with feedback access (management, `vibe:feedback` scope) see the real status as before.

**What this means for integrators**

A client that read `status` and expected `NEW` for such a ticket now gets its actual status — that is the fix. In addition, on a detection-flagged ticket that is already closed, a withdrawal (`PATCH {"status":"WITHDRAWN"}`) and an author reply are now rejected with `409 FEEDBACK_CLOSED`, like on any closed ticket; they used to be accepted because the gate consulted the masked status.

### FIX-0727-12: Connect keys: stored scopes are authoritative — vibe:ai / vibe:search are no longer added automatically

**Before**

A key issued via VibeCode Connect automatically gained the platform scopes `vibe:ai` and `vibe:search` on every request, even when they were neither requested nor consented. Such a key could reach the AI endpoints (`/v1/chat/completions`, `/v1/ai/*`) and the Search endpoint (`/v1/search`), and the spend was charged to the account the portal is bound to.

**After**

The scopes stored on a key are now authoritative — the platform no longer widens them automatically. A key issued via Connect reaches the AI and Search endpoints only when the corresponding scope is actually present on the key; otherwise the response is `403`. Rotating a key preserves this property. Keys created in the cabinet are unchanged.

### FIX-0727-13: Apps: a derived key and app-scope sync grant no more than the calling key holds

**Before**

Calling `POST /v1/apps` with an authoritative key (issued via VibeCode Connect, or derived from one) minted the app's paired key with the full set of default platform scopes (`vibe:infra`, `vibe:ai`, `vibe:search`, `vibe:storage`) — even when the calling key held none of them. Likewise `PATCH /v1/apps/:id` could write a `vibe:*` scope into the app declaration that the key does not hold. In both cases the derived key gained the ability to reach AI and Search, charged to the account the portal is bound to.

**After**

A derived key receives exactly the platform scopes the calling key actually holds. When an authoritative key lacks a scope, a request carrying it in the body returns `403` `SCOPE_GRANT_REQUIRES_CONSENT`, with the unconsented scopes listed in `error.details.unconsented`. Scopes the key does hold (for example a consented `vibe:storage`) pass through as before. The derived key inherits the caller's expiry. App-scope synchronisation to paired keys no longer adds `vibe:*` to an authoritative key — scope narrowing still applies. Keys created in the cabinet are unchanged: they still receive the default platform scopes.

### FIX-0727-14: Bitrix24 call timeout is now configurable, the internal retry after a timeout is removed

**Before**

The platform always aborted an HTTP call to Bitrix24 at the 15-second mark, and for read methods issued one internal retry after the abort — up to ~30 seconds before the `503 BITRIX_TIMEOUT` response. The retry also started a second concurrent execution of the same call on the portal: dropping the connection does not stop Bitrix24 from processing the request.

**After**

- The time limit of a single Bitrix24 call is now platform-configurable (default stays 15 seconds). On portals where Bitrix24 responds slowly the platform can raise the limit — requests that previously ended in a stable `503 BITRIX_TIMEOUT` now live to the real answer and return data.
- The internal retry after a timeout is removed for all methods. `503 BITRIX_TIMEOUT` on reads arrives roughly twice as fast (~15 seconds instead of ~30), and the request is no longer executed on the portal twice. `429` (rate limit) retries are untouched.
- The error text `Bitrix24 did not respond within 15s` now carries the actual limit (e.g. `within 60s`) — do not rely on the constant in the text.

## 2026-07-25

### NEW-0725-1: deploy: optional changelog field

[POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy) accepts a new optional `changelog` field — plain text up to 2000 characters describing what changed in this version. When a new app version ships, the text is posted to subscribers in the app's Bitrix24 messenger channel feed; if the field is omitted, only the version number is posted. The field works in both JSON and multipart deploy modes. Existing deploy calls keep working unchanged.

### NEW-0725-2: model calls with a short-lived token issued to a partner system

**Before**

The AI router endpoints `/v1/chat/completions`, `/v1/embeddings`, `/v1/audio/transcriptions` and `/v1/models` accepted a regular platform key only.

**After**

The same endpoints (and their `/v1/ai/...` aliases) now also accept a short-lived token of a new type. A Bitrix24 partner system obtains it over its own signed channel; the token is bound to one account and one employee, lives for one hour, and is admitted to exactly these eight routes — any other path answers `SCOPE_FORBIDDEN`. Spend on such a token is metered by the platform itself and charged synchronously against the account AI quota, so an exhausted quota is refused before the model is called. Only models enrolled in the quota program are available — `/v1/models` under the same token returns that list.

Refusal codes this route can now return: `TOKEN_INVALID`, `SCOPE_FORBIDDEN`, `MODEL_NOT_IN_QUOTA_PROGRAM`, `CREDENTIAL_NOT_PLATFORM`, `ACCOUNT_FROZEN`, `PORTAL_DELETED`, `PORTAL_BLOCKED`, `PORTAL_SUSPENDED`. The envelope is unchanged: `success` plus `error` with `code` and `message`.

Regular platform keys are unaffected: without a token of the new type the responses are byte-identical to before.

## 2026-07-24

### FIX-0724-1: activities: an unrecognized filter field name is now rejected with 400 UNKNOWN_FILTER_FIELD

**Before**

`GET /v1/activities` and `POST /v1/activities/search` silently dropped an unknown filter key: the request returned `200 success` with the filter trimmed to empty — that is, it returned the whole (owner-scoped or entire) activity set. For example `{"filter":{"ownerTypeId":2,"ownerId":3,"bogusField":123}}` ignored `bogusField` and returned every activity of the parent deal. This diverged from the documentation and from other CRM entities (companies, invoices), where such a filter was already rejected.

**After**

A filter field name that is not in the activity schema — and is not a `UF_*` custom field, the `id` key, or a special token — is now rejected before the Bitrix24 call with `400 UNKNOWN_FILTER_FIELD` and a list of available fields, the same as companies/quotes/contacts already do. The full list of filterable fields is returned by `GET /v1/activities/fields`.

**Impact on integrators**

Filtering by real activity fields (in camelCase or the native Bitrix24 UPPER case), by `UF_*` fields, operators (`>=`, `<`, `!`, etc.), ranges, and AND/NOT all work as before. If you relied on the silent drop of an unrecognized key, remove it from the filter.

### NEW-0724-2: bizproc activities and robots /fields now return field labels and descriptions

**Before**

`GET /v1/bizproc-activities/fields` and `GET /v1/bizproc-robots/fields` described each field only by its type and the readonly flag, with no human-readable labels.

**After**

Each of the 12 fields now carries a `label` and a `description` in the segment language, which makes building forms and hints easier. Field types are unchanged.

### FIX-0724-3: Downloading your own storage objects no longer returns 403

**Before**

`GET /v1/storage/objects/:key` could return `403` for an object the key owner legitimately owns but that physically lives under a different storage "family" prefix (for example, server files visible in a developer's listing). The object appeared in the listing, yet downloading it, minting a presigned URL or issuing a `HEAD` failed.

**After**

The scoped credentials account for the object's actual family (the portal stays bound to the caller's context), so download (`?download`), streaming (`?inline`) and `HEAD` work for your own objects regardless of family. Ownership checks are unchanged — someone else's object still returns `404`.

### FIX-0724-4: GET /v1/workflows honours the limit parameter

**Before**

`GET /v1/workflows` accepted `limit` but silently ignored it — the whole page of running workflows (up to 50) was always returned regardless of the requested size.

**After**

`limit` is respected: the response holds at most the requested number of records. Values above 50 are gathered page by page (capped at 500); `meta.total` still reports the total number of running workflows.

### FIX-0724-5: deploy: an app wrapped in a single archive folder no longer fails with ENOENT package.json

**Before**

Deploying (`POST /v1/infra/servers/:id/deploy`) to a standalone server an archive that wraps the project in a single top-level folder (e.g. `myapp/package.json` instead of `package.json` at the root) failed at the install step:

```
npm error enoent Could not read package.json ... open '/opt/app/package.json'
```

The uploaded archive was left beside the extracted content, so the auto-flatten of a single wrapping directory did not fire (the root held two entries — the archive and the folder) and `package.json` stayed nested.

**After**

The uploaded archive is removed before the flatten step, so a single wrapping directory collapses, `package.json` lands at the deploy root, and the install proceeds normally.

**Impact**

No client action required. Flat archives (files at the archive root) work as before; to be safe you can package flat: `tar -czf build.tar.gz -C <project_dir> .`.

### FIX-0724-6: /v1/users* endpoints no longer return 403 and 500 on keys with user access

**Before**

On a read-only (READONLY) key with `user` access, `GET /v1/users/me` returned `403 WRITE_BLOCKED_READONLY_KEY` even though it is a read endpoint. Separately, `GET /v1/users`, `GET /v1/users/:id`, `POST /v1/users/search` and `GET /v1/users/fields` returned `500 INTERNAL_ERROR` on accounts where one of the user fields (`UF_*`) had an empty definition.

**After**

`GET /v1/users/me` works on a read-only key. The other `/v1/users*` endpoints return data and skip the field with an empty definition instead of failing.

**Impact on integrators**

No action required — existing calls keep working, and the previously failing scenarios now respond correctly.

### NEW-0724-7: bizproc activity and robot callback delivery to a Black Hole app

Registering a bizproc activity or robot whose `handler` points at your Black Hole deploy server now reliably delivers the execution callback (event token, auth block, code and properties) to the app. Previously that callback could be lost: Bitrix24 online events are not retried, and a sleeping or waking server dropped the call. The platform intercepts the `handler` at registration, queues the call durably, and retries delivery with a server wake-up and backoff.

Affects [POST /v1/bizproc-activities](/docs/entities/bizproc-activities) and [POST /v1/bizproc-robots](/docs/entities/bizproc-robots) (and their update). New error code `SERVER_APP_MISMATCH` (400): the Black Hole server behind the given `handler` must belong to the same app that registers the activity. Registering such a `handler` through `/v1/batch` is not supported — use a single request (`BIZPROC_CALLBACK_BATCH_UNSUPPORTED`). Additionally, `POST /v1/bizproc-robots` now requires `code`, `name` and `handler` upfront — omitting any returns `400 MISSING_REQUIRED_FIELDS` instead of a raw Bitrix24 error (as activities already did).

The capability is rolling out gradually and is enabled per account: until it is on for your account, registration behaves as before, without managed delivery. **Once it is on, batch registration behaviour changes:** an attempt to register a BH `handler` via `/v1/batch` starts being rejected (`BIZPROC_CALLBACK_BATCH_UNSUPPORTED`) — move such registrations to a single `POST /v1/bizproc-activities` or `/v1/bizproc-robots`.

## 2026-07-23

### NEW-0723-1: app blueprints library — spec via API key

Ready-made specs for popular apps are now available via a key: `GET /v1/app/blueprints/:slug?locale=ru|en` returns the raw spec markdown (`Content-Type: text/markdown`). The endpoint requires `Authorization: Bearer <key>`; an unknown or hidden blueprint returns `404 BLUEPRINT_NOT_FOUND`. The copy-paste "AI prompt" shown when creating a key carries the spec link — the AI agent fetches the spec with the same key. The former anonymous path `/api/public/blueprints/:slug.md` has been removed.

### NEW-0723-2: sources of a deleted server: access, cleanup, and an honest answer for purged bytes

Sources outlive their server — a long-standing platform guarantee — but the API gave you no way to reach them: the whole `/v1/infra/servers/:id/sources*` surface answered `404` for a deleted server, so the owner could neither list their versions, nor untag them, nor delete them. For a version tagged `published` or `manual` that was a dead end: untagging is the only sanctioned way past `409 PROTECTED_BY_TAG`, and untagging was exactly what you could not do.

Read and cleanup verbs now work on a deleted server: version list, version metadata, download, `tag`, `PATCH`, `DELETE` and `cleanup`. Saving a new version (`POST /sources`) still answers `404` — a dead server accepts no new deposits.

So that a deleted server can be found at all, [GET /v1/infra/servers](/docs/infra/servers/list) accepts `?includeDeleted=true`. The default listing is unchanged. Every row now carries a `deletedAt` field (`null` for live servers).

Separately: a version whose bytes were already purged from storage now answers `410` with code `SOURCE_VERSION_BYTES_PURGED` instead of `404`. The difference matters — `404` claimed the version did not exist, when in fact its record is alive and the recovery is different: re-upload the archive rather than look for it elsewhere. The code arrives on download and on deploy by `{"source": {"versionId": "vN"}}`.

**Affected endpoints:** [GET /v1/infra/servers](/docs/infra/servers/list), [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy), `GET /v1/infra/servers/:id/sources`, `GET /v1/infra/servers/:id/sources/:versionId/download` — the sources contract lives on the [Source storage](/docs/source-storage) page

### FIX-0723-3: galaxy app deploy: a mid-build tunnel drop is no longer masked as "host unreachable"

**Before**

If the host tunnel blipped during the build and no healthy container of this deploy resulted (the build was interrupted, the container never came up, the exec channel was busy, or the app crashed), the deploy (`POST /v1/infra/servers/:id/deploy`) returned `502 GALAXY_HOST_UNREACHABLE` advising "retry once it reconnects". The host was often reachable — the advice was misleading, and the caller never saw the real cause (for example, their own app failing to start).

**After**

When the host is reachable after the blip but the deploy did not bring the app up, the deploy returns a new code `502 GALAXY_DEPLOY_INTERRUPTED` — "the host is reachable, but the deploy was interrupted before the app started: re-send the same deploy; if the app repeatedly fails to start, fix it first (the start command, port, dependencies, environment variables, or memory limit)". It stays retryable — the slot is intact, no need to delete and recreate it. The `GALAXY_HOST_UNREACHABLE` code now stays only for a genuinely unreachable host (no probe reached it). If the app truly crash-loops, that is reliably surfaced on the retry by the normal liveness check (code `GALAXY_APP_START_FAILED`).

### NEW-0723-4: deploy step timeout error now carries a recovery hint

The `DEPLOY_TIMEOUT` error from [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy) now carries a structured `error.hint` object (`reason`, `recovery`, `recoveryAction`) tied to the timed-out step (`error.step`). For user commands (`install`, `preStart`) the hint explains that the command did not finish within its time budget and advises making it non-interactive and self-exiting, and launching long-running services from the `start` command or detached (`docker compose up -d`). For the runtime install step (`runtime` — a platform step, not a user command) and other service steps it points at a possible tunnel stall and `POST /v1/infra/servers/:id/repair`. The field is additive: the existing `error.code`, `error.message` and `error.step` are unchanged, no integration change is required; the hint arrives in both JSON mode and the SSE `error` event.

### FIX-0723-5: a failed deploy step now shows the real cause, not a benign warning

**Before**

When a deploy step failed, `data.steps[].stderr` (and hence `error.message`) could carry only a benign one-stream warning, losing the real cause of the failure.

**After**

Both streams are returned together, labelled `stderr:` and `stdout:`; the real cause is no longer hidden. The response shape and field name are unchanged.

### FIX-0723-6: list sorting by a non-unique field no longer drops records on the second page

**Before**

A list or search sorted by a non-unique field (for example the date field `begindate`), over more than 50 records, could silently return fewer records than exist: at the page boundary some records sharing the same sort-field value were lost. The response was a 200 with no incompleteness signal. Affected the CRM smart-process-backed entities — deals, leads, contacts, companies, quotes, invoices and smart-process items `/v1/items/{entityTypeId}` — on `GET /v1/{entity}`, `POST /v1/{entity}/search`, inside `POST /v1/batch` sub-calls and per-entity `POST /v1/{entity}/batch`. The same instability class affected numeric aggregation (`POST /v1/{entity}/aggregate` with `sum`/`avg`/`min`/`max`/`groupBy`): the record fetch for the aggregate ran with no order, so over more than 50 records some rows could be lost and skew the result.

**After**

A secondary `id` key is appended to the sort, making the order fully deterministic, so paginated reads no longer drop or duplicate records regardless of the sort field. Your sort stays the primary key; records with an equal sort-field value are ordered by ascending `id`. No request changes are needed.

### FIX-0723-7: reopening a ticket via a comment no longer leaves the resolution stamp

**Before**

A team comment through `POST /v1/feedback/:id/comments` that moved a ticket from `RESOLVED` or `WITHDRAWN` back into an active status (`NEW`, `REVIEWING`, `AWAITING_USER`, `NEEDS_REVIEW`) did not reset `resolvedAt` and `resolvedBy`. They lingered from the prior close, so on reads (`GET /v1/feedback/:id`, `GET /v1/feedback`) a reopened ticket looked both active and resolved.

**After**

Such a comment clears `resolvedAt` and `resolvedBy` — on reads an active ticket no longer carries a resolution date. `resolution` is left as is: it mirrors the comment body. A comment that sets `RESOLVED` still stamps the fields; moving to `ARCHIVED` and a plain transition between active statuses leave the stamp untouched. This aligns the behaviour with the clear already applied on `PATCH /v1/feedback/:id`.

### FIX-0723-8: list filters on statuses / departments / storages / currencies / products are no longer silently ignored

**Before**

`GET /v1/statuses`, `/v1/departments`, `/v1/storages`, `/v1/currencies`, `/v1/products` run on legacy Bitrix24 methods (`crm.status.list`, `department.get`, `disk.storage.getlist`, `crm.currency.list`, `crm.product.list`) that silently ignore non-filterable keys and operators. An unknown or unsupported filter field — e.g. `filter[system]` on statuses, `filter[module]` on storages, or `filter[price]` on products — as well as operators `$gt` / `$contains` / `$ne` returned `200` with the whole table. The client received the full set instead of the expected subset — a silent failure with wrong data.

**After**

For these entities the filter is validated before the Bitrix24 call: only fields the method actually filters on (live-verified) are allowed. Any other field, operator, or empty set returns `400 UNSUPPORTED_FILTER` listing the filterable fields. Allowed fields per entity: `statuses` — id, entityId, statusId, name, sort, semantics, categoryId; `departments` — id, name, parentId, headId; `storages` — id, name, code, entityType, entityId; `products` — id, name, code, xmlId, active, sectionId, sort. `crm.currency.list` filters on nothing — any `filter` on `/v1/currencies` returns `400` with a hint to filter client-side.

### BC-0723-9: POST /v1/apps no longer returns the prefix and suffix fields in the create response

> Old format supported until: 21.07.2026

**Before**

The POST /v1/apps create response carried two `vibe_app_` values — a short prefix and the full rawKey. The short prefix was mistaken for the key, and a request using it returned 401.

**After**

The create response carries one `vibe_app_` value — the working rawKey. The prefix and suffix fields remain on GET /v1/apps and GET /v1/apps/:id for masked-key display.

**What integrators should do**

Use the rawKey field from the create response as X-Api-Key. If you need the masked prefix, read it from GET /v1/apps or GET /v1/apps/:id instead of the create response.

### FIX-0723-10: POST /v1/batch now persists phone and email on lead and contact create and update

**Before**

Through the shared POST /v1/batch the phone and email (multifield) values were dropped on a lead or contact create or update. The call returned success but the field was not saved. The same payload through the single POST /v1/leads or PATCH /v1/contacts/:id and through POST /v1/{entity}/batch saved correctly.

**After**

The shared POST /v1/batch serializes multifields the same way the single calls do. Phone and email persist on create and update.

### FIX-0723-11: Search and research no longer return 402 INSUFFICIENT_BALANCE on a funded account

**Before**

`POST /v1/search` and `POST /v1/research` with the platform-managed engine (`bitrix-search`) on a personal key could return `402 INSUFFICIENT_BALANCE` even with enough Vibe balance on the account. The pre-flight balance check looked the account up by the key owner, while the account is now one per portal — so it was not found.

**After**

The balance check and charge always resolve the account by the portal. With a positive balance the request runs and is charged correctly; `402` is returned only on a genuine shortfall. The request and response shape are unchanged.

### FIX-0723-12: POST /v1/apps reports the plan requirement clearly on the cloud-shared path

**Before**

Creating an app via the cloud-shared key-issuance path (the unified cloud↔box scheme, rolled out per portal cohort) on a portal without the required Bitrix24 plan made [POST /v1/apps](/docs/apps/create) return an opaque `502 CONNECTOR_APP_INSTALL_FAILED` with no cause.

**After**

A plan-access denial is now classified up front: before calling the connector, `POST /v1/apps` checks the portal's access state and, when it's missing, returns `403 INT_TARIFF_REQUIRED` right away, with a readable message. Other cloud-shared issuance failures (module not installed, forbidden by the portal admin, other errors) are classified as before.

**Impact on integrators**

No action required, successful calls are unchanged. If you handled `502 CONNECTOR_APP_INSTALL_FAILED` on app creation, also handle `403 INT_TARIFF_REQUIRED` and prompt the user to upgrade their Bitrix24 plan.

### FIX-0723-13: starting a server no longer errors when the machine is already running

**Before**

[POST /v1/infra/servers/:id/start](/docs/infra/lifecycle/start) for a server in the `error` state
asked the cloud to start the machine and surfaced any rejection as `502 PROVIDER_ERROR`. When the
machine was already running — for example, brought back by automatic recovery after preemption —
the cloud rejected the call with «instance already in RUNNING state», and the endpoint returned an
error for an operation that had in fact succeeded. Clients saw a `502` and could not tell it apart
from a genuine failure.

**After**

That rejection is now treated as an idempotent success: when the machine is already running or in a
transitional state, the call returns `200` and the server moves to `provisioning`, exactly as on a
normal start. Genuine failures — insufficient permissions, exhausted quota, machine not found — still
return `502 PROVIDER_ERROR`.

This is the same idempotency criterion the agent start endpoint and the internal server wake path
already applied.

### BC-0723-14: the /v1/me deployment.standalone.requiredFields.create shape is now an object + documents the name slug

> Old format supported until: 22.01.2027

**Before**

In the `GET /v1/me` response, the per-kind sub-block `deployment.standalone.requiredFields.create` was an array `["provider", "name", "plan", "region"]` — the format of `name` was not stated; the sibling `deployment.galaxyApp.requiredFields.create` said only "required" for `name`. A name with non-Latin or uppercase characters was rejected by [POST /v1/infra/servers](/docs/infra/servers/create) with `400 INVALID_REQUEST`, but self-discovery never surfaced that constraint.

**After**

`deployment.standalone.requiredFields.create` is now an object (like its sibling `deployment.galaxyApp.requiredFields.create`), and in both sub-blocks `name` carries its format: a lowercase-Latin slug matching `^[a-z][a-z0-9-]*$`, 2–63 characters long. Put a human-readable label in the optional `displayName` field.

**What integrators should do**

The flat `deployment.requiredFields["POST /v1/infra/servers"]` (the array `["provider","name","plan","region"]`) is unchanged — if you read it, no action is needed and the set of required fields is the same. If your code parsed the per-kind sub-block `deployment.standalone.requiredFields.create` as an array (`.forEach` / `.includes("name")` / `.length` / `[0]`), switch to reading it as an object: the keys are field names (`provider`/`name`/`plan`/`region`) and the values are their descriptions.

### NEW-0723-15: GET /v1/contacts/fields now returns label and description for every field

The [GET /v1/contacts/fields](/docs/entities/contacts/fields) response now carries human-readable `label` and `description` for all 28 static contact fields. Previously the base fields (`name`, `lastName`, `typeId` and others) came back with only `type` and `readonly`, with no explanation of their meaning. Labels are localized to the segment language (English on `.com`). You can read a field's semantics programmatically from the response instead of cross-referencing the static documentation. In addition, `GET /v1/openapi.json` exposes these labels and descriptions (in English) as `title` and `description` annotations in the `Contact` and `ContactInput` schemas.

### NEW-0723-16: smart-processes: relations and linkedUserFields fields in the input schema

The `relations` (parent/child CRM entity links, e.g. linking a smart process to deals) and `linkedUserFields` fields are now declared in the input schema and shown in `GET /v1/smart-processes/fields`. Pass them in `POST /v1/smart-processes` and `PATCH /v1/smart-processes/:entityTypeId` to link a smart process to other CRM entities and surface it in user fields. Filtering and sorting by these fields are not supported — they are nested write structures, not query fields.

### FIX-0723-17: bizproc-activities and bizproc-robots: documentType type in /fields corrected to array

**Before**

`GET /v1/bizproc-activities/fields` and `GET /v1/bizproc-robots/fields` reported `documentType` as type `object`, while the field is a three-element array (`[moduleId, entity, documentType]`), as already declared for `bizproc-templates`.

**After**

The `documentType` type in `/fields` is now `array` across all three entities — consistent with the real contract.

### FIX-0723-18: PATCH /v1/bizproc-templates returns a numeric id

**Before**

[PATCH /v1/bizproc-templates/:id](/docs/entities/bizproc-templates/update) returned `data.id` as a string (`"1215"`), while `POST` returns a number (`1215`). A client comparing the id from the create response with the update response saw a false mismatch.

**After**

The `PATCH` response returns `data.id` as a number (`1215`) — the same as `POST`.

### FIX-0723-19: userfields: label type in the create schema corrected to string

**Before**

The OpenAPI schema for `POST /v1/userfields/{entity}` declared `label` as an `object`. B24 `crm.<entity>.userfield.add` accepts `LABEL` as a string only, so an SDK generated from the spec (where `label` was an object) sent the wrong type and failed. The OpenAPI spec is a public contract — clients generate SDKs from it, and anyone whose type was "object" had a broken client.

**After**

`label` in the create schema is declared as `string` (portal default-language label). Multilingual labels are set via `editFormLabel` / `listColumnLabel` / `listFilterLabel` (PATCH after create). No runtime change — only the generated spec was corrected.

### FIX-0723-20: sleep-now on a galaxy app now returns 400 — manage it from the Galaxies page

**Before**

`POST /v1/infra/servers/:id/sleep-now` on a galaxy-hosted app (`GALAXY_APP`) put the container to sleep and returned `200`. This diverged from the session route, which already rejected such an app, and could desync the container state from its host.

**After**

The same call on a galaxy app returns `400` with `error.code = "GALAXY_APP_USE_GALAXY_ROUTE"` and changes no state: the container stays `RUNNING`. Manage the app's lifecycle through the galaxy routes instead. Standalone servers keep their existing sleep-now behavior.

### FIX-0723-21: a task comment is no longer served under a foreign task

**Before**

On legacy portals `GET /v1/tasks/:taskId/comments/:id` returned `200` with the comment even when the comment did not belong to task `:taskId`: the same comment was served under any task, and the response `taskId` was a plain echo of the path.

**After**

The comment is checked against the task in the path before it is returned. If the comment does not belong to `:taskId`, the endpoint responds `404` with code `NOT_FOUND` and message "Comment not found". The response `taskId` now matches the real parent task. Fetching a comment through its real task keeps working unchanged.

### FIX-0723-22: company type is a single field typeId, not companyType

**Before**

The «company type» field name differed across layers. [POST /v1/companies](/docs/entities/companies/create) with a `companyType` field silently ignored the type — the company was created with the default type; the only way to set it was the `typeId` field. Reads (`GET`, search) always returned the type in the `typeId` field. The filter, however, accepted `companyType` but not `typeId`.

**After**

Company type is a single `typeId` field across all operations: create and update, read and search, filter (`filter[typeId]`) and grouping (`groupBy: typeId`). The values are unchanged — `CUSTOMER`, `SUPPLIER`, `COMPETITOR` (list: `GET /v1/statuses?filter[entityId]=COMPANY_TYPE`). The field is now described in [GET /v1/companies/fields](/docs/entities/companies/fields).

**Impact on integrators**

Set the type with the `typeId` field. Reads are unchanged — the type was always returned in `typeId`. On create and update `companyType` is no longer documented (it never persisted a value). In filters and grouping `typeId` now works, while `companyType` returns `400` (`UNKNOWN_FILTER_FIELD` in filters, `INVALID_AGGREGATION_FIELD` in grouping) — replace the name with `typeId`.

## 2026-07-22

### BC-0722-1: server icon is now served as PNG

> Old format supported until: 21.07.2026

The server icon is now served as a 256×256 PNG (`Content-Type: image/png`) — the platform renders it from your uploaded SVG. The upload endpoint (`POST /v1/infra/servers/:id/icon`) still only accepts SVG, and may now return `400 ICON_RASTERIZE_FAILED` when the file can't be rasterized to PNG.

### NEW-0722-2: scheduled wake (wake-schedules) is now available on every Bitrix24 account

The wake-window CRUD — [GET|POST /v1/infra/servers/:id/wake-schedules](/docs/infra/wake-schedules/list), [PATCH|DELETE /v1/infra/servers/:id/wake-schedules/:scheduleId](/docs/infra/wake-schedules/update) — is now available on every Bitrix24 account for standalone servers (`kind: "STANDALONE"`): the call no longer returns `403 WAKE_SCHEDULE_DISABLED`. The platform brings a sleeping server up by a given cron-expressed moment, and the task itself is fired by the app's own cron once the machine is already up. Galaxy apps (`kind: "GALAXY_APP"`) are not part of this rollout yet and still return `403 WAKE_SCHEDULE_GALAXY_DISABLED`. The `GET /v1/infra/servers/:id` response and the server list now include the additive fields `nextScheduledWakeAt` (the next scheduled wake time, ISO 8601 or `null`) and `wakeScheduleCapable` — existing fields are unchanged.

### NEW-0722-3: deploy 409 SERVER_NOT_READY now signals it is retryable

[POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy) now adds `error.retryable: true`, `error.retryAfter` (seconds) and a `Retry-After` header to the `409 SERVER_NOT_READY` response returned when the server is already restoring its connection (a repair started by a concurrent deploy or repair). It is a machine signal that retrying is worthwhile: wait the given interval and re-issue the deploy. Existing clients are unaffected — the error code and message are unchanged, the fields are additive.

### NEW-0722-4: New endpoint to activate the Marketplace trial for the key's portal

`POST /v1/portals/:id/activate-market-trial` activates the one-time Bitrix24 Marketplace trial for the portal the calling key belongs to. Previously activation was only possible from the web cabinet — API keys had no programmatic path.

`:id` must match the key's portal, otherwise `403 PORTAL_MISMATCH`. No request body. Rate-limited to 3 requests per hour per portal.

Success response: `{ "success": true, "data": { "status": "activated", "trialEndsAt": "..." } }` (or `"status": "already_active"` when the trial/access already exists). Errors: `403 WRITE_BLOCKED_READONLY_KEY` (read-only key), `403 PURPOSE_KEY_FORBIDDEN` (special-purpose key may not activate the trial), `404 NOT_FOUND` (portal not found), `409 ALREADY_ACTIVATED` (trial already activated), `409 TRIAL_ACTIVATION_UNAVAILABLE` (trial not available for this portal), `503 TRIAL_ACTIVATION_RETRY` (transient error, retry later).

### FIX-0722-5: the portal plan probe no longer stops at a scope-limited developer key

**Before**

On a portal with several developer-key holders the plan state could stay unread: if the first probed key answered with portal data but without the full licence block (a scope-limited key), the probe stopped there and never reached a key able to read the rest. The plan state stayed unknown, so a portal entitled to create servers could receive `402` from [POST /v1/infra/servers](/docs/infra) while `GET /v1/me` reported `capabilities.servers.create.available: false`. Calling `GET /v1/me?refresh=tariff` did not clear the denial.

**After**

The probe keeps going until a key returns the full licence block. An entitled portal is allowed to create servers and `capabilities.servers.create.available` becomes `true`. Response shapes are unchanged and no client action is required. The state refreshes with the portal's next plan refresh (within an hour) or immediately via `GET /v1/me?refresh=tariff`.

**Integrator impact**

No action required. A client branching on `402` at server creation keeps working — on affected portals that response simply stops arriving.

### FIX-0722-6: the `region` field in infrastructure responses always returns a region id

**Before**

For a server created and running on the international segment, [GET /v1/infra/servers](/docs/infra/servers) and `GET /v1/infra/servers/:id` could return an internal **placement-zone** identifier in the `region` field rather than a catalog region id. The value matched no `id` in [GET /v1/infra/providers/:providerId/regions](/docs/infra/providers), so a server could not be matched to its catalog region through that field, and it exposed internal placement detail.

**After**

`region` always carries a neutral region id from the same namespace as the catalog — for example `bc-eu-central`. The value equals the `id` of the corresponding `GET /v1/infra/providers/:providerId/regions` entry, so a server maps to its catalog region directly. The placement zone is an internal detail: server creation takes a region, and the platform picks the zone inside it.

**What integrators should do**

Nothing, if you pass a `region` value taken from the regions catalog — that flow is unchanged. If your code compared a server response's `region` against a string obtained earlier from a **server** response rather than from the catalog, that comparison is now reliable: both ends are in one namespace. Identifiers from the previous catalog are still accepted on input.

## 2026-07-21

### FIX-0721-1: built-in search engine name and description, cloud provider name

The public name of the platform search engine now matches the product name: `GET /v1/search/providers`
and `/v1/me` return `Bitrix24 AI Search` in the `name` field (latin locales). The provider identifier
`bitrix-search` is unchanged — no client action is required.

The source-citation claim was removed from the same provider's description: the capability depends on
the engine bound to your instance and is declared machine-readably in `capabilities.output.citations`
of the same response.

`GET /v1/infra/providers` on the international segment now returns the `Bitrix24 Cloud` brand in the
`name` field instead of `Bitrix Cloud`. The provider identifier `bitrix-cloud` is unchanged.

**Before**

`"name": "Bitrix AI Search"` · `"description": "Platform AI search with agentic mode and source citations"` · `"name": "Bitrix Cloud"`

**After**

`"name": "Bitrix24 AI Search"` · `"description": "Platform AI search with agentic mode"` · `"name": "Bitrix24 Cloud"`

### FIX-0721-2: creating a business-process template now accepts the template file

**Before**

[POST /v1/bizproc-templates](/docs/entities/bizproc-templates) answered `422 Incorrect field TEMPLATE_DATA!` for any body — creating a template was impossible: the field holding the `.bpt` file content was not in the entity schema and never reached Bitrix24.

**After**

The `templateData` field (a `.bpt` file as a `[filename, base64 content]` array) is accepted and forwarded to Bitrix24, and the template is created. The field is required on create: without it the request is rejected with `400 MISSING_REQUIRED_FIELDS` before Bitrix24 is called (previously the raw `Incorrect field TEMPLATE_DATA!` error came back). It is write-only and surfaces in the field reference at `GET /v1/bizproc-templates/fields`, but is not returned on reads.

### NEW-0721-3: Aggregated source registry — GET /v1/me/sources

A new endpoint [GET /v1/me/sources](/docs/source-storage) — the programmatic twin of the cabinet «Application sources» page. It returns source snapshots across every server and application the key owns (an account-admin key sees the whole account), with pagination (`page`/`limit`/`search`) and the standard `{ success, data, total, page, limit }` envelope. Each row carries a drill-in pointer — `listEndpoint` and `latestDownloadEndpoint` — plus `reachableViaApi` and, for server rows, `blackholeStatus`. Unlike [GET /v1/infra/servers](/docs/infra/servers/list), which is scoped to the calling key's own servers, this registry also spans a server bound to another key of the same owner.

The server-scoped source endpoints ([POST /v1/infra/servers/:id/sources](/docs/source-storage) and the sibling list/download/tag/cleanup routes) are now documented, and the `versions[].serverContext` field (`{ serverId, serverName, serverDisplayName, linkedApp }`) is formalized in the contract.

### NEW-0721-4: optional error.b24Code field in 422 BITRIX_ERROR responses

`422 BITRIX_ERROR` responses now include an optional `error.b24Code` field — the raw Bitrix24 error code for programmatic handling (for example, `PERIOD_REQUIRED`, `INVALID_FILTER`). The change is additive: existing clients that parse only `error.code` and `error.message` are not affected.

### NEW-0721-5: Open Channels statistics — 6 dashboard methods

A new API section for contact-center dashboards: [POST /v1/openlines/stats](/docs/openlines/stats) (period aggregates), [GET /v1/openlines/operators](/docs/openlines/operators) (real-time operator load), [POST /v1/openlines/sessions/search](/docs/openlines/sessions), [POST /v1/openlines/sessions/stats](/docs/openlines/sessions/stats), [POST /v1/openlines/sessions/transfers](/docs/openlines/sessions/transfers), [POST /v1/openlines/ratings/search](/docs/openlines/ratings). Requires the `imopenlines` scope and the `report_open_lines` plan feature (otherwise `403 B24_TARIFF_RESTRICTION`).

**Rolling out** — the methods ship with the Bitrix24 update `imopenlines 26.700.0` and are not yet available on every account. Until the update reaches your account, the methods return `422 METHOD_NOT_YET_AVAILABLE` with the target version in the response — this indicates the rollout, not an integration error.

### FIX-0721-6: a Bitrix24 plan refusal is returned as 403 B24_TARIFF_RESTRICTION on every endpoint

**Before**

A Bitrix24 refusal caused by a plan restriction arrived as `422 BITRIX_ERROR` with an opaque message — there was no way to tell it apart from other Bitrix24 errors programmatically.

**After**

Such a refusal is returned as `403` with the `B24_TARIFF_RESTRICTION` code. The rule is platform-wide, not limited to Open Channels: any endpoint that calls a Bitrix24 method unavailable on the Bitrix24 account plan now answers with this code.

**Impact on integrators**

Clients with generic error handling keep working unchanged — the refusal is still an error, only a more precise one. If you branched on `422` specifically for plan refusals, move that branch to `403` and `error.code === 'B24_TARIFF_RESTRICTION'`. This code does not mean the integration is broken: the capability is not included in the Bitrix24 account plan, and retrying is pointless until the plan changes.

### FIX-0721-7: Versioned deploy runtimes install the advertised version on Ubuntu 24.04

**Before**

The `node20` runtime installed Node.js 18 (Ubuntu 24.04 has no Node 20 package), and `python311` plus the RAG runtimes (`node20-rag`, `python311-rag`) failed at the install step because the packages are absent from the distribution. The `GET /v1/infra/runtimes` `packages` field advertised `postgresql-14` while PostgreSQL 16 was installed.

**After**

`node20` now installs Node.js 20 (with a major-version check), `python311` installs Python 3.11, and the RAG runtimes install PostgreSQL 16 with the pgvector extension in the application database. The `packages` field reflects the real version (`postgresql-16`). Runtime identifiers and the `/deploy` request format are unchanged.

### FIX-0721-8: server repair status is correct when polled

**Before**

Polling [GET /v1/infra/servers/:id/repair-status](/docs/infra/lifecycle/repair-status) on a multi-node backend could briefly return `idle` even while the repair was still running — when the request landed on a different serving node than the one running the repair. A client polling the status in a loop could therefore wrongly conclude the repair had finished before it even started.

**After**

The endpoint reliably returns the real repair progress (`running` / `done` / `failed`) regardless of which node the poll lands on.

**Impact on integrators**

The response shape is unchanged and no client action is required.

### FIX-0721-9: apps on a standalone server no longer run with administrator privileges

**Before**

An app deployed to a standalone Black Hole server ran with administrator privileges and no isolation. Any vulnerability in the app itself (remote code execution, for example) immediately meant full control of the whole virtual machine: the server's connection keys, its service configuration and system files.

**After**

The app runs under a dedicated unprivileged account and sees only its own directory (`extractTo`, `/opt/app` by default), which it owns. System directories are read-only to it and privilege escalation is blocked. Ports below 1024 still work — the platform grants the specific permission needed.

Deploy commands (`install`, `preStart`) and `/exec` still run with administrator privileges. Nothing changed there and no `sudo` is needed.

No action required: if your app genuinely needs administrator privileges to start, the deploy automatically restores the previous mode, finishes successfully and adds a warning explaining why. To skip that attempt outright — relevant for nginx as the start command, MySQL over the root system socket, and Docker-driven starts — pass `"hardening": "off"` in the deploy body.

Two new `step` values appear in `data.steps[]`: `service_user` for handing the deploy directory to the unprivileged account, and `hardening` for the warning that the app was restored to the previous mode. Clients that switch on step names should account for them.

### BC-0721-10: a broken image_url candidate no longer fails the whole request

> Old format supported until: 21.01.2027

**Before**

A `content` array could carry several `image_url` parts. If any one of them held
something other than an image — for example a Base64-encoded HTML error page
declared as `image/png` — the platform forwarded it to the model unchanged. The
model could not decode it and the entire request failed with `502` and code
`ai_provider_unavailable`, even when the remaining images were valid.

Separately, parts with an unsupported MIME type, malformed Base64, or above the
20 MiB limit were rejected with `400 invalid_image_payload` — also for the whole
request.

**After**

Before calling the model, the platform inspects the actual content of every
`image_url` part by its byte signature rather than its declared MIME type. A part
whose content is a web response (HTML, XML, JSON, an HTTP response) or does not
decode is replaced in place with the text placeholder
`[image unavailable: <reason>]`. The remaining images are processed normally and the
request succeeds.

Positions are preserved: the length of the `content` array does not change, so any
candidate numbering on your side stays correct.

Rejected parts are visible in the response — `warnings` gains an entry with code
`IMAGE_CONTENT_REJECTED`, and the `X-Image-Parts-Rejected` header carries their
count. For streaming responses the header arrives with the start of the stream.

`400` is now returned only for structural errors: a missing `url` field, a string
that is neither a URL nor a data URI, an unsupported scheme, and `http://` in
production.

**What integrators should do**

If your code relied on `400 invalid_image_payload` to detect a rejected image, read
`warnings` or the `X-Image-Parts-Rejected` header instead. The request now succeeds,
and no image is dropped silently — every substitution is reflected in the response.

Model-side failures changed too. Previously any non-2xx provider response arrived as
`502`; now the status reflects the cause:

- a model response of `400` or `422` → `400` with code `ai_provider_rejected`.
  Retrying such a request unchanged will not help;
- a model-side rate limit (`429`) → `429` with a `Retry-After` header. Retry it after
  the stated delay. A streaming response cannot carry the header, so the delay
  arrives as a `retryAfter` field in the error frame;
- a model-side timeout (`408`) → `503` with code `ai_provider_timeout`.

Responses `401`, `403` and `5xx` still arrive as `502`. This affects
[POST /v1/chat/completions](/docs/ai/chat/completions) and
[POST /v1/embeddings](/docs/ai/embeddings).

Model-side errors now additionally carry a `providerStatusCode` field — the raw
HTTP status of the model response. It tells a model-side `429` apart from the
platform's own rate-limit `429` (which has no such field): both keep the same
`rate_limit_exceeded` code so SDKs retry uniformly, and the new optional field is
the distinguisher.

Data URIs may now also carry parameters between the type and `;base64` —
`data:image/jpeg;name=photo.jpg;base64,...` is no longer rejected.

## 2026-07-20

### NEW-0720-1: Application responses now return the publication status

Application responses — [list](/docs/apps/list), [app data](/docs/apps/get), [create](/docs/apps/create), [publish](/docs/apps/publish), and [unpublish](/docs/apps/unpublish) — now carry two new fields: `catalogStatus` (`PRIVATE` / `PUBLISHED` / `UNPUBLISHED`) and `publishedAt` (the publication date, ISO 8601, or `null`). Previously the publication status could not be read via V1 — you had to guess from the `placements` array, which is unreliable: an unpublished application may keep its previously bound codes, and `PRIVATE` and `UNPUBLISHED` are indistinguishable by `placements`. The fields are additive — existing calls keep working.

### FIX-0720-2: chat rename no longer reports a false success to a non-participant

**Before**

[PATCH /v1/chats/:chatId](/docs/chats/management/rename) called by a portal administrator who is not a member of the chat returned `{ "success": true, "data": true }` even though the chat title never changed: Bitrix24 answered such a call with a false success. The response gave no way to tell it apart from a real rename, so an integrator treated the operation as done. The caller could not even read that chat.

**After**

Membership is now checked before the rename. If the caller is not a member, the response is `404 CHAT_NOT_FOUND_OR_NO_ACCESS` — the same as for any other non-participant — and no rename is attempted. The false success is gone. A rename by a member who has the rights works exactly as before.

### NEW-0720-3: streaming chat-completions now ends a stalled upstream response with an explicit error

When a streaming call ([POST /v1/chat/completions](/docs/ai/chat/streaming) with `stream: true`) receives headers but the upstream model then goes silent mid-response and sends no new data for longer than the idle window, the proxy now terminates the call and emits a terminal error frame before `data: [DONE]`:

`data: {"error":{"code":"stream_idle_timeout","type":"server_error","retryable":true,"retryAfter":<seconds>}}`

Previously such a call hung indefinitely (the agent stayed in a "receiving stream response" state). The error is retryable — read the stream through to `[DONE]` and retry, honoring `retryAfter`. Normal (non-stalled) streams and reasoning models that continuously emit thinking tokens are unaffected.

### NEW-0720-4: tasks: new timeSpentInLogs field

The `timeSpentInLogs` field (actual tracked time in seconds, summed from the time-tracking log) is now declared in the tasks schema — available in `select`, filter, and sort on [GET /v1/tasks](/docs/entities/tasks/list) and `POST /v1/tasks/search`, and present in [GET /v1/tasks/fields](/docs/entities/tasks/fields).

Previously the field was returned only when `select` contained BOTH spellings at once (`timeSpentInLogs` and `TIME_SPENT_IN_LOGS`); either one alone now works. The field is read-only — recorded via the time-tracking endpoint, not via a task update.

### FIX-0720-5: GET /v1/files/:id?include=folder now returns the folder

**Before**

[GET /v1/files/:id](/docs/entities/files/get) with `?include=folder` answered `200` but without the `_included` block, even though [GET /v1/files/fields](/docs/entities/files/fields) advertised `folder` as includable — the include silently did nothing.

**After**

`?include=folder` attaches the folder under `_included.folder`, as `/fields` promises.

**Impact on integrators**

No action required. Clients reading `_included.folder` now get the object instead of nothing.

### FIX-0720-6: /v1/me: infra block now accurate for server cap, provider id, and breakdown

**Before**

`GET /v1/me` returned `infra.limits.max: 3` regardless of the enforced per-key server cap; `infra.providers` could return an internal provider id (disagreeing with `GET /v1/infra/providers`) with possible duplicates; `infra.limits.breakdown` counted managed-bot VMs under `direct` instead of `bots`.

**After**

`infra.limits.max` reflects the enforced per-key server cap; `infra.providers` returns the public provider id, consistent with `GET /v1/infra/providers`, with duplicates removed; `infra.limits.breakdown` counts bot VMs under `bots`. No integration change is needed — the values are simply correct now.

### FIX-0720-7: deal-categories: unknown filter fields are rejected, sort by id respects direction

**Before**

`GET /v1/deal-categories` with a filter on an unknown field silently returned the WHOLE pipeline table with `200` — Bitrix24 ignores unknown filter keys of the legacy method and returns the entire list. And the sort `?sort=id&order=desc` ignored the direction, always returning the same order.

**After**

A filter on an unknown or unsupported field (as well as operator prefixes `>`/`>=`/`!`/… and operator objects) is rejected before the Bitrix24 call with `400 UNSUPPORTED_FILTER`; the message lists the filterable fields (`id`, `name`, `sort`). Exact-match and `$in` filters on those fields work as before. The sort `?sort=id` now correctly respects `asc`/`desc`.

### FIX-0720-8: openline-configs: unrecognised fields in a write body no longer vanish silently

**Before**

`POST /v1/openline-configs` (and `PATCH`) silently ignored unrecognised body fields — Bitrix24 drops unknown keys of the `imopenlines.config.*` method. A body of only unknown fields then created a configuration with default values and returned `200`.

**After**

If the body has NO known field, the request is rejected with `400 VALIDATION_ERROR` before the Bitrix24 call, and the message lists the unrecognised fields. If a known field is present but some fields are unrecognised, the write proceeds as before, and the response carries `meta.warnings` listing the ignored fields (previously they disappeared without a trace). Read-only fields (`id`, `queue`, `dateCreate` and others) in a write body are now rejected with `400 READONLY_FIELD` — previously they passed as "known" and could lead to a configuration created with default values.

### FIX-0720-9: GET /v1/{entity}/fields signals partial metadata on a Bitrix24 failure

**Before**

When the dynamic field fetch to Bitrix24 (`*.fields`) failed (rate limit, `QUERY_LIMIT_EXCEEDED`, queue timeout), the endpoint silently returned 200 with only the static schema fields — many of which have no human-readable `label`. The response looked complete, so a client could not tell it apart from a correct one and built non-deterministic field mappings.

**After**

On a field-fetch failure the response is still 200 with the static fields, but now carries `meta.warnings: [{ "code": "fields_partial", "message": "..." }]` so the client can detect the incomplete set and retry. The warning is an object `{ code, message }` — the same channel and shape as `meta.warnings` on list/search, so a single `warning.code` parser works across every endpoint. The failure is now also logged on the Vibe side.

## 2026-07-19

### BC-0719-1: Cowork/Code: tier lineup renamed — Free / Pro / Max / Ultra, Ultra price reduced

> Old format supported until: 18.07.2026

**Before**

The `tier` field in [GET /v1/cowork/state](/docs/cowork/state) and [GET /v1/cowork/me](/docs/cowork/me) responses (as well as `recommendation.upgrade.nextTier` and the `tiers[]` catalog) took the values `FREE`, `START`, `PRO`, `MAX`. The `MAX` tier (×20) cost 40,000 Ꝟ/mo.

**After**

The lineup is renamed with a shift: `START` → `PRO`, `PRO` → `MAX`, `MAX` → `ULTRA`; the value set is now `FREE`, `PRO`, `MAX`, `ULTRA`. Tier multipliers are unchanged: `PRO` ×1 (base), `MAX` ×5, `ULTRA` ×20; `FREE` is 5% of `PRO`. The `ULTRA` price (former `MAX`, ×20 volume) dropped from 40,000 to 20,000 Ꝟ/mo. Window volumes were recalibrated against real usage: the five-hour window doubled on every tier while monthly volumes were reduced; limits of an already-paid period do not change — the new values apply from the next renewal. The rename applies atomically at release time: the `START` value is no longer returned, and the `ULTRA` value was added. Clients branching on the string values of `tier` / `nextTier` must update their mapping and mind the meaning shift (`PRO` is now the base tier, `MAX` the middle one); clients that render the server-provided `multiplier` / `feeVibes` as-is keep working unchanged.

### FIX-0719-2: Cowork/Code tier prices on the international version now use the dollar scale

**Before**

On the international version the tier catalog in [GET /v1/cowork/state](/docs/cowork/state) returned prices on the Russian-version scale: `feeVibes` 2000 / 10000 / 20000 for Pro / Max / Ultra. At 1 Vibe credit = 1 US dollar this read as 2000–20000 dollars per month.

**After**

The tier catalog on the international version returns the dollar grid: Pro — `feeVibes: 20`, Max — `100`, Ultra — `200` per month; the three quota windows are scaled consistently, so tier capacity in requests is unchanged. The Russian version is not affected.

**Impact on integrators**

If your client reads `tiers[].feeVibes` from [GET /v1/cowork/state](/docs/cowork/state) on the international version, the values dropped ×100 and now match the price actually charged on activation. No code changes are required.

## 2026-07-18

### FIX-0718-1: deploy reuses the application's server instead of creating a duplicate

**Before**

`POST /v1/infra/servers` with `source` always created a new server even when the application that owns the key already had one — a second, idle server was billed. And `POST /v1/infra/servers/:id/deploy` returned `WRONG_KEY` when the calling key differed from the one that created the server (for example, an application has both a personal key and an authorization key).

**After**

When the calling key belongs to an application that already has a live server, `POST /v1/infra/servers` returns that server with `reused: true` instead of creating a new one. If the same request carries `source` and the reused server is a galaxy app (`kind: "GALAXY_APP"`) **that has no running container yet** (never deployed, or its previous deploy failed), the source is deployed to its own server right away (the response carries `reused: true` and `deploying: true`, and the server's status is `provisioning` while the build runs) — just like an ordinary create with `source`: poll `GET /v1/infra/servers/:id` until status is `running`, no second deploy call is needed. If the galaxy app is **already serving**, the response carries `reused: true` and `next: "deploy"` — deploy your source with a separate `POST /v1/infra/servers/:id/deploy` call (so the live container is untouched while the build runs). For a plain server, or a request without `source`, the response carries `reused: true` and `next: "deploy"` — deploy your source with a separate `POST /v1/infra/servers/:id/deploy` call. `POST /v1/infra/servers/:id/deploy` now accepts any key of the same application and deploys to its server.

**Integrator impact**

No changes required. Duplicate servers are no longer created. Previously a one-shot create with `source` against an application's existing server returned `next: "deploy"` and dropped the supplied `source` — now the source is deployed right away. Both a deploy and a status read (`GET /v1/infra/servers/:id`) of the application's server work under any of the application's keys, regardless of which one you call the API with.

### FIX-0718-2: DELETE /lock now releases a stuck lock even on a deleted server

**Before**

[`DELETE /v1/infra/servers/:id/lock`](/docs/infra/deploy/lock) returned `404 NOT_FOUND` when the server had been deleted — even though the operation lock remained in the platform's memory and kept the server busy. So the "previous server deleted, lock stuck, next deploy fails with `EXEC_BUSY`" scenario had no way out: such a lock could not be released via the API.

**After**

`DELETE /lock` releases a stuck lock even on a deleted server — as long as it still belongs to your API key (ownership remains the only check; the lock holds no data and no cloud resources). A successful call returns `200` with `data.released: true`. `404 NOT_FOUND` now means only "the server does not exist or belongs to another key".

### NEW-0718-3: connector app-install error codes are now also possible on cloud portals (phased rollout)

[POST /v1/apps](/docs/apps) on a cloud portal can now also install the application through the `vibecodeconnector` module and therefore return the same connector error codes that were previously only possible on self-hosted portals: `403 CONNECTOR_APP_INSTALL_FORBIDDEN` (the Bitrix24 portal administrator forbade the user from installing applications), `409 CONNECTOR_MODULE_NOT_INSTALLED` (the `vibecodeconnector` module is not installed on the portal), and `502 CONNECTOR_APP_INSTALL_FAILED` (other install failures). The change is additive: the successful-install response is unchanged, and the rollout is phased — on most cloud portals the install path is unchanged for now. Clients that already handle these codes on self-hosted portals need no change; clients that did not should add handling.

## 2026-07-17

### FIX-0717-1: deploying to a sleeping galaxy host now answers before client timeouts

**Before**

[POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy) on a sleeping shared host held the connection open for up to ~6.5 minutes while the host woke. HTTP clients with a typical response-headers timeout (~300 seconds — the Node fetch default) dropped the connection before the platform answered: the deploy surfaced as an opaque `fetch failed` network error with no error code and no recovery steps. A failed wake also put the host back to sleep, so every retry restarted the host boot from zero.

**After**

The platform wakes the host in the background and waits for it to connect for at most ~4 minutes. If the host connects in time, the deploy completes in one call as before. If not, the platform immediately returns a retryable `502 GALAXY_HOST_UNREACHABLE` with a `hint` (re-send the same request in 1-2 minutes, do not delete the slot), and the host keeps booting in the background — a re-sent deploy joins the boot already in progress instead of restarting it. `deployment.galaxyApp._rules` and `deployment.standalone._rules` (GET /v1/me) now recommend an HTTP client timeout of at least 690 seconds — strictly above the platform's 660-second window.

**Impact on integrators**

No request changes are required. If a deploy to a sleeping galaxy host previously ended for you in a network error with no platform response, you will now get either a success or a 502 with retry instructions.

### NEW-0717-2: server user search explains an empty list

The [GET /v1/infra/servers/:id/b24-users](/docs/infra/access/b24-users) endpoint now returns an additional `hint` field when the list is empty because there is no Bitrix24 access — the app is not yet authorized on the account, or the key was revoked. Existing calls are unaffected: the field is additive and absent on a successful response.

### NEW-0717-3: the catalog field references now report nullable fields

The [GET /v1/catalog-prices/fields](/docs/entities/catalog-prices/fields), [GET /v1/catalog-sections/fields](/docs/entities/catalog-sections/fields) and [GET /v1/catalog-products/fields](/docs/entities/catalog-products/fields) references now add `"nullable": true` to fields that can return `null`: for prices these are `quantityFrom`, `quantityTo` and `extraId`, for sections — `iblockSectionId`, `xmlId`, `code` and `description`, for products — `iblockSectionId`, `code`, `weight`, `purchasingPrice`, `purchasingCurrency`, `quantity` and `quantityReserved`. The same flag arrives in `data.entities[].fieldsDetailed` of the [GET /v1/guide](/docs/keys-auth/guide) response, which an OAuth-application key reads without a session, and in the machine-readable [GET /v1/openapi.json](/docs/cli) spec such fields are described with a union type like `["number", "null"]`. A client building a typed model from the reference now gets the correct nullability and no longer breaks on the first `null`. The field set, the types and the response values are unchanged.

### NEW-0717-4: EXEC_BUSY tells you when to retry

The `409 EXEC_BUSY` response (another operation holds the server lock) on [POST /v1/infra/servers/:id/exec](/docs/infra/deploy/exec) and `POST /v1/infra/servers/:id/deploy` now carries a retry hint: the `Retry-After` response header (in seconds) plus two new error-body fields — `retryable: true` and `retryAfter` (in seconds). The `retryAfter` value is a short poll interval (keep retrying with it until the lock clears), not the full time until the lock auto-expires; the full upper bound stays in `error.hint.autoExpiresInSeconds`. The change is additive: the code, `message`, and `hint` are unchanged, so clients matching on `error.code` keep working without changes.

### FIX-0717-5: deploying a galaxy app with an oversized archive returns 413, not "host unreachable"

**Before**

[POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy) with a `source.content` larger than the upload limit returned `502 GALAXY_HOST_UNREACHABLE` — a message that blamed an unreachable host and advised retrying "once it reconnects". The host was fully reachable, and re-sending the same archive failed identically: the integrator was stuck in an endless loop of useless retries.

**After**

The same case now returns `413 GALAXY_UPLOAD_TOO_LARGE` with a structured `error.hint`. The cause is deterministic (the archive is too large), not transient, so re-sending it unchanged will not help. The `hint` advises shrinking the archive — exclude `node_modules`, `.git` and build artifacts (the platform installs dependencies on the host). A galaxy application accepts the inline `source.content` only (`source.url` is rejected with `400 GALAXY_DEPLOY_CONTENT_ONLY`), so shrinking the archive is the only recovery. A separate adjacent case: a request body exceeding the platform's hard outer limit (500 MB on the base64 body ≈ ~375 MB binary archive) is now rejected at the edge with a coded `413 PAYLOAD_TOO_LARGE` (on the Vibe-REST `/v1/` routes — deploy/upload/create; OpenAI-compatible AI routes return their error in their own envelope) instead of raw HTML — previously the client got an undecodable response.

**Impact on integrators**

No change is required: successful deploys are unaffected. Clients that branched on the error code for this failure now see an honest `413 GALAXY_UPLOAD_TOO_LARGE` instead of the misleading `502 GALAXY_HOST_UNREACHABLE` — the latter remains for a genuine tunnel drop during the build.

### NEW-0717-6: displayName and description on deploy set the Bitrix24 catalog card

[POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy) accepted two new optional body fields — `displayName` and `description`. [POST /v1/infra/servers](/docs/infra/servers/create) (server creation) accepted an optional `description`. The values become the app's name and description on the Bitrix24 catalog card. If a deploy omits `displayName` and `description`, the response includes a `warnings: string[]` entry nudging the client to set them; the one-shot galaxy create-and-deploy (a `POST /v1/infra/servers` body carrying `source`) likewise returns `warnings` in its 201 response when the fields are absent. Backward-compatible — requests without the new fields keep working as before.

### FIX-0717-7: galaxy deploy returns 503 on transient database overload

**Before**

On a brief database connection-pool exhaustion during a galaxy app deploy, [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy) returned a generic `502 GALAXY_APP_DEPLOY_FAILED` — the same code as a real build failure. A client could not tell a transient overload from a terminal error and often treated the response as final.

**After**

A transient database overload is now shed as `503 POOL_EXHAUSTED` with a `Retry-After` header (seconds to wait before retrying). A genuine build failure stays `502 GALAXY_APP_DEPLOY_FAILED`.

**Integrator impact**

No action required. If your client retries, a `503` now carries an explicit backoff signal via `Retry-After` instead of an opaque `502`.

### FIX-0717-8: filtering and sorting of the task comment list work on both cards

**Before**

A [GET /v1/tasks/:taskId/comments](/docs/entities/task-comments/list) request with a `filter` parameter or with a sort other than `ID` on a Bitrix24 account with the new task card returned `200` and an empty list, even when the task did have comments. No error arrived, so there was no way to tell "nothing matched the filter" from "the filter did not work".

**After**

Such a request returns the matching comments. Filtering and sorting work by the fields `ID`, `AUTHOR_ID` and `POST_DATE`, and in a filter the field name may carry a `!`, `>`, `>=`, `<` or `<=` prefix. Filtering by `AUTHOR_NAME` and sorting by `AUTHOR_NAME` or `AUTHOR_EMAIL` on the new card answer with `400` and the code `UNSUPPORTED_FILTER_FIELD` or `UNSUPPORTED_SORT_FIELD` and point at `AUTHOR_ID`, on the old card those fields are still accepted. An `offset` parameter is added — it is honoured on the new card for a request with `filter` or a sort other than `ID`, and ignored on the other read paths. The `INVALID_FILTER` code now also arrives when `filter` is a scalar or an empty array instead of an object (`0`, `false`, `""`, `[]`), the value of the field `ID` or `AUTHOR_ID` is not a number, the value of `POST_DATE` does not parse as a date, or a field value is an object or an array instead of a scalar. A `truncated` field with the value `true` is added to `meta` — the scan window of the history was exhausted, and some comments stayed beyond its edge.

**Impact on integrators**

Nothing to change: a request that used to hand back an empty list starts handing back data. Mind three edges. First — filtering by `AUTHOR_NAME` and sorting by `AUTHOR_NAME` or `AUTHOR_EMAIL` on a Bitrix24 account with the new card now answer with `400` instead of an empty `200`, move such a request to `AUTHOR_ID`, an employee identifier by name comes from `GET /v1/users`. Second — `meta.total` for a filtered request against the new card counts the matching comments within the scanned window rather than across the whole task history, and with `meta.truncated: true` it is an incomplete number. Third — the value of `POST_DATE` on the new card is compared against `createdAt` in UTC, so a result on a day boundary may differ from the selection on the old card. On Bitrix24 accounts with the old card the behaviour is unchanged.

## 2026-07-16

### BC-0716-1: json_object on a reasoning model recovers JSON; the 422 body is refined

> Old format supported until: 15.01.2027

**Before**

A [POST /v1/chat/completions](/docs/ai/chat/completions) request with a `json_object` `response_format` on a reasoning model (e.g. `bitrix/bitrixgpt-5.5-agent`) consistently returned `422 structured_output_truncated`, even when the model finished on its own (`finish_reason: "stop"`) and placed a complete valid JSON into the service `reasoning_content` channel — the answer was lost. Every such error body carried `error.suggestedMaxTokens` and `error.param`, and the text claimed "finish_reason=length" regardless of the real stop reason.

**After**

If a model on `json_object` finished on its own and valid JSON sits in `reasoning_content`, the platform recovers it and returns `200` with that JSON in `content` (in streaming mode — as a `content` chunk before the terminal `finish_reason` chunk). The `422` body is now truthful: `error.suggestedMaxTokens` and `error.param` are present **only** on a genuine truncation (`finish_reason: "length"`); on any other stop reason (`stop`, etc.) those fields are **omitted** and the text names the actual `finish_reason`. `json_schema` behaviour is unchanged — there the strict schema is enforced model-side.

**What to do**

Nothing, if you handle `422` by `error.code`. If your code **unconditionally** reads `error.suggestedMaxTokens` or `error.param` on a `structured_output_truncated` error, make the read optional: those fields are now absent when `finish_reason !== "length"`. Streaming clients using `response_format` should accumulate `content` across all deltas up to `data: [DONE]`.

### FIX-0716-2: env sent as a file in a multipart deploy is no longer silently ignored

**Before**

In `multipart/form-data` for [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy), an `env` field sent as a file or Blob was silently ignored — the deploy reported success but the app started without environment variables.

**After**

Such a request returns 400 with code `VALIDATION_ERROR` and a hint to send `env` as a text field containing a JSON string.

**Impact on integrators**

The correct approach (a text `env` field with a value like `{"KEY":"value"}`) is unaffected. Anyone who sent `env` as a file or Blob now gets an explicit error instead of a false success.

### FIX-0716-3: large source.content deploys no longer fail with Gateway HTTP 413

**Before**

On some accounts [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy) with `source.content` (a base64 archive) failed at the `download` step with `{ "code": "DEPLOY_FAILED", "message": "Gateway HTTP 413", "step": "download" }` whenever the base64 body exceeded ~1 MB (about 768 KB of raw `tar.gz`) — despite the documented 500 MB limit. The workaround was uploading via `source.url`.

**After**

The 500 MB inline-upload limit (`source.content` and `multipart`) applies on all accounts. `source.url` keeps working as before.

### BC-0716-4: POST /v1/infra/servers rejects unknown body fields

> Old format supported until: 15.01.2027

**Before**

An unknown field in the request body was silently ignored. A request with `deployMode: "STANDALONE"` (a non-existent field) returned `201` and created a galaxy app instead of the expected dedicated server — the real field is `placement: "dedicated"`.

**After**

[POST /v1/infra/servers](/docs/infra/servers/create) rejects a body that carries an unknown field with `400 UNKNOWN_PARAM`. `details.unknownFields` lists the extra fields, `details.suggestions` proposes the correct name (`deployMode` → `placement`), and `details.validParams` is the full list of accepted fields.

**What integrators should do**

Remove fields that are not in the create parameter list, or fix the typo using `details.suggestions`. The placement model is set via the `placement` field (`auto` by default, `dedicated` for a dedicated virtual machine).

### NEW-0716-5: /v1/sites/fields now describes the allowed values of the type field

`GET /v1/sites/fields` now returns the allowed values of the `type` field in `type.enum` with labels: `PAGE` (landing), `STORE` (online store), `KNOWLEDGE` (Knowledge Base 2.0), plus `VIBE` (a site from the constructor) and `SMN` (a link with the "Site Management" module). The `VIBE` and `SMN` values appear in responses only and are read-only — a site of that type cannot be created through the API.

### NEW-0716-6: Task favorite and pin without edit permission

Four new endpoints for per-user task actions that Bitrix24 allows with read access only (not edit): `POST /v1/tasks/:taskId/favorite` adds the task to favorites, `DELETE /v1/tasks/:taskId/favorite` removes it, `POST /v1/tasks/:taskId/pin` pins the task in the current user's task list, and `DELETE /v1/tasks/:taskId/pin` unpins it. Previously the only way to change a task was `PATCH /v1/tasks/:id`, which requires edit permission and returned "No access to edit the task", so an allowed per-user action was unreachable.

### NEW-0716-7: preemptible-plan advisory in scheduled-wake responses

The [POST /v1/infra/servers/:id/wake-schedules](/docs/infra/wake-schedules/create) and [PATCH /v1/infra/servers/:id/wake-schedules/:scheduleId](/docs/infra/wake-schedules/update) responses now also carry two additional top-level fields: `preemptibleAdvisoryCode` and `preemptibleAdvisory`. When the server runs on a preemptible plan, `preemptibleAdvisoryCode` is `"PREEMPTIBLE_BEST_EFFORT"` and `preemptibleAdvisory` is a short English explanation of the same fact: waking such a server by the window's moment is not guaranteed, and the window may be skipped when no free capacity is available. For a server on a non-preemptible plan, both fields are `null`. The advisory does not block window creation or updates — it follows the same non-blocking pattern already used by the `tzWarning`/`tzWarningCode` fields. Existing integrations that don't read the new fields keep working unchanged.

## 2026-07-14

### NEW-0714-1: OpenAPI spec: 3.1 validity, field semantics, and a scope slice

**Before**

The machine-readable spec [GET /v1/openapi.json](/docs/cli) carried the 3.0 `nullable` keyword (invalid under 3.1), left `{entityTypeId}` undeclared on batch/aggregate/fields/products, shipped no property descriptions/allowed-values/examples, described only `vibe_app_` keys, and served one monolith.

**After**

The spec is valid under OpenAPI 3.1: nullable fields use the union type `["<type>","null"]` and every path parameter is declared. Properties now carry `title`/`description`, allowed values (`x-enumValues` plus an in-description decode), and examples. Security schemes name all three key families (`vibe_api_`/`vibe_app_`/`vibe_live_`), every operation carries `x-required-scope`, and the root exposes an `x-scopes` catalog. Root `tags`/`externalDocs`, a `webhooks` block for bot events, and a multifield `anyOf` were added. `GET /v1/openapi.json?scope=<scope>` (e.g. `?scope=crm`) returns a single-scope slice so it fits an agent context window. Pointers to the spec were added to `/v1/me` and `/v1/guide`. A full port of per-operation curl examples is a separate follow-up.

### FIX-0714-2: Generic 5xx errors on AI endpoints now use the OpenAI-compatible envelope

[AI endpoints](/docs/ai) (`/v1/ai/*`, `/v1/models`, `/v1/chat/*`, `/v1/audio/*`) are documented with an OpenAI-compatible error format. Pool-exhaustion errors already followed this, but a generic (unexpected) `5xx` on these endpoints returned the plain V1 envelope instead.

**Before**

A generic `5xx` on an AI endpoint: `{"success": false, "error": {"code": "...", "message": "..."}}` — not the shape OpenAI SDK clients expect for these paths.

**After**

The same case now returns `{"error": {"message": "...", "type": "...", "code": "..."}}` — one envelope for every error on AI endpoints, including generic `5xx`.

**Impact on integrators**

Clients on the OpenAI SDK that already read `error.type`/`error.code` (the standard path for these endpoints) see no change. Code that expected top-level `success`/`error.code` specifically on a generic `5xx` from an AI endpoint should switch to `error.type`/`error.code`.

### FIX-0714-3: `versions` in the app source-history list is capped at 500 entries

`GET /v1/apps/:id/sources` returned the entire version history with no limit — for apps with very long histories this was an unbounded read.

**Before**

`data.versions` — the entire version list with no size limit; `data.totalVersions` always equaled `data.versions.length`.

**After**

`data.versions` holds at most the 500 most recent versions (by `savedAt`, descending). `data.totalVersions` and `data.totalSizeBytes` are still computed over the full set — aggregate accuracy doesn't depend on the cap.

**Impact on integrators**

For apps with up to 500 versions of history, behavior is unchanged. For apps with more than 500 versions, `data.versions.length` may now be smaller than `data.totalVersions` — code that relied on them being equal should use `data.totalVersions`/`data.totalSizeBytes` for aggregates and not treat `data.versions` as the complete list.

### FIX-0714-4: Telephony: `userId`/`duration` are now genuinely validated, not just checked for truthy

`POST /v1/calls/register`, `/v1/calls/:callId/show`, `/v1/calls/:callId/hide`, `/v1/calls/:callId/finish` accepted `userId` (and `duration` on `finish`) without checking its type or shape — any truthy value (for example the string `"abc"` or an object) was forwarded to Bitrix24 and failed there with an opaque upstream error.

**Before**

`{"userId": "abc"}` (or any other truthy value that wasn't a positive integer) passed validation and was sent to Bitrix24; the `Required: userId (number)` error only appeared for a fully empty/falsy value.

**After**

`userId` is accepted as a positive integer or a numeric string (`"42"`), otherwise a clean `400 MISSING_PARAMS` with the refined text `Required: userId (positive integer)` (plus `phoneNumber` for `register`). `duration` on `finish` follows the same rule — a non-negative number or a numeric string, otherwise `400`.

**Impact on integrators**

Correct calls (`userId` as a number or numeric string) are unchanged. Calls that previously "got through" with an invalid `userId`/`duration` (not a number, not a numeric string) now get an explicit `400` instead of an opaque Bitrix24-side error.

### FIX-0714-5: Creating a comment on a missing task — an explicit error instead of a false success

[POST /v1/tasks/:taskId/comments](/docs/entities/task-comments/create) and its batch counterpart [POST /v1/tasks/:taskId/comments/batch](/docs/entities/task-comments/comments-batch) (`action: create`) call Bitrix24 to create a comment on the legacy task card. If the task doesn't exist or isn't accessible to the key, Bitrix24 doesn't create the comment and returns no identifier.

**Before**

Both endpoints responded with success and an empty identifier — the single call as `201 {"success": true, "data": {"id": null}}`, the batch item as `{"success": true, "id": null}`. No comment was created, but the integrator couldn't distinguish this from the normal case.

**After**

The single call now returns `404 TASK_NOT_FOUND`. The batch variant marks the corresponding item as `{"success": false, "error": "TASK_NOT_FOUND"}` without cancelling the rest of the batch. A separate, unrelated case is unchanged: on the new task card the comment goes through chat, and if the system couldn't recover its id via a follow-up search, the response is still `success: true` with `id: null` — the comment was genuinely created in that case.

**Impact on integrators**

Code that checks `data.id` / `data[i].id` for `null` as an error signal keeps working unchanged and now gets a more precise error code. Code that relied on a silent success with `id: null` for an inaccessible task should handle `404` / `TASK_NOT_FOUND` explicitly.

### FIX-0714-6: Rate limit for `/v1/search`, `/v1/research`, `/v1/batch` is now per-portal

**Before**

The limits (`/v1/search` 60/min, `/v1/research` 20/min, `/v1/batch` 30/min) were effectively keyed by source IP, not by portal. A portal using multiple API keys (or several portals behind one shared egress IP) could exceed the documented cap, and the limit was bypassable by IP rotation.

**After**

The limit is now keyed **per portal**: all API keys of one portal share a single bucket (60 / 20 / 30 requests per minute respectively). The documented per-tenant cap is now enforced correctly and cannot be bypassed by using more keys or rotating IPs.

**Integrator impact**

If your portal spread `/v1/search` / `/v1/research` / `/v1/batch` traffic across several API keys, the effective ceiling is now the single portal limit, not the sum across keys. On exceeding it you get `429` with a `Retry-After` header (as before).

### FIX-0714-7: Calendar: working section batch-delete, clean update errors, no leaked sync fields

**Before**

- `POST /v1/calendar-sections/batch` with `action: "delete"` had no channel for the `type`/`ownerId` that `calendar.section.delete` mandates — every item failed on both platforms.
- `PATCH /v1/calendar-sections/{id}` without `type`/`ownerId`/`name` forwarded to Bitrix24 and returned a raw `422` leaking the internal method name.
- `GET /v1/calendar-sections` returned the undocumented raw Bitrix24 fields `GAPI_CALENDAR_ID`, `CAL_DAV_CON`, `SYNC_TOKEN`, `PAGE_TOKEN`, `EXTERNAL_TYPE` (three of them sync tokens).
- `GET /v1/calendar-events` and `GET /v1/calendar-events/{id}` leaked the internal `attendeesEntityList` field — the schema tried to strip it but no-op'd on a key-casing mismatch.

**After**

- Section batch-delete reads `type`/`ownerId` from the body alongside `ids` and threads them into every delete command. A missing anchor is a clean `400 MISSING_REQUIRED_PARAMS` before the Bitrix24 call.
- Section partial-update requires the `type`/`ownerId`/`name` anchors (sections have no get-by-id to backfill) — a clean `400`, no raw `422`, no leaked method name.
- Both calendar read paths strip the listed internal/sync fields from the response.

**Affected endpoints:**

- [`POST /v1/calendar-sections/batch`](/docs/entities/calendar-sections/create)
- [`PATCH /v1/calendar-sections/{id}`](/docs/entities/calendar-sections/update)
- [`GET /v1/calendar-sections`](/docs/entities/calendar-sections/list)
- [`GET /v1/calendar-events`](/docs/entities/calendar-events/list)

### FIX-0714-8: PATCH catalog-product-properties works again (was an inescapable catch-22)

**Before**

Updating a product property was impossible under any body: `PATCH` without `iblockId` → `422` ("Required fields: iblockId" — B24 mandates it on every update), and `PATCH` **with** `iblockId` → `400 READONLY_FIELD` (create-only field). The entire UPDATE verb was dead — no field could be changed after create.

**After**

`iblockId` is now carried over automatically from the existing record (a pre-fetch, like `catalog-sections`), so `PATCH {name:"…"}` reaches B24 with the required `iblockId` and returns `200`. You still don't send `iblockId` in the body (and it is still rejected as read-only if you do) — the service supplies it.

**Integrator impact**

If your product-property `PATCH` always failed `422`/`400`, now send only the fields you're changing (`PATCH {name:"…"}`); `iblockId` is not required.

### FIX-0714-9: entityTypeId validation: junk forms → 400 instead of silent truncation

**Before**

Five surfaces parsed `entityTypeId` (the smart-process / dynamic-entity TYPE selector) leniently — with unanchored `parseInt` or coercing `Number()` — and a junk form silently turned into a DIFFERENT (within-portal) entity type:

- The path `entityTypeId` (`/v1/items/:entityTypeId/...`, `/v1/categories/:entityTypeId/...` — CRUD and `/aggregate`): `GET /v1/items/1058abc` truncated to `1058`, `1e3` → `1`, `1.5` → `1`.
- Global `POST /v1/batch`: `params.entityTypeId` via `Number()` accepted fractions (`1.5`), hex (`'0x10'` → 16), overflow to `Infinity`, plus the array form `[1058]` → 1058.
- `/v1/items/:entityTypeId/userfields/*`: its own parser — `2abc` resolved the userfields of type `2`.
- `POST /v1/smart-processes/batch`: `ids: ['1030abc']` truncated to `1030` — delete/update silently ran against a REAL, DIFFERENT type; a fractional number (`1030.5`) passed too.
- `POST /v1/triggers/fire` (`entityType="item"`): `entityTypeId: '1038abc'` → the automation trigger fired against type `1038`.

**After**

All five surfaces require the canonical positive-integer form (`/^[1-9]\d*$/` for strings, `Number.isInteger` for numbers): any other form → `400` with each surface's existing error code (`INVALID_DYNAMIC_PARAM` / `INVALID_ENTITY_TYPE_ID` / `BATCH_ITEM_VALIDATION` / `MISSING_PARAMS`) BEFORE any Bitrix24 call. Aggregate now shares the CRUD routes' validator instead of an inline copy.

**Integrator impact**

Forms that previously coerced to a correct value and were served — `007` → 7, `%20`-spaces, `+2` → 2, the array form `[1058]` in batch — are now rejected with `400` as well: the value must be a canonical integer with no prefixes, suffixes or leading zeros. A boolean in batch was rejected before too (it coerced into a reserved type); only its error code changes — now `INVALID_DYNAMIC_PARAM`. Correct calls are unchanged.

### FIX-0714-10: Reopening feedback clears the resolution fields

**Before**

`PATCH /v1/feedback/:id` (and the admin endpoint `PATCH /api/platform/feedback/:id`, which the admin UI reopens through) moving a ticket back to an active status (`NEW`, `REVIEWING`, `AWAITING_USER`, `NEEDS_REVIEW`) from `RESOLVED`/`WITHDRAWN` did not reset `resolvedAt`, `resolvedBy`, or `resolution` — they lingered from the prior close, so a reopened ticket looked both active and resolved.

**After**

Moving a ticket **out of** `RESOLVED`/`WITHDRAWN` into an active status clears `resolvedAt`, `resolvedBy`, and `resolution`. `RESOLVED`/`WITHDRAWN` still set the resolution stamp; `ARCHIVED` leaves the fields untouched (archiving preserves the resolution history). A plain transition between active statuses (e.g. `AWAITING_USER → REVIEWING`) leaves the fields alone — on active tickets `resolution` mirrors the last team comment. An explicit `resolution` in the same request still wins over the clear.

**Integrator impact**

If you read `resolvedAt`/`resolution` on a reopened ticket and got the prior close's values, they are now `null` for an active ticket.

### FIX-0714-11: `INVALID_JSON_BODY` no longer quotes the engine parser text

**Before**

Six route groups (`/api/billing/*`, `/v1/apps*`, `/v1/bots*`, `/v1/keys*`, `/v1/note*`, `/v1/infra/servers/*` deploy/exec/upload) answered malformed JSON with `400` and a message like `Invalid JSON: Unexpected token } in JSON at position 41` — raw V8 engine text (a runtime fingerprint and an implementation detail). The deploy/exec/upload group set no error code at all.

**After**

All six now return the single static message `Request body is not valid JSON.` — matching `/v1/<entities>` (the same class is closed there by a separate fix). On V1 surfaces the code is `INVALID_JSON_BODY` (deploy/exec/upload now sets it too); the `400` status is unchanged.

**Integrator impact**

If your code parsed the message text (e.g., extracted the error position), rely on the `INVALID_JSON_BODY` code instead; the position is no longer reported.

### FIX-0714-12: Quote GET response returns amount, currency, and dates again (were null)

**Before**

Reading a quote (`GET`/list/search `/v1/quotes`) returned `null` for `amount`, `currency`, `beginDate`, `closeDate` — the values leaked only under the raw Bitrix24 keys (`opportunity`, `currencyId`, `begindate`, `closedate`). Writes worked, but the READ projection dropped every aliased field: **a quote's total and currency were 100% invisible via the documented API**.

**After**

The READ branch now reverse-maps the declared aliases (mirroring the write mapping): `opportunity → amount`, `currencyId → currency`, `begindate → beginDate`, `closedate → closeDate`, with type coercion. The raw Bitrix24 keys no longer appear in the response.

**Integrator impact**

If you read a quote's `amount`/`currency` and got `null`, they are now populated. Code that worked around it by reading the raw `opportunity`/`currencyId` from the response will no longer find them there — switch to the documented `amount`/`currency`.

### FIX-0714-13: Write-path validation: phantom checklist → 404, garbage types and junk `:id` → 400

**Before**

- `POST /v1/tasks/:taskId/checklist` against a nonexistent task returned `201` with a plausible `id`, yet nothing was created (the item was never GETtable).
- `POST /v1/warehouses` accepted non-string `title`/`address` (numbers, objects) and forwarded them to Bitrix24 with unpredictable results; `POST /v1/doc-templates` likewise passed non-string `name`/`region` and non-numeric `numeratorId` through.
- A non-numeric `:id` on entities with a typed numeric id (`GET/PATCH/DELETE /v1/quotes/abc`, `/v1/deals/1.5`, `/v1/leads/1e3`) went to Bitrix24 verbatim — returning an opaque B24 error instead of a clear code. On smart-processes `12abc` was `parseInt`-truncated to `12` and hit the WRONG type.

**After**

- Checklist: the parent task is verified before the item is created; a missing (or invisible-to-the-key) task → `404 TASK_NOT_FOUND`.
- Warehouses and document templates: a wrong-typed value → `400 INVALID_PARAMS` with no Bitrix24 call (a numeric string in `numeratorId` is still accepted).
- Entities with an explicitly typed numeric id: a non-canonical-integer `:id` → `400 INVALID_PARAMS` before any Bitrix24 call (id `0` — the main deal funnel of `categories` — stays valid). Smart-processes keep `INVALID_ENTITY_TYPE_ID` and now reject `12abc` on GET/PATCH/DELETE instead of truncating it to `12`. Entities whose id type is not declared in the schema keep their prior pass-through behavior.

Affected endpoints: [POST /v1/tasks/:taskId/checklist](/docs/entities/tasks/checklist), [POST /v1/warehouses](/docs/entities/warehouses/create), [POST /v1/doc-templates](/docs/entities/doc-templates/create) + GET/PATCH/DELETE on entities with a typed numeric id.

**Integrator impact**

If your code relied on the phantom checklist `201` or sent garbage-typed values hoping for the best, you will now get an explicit `4xx` with a code. Correct calls are unchanged.

### FIX-0714-14: Five silent false-success / hint defects: an honest response instead of a fake success

**Before**

- `PATCH /v1/userfields/{entity}/{id}` with `label` was a silent no-op on update: `200`, but the field label never changed (Bitrix24 `crm.*.userfield.update` ignores `LABEL`).
- `POST /v1/humanresources/nodes/{id}` with `parentId` faked a successful reparent: `name` was applied, `parentId` was silently ignored, and the response echoed the stale parent.
- `POST /v1/chats/messages/bulk` did not resolve the `dialogId: "me"` alias inside the bulk loop (single routes do) → messages went to the wrong dialog.
- `POST /v1/bots/{botId}/chats/{dialogId}/users` — the `USERS_NOT_ADDED` safety-net was dead code (it never matched the v2 method's response shape) → a failed add passed as success.
- `crm.item.list` errors received an irrelevant "Maximum 50 records…" hint even when the cause was something else (e.g. "entity type does not exist").

**After**

- `label` on update fans out to the real `EDIT_FORM_LABEL`/`LIST_COLUMN_LABEL`/`LIST_FILTER_LABEL` params — the label actually changes.
- `parentId` and `type` are now create-only: on `PATCH` they are rejected with `400` (reparent via `POST /v1/humanresources/nodes/{id}/move`) instead of faking success.
- Bulk message reads resolve `dialogId: "me"` per item, like the single routes.
- The bot-chat add safety-net fires again: users that were not added come back in `warning.USERS_NOT_ADDED`.
- Known-limitation hints are gated on the error message's relevance, not the method name alone.

**Affected endpoints:**

- [`PATCH /v1/userfields/{entity}/{id}`](/docs/userfields/crm/update)
- [`PATCH /v1/humanresources/nodes/{id}`](/docs/humanresources/nodes/update)
- [`POST /v1/chats/messages/bulk`](/docs/chats)
- [`POST /v1/bots/{botId}/chats/{dialogId}/users`](/docs/bots)

### FIX-0714-15: GET /v1/task-time now honestly returns more than 50 rows when limit>50

**Before**

[GET /v1/task-time](/docs/entities/tasks/time) with a `limit` above 50 returned only 50 rows even though `meta.limit` echoed the requested value and `meta.hasMore` could mislead. A client paginating with a step above 50 silently lost rows.

**After**

The request now returns up to `limit` rows (max 500), collected page-by-page on the backend; `meta.total` and `meta.hasMore` match the window actually returned. With a `limit` above 50, `offset` now points at the correct position instead of shifting onto the first pages.

### NEW-0714-16: GET /v1/companies/fields and catalog-prices system fields now carry label and description

[GET /v1/companies/fields](/docs/entities/companies/fields) now returns human-readable `label` and `description` for every company field. [GET /v1/catalog-prices/fields](/docs/entities/catalog-prices/fields) adds the same metadata to the system fields `extraId`, `priceScale` and `timestampX`. Labels are localized to the segment language. A field's meaning can now be read programmatically from the response instead of cross-referencing static documentation.

### FIX-0714-17: /stop and /reboot distinguish a missing server from a wrong status

**Before**

[POST /v1/infra/servers/:id/stop](/docs/infra/lifecycle/stop) and [POST /v1/infra/servers/:id/reboot](/docs/infra/lifecycle/reboot) on a server that was not in `running` status (e.g. sleeping) returned a flat `404 NOT_FOUND` reading "Running server not found" — from which you could not tell the server still existed, so an agent concluded it had been deleted.

**After**

Both routes now behave like [/start](/docs/infra/lifecycle/start) and [/wake](/docs/infra/lifecycle/wake): `404 SERVER_NOT_FOUND` only when no server with this `id` exists; `422 SERVER_WRONG_STATE` when the server exists but is not in `running` status. `error.currentState` carries the current state and `error.availableActions` lists the actions available now (`wake`/`start`/`repair`/`delete`).

### FIX-0714-18: clearer /fields error for task comments and task time entries

**Before**

[GET /v1/tasks/:taskId/comments/fields](/docs/entities/task-comments) returned a confusing `400 INVALID_PARAMS` reading "taskId and id must be positive integers" (you asked about fields, the answer was about an id), and [GET /v1/tasks/:taskId/time/fields](/docs/entities/tasks/time) leaked a raw Bitrix24 error exposing an internal PHP method and an HTML tag.

**After**

Both entities recognise the `fields` segment and return a clear `400 WRONG_PATH`: they have no `/fields` method (the field schema is documented in `/v1/guide`), and the message lists the valid routes. A non-numeric or fractional id on the by-id routes is now rejected as `400 INVALID_PARAMS` before the Bitrix24 call — no internal error leaks out.

### FIX-0714-19: `/v1/ai/credentials*` rate limits are now per-portal; `CREDENTIAL_NOT_FOUND` carries a hint

**Before**

- The BYOK route limits (`POST /v1/ai/credentials`, `/:id/test`, `/:id/fetch-models` — 10/min; `/:id/models` add/delete — 30/min) were effectively keyed by source IP: credential probing was bypassable by IP rotation, and tenants behind one shared egress IP shared a bucket. `PATCH /:id` (which verifies the key upstream when `credentials` is sent — the same oracle as `/:id/test`) had no limit at all.
- The `404 CREDENTIAL_NOT_FOUND` from `/v1/search` and `/v1/research` carried only the provider slug — no pointer to how to configure a key.

**After**

- The limit is keyed **per portal**: all API keys of one portal share a single bucket; IP rotation and key count no longer affect the cap. On exceeding it you get `429` with `Retry-After`. Additionally `PATCH /:id` (which verifies the key upstream, like `/:id/test`) previously had NO limit — it is now also 10/min per portal.
- The `CREDENTIAL_NOT_FOUND` response now includes a `hint` field with the exact recipe: `POST /v1/search/credentials {provider, apiKey}`; provider list — `GET /v1/search/providers`.

### FIX-0714-20: PATCH for bizproc templates, robots and activities via /v1 now applies changes

**Before**

`PATCH /v1/bizproc-templates/:id`, `/v1/bizproc-robots/:code` and `/v1/bizproc-activities/:code` with metadata fields (`name`, `description`, `autoExecute`) returned `422 BITRIX_ERROR "No fields to update."` — updates were impossible (the same in batch requests). An `autoExecute` value sent as a number was additionally rejected as `Incorrect field AUTO_EXECUTE!`.

**After**

Fields are applied correctly (including `autoExecute` sent as a number); the endpoint confirms success and returns the `id` of the updated entity. Both single `PATCH` and batch requests work. Creation (`POST`) is unchanged.

### FIX-0714-21: Transcription: wallet check before the recognition call

**Before**

`POST /v1/audio/transcriptions` (and `/v1/ai/audio/transcriptions`) did not check the wallet before calling the upstream: a PREPAY account past its overdraft (but not yet frozen by the background sweep) still triggered recognition and slid the balance deeper negative. Chat and embeddings already rejected such calls up front; a fully frozen account was always blocked globally (`ACCOUNT_FROZEN`).

**After**

Same as chat and embeddings: the wallet check runs **before** the Whisper call. An exceeded overdraft → `402 insufficient_balance`, recognition never starts. BYOK keys (USER scope) are free — no check, no behavior change.

### FIX-0714-22: app deletion is no longer blocked by a galaxy host on its key

**Before**

`DELETE /v1/apps/:id` returned `409 APP_HAS_ACTIVE_SERVERS` when a galaxy host (shared account infrastructure) happened to sit on the application's key. The app could not be deleted, and the response gave no explanation.

**After**

A galaxy host is excluded from the blocking-servers check: it is managed at the account level, not the key level, so it must not block app deletion. Standalone application servers (including application containers) still block deletion with `409 APP_HAS_ACTIVE_SERVERS` — rebind them to another key first.

### FIX-0714-23: OpenAPI: per-entity batch endpoint body is now documented correctly (action + items/ids/calls)

**Before**

The spec (`GET /v1/openapi.json`) documented every per-entity batch body as `{create:[], update:[], delete:[]}`. The runtime (shared batch handler) requires `{action, items|ids|calls}` and returns `400 INVALID_BATCH_ACTION` for the documented shape. A client generated from the spec (codegen / AI agent) got **100% batch-write failure** across all ~45 per-entity batch endpoints. The feature itself works — only the spec was wrong (the global `POST /v1/batch`, `/v1/tasks/{taskId}/comments/batch`, and `/v1/guide` already documented the correct shape).

**After**

The spec generator emits the correct shape: a single `action` (`create`/`update`/`delete`/`list`/`get`/`fields`); `create`/`update` send `items`, `delete` sends `ids`, reads send `calls`. Matches the runtime and the global `/v1/batch`.

**Integrator impact**

If you generated a client from `openapi.json` and batch-write failed with `INVALID_BATCH_ACTION`, regenerate it: the body is now `{action:"create", items:[…]}` instead of `{create:[…]}`. Hand-written clients that already sent `{action,…}` are unaffected.

### FIX-0714-24: Currencies: fullName, format, and decimals now persist on a flat write

**Before**

`POST`/`PATCH /v1/currencies` with flat `fullName`, `formatString`, `decimals`, `decPoint`, `thousandsSep` returned success but **silently dropped** the values — Bitrix24 stores them per-language (`LANG`) and the API sent them flat. The documented workaround "send a raw `LANG`" did not work either: `LANG` is a read-only field, so the request was rejected.

**After**

The API packs the flat localizable fields into **your** language's localization (the API key's language) before the Bitrix24 call, so a flat write persists and reads back (`POST {fullName:"…",decimals:3}` → `GET` returns them). This works on every write path: single-route, `POST /v1/currencies/batch`, and the global `POST /v1/batch`. A raw `LANG` in the body is still rejected as read-only. Setting different values for several languages at once via the API is not yet supported. The write language is the API key's locale (ru or en) and may differ from the currency's display language on the portal — on portals with another locale (de/pl/ua…) the edit lands under en.

**Integrator impact**

If you worked around the bug with a raw `LANG` (and hit `400 READONLY_FIELD`), drop it and send the flat fields. Flat requests that already worked now also persist the values.

### FIX-0714-25: Aggregate enforces required filters; infra validation no longer leaks raw Zod

**Before**

- `POST /v1/<entity>/aggregate` on an entity that mandates a filter (e.g. `catalog-products` needs `iblockId`) let an empty request reach Bitrix24 and returned a raw `422`, while GET-list/search return a clean `400` on the same condition.
- `POST /v1/infra/servers/:id/{deploy,exec,upload,logs}` put a multi-line JSON-serialized Zod issue array into `error.message` on a body-validation error (a raw validator fingerprint).

**After**

- Aggregate checks required filters/params before the Bitrix24 call: a missing required filter → `400 MISSING_REQUIRED_FILTER` (e.g. `catalog-products`→`iblockId`); a missing required list-param → `400 MISSING_REQUIRED_PARAMS` (e.g. `calendar-events`→`type`,`ownerId`; `humanresources-nodes`→`type`). Like list/search.
- Infra validation formats the error compactly (`field: message; …`), matching the sibling `infra.ts`. The code (`VALIDATION_ERROR`) and `400` status are unchanged.

**Integrator impact**

If you caught a raw `422` from a filter-less aggregate, you now get `400 MISSING_REQUIRED_FILTER`. If you parsed infra `error.message` as JSON, it is now a flat `field: message` string.

### FIX-0714-26: Batch: per-entity batch works for items, and folder creation via batch

**Before**

- `POST /v1/items/{entityTypeId}/batch` returned `404` — the per-entity batch route for dynamic-param entities (`items`, `categories`) was mounted at the param-less path (`/v1/items/batch`), so the documented path didn't resolve and the `entityTypeId` never reached the Bitrix24 command. The global `POST /v1/batch` fallback worked.
- `POST /v1/folders/batch` with `action: "create"` failed every item with `ERROR_ARGUMENT`: batch-create sent `fields[...]`, but `disk.folder.addsubfolder` expects the parent folder as a top-level `id` and the rest under `data[...]`.

**After**

- The per-entity batch route for `items`/`categories` is mounted with the `:{entityTypeId}` segment and threads the validated `entityTypeId` (positive integer; dedicated-API ids like `deals=2` are rejected with a pointer, same as the single routes) into every command — for **all** actions: `create`/`update`/`delete` and the read actions `list`/`get`/`fields`.
- Folder batch-create mirrors the single-route shape: `id=<parent>&data[...]`. A missing `parentId` is a clean per-item `400` before the Bitrix24 call.

**Affected endpoints:**

- [`POST /v1/items/{entityTypeId}/batch`](/docs/entities/items/create)
- [`POST /v1/folders/batch`](/docs/entities/folders/create)

### NEW-0714-27: recover a stuck server exec channel

A new endpoint [POST /v1/infra/servers/:id/unstick](/docs/infra/deploy/exec) force-frees a Black Hole server's stuck command channel when a deploy or exec keeps returning `EXEC_BUSY` ("Another command is running") even after `DELETE /v1/infra/servers/:id/lock`. It releases the platform-side lock and bounces the agent tunnel — on reconnect the agent finishes the stuck command and frees its mutex. The server is not rebooted.

If a legitimate operation (a deploy, exec, or harden) is still running on the server when you call it, the endpoint returns `409 OPERATION_IN_PROGRESS` by default and leaves it alone — only a genuinely stuck channel should be unstuck. Retry with `?force=true` if you are certain the command channel is hung.

Response: `{ success: true, data: { backendLockReleased, agentBounced, reconnected } }`. Error codes: `404 SERVER_NOT_FOUND`, `409 CONFLICT` (a recovery is already running), `409 OPERATION_IN_PROGRESS` (an operation is running on the server — retry with `?force=true`), `409 GALAXY_UNSTICK_UNSUPPORTED` (not supported for galaxy hosts or galaxy apps), `502 GATEWAY_ERROR`. The `/exec` error (`EXEC_BUSY`) and a `/deploy` failure (code `DEPLOY_FAILED`, message "Another command is running") now also carry a `hint` pointing at this endpoint.

### NEW-0714-28: server description in `PATCH /v1/infra/servers/:id`

**Before**

`PATCH /v1/infra/servers/:id` accepted only `displayName`. There was no `description` field in the contract, and `GET` responses did not expose one.

**After**

`PATCH /v1/infra/servers/:id` accepts an optional `description` field (string, up to 500 characters; an empty string or `null` clears the description; omitting the field leaves the current value unchanged). The value is synced to the application's catalog card. The `description` field is now returned in `GET /v1/infra/servers`, `GET /v1/infra/servers/:id`, and in the `PATCH` response. Existing requests without `description` keep working unchanged.

### FIX-0714-29: galaxy app deploy returns an honest error instead of a false success when the connection drops mid-build

**Before**

If the connection to the host dropped during a galaxy app build (common under heavy-build load), `POST /v1/infra/servers/:id/deploy` could return `200` with status `running` and a `[recovered]` note in `buildLog`, even though the new version never built or started — the previous container kept running. A retry hit the same drop and again reported a false success.

**After**

A deploy is treated as recovered only if it completed fully: the container running under the app's name is the one this attempt started, and the deploy ran to the end. If the connection dropped during the build — or at any point before the deploy completed — and the new version did not come up fully, the endpoint returns a retryable `502` with code `GALAXY_HOST_UNREACHABLE` and a hint to re-send the same deploy without deleting the server — instead of a false `200`. Only if the deploy completed fully and just the final response was lost does recovery to `200` work as before.

### BC-0714-30: renaming a catalog app reaches Bitrix24, publish loses menuTitle

> Old format supported until: 14.01.2027

**Before**

[PATCH /v1/apps/:id](/docs/apps/update) with a `title` field on an app that is in the catalog returned `200` but changed nothing the user could see: the catalog card and the placement bindings on the Bitrix24 account (the left-menu item, CRM tabs) kept the old name. There was never a failure — the call always succeeded.

For [POST /v1/apps/:id/publish](/docs/apps/publish) the body was not validated, and the placement title was set by a separate `menuTitle` field.

**After**

For an app in the catalog, `title` is a single operation on the display name: the name is synchronised into the catalog card (`catalogTitle` is written together with `title`) and re-bound into the placements on the Bitrix24 account. A call that always returned `200` can therefore now fail honestly:

- `400 NO_USER_TOKEN` — the app is not authorised on the Bitrix24 account, so there is nothing to re-bind the placements with;
- `400 TITLE_TOO_LONG_FOR_CATALOG` — a catalog app name is capped at 100 characters, while `title` allows 255;
- `502 BITRIX_PARTIAL_REBIND` — Bitrix24 rejected the binding. The name is not written in that case, and repeating the same request repairs the state.

The body of `POST /v1/apps/:id/publish` is now validated, and the `menuTitle` parameter is removed: the placement title is always the app's display name. An empty body still works — publication takes the values from the app record.

**What integrators should do**

- Drop `menuTitle` from the publish body: the field is ignored, the menu item name comes from `title` and `catalogTitle`.
- Keep a catalog app name within 100 characters.
- Handle the rejections on a rename: on `NO_USER_TOKEN` authorise the app on the Bitrix24 account, on `BITRIX_PARTIAL_REBIND` repeat the request.
- Note the side effect: renaming through `title` now also writes `catalogTitle` — for an app in the catalog the two fields are kept in sync.

### NEW-0714-31: GET /v1/tasks/:taskId/comments/fields — task-comment field schema

Task comments gained a `/fields` method like every other entity: `GET /v1/tasks/:taskId/comments/fields` returns a static 5-field schema (`id`, `taskId`, `authorId`, `message`, `createdAt`) with type, read-only flag, and segment-localized label and description. The method makes no Bitrix24 call. This path previously returned `400 WRONG_PATH` — the field set was only available from the static docs.

### FIX-0714-32: per-entity batch with action list now applies filter

**Before**

`POST /v1/{entity}/batch` with `action: "list"` ignored `filter`: field names were not mapped to their Bitrix24 names (e.g. a leads `statusId` was not turned into `stageId`), and operators `$gt` / `$contains` / `$in` and others had no effect. The call returned `200` with the whole table — a silent failure with wrong data. The global `POST /v1/batch` and `POST /v1/{entity}/search` filtered correctly.

**After**

Per-entity batch runs `filter` through the same translator `search` and the global `/v1/batch` use. Field aliases and operators (`$gt`, `$gte`, `$lt`, `$lte`, `$ne`, `$contains`, `$in`, `$nin`, prefix `>=`, `<=`, `!`, etc.) are applied. An invalid `filter` (an unknown field on an entity with a complete schema, an unsupported operator, the `@` / `!@` prefixes, or `$or` / `$and` logic tokens) now returns `400` naming the call index instead of silently returning the whole set — matching the single endpoints.

### FIX-0714-33: auto-pagination no longer duplicates records across page boundaries

**Before**

For Bitrix24 list methods without sort support (e.g. the storage-object list) a record on a page boundary could shift between the fetches of adjacent pages and land in both — with `limit > 50` the response carried a duplicate that occupied a slot, and the client processed the same record twice.

**After**

After all pages are stitched, the result is deduplicated by `id` (the first occurrence is kept). For stably-sorted methods nothing changes (no duplicates — a no-op).

### FIX-0714-34: requisite-preset field list no longer comes back empty; create no longer returns a foreign record

**Before**

`GET /v1/requisite-presets/:presetId/fields` could return `200` with an empty `data: []` even when the preset had fields: the Bitrix24 method returns `result` sometimes as an array `[{…}]` and sometimes as an object-map `{"0":{…},"1":{…}}`, and the handler accepted only the array form. On create (`POST …/fields`) the echoed record could be a DIFFERENT existing field — the created row was read back by the id from the `add` response, and on some portals a read by that id returned another field.

**After**

The list normalizes both Bitrix24 response shapes (array and object-map) — fields are no longer dropped. The created record is echoed only when its `fieldName` matches the one that was created; on any mismatch the response carries `{ id }` (the row object is not substituted), so a client never receives a foreign record.

**Integrator impact**

A preset field's `id` is a Bitrix24 positional identifier: it can change after write operations on the preset and, on some portals, is not a stable key. Do not cache `id` across preset mutations — re-fetch the field list before a `get`/`update`/`delete` on a specific field.

### FIX-0714-35: placement bind for earlier-created apps is no longer rejected over the handler

**Before**

[POST /v1/placements/bind](/docs/keys-auth) could return `400` with code `PLATFORM_HANDLER_UNRESOLVABLE` for an app created before the switch to the single platform handler (such an app kept its own technical address as the handler). The bind was rejected even when the platform handler `/v1/bitrix-handler` was available — the app could not be re-published through the API.

**After**

The bind succeeds: the placement handler is registered on the platform `/v1/bitrix-handler`, and the response carries `handlerRewritten: true` plus `requestedHandler` with the original value. `PLATFORM_HANDLER_UNRESOLVABLE` is now returned only when the platform handler is genuinely unavailable. [POST /v1/placements/unbind](/docs/keys-auth) removes such a placement by the same address.

## 2026-07-13

### FIX-0713-1: OAuth app key platform scopes now sync on create and edit

**Before**

A key issued together with an OAuth app via [POST /v1/apps](/docs/apps) did not receive the platform scopes (`vibe:infra`, `vibe:ai`, `vibe:search`, `vibe:storage`) that a cabinet-created key gets. As a result [POST /v1/infra/servers](/docs/infra/servers) under such a key returned 403 `INFRA_SCOPE_REQUIRED`. Adding `vibe:infra` to the app's scopes via [PATCH /v1/apps/:id](/docs/apps) changed only the app, not the paired key — so it had no effect on access.

**After**

The paired key created via `POST /v1/apps` now receives the same default platform scopes as a cabinet key. A `vibe:*` scope change via `PATCH /v1/apps/:id` (both add AND remove) now propagates to the app's active keys. A read-only (READONLY) key is rejected with 403 `WRITE_BLOCKED_READONLY_KEY` on three write operations: server creation (`POST /v1/infra/servers`), app scope change (`PATCH /v1/apps/:id`), and READWRITE key issuance (`POST /v1/apps` with `mode: "READWRITE"`).

**Impact on integrators**

Apps created via the API can now manage infrastructure without re-creation. A key whose app already declares `vibe:infra` while the key itself lacks it (the old divergence) is fixed with one edit: remove `vibe:infra` from the app's scopes and add it back via `PATCH /v1/apps/:id` — the second edit syncs the key; or re-create the app. A READONLY key can still create a READONLY app (`mode: "READONLY"`).

### FIX-0713-2: product-sections: unsupported filters return 400 instead of the whole table

**Before**

Operators (`>`, `<`, `!`, `%`, `$ne`, `$contains`, `$nin`) and non-exact-match fields (`sort`, unknown) in the filter of [GET /v1/product-sections](/docs/entities/product-sections/list) and [POST /v1/product-sections/search](/docs/entities/product-sections/search) were silently ignored — the code 200 and the whole list were returned unfiltered.

**After**

Such filters are rejected with `400 UNSUPPORTED_FILTER`. Filter by exact match or `$in` on `id`, `name`, `xmlId`, `code`, `catalogId`, `sectionId`. Sorting (`order`/`sort`) is unchanged.

**Impact on integrators**

Calls with operators or `filter[sort]` that previously returned 200 with unfiltered data now return `400` — switch to exact match or `$in`.

### FIX-0713-3: the reason for a failed first server provisioning is now visible

**Before**

If a server failed to come up on its first creation (a preemptible plan evicted, no free capacity), it silently ended up in `sleeping` status with no explanation. A client polling [GET /v1/infra/servers/:id](/docs/infra/servers/get) saw `sleeping` and could not tell why the deploy was blocked.

**After**

Such a server now moves to `status: "error"` with a populated `provisionError` (a human-readable reason) and a new `provisionErrorCode` field — a machine-readable failure category (`PREEMPTIBLE_EVICTION` / `PROVISION_TIMEOUT` / `NO_CAPACITY` / `GENERIC`). The `provisionErrorCode` field is added to [GET /v1/infra/servers/:id](/docs/infra/servers/get) and [GET /v1/infra/servers](/docs/infra/servers/list) next to `provisionError` (additive, `null` when there were no errors).

**Integrator impact**

No action needed: `error` is an already-existing status. Recover such a server via `POST /v1/infra/servers/:id/start` or `/repair` (not `/wake`: on `error` status it returns `422`; the `availableActions` field in the response points to the available action).

### NEW-0713-4: POST and PATCH /v1/tasks/:taskId/time accept createdDate

An optional `createdDate` field is now forwarded on [POST /v1/tasks/:taskId/time](/docs/entities/tasks/time/create) and [PATCH /v1/tasks/:taskId/time/:itemId](/docs/entities/tasks/time/update) — the time entry lands on the given date instead of the current moment (backfilling last week's tracks). ISO 8601 with offset, bare ISO, and `YYYY-MM-DD` are all accepted and passed to `CREATED_DATE` verbatim. When the field is omitted the behaviour is unchanged — the date equals the creation moment.

### FIX-0713-5: meta.hasMore no longer stays stuck at true with filter + offset

**Before**

When paginating a filtered list (for example [GET /v1/deals](/docs/entities/deals/list) with `filter` and `offset`), `meta.hasMore` stayed `true` at every offset — even well past `meta.total`. A `while (meta.hasMore) { offset += limit }` loop ran forever.

**After**

When Bitrix24 ignores an offset beyond the filtered set and returns the whole set, `meta.hasMore` is derived from the request window (`offset + limit < meta.total`) instead of being forced to `true`. At `offset=0` the response is unchanged; once the window passes `total`, `hasMore` becomes `false`. Applies to list and `POST /search` across all entities.

### FIX-0713-6: GET /v1/lists and /v1/lists/:iblockId/elements honour offset

**Before**

[GET /v1/lists/:iblockId/elements](/docs/lists/elements) and [GET /v1/lists](/docs/lists/lists) silently ignored `offset` — `?limit=50&offset=50` returned the same first page, and a client paginating by `offset` never reached rows 51 and beyond.

**After**

`offset` is mapped to `start`, the native pagination param for these methods, so `offset`-based paging works. An explicit `start` keeps priority when both are supplied.

### NEW-0713-7: New preserveEnv flag keeps .env across cleanDeploy

The `POST /v1/infra/servers/:id/deploy` body gained an optional boolean `preserveEnv` (default `false`). When `cleanDeploy: true` (which wipes the app directory including the `.env` file) and `preserveEnv: true`, the existing `.env` is read before the wipe and restored afterwards unless this request also passes `env` (a supplied `env` wins). Without the flag a re-deploy with `cleanDeploy` could boot the app with no environment variables — on its default port.

### NEW-0713-8: failed galaxy app builds now carry a `buildHint`

**Before**

A galaxy app build failure returned only `GALAXY_APP_BUILD_FAILED`
(and `GALAXY_APP_START_FAILED`) with a `buildLog` tail — you had to read the log
by hand to find the cause. `GET /v1/infra/servers/:id` for an ERROR app returned a
short `provisionError` but no ready recommendation.

**After**

The response now carries the parsed cause. The error body of
`POST /v1/infra/servers/:id/deploy` (502) additionally includes `error.category`
(a machine category such as `MODULE_NOT_FOUND`, `INSTALL_AUTH`, `RESOURCE`) and
`error.buildHint` — a localized recommendation line naming the concrete next step,
whenever the failure is classifiable. `GET /v1/infra/servers/:id` for an ERROR app
adds the same `data.buildHint` field. The fields are additive: when the cause is
unrecognized they are absent (`buildHint` is `null`), and `provisionError`/`buildLog`
keep arriving as before — existing requests are unaffected.

### FIX-0713-9: concurrent identical source saves no longer mint a duplicate version

**Before**

Two simultaneous saves of identical bytes to the same server (`POST /v1/infra/servers/:id/sources`, and the auto-save on deploy) could, under rare timing, create **two** byte-identical versions instead of one — content deduplication was best-effort.

**After**

Deduplication is deterministic: identical bytes submitted concurrently always converge on a single version. On `POST /v1/infra/servers/:id/sources` both responses return the same `versionId` and the losing request gets `deduplicated: true`; the deploy auto-save converges on that same single version (the deploy response shape is unchanged).

## 2026-07-12

### NEW-0712-1: The AI quota exhaustion response now points to the top-up path

The `402 ai_quota_exhausted` error with `reason: wallet_empty` now additionally returns the `hint` and `topupUrl` fields. `hint` is a short English message: the monthly AI quota and the account balance are exhausted, and an account admin can top up the balance to resume. `topupUrl` is a link the admin opens to top up. The fields are additive: the existing response fields (`reason`, `resetAt`) are unchanged, and the `reason: breaker` and `reason: wallet_off` branches do not carry them. This lets an agent client relay the top-up path to the human. The error is returned by model calls, see [POST /v1/chat/completions](/docs/ai/chat/completions).

### FIX-0712-2: `sleep-now` on a server with a long-past computed wake now sleeps honestly

**Before**

For a server with an enabled wake schedule, `POST /v1/infra/servers/:id/sleep-now` could return `{ data: { slept: false, reason: "WAKE_IMMINENT" } }` forever when the denormalized "next wake" was left far in the past (the server was woken outside the scheduler and the stamp was never recomputed). The server never slept and kept its plan running around the clock.

**After**

A stamp older than a dynamic window (grace = margin + typical lead) is treated as stale rather than "imminent": `sleep-now` sleeps the server honestly and answers `{ success: true }`, after which the scheduler quietly rolls the anchor forward to the future window without waking it. `WAKE_IMMINENT` stays a normal response only for a genuinely near wake.

## 2026-07-11

### FIX-0711-1: galaxies: GALAXY_HOST_UNREACHABLE is now actionable — structured hint and a precise provisionError

**Before**

When a galaxy host was temporarily unreachable, [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy) answered with a bare 502 `GALAXY_HOST_UNREACHABLE`, and a slot created in one call via [POST /v1/infra/servers](/docs/infra/servers/create) with `source` moved to an error status with the text "Deploy failed unexpectedly — please retry; details are in the server logs". Neither the cause nor the recovery path was reported — clients deleted the slot and created a new one, which does not help: the fresh slot lands on the same host.

**After**

The 502 `GALAXY_HOST_UNREACHABLE` response — on deploy, exec and delete ([DELETE /v1/infra/servers/:id](/docs/infra/servers/delete)) — carries a structured `error.hint`: the condition is transient, the slot and its data are intact, retry the same request in 1–2 minutes, do not delete the slot. On the create-with-`source` path, `provisionError` now contains the real cause ("Galaxy host … became unreachable during build …") and the same advice to re-send the deploy to the existing slot. Additionally: when several servers work under one OAuth application, a saved source version is no longer lost to a version-numbering conflict — neither on the deploy auto-save nor on an explicit save via [POST /v1/infra/servers/:id/sources](/docs/source-storage).

**Impact on integrators**

The change is additive: response codes and statuses did not change; the `error.hint` field was added and the `provisionError` text became precise. No client updates required; AI agents should read `hint.recovery` — it states exactly what to do.

### NEW-0711-2: distinct error code when the Vibecode Connector module is not installed on the portal

Issuing an application key through the connector module ([POST /v1/apps](/docs/apps)) now returns `409` with code `CONNECTOR_MODULE_NOT_INSTALLED` and a clear "install the module" message when the portal has no `vibecodeconnector` module, instead of the previous opaque `502 CONNECTOR_APP_INSTALL_FAILED`. This is a legitimate, permanent state (especially for a self-hosted portal), not a transient failure — retrying will not help, the module must be installed on the portal. Other issuance error codes are unchanged.

### FIX-0711-3: GET /v1/ai/quota pacing can now be populated by the platform by default (gradual rollout)

The platform can now enable pacing (AI-quota day/week smoothing) by default for an account — no admin action required. The rollout is staged (pilot accounts → all accounts): until an account falls under the platform default-on, the `data.pacing` field in the [GET /v1/ai/quota](/docs/ai/consumption/quota) response stays `null`, same as before. Once an account is under default-on: `mode: "ignore"` is an informational mode, and `active: false` means window limits do not reject requests (a `429 ai_pacing_limited` rejection is impossible). The response shape is unchanged; integrators who already treat `data.pacing` as an optional field need no changes.

## 2026-07-10

### NEW-0710-1: wake-schedule window management (wake-schedules)

New CRUD contract on Black Hole servers: `GET`/`POST /v1/infra/servers/:id/wake-schedules`, `PATCH`/`DELETE /v1/infra/servers/:id/wake-schedules/:scheduleId`. Lets you declare one or more recurring wake windows (`cronExpr` + a required IANA `timezone`, optional `label`/`lead`/`enabled`) — the platform wakes a sleeping server ahead of the window, and the app's own in-VM cron runs the task from there.

Rolling out gradually and not yet available on every portal — until enabled on a given portal, the call returns `403` with code `WAKE_SCHEDULE_DISABLED`. Available only for BLACKHOLE-mode servers (otherwise `400 BLACKHOLE_ONLY`) and not yet supported for galaxies (`400 GALAXY_NOT_SUPPORTED` on both the host and a nested app — coming later). The minimum cadence between occurrences and the per-server window cap (50) are platform-configured; violating either returns `400 CADENCE_TOO_LOW` and `403 WAKE_SCHEDULE_LIMIT` respectively. The create/update response additionally carries a `tzWarning` field — a heads-up that the VM's in-VM timezone may have drifted from the window's timezone if the server hasn't been redeployed since. **`PATCH` replaces the whole window (PUT semantics): omitted optional fields reset to their defaults — `enabled`→`true`, `label`/`lead`→null.** Send the full window object on update.

Affected endpoints: `GET|POST /v1/infra/servers/:id/wake-schedules`, `PATCH|DELETE /v1/infra/servers/:id/wake-schedules/:scheduleId`.

### NEW-0710-2: machine-readable tz-warning code in wake-schedules responses (`tzWarningCode`)

The wake-schedule create/update response (`POST`/`PATCH /v1/infra/servers/:id/wake-schedules[/:scheduleId]`) now additionally carries a `tzWarningCode` field alongside the existing text `tzWarning` — `"SINGLE_ZONE"` / `"MULTI_ZONE"` / `null` (when the server ends up with zero enabled windows after the mutation). It's a machine-readable companion to the same advisory, letting clients localize the message themselves instead of rendering the raw English `tzWarning` text. The field is additive — `tzWarning` is unchanged and stays for backward compatibility.

Affected endpoints: `POST|PATCH /v1/infra/servers/:id/wake-schedules[/:scheduleId]`.

### FIX-0710-3: wake-schedules: schedules rejected on always-on servers

`POST`/`PATCH /v1/infra/servers/:id/wake-schedules` now reject creating or updating a wake window on an always-on (24/7) server — the call returns `400` with code `ALWAYS_ON_CONFLICT`. Such a server runs on a non-preemptible plan and never auto-sleeps, so a wake schedule would silently break the paid always-online guarantee. Ordinary sleeping Black Hole servers and preemptible agents/bots are unaffected. Turn off always-on to declare wake windows.

Affected endpoints: `POST /v1/infra/servers/:id/wake-schedules`, `PATCH /v1/infra/servers/:id/wake-schedules/:scheduleId`.

### FIX-0710-4: wake-schedules can now be declared on nested galaxy apps

**Before**

`POST`/`PATCH /v1/infra/servers/:id/wake-schedules` returned `400 GALAXY_NOT_SUPPORTED` for any galaxy-family server — both the host itself and nested apps (`kind=GALAXY_APP`).

**After**

Nested galaxy apps (`kind=GALAXY_APP`) are now accepted — you can declare a wake window on a specific app, and the platform wakes it (and the host, if needed) ahead of the window. The galaxy host itself still returns `400 GALAXY_NOT_SUPPORTED` — schedule the individual apps, not the host.

Affected endpoints: `POST|GET /v1/infra/servers/:id/wake-schedules`, `PATCH|DELETE /v1/infra/servers/:id/wake-schedules/:scheduleId`.

### FIX-0710-5: sleep-now declines to sleep when a scheduled wake is imminent

**Before**

[POST /v1/infra/servers/:id/sleep-now](/docs/infra/lifecycle/sleep-now) always put the server to sleep immediately, even when the next scheduled wake was a minute away — the server woke straight back up.

**After**

When the server has an enabled wake schedule and the next wake falls within the guard margin, the call does not sleep the server and responds `200` with `{ "success": true, "data": { "slept": false, "reason": "WAKE_IMMINENT" } }`. Otherwise the server sleeps and the scheduler wakes it at the next window.

**Impact on integrators**

The `data` block with `slept: false` arrives only when the call declines to sleep the server — check for its presence if you rely on the server always sleeping after this call. On a successful sleep the response is just `success: true`. Existing calls to servers without a schedule are unchanged.

### FIX-0710-6: non-streaming chat/completions and embeddings no longer abort at 360 seconds

**Before**

A non-streaming (`stream:false`) `POST /v1/chat/completions` (and `POST /v1/embeddings`) request whose generation took longer than ~2 minutes reliably aborted at ~360 seconds with `{"code":"ai_provider_unavailable","message":"This operation was aborted"}` — regardless of the client's own timeout. The doomed generation also burned triple the compute.

**After**

Such a request now runs within a single ~850-second budget (one attempt for the whole budget, no compute tripling). If generation still does not finish in budget, a meaningful `503` with code `ai_provider_timeout` is returned, with a localized `userMessage` and a hint (reduce the request size or use streaming mode `stream:true`) — without a `Retry-After` header (the timeout is not transient). A client disconnect now immediately cancels the provider-side generation. Streaming mode (`stream:true`) was never affected by this limit.

### FIX-0710-7: requisite-presets/:presetId/fields and requisite-links: list sorting, filtering, and parameter validation

**Before**

[GET /v1/requisite-presets/:presetId/fields](/docs/entities/requisite-presets) ignored `sort`/`order` and any `filter[...]` — it always returned the full list in Bitrix24 order. [GET /v1/requisite-links](/docs/entities/requisite-links) and [POST /v1/requisite-links/search](/docs/entities/requisite-links) accepted only plain equality; comparison operators (`$gte`, `>`, etc.), `sort`/`order`, and unknown fields were silently dropped, so the whole table came back.

**After**

Both lists now honour `sort`/`order` (including the `order[field]=asc|desc` form) and `filter`. requisite-links supports comparison operators (`$gt`/`$gte`/`$lt`/`$lte`, `$in`/`$nin`, `>=`/`>` prefixes). An unknown filter or sort field now returns `400` (`UNKNOWN_FILTER_FIELD` / `UNKNOWN_SORT_FIELD`), a top-level logical operator (`$or`/`$and`) returns `400 INVALID_FILTER_OPERATOR`, and filtering by `entityId` without `entityTypeId` returns `400 MISSING_ENTITY_TYPE_ID` instead of a raw "Access denied".

**Impact on integrators**

Requests on documented fields keep working and now actually sort/filter. If you relied on an unknown parameter being silently ignored, it now returns `400` — drop the typo or use a field from the response.

### FIX-0710-8: Bank details sorting by id honors the direction

**Before**

Listing bank details ([GET /v1/bank-details](/docs/entities/bank-details/list)) sorted by `id` descending (`?sort=-id`) returned records in ascending order — the sort direction was silently ignored.

**After**

`?sort=-id` (and `?sort=id`) sorts by the identifier in the requested direction.

**Integrator impact**

No code change required — requests that relied on sorting by `id` now return the expected order.

### NEW-0710-9: GET /v1/quotes/fields — ~26 quote fields declared with human-readable labels

The quotes entity schema now declares ~26 fields that Bitrix24 returned but that were undeclared: `quoteNumber`, `updatedBy`, `lastActivityBy`, `lastActivityTime`, `content`, `terms`, `leadId`, `storageTypeId`, `storageElementIds`, `personTypeId`, `webformId`, `lastCommunicationTime`, `contactIds`, `contacts`, `locationId`, `taxValue`, `actualDate`, `mycompanyId`, `utmSource`/`utmMedium`/`utmCampaign`/`utmContent`/`utmTerm`, `lastCommunicationCallTime`/`lastCommunicationEmailTime`/`lastCommunicationImolTime`/`lastCommunicationWebformTime`. They now appear in [GET /v1/quotes/fields](/docs/entities/quotes/fields) with readable labels (instead of service names like `STORAGE_TYPE_ID`/`UTM_SOURCE`), and their values are type-coerced (numbers, ISO dates) in list/get responses. Labels were also added to the previously label-less `stageId`, `opened`, `closed`.

### FIX-0710-10: a PATCH with no writable field is rejected with an explicit error

**Before**

`PATCH /v1/{entity}/:id` with an empty body — or a body carrying no recognized writable field (for example because of a typo in a field name) — reached Bitrix24, which silently ignored the request and returned success. The wrapper then replied `200` with the unchanged object, so the client believed the edit had applied while nothing actually changed — a silent-drift risk, especially for AI agents. This affected all three update surfaces: single `PATCH /v1/{entity}/:id`, per-entity `POST /v1/{entity}/batch` and global `POST /v1/batch`.

**After**

An update with an empty body returns `400 EMPTY_UPDATE_BODY` (a per-item error in batch calls) on all three surfaces, before Bitrix24 is called. For `/v1/catalog-products/:id` a strict check is added: a `PATCH` that carries no recognized writable field returns `400 NO_RECOGNIZED_UPDATE_FIELDS`. A meaningful update always carries at least one field — send a recognized field (custom properties `propertyNNN` and `UF_*` fields are accepted too). Entities that already validated their fields behave as before.

Catalog products additionally expose in `GET /v1/catalog-products/fields`, and now accept for explicit `select`, filter and sort, the fields from the product-update contract — `code`, `xmlId`, `sort`, `vatId`, `height`, `length`, `width`, `previewText`, `detailText` and others (filtering by them previously returned `400 UNKNOWN_FILTER_FIELD`). Filter and sort reliably by the indexable fields (`code`, `xmlId`, `sort`, `vatId`, dimensions); for the full-text ones (`previewText`, `detailText`) Bitrix24 may ignore the filter. List responses without an explicit `select` are unchanged.

### FIX-0710-11: leads, companies, quotes — /fields date keys are now createdTime and updatedTime

**Before**

`GET /v1/leads/fields`, `GET /v1/companies/fields` and `GET /v1/quotes/fields` advertised `createdAt` and `updatedAt`, while list/get/search responses always returned `createdTime` and `updatedTime` — the value was never readable under the `createdAt`/`updatedAt` name. Filter and sort accepted the `createdAt`/`updatedAt` names.

**After**

These entities' `/fields` advertise the real keys `createdTime` and `updatedTime` — like contacts, invoices and items. The keys in the response body are the same (`createdTime`/`updatedTime` were always returned), but the value is now normalized to ISO-8601 in UTC (`2026-04-15T07:00:00.000Z`) — previously it arrived in the raw Bitrix24 form with the account offset (`2026-04-15T08:00:00+01:00`). Same instant, only the representation changes.

**Impact on integrators**

Read dates from `createdTime` and `updatedTime` (the keys did not change). If you compare the date string byte-for-byte or cache by it, account for the offset → `Z` shift (same instant). In filter and sort use `createdTime`/`updatedTime`; the former `createdAt`/`updatedAt` now return `400 UNKNOWN_FILTER_FIELD` (filter) and `400 UNKNOWN_SORT_FIELD` (sort). On write, `createdAt`/`updatedAt` are no longer rejected as read-only — they are ignored as unknown fields (like contacts/invoices/items); the creation/update timestamp still cannot be set.

### FIX-0710-12: sleep settings: flipping to always-on is now rejected while wake windows are active

`PATCH /v1/infra/servers/:id/sleep` now rejects setting `sleepAfterMinutes: null` (always-on, 24/7) on a server that still has enabled wake-schedule windows — the call returns `400` with code `ALWAYS_ON_CONFLICT`. This is the reverse direction of an existing gate: creating a wake window on an always-on server was already rejected, and now the opposite transition is rejected symmetrically — otherwise the server would keep sleeping on schedule, silently breaking the paid always-online guarantee. Delete or disable the wake windows first, or keep a sleep timeout instead of "Never", to turn always-on on.

Affected endpoints: `PATCH /v1/infra/servers/:id/sleep`.

### NEW-0710-13: placement.bind on a self-hosted Bitrix24 returns a clear SESSION_REQUIRES_ADMIN for a non-administrator

On a self-hosted (BOX) Bitrix24, binding a placement via the developer-key path requires the key's user to be an account administrator. Previously a non-administrator request returned an opaque `502 BITRIX_UNAVAILABLE`.

Now [POST /v1/placements/bind](/docs/keys-auth) recognises the access-denied signal from Bitrix24 and returns `403 SESSION_REQUIRES_ADMIN` with a hint: bind from an account-administrator account, or ask an administrator to grant those rights. The requirement is visible up front in [GET /v1/me](/docs/keys-auth) — the `placements.bindPrerequisite` block for self-hosted accounts now includes the `SESSION_REQUIRES_ADMIN` code.

### FIX-0710-14: GET /v1/me capabilities now reflect read-only (READONLY) mode

**Before**

For a READONLY-mode key, [GET /v1/me](/docs/keys-auth) returned `capabilities.managedBots.create`, `agents.create`, `servers.create` and `apps.*` as `available: true`, even though every write is blocked with `403 WRITE_BLOCKED_READONLY_KEY`. An agent saw "can create" and hit the rejection.

**After**

For a READONLY key these write capabilities are returned as `available: false` with `reason: "WRITE_BLOCKED_READONLY_KEY"` and a hint to switch the key to read+write. Read-only capabilities and the AI Router are unchanged. For READWRITE keys the response is unchanged.

### FIX-0710-15: the feedback author can reply to their own ticket again without the vibe:feedback scope

**Before**

[POST /v1/feedback/:id/comments](/docs/keys-auth) rejected the ticket author with `403 FEEDBACK_SCOPE_REQUIRED` when the key lacked the `vibe:feedback` scope — even though the author branch was documented. The author could not reply to their own `AWAITING_USER` ticket, and it stalled.

**After**

The author check now runs before the scope gate: the author replying with the same key that created the ticket reaches the author branch (`authorType=USER`, ball-court rule `AWAITING_USER → NEEDS_REVIEW`) even without the `vibe:feedback` scope. A key that is neither the author nor scoped still gets `403 FEEDBACK_SCOPE_REQUIRED`.

### NEW-0710-16: AI quota pacing: pacing field in the response and 429 ai_pacing_limited error

The [GET /v1/ai/quota](/docs/ai/consumption/quota) response gained a `pacing` field — the state of monthly AI quota pacing (peak smoothing via a daily and a weekly limit layered on top of the overall monthly limit; enabled by the portal admin from the `/ai` cabinet page). `null` when pacing is off at the platform level or not configured for the account; otherwise an object `{ mode, active, day, week }`: `mode` is the enforcement behavior on a tripped limit (`wallet`/`block`/`ignore`), `active` signals whether tripping a window will reject a call right now (`false` in observation mode, and always `false` in `ignore` mode — windows are tracked as informational only, `429` is never returned), `day` and `week` are each `{ pctUsed, resetAt }` as a percentage of that window's OWN limit. The response is still cached for 30 seconds, so pacing state can lag by that long.

When the daily or weekly limit trips, calls to [POST /v1/chat/completions](/docs/ai/chat/completions), [POST /v1/embeddings](/docs/ai/embeddings), and [POST /v1/audio/transcriptions](/docs/ai/audio/transcriptions) can now return `429` with the body `{ success: false, error: { code: "ai_pacing_limited", type: "rate_limit_exceeded", message, reason, overageDenied, resetAt, retryAfter } }` and a `Retry-After` header. `reason` names the tripped window (`day_window` or `week_window`); `overageDenied` names why paid overage was refused (`wallet_empty`, `breaker`, `wallet_off`), or is `null` under the hard-block mode. Retrying before `Retry-After`/`resetAt` will not help — the quota is not any more available in the meantime. Pacing is off by default — the platform enables it.

### FIX-0710-17: GET /v1/pages, /v1/sites and POST /v1/{pages,sites}/search now honour offset

**Before**

Listing pages or sites with an offset (`GET /v1/pages?offset=50`, `POST /v1/pages/search` with `offset`) returned `422 BITRIX_ERROR "Unknown parameter: start"`. The first page (no `offset`) worked.

**After**

`offset` for `pages` and `sites` is handled correctly: the requested window `[offset, offset+limit)` is returned, with `meta.total` and `meta.hasMore` computed from the row count. No client change is needed — calls without `offset` behave as before.

### NEW-0710-18: /fields of documents, catalogs, prices, telephony lines, basket items and leads gain richer metadata

`GET /:entity/fields` of several entities now carries more complete schema metadata. Documents ([GET /v1/documents/fields](/docs/entities/documents/fields)), catalogs ([GET /v1/catalogs/fields](/docs/entities/catalogs/fields)), catalog prices ([GET /v1/catalog-prices/fields](/docs/entities/catalog-prices/fields)) and telephony lines now expose a human-readable `label` and `description` per field, returned in English.

For basket items ([GET /v1/basket-items/fields](/docs/entities/basket-items/fields)) the fields `weight`, `vatRate`, `measureCode`, `measureName`, `dimensions`, `productXmlId`, `catalogXmlId` are marked with a `nullable` flag — they may come back empty. For leads ([GET /v1/leads/fields](/docs/entities/leads/fields)) the same flag marks `secondName`, `sourceDescription`, `comments`, and the previously-undeclared fields `originatorId`, `dateClosed`, `lastCommunicationTime` and the tags `utmSource`/`utmMedium`/`utmCampaign`/`utmContent`/`utmTerm` are now declared — filter and sort work on them, and `dateClosed` is normalized to ISO-8601.

For landing sites the group-by dimensions are declared, so [POST /v1/sites/aggregate](/docs/entities/sites/aggregate) with `groupBy` (`type`, `active`, `deleted`, `lang`, `tplId`, `domainId`, `createdById`, `modifiedById`) no longer answers `Available: .`. For documents aggregation is disabled (every numeric field is an identifier): [POST /v1/documents/aggregate](/docs/entities/documents/list) returns `404`.

Existing calls keep working unchanged — this is additional field metadata.

### NEW-0710-19: Idempotent server creation — Idempotency-Key header

[POST /v1/infra/servers](/docs/infra/servers/create) now accepts an optional `Idempotency-Key` header when creating a standalone server. A retry with the same key — for example after a lost response or a network drop — does not create a second server: it returns the same server the first request created, with status 201 and an `Idempotent-Replayed: true` response header. The key is a 1–255 character string from `[A-Za-z0-9_.:-]`; it is scoped to your API key.

On a replay the one-time SSH credentials (`ssh.privateKey` / `ssh.password`) are NOT re-exposed — they are `null` in the response body and a `note` field explains this. Keep the credentials from the first create response.

New error codes: `400 INVALID_IDEMPOTENCY_KEY` (the key fails validation), `400 IDEMPOTENCY_UNSUPPORTED_WITH_GRADUATION` (the key together with `graduateFrom` for a dedicated server is not supported), `409 IDEMPOTENCY_KEY_ALREADY_USED` (the key was already used for a server that has since been deleted), `409 IDEMPOTENCY_CONCURRENT_RETRY` (a concurrent request with the same key is still in progress — retry shortly).

The header applies to standalone servers only. On galaxy-placement portals a well-formed key is ignored without an error, and the retry protection does not extend to that path.

Additionally: the create response now returns the canonical server name (suffixed when a name collision is resolved) instead of the name from the request — these previously diverged on a same-name collision.

### NEW-0710-20: localized message on OPEN-mode switch denials

Responses from [PATCH /v1/infra/servers/:id/mode](/docs/infra/access/mode) with codes `OPEN_MODE_DISABLED` (OPEN mode is disabled platform-wide) and `OPEN_MODE_NOT_ALLOWED` (OPEN mode is not allowed by portal policy) now additionally carry an `error.userMessage` field — a localized, human-readable message pointing to the [Deploy API](/docs/infra/deploy) as the supported replacement for direct SSH. The field is additive: `error.message` (English technical string), `error.code` and the HTTP status are unchanged. It matches the shape of the existing `error.userMessage` on the `OPEN_MODE_REQUIRES_COMMERCIAL` denial. The `userMessage` text depends on the key owner's locale.

### BC-0710-21: Open line config fields normalized to camelCase and described in /fields

> Old format supported until: 10.01.2027

**Before**

[GET /v1/openline-configs](/docs/openlines/config/list), [GET /v1/openline-configs/{id}](/docs/openlines/config/get) and [POST /v1/openline-configs/search](/docs/openlines/config/search) returned most configuration fields in the Bitrix24-native form — upper case with underscores (`CRM_CREATE`, `WELCOME_MESSAGE`, `QUEUE_TIME`, and others). The [GET /v1/openline-configs/fields](/docs/openlines/config/fields) reference described only 6 fields, so the rest were invisible to programmatic discovery.

**After**

All configuration fields are normalized to a single camelCase form (`crmCreate`, `welcomeMessage`, `queueTime`, and so on), and `/fields` describes the full field set with human `label` and `description` values. Filtering and sorting by the new camelCase names work. On write (`create`/`update`) both cases are still accepted — existing calls that send upper-case names in the body keep working.

**What integrators should do**

Read response fields by their camelCase names: `config.crmCreate` instead of `config.CRM_CREATE`. The mapping is a direct transliteration from upper case to camelCase (`WELCOME_BOT_ID` → `welcomeBotId`, `WORKTIME_TO` → `workTimeTo`, `LINE_NAME` → `name`). The full list of new names is in the `/fields` reference.

### NEW-0710-22: Added GET /v1/warehouses/fields — warehouse field schema

A new [GET /v1/warehouses/fields](/docs/entities/warehouses/list) endpoint returns the schema of the 19 warehouse fields: for each field — its `type`, a read-only flag (`readonly`), a human `label`, and a `description`. Warehouses are a custom route (no entity schema), so they previously lacked the field reference that auto-generated entities have. The response is `{ success: true, data: { fields: { … } } }`. Requires the `catalog` scope.

### FIX-0710-23: app publish recovers from a placement drift

**Before**

On publish ([POST /v1/apps/:id/publish](/docs/apps)) or a placement update ([PATCH /v1/apps/:id](/docs/apps)), if a placement was registered on the Bitrix24 side but missing from the app (drift after an unpublish), the bind failed with "Handler already binded" and the placement stayed out of sync.

**After**

On that error the platform sweeps the stale binding once and retries — the placement syncs automatically. Recovery fires only on the confirmed conflict, so a live placement is never stripped by mistake; placements that need non-portable OPTIONS (chat widgets, the background worker) are excluded from auto-recovery and still surface a warning.

### FIX-0710-24: storage: object sha256 is populated on direct upload

**Before**

The `sha256` field in the storage-object upload response was always `null` for user objects, even though the schema described it as "computed on upload".

**After**

For direct upload ([POST /v1/storage/objects/upload](/docs/storage), files up to 10 MB) `sha256` now carries the SHA-256 hash of the object content — usable for integrity checks and duplicate detection (identical content yields an identical hash). Presigned and multipart uploads send bytes straight to storage bypassing the platform, so `sha256` stays `null` there for now.

### FIX-0710-25: pacing.active in GET /v1/ai/quota no longer reports an active limit when the quota is zero

**Before**

With pacing enabled on a Bitrix24 account whose monthly quota is zero (a hard stop via the `monthlyVibes = 0` override, or usage not yet initialized), `data.pacing.active` returned `true` — even though a `429 ai_pacing_limited` rejection is structurally impossible in that state: requests are rejected by the monthly limit, not by a pacing window.

**After**

`data.pacing.active` returns `true` only when exceeding the day or week window can actually produce `429 ai_pacing_limited`. With a zero quota the field honestly reports `false`. The response shape is unchanged; clients that built backoff logic on `active` need no changes — the signal is simply more accurate.

## 2026-07-09

### FIX-0709-1: transient DB overload now returns 503 with Retry-After instead of 500

**Before**

In a rare form of transient DB overload (connection exhaustion), some requests (including `POST /v1/infra/servers`) returned `500`.

**After**

Such requests return `503` with code `POOL_EXHAUSTED` and a `Retry-After` header. The error is transient — retry the request with backoff.

**Impact on integrators**

Clients that retry on 5xx must now handle `503` and honor `Retry-After`. `POST /v1/infra/servers` is non-idempotent — a retry can create a duplicate server, so retry with backoff rather than immediately.

### FIX-0709-2: placements/bind reports the Bitrix24 plan requirement clearly

**Before**

Binding a placement with an app key on a portal whose Bitrix24 plan does not permit developer-key REST calls made Bitrix24 deny access, and `POST /v1/placements/bind` returned an opaque `502 BITRIX_UNAVAILABLE` with no cause. The plan is checked on the developer-key path, but `GET /v1/me` gave no advance signal about this prerequisite.

**After**

The denial is now classified: `POST /v1/placements/bind` returns `403` with code `INT_TARIFF_REQUIRED` and a readable `userMessage` — the developer-key path needs a commercial Bitrix24 plan on the portal. An app key's `GET /v1/me` gains a `placements.bindPrerequisite` block that describes the Bitrix24-side prerequisite up front and lists the error codes. Other bind failures (unknown clientId, stale embedding) still return `502 BITRIX_UNAVAILABLE`.

**Impact on integrators**

No action required, successful calls are unchanged. If you handled `502 BITRIX_UNAVAILABLE` on bind, also handle `403 INT_TARIFF_REQUIRED` and prompt the user to upgrade the portal to a commercial Bitrix24 plan.

### FIX-0709-3: bindPrerequisite in GET /v1/me now reflects the portal plan

**Before**

The `placements.bindPrerequisite` block on an app key described the bind prerequisite identically for every portal: `subscriptionRequired: true`, a remedy that does not apply to a portal whose access is granted by the Bitrix24 plan, and an `errorCodes` list carrying codes such a portal never receives. On it the bind denial arrives as `INT_TARIFF_REQUIRED`.

**After**

The block now depends on the portal. On a portal whose access is granted by the Bitrix24 plan, `subscriptionRequired` is `false`, the `note` describes the commercial-plan requirement, and `errorCodes` carries `INT_TARIFF_REQUIRED` and `BITRIX_UNAVAILABLE` — only the codes that portal can actually receive.

**Impact on integrators**

Successful calls are unchanged. If you read `errorCodes` from `bindPrerequisite` as an exhaustive list, note that it is now narrowed to the codes reachable on the calling portal. The codes themselves and the behavior of `POST /v1/placements/bind` are unchanged.

### BC-0709-4: CRM, task and landing fields aligned with the real Bitrix24 contract

> Old format supported until: 09.01.2027

**Before**

`GET /v1/deal-categories` returned `isLocked` as the string `"Y"`/`"N"`, and `GET /v1/payments` returned `paySystemIsCash` as the string `"Y"`/`"N"`. `GET /v1/leads` exposed the internal `searchContent` field (Bitrix24's full-text search index) in every response. The payment fields `paySystemXmlId`, `dateMarked`, `dateResponsibleId` passed through as-is, with no date normalization.

**After**

`isLocked` (pipelines) and `paySystemIsCash` (payments) are now boolean (`true`/`false`). `searchContent` is no longer returned by `GET /v1/leads`. Payments now declare `paySystemXmlId` (string), `dateMarked` and `dateResponsibleId` (datetimes normalized to a single ISO form with a `Z` suffix). Tasks gained `changedBy`/`closedBy`/`statusChangedBy` (read-only — writing them returns `400 READONLY_FIELD`). `GET /v1/currencies/fields` now returns human-readable `label` values and a `lang` field; `GET /v1/deal-categories/fields` returns `label` values; `GET /v1/sites/fields` sets a `nullable` flag on fields that may come back empty.

**What integrators should do**

Read `isLocked` and `paySystemIsCash` as boolean instead of comparing to the string `"Y"`. If your code relied on the leads `searchContent` field, stop: it was an internal, undocumented field.

### FIX-0709-5: creating an entity with an empty body is rejected with an explicit error

**Before**

Creating an entity with an empty body (or no body at all but a `Content-Type: application/json` header) reached Bitrix24 and silently created an entity with default values — including deals, leads, contacts, companies, invoices and smart processes. A retry or a bodyless client thus spawned junk records in CRM. This affected all three create surfaces: single `POST /v1/{entity}`, per-entity `POST /v1/{entity}/batch` and global `POST /v1/batch`.

**After**

Such a request returns `400 EMPTY_CREATE_BODY` (a per-item error in batch calls) before Bitrix24 is called, on all three surfaces. A meaningful create always carries at least one field — send the fields you need in the request body. Entities that already validate their required fields behave as before.

### FIX-0709-6: releasing a server lock accepts an empty JSON body

**Before**

[DELETE /v1/infra/servers/:id/lock](/docs/infra) with a `Content-Type: application/json` header and an empty body returned `400` (empty JSON body). Releasing a stuck lock required sending an explicit `{}`, and a client unaware of that hit a dead end.

**After**

An empty body with that header is accepted as `{}`; a bodyless request works and releases the lock. An explicit `{}` still works too.

### FIX-0709-7: portal events now wake a sleeping galaxy app

**Before**

A Bitrix event sent to a sleeping galaxy app's subscription did not wake it — delivery retried and
was lost after the attempts ran out.

**After**

The platform wakes a sleeping galaxy app on event delivery and delivers the event once it is up —
same as for a standalone server.

### NEW-0709-8: camelCase keys inside communications when creating an activity

[POST /v1/activities](/docs/entities/activities/create) now accepts the nested keys of `communications` items in camelCase (`type`, `value`, `entityTypeId`, `entityId`) — consistent with the rest of the API. Previously the nested keys were accepted only in Bitrix24 UPPER case (`TYPE`, `VALUE`, `ENTITY_TYPE_ID`, `ENTITY_ID`), and the camelCase form was silently dropped — `communications: [{ "type": …, "value": … }]` returned `422` "COMMUNICATIONS is not defined or invalid", while `[{ "TYPE": …, "VALUE": … }]` created the activity. UPPER case still works; if both forms of the same key appear in one object, the UPPER-case one wins.

### FIX-0709-9: unpublishing removes the tab across all of the app's handlers

**Before**

[POST /v1/apps/:id/unpublish](/docs/apps/unpublish) unbound the placement only by the handler matching the assumed platform address. If the tab was bound to the app's own technical address, Bitrix24 did not find it and did not remove it — the tab stayed stuck in the CRM card and could no longer be removed via the API.

**After**

Unpublishing now removes the placement across all of the app's handlers, including one bound to the server's technical address — the orphaned tab disappears.

### NEW-0709-10: Key self-description in GET /v1/guide

The `GET /v1/guide` response now carries a `data.keysAuth` block. It describes the two self-description endpoints — `GET /v1/me` and `GET /v1/guide` — and links to their documentation: the response contract of each, the key access mode, and the overview of key types.

Both endpoints work with the `X-Api-Key` header alone, no session token is required.

The field is additive: existing clients are unaffected.

**Affected endpoints:** [GET /v1/guide](/docs/keys-auth/guide)

### FIX-0709-11: GET /v1/{entity}/:id now honours ?select=

The `?select=` field projection was ignored on single-record reads by id: the response always came back with every field, even though `/v1/me` states `?select=` works "on list, get by id, and POST /search". Get-by-id now projects the response the same way list and search do, bringing the behaviour in line with what was documented.

**Before**

`GET /v1/leads/42?select=id,title` returned the full record (all fields).

**After**

`GET /v1/leads/42?select=id,title` returns only `id` and `title`. The comma (`?select=id,title`), array (`?select[]=id&select[]=title`) and indexed (`?select[0]=id&select[1]=title`) forms are all accepted; `id` is always included in the response. An unknown field name is silently skipped — it is not an error. When `?select=` and `?include=` are combined, the included relation is preserved in the response. The indexed form (`?select[0]=…`) previously returned 500 on the list endpoint `GET /v1/{entity}` too — it now projects correctly there as well.

### FIX-0709-12: /fields for orders, basket items, requisite presets and document templates now matches the real B24 contract

**Before**

`GET /:entity/fields` (and the OpenAPI schema generated from it) declared fields that Bitrix24 never returns: `provider` on document templates; `reserved`, `sumPaid`, `dateBill`, `datePayBefore`, `datePaid`, `empPaidId`, `userEmail`, `userName` at the order top level; `module`, `fUserId`, `lid`, `dateRefresh`, `subscribe`, `reserved`, `reserveQuantity` on basket items; `originatorId` on requisite presets. Filtering and sorting by those fields silently did nothing. Meanwhile real fields were left undeclared: `requisiteLink` on the order, `type`/`properties`/`reservations` on the basket item. Requisite presets accepted `countryId`/`entityTypeId` on update, where Bitrix24 silently ignores them.

**After**

The non-existent fields are removed from `/fields` and OpenAPI. The real fields are declared: on the order — `requisiteLink` (object `requisiteId`/`bankDetailId`/`mcRequisiteId`/`mcBankDetailId`, read-only); on the basket item — `type`, `properties`, `reservations` (read-only). On requisite presets, `countryId` and `entityTypeId` are marked create-only: an update returns `400 READONLY_FIELD` instead of a silent no-op.

**Impact on integrators**

list/get responses are unchanged — the removed fields were never returned. If a request filtered or sorted by a removed field it now returns `400` — use the real fields from `/fields` (for example, an order's payment dates and sums live inside the `payments` array, not at the top level). Updating `countryId`/`entityTypeId` on a requisite preset is now explicitly rejected — set those fields on create only.

**Affected endpoints:** [GET /v1/orders/fields](/docs/entities/orders/fields), [GET /v1/basket-items/fields](/docs/entities/basket-items/fields), [GET /v1/requisite-presets/fields](/docs/entities/requisite-presets/fields), [GET /v1/doc-templates/fields](/docs/entities/doc-templates/fields)

### NEW-0709-13: GET /:entity/fields returns human-readable labels and descriptions for deals, leads, invoices, activities, statuses and timeline comments

The response of `GET /v1/deals/fields`, `/v1/leads/fields`, `/v1/invoices/fields`, `/v1/activities/fields`, `/v1/statuses/fields` and `/v1/timelines/fields` now carries a human-readable `label` and `description` for every field, returned in English. Fields with magic-number codes gained `enum` dictionaries: deals — `stageSemanticId` (P — in progress, S — success, F — failure); activities — `typeId`, `direction`, `priority`, `status`, `notifyType` and `descriptionType`. Existing calls keep working unchanged — these are additive metadata fields, the response shape does not change.

### FIX-0709-14: POST /v1/batch — unified sub-call error shape and totals only for list/search

**Before**

A Bitrix24 error inside a successful 200 [POST /v1/batch](/docs/batch) response (for example, a `get` of a missing element) landed in `data.errors[<id>]` in Bitrix24's native `{ error, error_description }` form — not the V1 envelope `{ code, message }` used by validation errors and every other API response. Meanwhile `data.totals[<id>]` was populated for any action, including `get`/`create`/`update`/`delete`, where a lone number next to a single record is meaningless.

**After**

A sub-call error is mapped to `{ code, message }` (`error` → `code`, `error_description` → `message`), consistent with all other errors. `data.totals[<id>]` is populated only for `list` and `search` actions, where a match count actually has meaning.

### FIX-0709-15: GET /v1/openline-configs — empty-value normalization in the response

**Before**

[GET /v1/openline-configs](/docs/openlines/config/list) and [GET /v1/openline-configs/:id](/docs/openlines/config/get) returned service fields in shapes awkward for clients: `KPI_FIRST_ANSWER_LIST`, `KPI_FURTHER_ANSWER_LIST`, `DEFAULT_OPERATOR_DATA` came back as `null` (client `.map`/`.length` broke on them); `AUTO_CLOSE_TEXT` for an unset value came back as `""` in the card but `null` in the list; `WORKTIME_HOLIDAYS`/`WORKTIME_DAYOFF` for an empty set came back as `[""]` (an array with one empty string).

**After**

Lists are normalized: `null` → `[]`. `AUTO_CLOSE_TEXT` converges on a single `null` for an unset value in both the list and the card. `WORKTIME_HOLIDAYS`/`WORKTIME_DAYOFF` return `[]` for an empty set. The list and the card now return the same shape for these fields.

### FIX-0709-16: GET /v1/users/fields returns the possible values (items) for enumeration user-fields

**Before**

A custom enumeration user-field (`UF_USR_*` of the "list" type) came back from `GET /v1/users/fields` as `{ "type": "string", "label": "…" }` — with no list of possible values. Reason: for UF fields the `user.fields` method returns only a label, not the type or the variants, so an enumeration was indistinguishable from a string.

**After**

Such a field now carries its real type and the list of variants: `{ "type": "enumeration", "label": "…", "items": [ { "ID": "…", "VALUE": "…", "DEF": "…", "XML_ID": "…" }, … ] }`. The values are read from `user.userfield.list` — the key needs the `user.userfield` scope for this; without it the field is still returned with its label but without `items` (graceful degradation). Other UF fields (`money`, `date`, …) likewise now show their real type instead of `string`.

### FIX-0709-17: the empty-queue hint now appears after sustained emptiness

**Before**

[GET /v1/bots/:botId/events](/docs/bots/events/polling) incremented the empty-response counter on exactly every request, and the `hint` field appeared strictly after the fifth consecutive empty response. The number in the hint text matched the number of requests made.

**After**

The empty-response counter is updated periodically rather than on every request, so `hint` appears after the queue stays empty for a sustained period — at the recommended 2–5 second polling interval, after roughly a couple of minutes of continuously empty polling. The number N in the text reflects the count of observed empty periods, not the exact number of requests made. The "an event was delivered → counter and hint reset" rule is unchanged.

**Impact on integrators**

No code change is needed. If you relied on `hint` appearing strictly on the fifth request or read N as an exact request count — use `persisted` and the presence of events as the primary signal, and treat `hint` as a diagnostic hint.

## 2026-07-08

### FIX-0708-1: GET /v1/doc-templates and POST /search now honour order and offset

**Before**

The `order` and `offset` parameters on `GET /v1/doc-templates` and `POST /v1/doc-templates/search` were silently ignored: the list always came back sorted ascending by `id`, and `offset` did not move the window. The underlying Bitrix24 method returns templates as an id-keyed object, and the requested order was lost while unwrapping the response.

**After**

Sorting (`order[field]=asc|desc`, including multi-field) and windowing (`offset`/`limit`) are applied on the Vibecode side: the full template set is fetched, sorted, and sliced to the requested window. `total` and `hasMore` are computed from the actually collected set.

**Impact on integrators**

If you relied on the implicit ascending-by-`id` order at `offset=0` with no sort, nothing changes — that stays the default. `POST /v1/doc-templates/batch` (batch list) is unaffected. String order (`name`, `region`) is byte-wise, not locale-aware.

### NEW-0708-2: GET /v1/apps/:id/sources — new linkedServerSources field

[GET /v1/apps/:id/sources](/docs/source-storage) now additionally returns a `linkedServerSources` field — source versions stored under a server (via `POST /v1/infra/servers/:id/sources` or auto-save on deploy), grouped by server, each with its own `serverContext`. Such versions previously did not appear in this response when saved under a personal key — they are now visible.

The field is additive: `versions`, `totalVersions`, `currentVersionId` and `totalSizeBytes` are unchanged and still list only app-scoped versions. Alongside it come `linkedServerSourcesTruncated` (a truncation flag for very large histories) and `linkedServerHint`, which points to `GET /v1/infra/servers/:serverId/sources` — the authoritative full list and download for those versions. The section is populated for the app author (personal key) and the portal administrator; it is empty when called with an OAuth-app key, and is not computed on a `?sha256=` probe.

### NEW-0708-3: preemptible field on the server plans list response

The [GET /v1/infra/providers/:id/plans](/docs/infra/providers/plans) response now formally documents the `preemptible` field on each plan. A preemptible plan is cheaper, but the cloud force-restarts such a machine roughly once a day — it is not suitable for continuous 24/7 workloads. For a server, agent or bot that must run without interruption, pick a non-preemptible plan (`preemptible` is `false` or absent).

The field was already returned by the runtime — this entry formalizes it in OpenAPI and the docs; no change to existing integrations is required.

### NEW-0708-4: GET /v1/models/:id now reports a retired model's successor price and a replaced_by field

For a model that has been retired, the by-id detail request now returns a `replaced_by` field with the identifier of the successor model that actually serves the calls, and the `pricing` field shows that successor's price — the price the request is billed at. Previously `pricing` showed the retired row's own zero price, making the model look free while a paid successor served the calls. Ordinary models and existing calls are unchanged.

### FIX-0708-5: list auto-pagination preserves row order for limits above 550

**Before**

Auto-paginated list reads — `GET /v1/{entity}?limit=…` and `POST /v1/{entity}/search` — returned rows out of order when `limit` exceeded ~550: internal result pages were merged in the wrong sequence relative to what Bitrix24 returned, so the `order` parameter was not honored across the final array. When the collection held more rows than `limit`, the window trim could drop rows from the middle of the sorted set while keeping later ones.

**After**

Pages are merged strictly in Bitrix24 return order: rows arrive in the requested sort for any `limit`, and the `limit` trim no longer drops rows from the middle of the set because of wrong merge order.

**Impact on integrators**

No changes required. If you re-sorted large result sets on your side as a workaround, that is no longer needed.

### FIX-0708-6: auto-pagination no longer silently drops a page when a batch sub-request fails

**Before**

With `limit > 50` the list is assembled from batch sub-requests of 50 records each. If Bitrix24 rejected one sub-request (most often on its request limit — `QUERY_LIMIT_EXCEEDED`), that page silently fell out of the middle of the set: the response stayed `200` with an undetectable 50-record hole in the data (e.g. records 1–200 and 251–600 without 201–250), while `meta.total` and `meta.hasMore` looked self-consistent.

**After**

For lists, plain search, batch sub-calls and aggregations the result is always a contiguous prefix of the set: records past the failed page are dropped, `meta.hasMore` stays `true`, and the response carries `meta.pageErrorSample { code, message }` with the failure reason — mirroring windowed search's `meta.windowErrorSample`. The field is added to list responses (e.g. [GET /v1/deals](/docs/entities/deals/list)), to `POST /v1/{entity}/search` (e.g. [deals](/docs/entities/deals/search)), to per-sub-call `meta` of [POST /v1/batch](/docs/batch) and to `data.meta` of `POST /v1/{entity}/aggregate` (where it explains why `recordsProcessed` is below `totalRecords`). In windowed search (a wide date range) the set is assembled from windows, so a page lost inside one window can leave that window's tail missing — there the incompleteness signal is `meta.pageErrorSample` itself, not `meta.hasMore`. In all cases the field appears only when the returned page is actually shorter than `limit`: a full page is never flagged with a false alarm. Incomplete responses are never cached: a retry goes straight to Bitrix24.

**Impact on integrators**

No client changes required: result sets that could previously contain an invisible hole are now correct, and the shortfall reason is visible in `meta.pageErrorSample`. Fetch the remainder by retrying with `offset` equal to your original `offset` plus the number of records received — except for windowed searches over a wide date range (`offset` is not supported there: narrow the range or retry later).

### BC-0708-7: structured output: a truncated or empty result now returns 422 instead of an empty 200

> Old format supported until: 08.07.2026

**Before**

[POST /v1/chat/completions](/docs/ai/chat/completions) with `response_format` (`json_object` or `json_schema`) could return `200` with `content: null` (or a truncated, unparseable JSON string) plus a warning that clients ignored, when generation was cut off. This happened most often on reasoning models: the reasoning phase consumed the whole `max_tokens` budget before the model produced the JSON. The response looked successful but could not be parsed.

**After**

Such a request now returns `422` with `code: "structured_output_truncated"`, plus `finishReason`, `suggestedMaxTokens` (a larger `max_tokens` to retry with), and `param: "max_tokens"`. In streaming mode a `{"error":{"code":"structured_output_truncated"}}` frame is emitted before `data: [DONE]` — read the stream through to `[DONE]`. Additionally, for free reasoning models given a too-small `max_tokens` the platform raises the budget to a safe minimum and tags the successful response with a `MAX_TOKENS_RAISED` warning. The truncated attempt still consumes and bills tokens.

**What integrators should do**

Handle `422 structured_output_truncated` in your error branch and retry with a larger `max_tokens` (you can use the `suggestedMaxTokens` value). For strictly deterministic JSON, set a generous `max_tokens` or use a non-reasoning model.

## 2026-07-07

### FIX-0707-1: smart-processes: linkedUserFields accepts Y/N and boolean values

**Before**

[POST /v1/smart-processes](/docs/entities/smart-processes/create) and [PATCH /v1/smart-processes/:entityTypeId](/docs/entities/smart-processes/update) with `linkedUserFields` only worked when the flag value was strictly `"true"`/`"false"`. A value in the `"Y"`/`"N"` convention (used by every other smart-process field) or a boolean `true`/`false` was silently ignored: the request returned `success: true`, but the display in the user field was not enabled.

**After**

`linkedUserFields` values are normalized the same way as the nested `relations[].isChildrenListEnabled` flag: `true`/`"Y"`/`"yes"`/`1` → enabled, `false`/`"N"`/`"no"`/`0` → disabled. Existing calls with `"true"`/`"false"` keep working unchanged.

**Impact on integrators**

No action needed — calls that previously "silently did nothing" with `"Y"` are now applied correctly.

### BC-0707-2: Order-card nested fields normalized

> Old format supported until: 06.01.2027

**Before**

`GET /v1/orders/{id}` returned the nested `clients`, `payments`, `basketItems` arrays in raw Bitrix24 shape: boolean fields as `"Y"` and `"N"` strings (`payments[].paid`, `clients[].isPrimary`, `basketItems[].vatIncluded`, and others), dates inside `payments` and `basketItems` with a `+03:00` offset, and `companyId` set to `0` when no company is bound. The `accountNumber` field was silently ignored on create and update.

**After**

Nested Y/N fields now arrive as boolean (`true` or `false`); nested dates are normalized to UTC (ending in `Z`); `companyId` is `null` when no company is bound instead of `0`; `accountNumber` became read-only — sending it on create or update returns `400` with code `READONLY_FIELD`.

**What integrators should do**

Read nested Y/N fields as boolean instead of comparing to the string `"Y"`; treat `null` instead of `0` as "no company bound"; stop sending `accountNumber` in the create and update body — the number is assigned automatically.

### FIX-0707-3: The /v1/openapi.json spec now matches actual runtime

**Before**

The machine OpenAPI spec was generated from static entity metadata and drifted from real responses: no field was marked nullable, nested order-card arrays were typed as a string, list methods were missing `filter` and `select`, and operations declared only success codes and `403`.

**After**

The spec now reflects the contract. Nullable fields are emitted as `type: ["<type>", "null"]`. Object and array-of-object fields are typed honestly, including the nested `clients`, `payments`, `basketItems`, `propertyValues` of `GET /v1/orders/{id}`. List methods declare the `filter` and `select` query params. Operations carry the standard error codes `400`, `401`, `404`, `422` in the single `{ success:false, error:{ code, message } }` envelope. `*Input` schemas declare create-required fields. Additionally `GET /v1/orders/fields` returns `clients` as an array instead of object. An SDK generated from the spec now types responses correctly.

### BC-0707-4: /search: auto-windowed search now returns the real Bitrix24 error on total failure

> Old format supported until: 07.09.2026

**Before**

Any failed auto-windowed `POST /v1/{entity}/search` returned `502 { "error": { "code": "WINDOWED_SEARCH_FAILED" } }` with a generic "add autoWindow:false".

**After**

The response matches the same query at a narrow range — the real code and message: a rejected filter/sort field → `400 UNKNOWN_FILTER_FIELD` / `400 INVALID_PARAMS`; no access → `403`; request limit / queue overload → `429` + `Retry-After`; timeout → `503`; Bitrix24 unavailable → `502 BITRIX_UNAVAILABLE`. Partial window failure (status `200`) now carries `meta.windowErrorSample` `{ code, message }`.

**What integrators must do**

If you branched on `error.code === "WINDOWED_SEARCH_FAILED"` (e.g. to retry with `autoWindow:false`) — branch on the real codes instead. The `autoWindow:false` workaround remains; it is useful where it actually helps (the `429 QUEUE_TIMEOUT` hint names it). The total-failure response no longer carries the `meta` block (`autoWindowed`/`windowCount`/`windowErrors`) — the signal is now in the error code/message itself; `meta.windowErrorSample` remains on partial failure (status `200`).

### FIX-0707-5: lists on a portal without the module return 409 consistently, not 429

**Before**

On a portal where the Universal Lists module is disabled, calls to [/v1/lists](/docs/lists) returned the clear `409 LISTS_MODULE_NOT_ENABLED` only for the first few requests. After that the built-in error-loop protection tripped and every subsequent call returned `429 ERROR_LOOP_DETECTED`, hiding the real cause (the module is not installed).

**After**

The "method unavailable on this portal" signal is no longer counted by the error-loop protection, so `lists.*` calls on a module-off portal return `409 LISTS_MODULE_NOT_ENABLED` consistently no matter how many times they repeat. The response stays actionable: enable the module on the portal and retry.

### NEW-0707-6: error.hint on the 400 for a server create missing source and provider/plan/region

[POST /v1/infra/servers](/docs/infra/servers/create), when rejected with `400 INVALID_REQUEST` because `provider`/`plan`/`region` are missing (and no `source` was passed) on a galaxy-placement portal, now additionally returns an `error.hint` object with `reason` (why the request was rejected on this portal), `recovery` (the recommended one-shot path plus the working two-step alternative) and `example` (a paste-ready one-shot body skeleton). `error.code` and `error.message` are unchanged — the hint is strictly additive; portals without galaxy placement get the previous response, without `hint`.

The hint is also returned on `400 RUNTIME_PARAM_REMOVED` (a create with `runtime` but no `source` on a galaxy-placement portal), and a body carrying `placement: "dedicated"` gets a separate hint variant — for a dedicated server, keeping the intent and adding the missing `provider`/`plan`/`region` tuple, instead of steering the caller into a galaxy container.

### FIX-0707-7: Galaxy deploy checklist in /v1/me now matches the actual contract

**Before**: step 2 of `deployment.galaxyApp.checklist` instructed [POST /v1/infra/servers](/docs/infra/servers/create) `{ name }` with no `source` and no `provider`/`plan`/`region` — that call always failed with `400 INVALID_REQUEST`. The CREATE rule did not explain that the two-step path requires the full `provider`/`plan`/`region` tuple, and `newAppPlacement.note` promised a dedicated standalone VM where the create actually returns a galaxy slot with `next: "deploy"`. The favicon guide pointed to the same broken order; the never-deployed-slot reap window was stated as "~12-20 min" versus the actual ~20-25.

**After**: the recommended path is a single call — `POST /v1/infra/servers { name, source, runtime, start }` (omit `provider`/`plan`/`region`). The two-step path is documented truthfully: a create without `source` requires the full `provider`/`plan`/`region` tuple (values are informational for a galaxy — the app inherits its host), and on galaxy placement returns a slot with `next: "deploy"`; a never-deployed slot is reaped to ERROR after ~20 min (swept every 5 min). Favicon: the primary path is a self-hosted `/icon.svg` inside the archive (no id needed); the platform-hosted URL remains an alternative via the two-step order or a re-deploy. The status-polling step gained a `status=error` branch → `provisionError`/`buildLog` → re-deploy.

**Impact on integrators**: agents following the checklist now deploy on the first call. Endpoint behavior is unchanged — only the `/v1/me` texts and the `/v1/openapi.json` description were updated; existing integrations keep working as is.

### NEW-0707-8: Company AI quota is available via the API

The new [GET /v1/ai/quota](/docs/ai/consumption/quota) endpoint returns the monthly AI quota state of your Bitrix24 account: the percentage of the limit consumed (`pctUsed`, an honest value — above 100 on overspend), the exhaustion flag (`exhausted`), the reset date (`resetAt`, a rolling 30-day window), and a per-model breakdown — request counts, tokens, and each model's share of the monthly limit (`byModel[].pctOfLimit`). Absolute limit values in Vibe credits are not exposed — percentages only, same as the dashboard. Requires the `vibe:ai` scope.

## 2026-07-06

### NEW-0706-1: pricing.perCall and pricing.perMinute fields in the model catalog

[GET /v1/models](/docs/ai/models/list) and [GET /v1/models/{model}](/docs/ai/models/get) responses gain optional fields in the `pricing` object: `perCall` — the cost of a single request in Vibe credits, `perMinute` — the cost of one minute of audio in Vibe credits (for speech-to-text models). The fields appear only for models whose corresponding base price is above zero; for all other models the `pricing` object is unchanged — existing requests keep working as before.

### NEW-0706-2: New 402 error code ai_quota_exhausted on AI endpoints

When the portal monthly AI quota control is active, [POST /v1/chat/completions](/docs/ai/chat/completions), [POST /v1/embeddings](/docs/ai/embeddings) and [POST /v1/audio/transcriptions](/docs/ai/audio/transcriptions) may return 402 with `{ success: false, error: { code: "ai_quota_exhausted", type: "insufficient_quota", reason, resetAt? } }`. The `reason` field distinguishes three cases: `breaker` — the hourly over-quota spending limiter fired, `wallet_empty` — the quota is exhausted and the portal balance has no funds, `wallet_off` — over-quota usage is not available for this portal. `resetAt` is when requests will pass again (may be absent for a permanently disabled portal). While the portal quota is not exhausted, endpoint behavior is unchanged.

### FIX-0706-3: Over-quota AI usage is charged at the model's base catalog price

**Before**

With the portal monthly AI quota control active, over-quota requests to [POST /v1/chat/completions](/docs/ai/chat/completions), [POST /v1/embeddings](/docs/ai/embeddings) and [POST /v1/audio/transcriptions](/docs/ai/audio/transcriptions) were charged to the portal balance at internal quota-program rates — with off-peak discounts applied; the effective price was not visible in the model catalog.

**After**

Over-quota usage is charged at the model's base price from the public catalog — the same one returned in the `pricing` field of [GET /v1/models](/docs/ai/models/list), including the new `perCall` and `perMinute` for non-token models. Off-peak discounts apply only to quota consumption, not to the money balance. Usage within the quota is still not charged to the portal balance.

**Impact on integrators**

No changes required. The cost of over-quota usage can now be computed upfront from the model's catalog price.

### NEW-0706-4: The bitrix/embeddings model is available in the API

The [POST /v1/embeddings](/docs/ai/embeddings) endpoint is now served by the `bitrix/embeddings` model — turning text into vector representations for semantic search, clustering, and retrieval (RAG). The model is free and billed on input tokens only. For the list of embedding-capable models see [GET /v1/models](/docs/ai/models/list).

### FIX-0706-5: Deploy reports an honest error when the new build did not bind the port

Deploy via [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy) now verifies the port is held by the NEW service. If a previous process keeps listening and the new build crash-loops with EADDRINUSE, the deploy fails honestly instead of falsely succeeding; a port held by a leftover process of the same app is freed automatically where provable.

**Before**

The old version kept answering `200`, the deploy reported success, and the new build never came up — with no error and no hint.

**After**

The `healthcheck` step returns an error naming EADDRINUSE and the port, and the `stop_existing` step frees the port from a leftover process of the app (or warns and proceeds when it cannot).

### NEW-0706-6: filter: $nin (NOT IN) operator to exclude a set of values

**Before**

There was no way to select records whose field is NOT in a set of values: the `$in` (IN) operator existed, but its inverse did not. The Bitrix24-native field-name prefixes `@` (IN) and `!@` (NOT IN) (`{ "!@categoryId": [1, 3] }`) were not translated — such a deal filter returned `400 UNKNOWN_FILTER_FIELD`.

**After**

Added the `$nin` operator: `{ "filter": { "categoryId": { "$nin": [1, 3] } } }` returns records of every value except those listed (NOT IN). Symmetric to `$in`. The Bitrix24-native `@` / `!@` field-name prefixes are still unsupported, but now return a clear `400 INVALID_FILTER_FIELD` hinting to switch to `$in` / `$nin`, instead of a confusing error or a silently-ignored (thus full-set) filter.

### BC-0706-7: dedicated error code for an oversized exec command

> Old format supported until: 06.01.2027

**Before**

A command longer than 10000 characters at [POST /v1/infra/servers/:id/exec](/docs/infra/deploy/exec) was rejected with the generic `VALIDATION_ERROR` code, with no cause and no way out.

**After**

Such a request returns 400 with the dedicated `COMMAND_TOO_LONG` code and a structured `hint`: ship large payloads and scripts via [POST /v1/infra/servers/:id/upload](/docs/infra/deploy/upload), then run them with `bash /path/script.sh`. All other schema violations still return `VALIDATION_ERROR`.

**What integrators should do**

If your client handles `VALIDATION_ERROR` of this endpoint as the catch-all validation case — add handling for the `COMMAND_TOO_LONG` code (or treat any 400 uniformly).

### NEW-0706-8: hint in the exec timeout error

The `EXEC_TIMEOUT` error of [POST /v1/infra/servers/:id/exec](/docs/infra/deploy/exec) now carries a structured `hint` field (`reason` / `recovery` / `recoveryAction`): why the process was stopped (at `timeout` the whole process group is terminated forcibly, with no grace period) and what to do — run long operations as a detached background job and monitor it via [GET /v1/infra/servers/:id/logs](/docs/infra/deploy/logs), raise `timeout` up to 600 seconds, or use `?stream=true`. The field is additive: the previous `code` / `message` shape is unchanged, and the hint arrives both in JSON mode and in the SSE `error` event.

## 2026-07-05

### FIX-0705-1: Chat message send — a clear error when the text is empty

The message text goes in the `message` field. Previously a call to [POST /v1/chats/{dialogId}/messages](/docs/chats/messages/send) that carried the text under an unknown field name (e.g. `{"text": "hi"}`) silently dropped that field, and Bitrix24 returned a `422 BITRIX_ERROR` about an empty message — even though content was supplied.

**Before**

`{"text": "hi"}` → `422 BITRIX_ERROR` about an empty message, with no hint at the cause.

**After**

The same call now returns `400 MESSAGE_REQUIRED` right away and lists the unrecognized field(s), pointing at the `message` field. Empty text is still allowed together with an `attach` block (attachment-only message).

**Impact on integrators**

Correct calls that use the `message` field are unchanged. The error for a wrong field name is now precise.

### FIX-0705-2: API key in the Authorization: Bearer header — a clear error instead of INVALID_SESSION

The API key goes in the `X-Api-Key` header. Previously, if the key was mistakenly placed in `Authorization: Bearer` (that slot is for an OAuth app session token), the server returned `401 INVALID_SESSION`, so integrators chased an OAuth-session problem when the real cause was the wrong header.

**Before**

A `vibe_app_*` key in `Authorization: Bearer` → `401 INVALID_SESSION`.

**After**

The same request now returns `401 WRONG_AUTH_SCHEME` with a hint: an OAuth app key (`vibe_app_*`) goes in `X-Api-Key`, and `Authorization: Bearer` carries the session token (`vibe_session_*`) from [POST /v1/oauth/token](/docs/keys-auth); a client that can only send Bearer can use a personal key (`vibe_api_*`). Session tokens and personal keys in Bearer are unaffected.

**Impact on integrators**

Correct calls with the key in `X-Api-Key` and the session in `Authorization: Bearer` are unchanged.

### FIX-0705-3: multipart/create rejects XSS-prone content types for PUBLIC objects

**Before**

For `PUBLIC` objects the content types `text/html`, `application/javascript`, `application/x-javascript` and `image/svg+xml` were rejected on direct and presigned uploads, but not when starting a multipart upload. Calling [POST /v1/storage/objects/multipart/create](/docs/storage) with `visibility` = `PUBLIC` and such a type created a session, and once completed the object was served inline in the browser.

**After**

[POST /v1/storage/objects/multipart/create](/docs/storage) with `visibility` = `PUBLIC` and an XSS-prone content type now returns `415 STORAGE_FORBIDDEN_CONTENT_TYPE`, the same as direct and presigned uploads. No multipart session is opened. `PRIVATE` objects still accept any content type.

**Impact on integrators**

Behavior now matches what the Storage reference documents: XSS-prone content types are not allowed for `PUBLIC` objects on any upload path. To multipart-upload such a file, use `visibility` = `PRIVATE` or a safe content type.

### FIX-0705-4: binding a placement to a server's technical URL now routes through the platform handler

**Before**

POST /v1/placements/bind accepted a `handler` pointing at the app's technical Black Hole URL (app-*.vibecode…) and registered it with Bitrix24 verbatim. Bitrix24 posted the placement iframe straight to that URL, bypassing the platform: no session was minted, and on a server restricted to Bitrix24 users the placement looped on the login gate (on a public server the app returned its own 404 error).

**After**

Such a `handler` is automatically rewritten to the app's platform handler (/v1/bitrix-handler) — the placement opens and authenticates normally. The response gains `handlerRewritten: true` and `requestedHandler` with the original value. External (non-Black Hole) handlers are left unchanged. If the app's platform handler cannot be resolved, the bind is rejected with code `PLATFORM_HANDLER_UNRESOLVABLE` instead of registering an unusable URL. Additionally GET /v1/placements flags an already-misbound handler: `data.handlers[].misbound: true` plus a textual `warnings[]`.

Additionally, if the placement is already registered in Bitrix24 but missing from the app's list (drift — e.g. after an unpublish that didn't unbind on Bitrix24), the bind no longer fails with "Handler already binded": the platform clears the stale binding and retries the request once, healing the drift. If the bind fails for another reason (for example a commercial Bitrix24 tariff is required), the working placement is left in place.

### NEW-0705-5: New 402 error code ai_quota_exhausted on AI endpoints

When the portal monthly AI quota control is active, [POST /v1/chat/completions](/docs/ai/chat/completions), [POST /v1/embeddings](/docs/ai/embeddings) and [POST /v1/audio/transcriptions](/docs/ai/audio/transcriptions) may return 402 with `{ success: false, error: { code: "ai_quota_exhausted", type: "insufficient_quota", reason, resetAt? } }`. The `reason` field distinguishes three cases: `breaker` — the hourly over-quota spending limiter fired, `wallet_empty` — the quota is exhausted and the portal balance has no funds, `wallet_off` — over-quota usage is not available for this portal. `resetAt` is when requests will pass again (may be absent for a permanently disabled portal). While the portal quota is not exhausted, endpoint behavior is unchanged.

## 2026-07-04

### BC-0704-1: Overload rejections: 429/503 instead of 504

> Old format supported until: 31.07.2026

Overload rejections changed HTTP status codes (body codes are UNCHANGED — only the status changes). Rule of thumb: **429** — the request was NOT processed, safe to retry after `Retry-After` (the header is now always set); **503** — the platform or upstream is unhealthy, retry later; for write operations verify whether the change was applied before retrying. Application-level 504 is removed from the API.

**What changed:** `QUEUE_OVERFLOW` 503→429; `QUEUE_TIMEOUT` 504→429; `BITRIX_TIMEOUT` (Bitrix24 did not respond within 15 seconds — the write may have been applied, re-read the entity before retrying) →503; `ai_provider_timeout` 504→503; `UPSTREAM_TIMEOUT` (web search `/v1/search` — the upstream provider did not respond in time) 504→503; `RUNTIME_TIMEOUT` / `GATEWAY_TIMEOUT` / `WAKE_TIMEOUT` 504→503.

### NEW-0704-2: New AI overload code: 429 ai_congested

AI requests to the platform cluster now pass through an admission gate: on pool overload the response is `429` with body code `ai_congested` and a `Retry-After` header. Retry is safe (the request never ran, no charge). BYOK keys and external providers are not affected. Disabled by default — enabled by the platform.

### BC-0704-3: read-only (READONLY) keys can no longer write on hand-written endpoints

> Old format supported until: 03.07.2026

**Before**

An APP key with the `accessMode: READONLY` access mode still reached the write to Bitrix24 on a number of hand-written endpoints (requisites and presets, user fields, timeline pin/note/bind, telephony, mail, disk, business processes, user invite and deactivation, and others) — the guard checked only the scope, not the key access mode.

**After**

Any write attempt with a read-only key returns `403` with the `WRITE_BLOCKED_READONLY_KEY` code. Read endpoints are not affected.

**What integrators should do**

If your integration was writing with a read-only key, switch the key to read and write mode on the key management page.

### FIX-0704-4: galaxy app deploy now unpacks wrapped archives correctly and reports an empty source.content clearly

**Before**

[POST /v1/infra/servers/:id/deploy](/docs/infra/deploy) for a galaxy app (`kind=GALAXY_APP`) whose `source.content` project files sat inside a single wrapping folder (a typical macOS-built zip) built the app with an empty build context and failed at runtime with `npm error enoent Could not read package.json`. When `source.content` did not unpack into any files at all (a versionId, a path, or an empty archive), the deploy raised the same confusing build error.

**After**

Such archives now deploy correctly — the app files end up at the build-context root. And when `source.content` unpacks to an empty context, the deploy returns `GALAXY_APP_BUILD_FAILED` right away with a clear empty-build-context message, hinting that `source.content` must be a base64 archive (tar.gz or zip) of your project files, not a versionId or a path.

## 2026-07-03

### FIX-0703-1: binding a placement via a developer key no longer returns 500 after an unpublish/republish cycle

**Before**

`POST /v1/placements/bind` for a developer-key-managed app (type `local.*`) could consistently return `500` (`BITRIX_UNAVAILABLE`, `INTERNAL_SERVER_ERROR`) when binding a placement (e.g. `CRM_DEAL_DETAIL_TAB` or `CRM_CONTACT_DETAIL_TAB`) after the app was unpublished and published again. The previous placement registration remained on the Bitrix24 side, and registering a new one on top of it failed with an internal error. Retries returned the same error.

**After**

Before registering the placement, the request now clears its previous registration on the Bitrix24 side, so the bind succeeds even after an unpublish/republish cycle. No change to your call.

### FIX-0703-2: /v1/sites — filtering by knowledge-base (KNOWLEDGE) and group (GROUP) type is no longer ignored

**Before**

`POST /v1/sites/search` (as well as `GET /v1/sites` and `POST /v1/sites/aggregate`) with `filter[type]=KNOWLEDGE` or `filter[type]=GROUP` silently returned ordinary landing sites (`PAGE` / `STORE` / `VIBE`) instead of knowledge bases or group pages. The cause is on the Bitrix24 side: the `landing.site.getList` method binds the `TYPE` filter to an internal area (`scope`), and without the `scope` parameter the `KNOWLEDGE` / `GROUP` types are not part of the default area — so the type filter was silently dropped. The only workaround was to add `scope` manually (see [List sites](/docs/entities/sites/list)).

**After**

When the filter names one such type and `scope` is not passed explicitly, Vibecode supplies the matching area itself (`type=KNOWLEDGE` → `scope=KNOWLEDGE`, `type=GROUP` → `scope=GROUP`) — and the request returns exactly the knowledge bases / group pages. An explicitly passed `scope` always wins and is never overridden. If the type is given as a list or operator (for example `{"type":{"$in":["KNOWLEDGE","PAGE"]}}`), where a single area cannot be chosen, `meta.warnings` carries a hint with code `TYPE_REQUIRES_SCOPE`.

**Impact on integrators**

No action required. Requests with `filter[type]=PAGE` / `STORE` / `VIBE` and requests without a type filter work as before. `MAINPAGE` is an area, not a site type (its sites have type `VIBE`), so `MAINPAGE` is not derived from the type filter. Behavior for `/v1/pages` is unchanged.

### FIX-0703-3: page and site aggregation returns count again

**Before**

[POST /v1/pages/aggregate](/docs/entities/pages/aggregate) and [POST /v1/sites/aggregate](/docs/entities/sites/aggregate) returned `count: 0` and `meta.totalRecords: 0` even when pages and sites existed — in every form: without a filter, with a filter, with a `count` expression, and as the top-level `count` alongside `groupBy`. Per-group counts under `groupBy` were already correct.

**After**

`count` and `meta.totalRecords` reflect the actual number of records; the top-level `count` under `groupBy` equals the sum of the group counts.

**Integrator impact**

No action required — the response is now correct.

### NEW-0703-4: Quality and timestamp parameters in audio transcription

Audio transcription [POST /v1/audio/transcriptions](/docs/ai/audio/transcriptions) accepts five new optional fields. Recognition quality: `prompt` — a context hint (conversation topic, style, correct spelling of terms, up to 2000 characters), `hotwords` — a comma-separated list of special words (rare terms, brands, names, up to 500 characters), `vad_filter` — a silence filter applied before recognition (fewer hallucinations on recordings with pauses). Output control: `temperature` — decoder temperature from 0 to 1, `timestamp_granularities[]` — timestamp granularity `word`/`segment` (only with `response_format=verbose_json`; with `word`, each segment gains a `words` array with per-word timing and probability). The fields are passed in `multipart/form-data` alongside `file` and are compatible with the OpenAI contract. Invalid values are rejected with the `invalid_prompt`, `invalid_hotwords`, `invalid_temperature`, `invalid_vad_filter`, and `invalid_timestamp_granularities` codes.

### FIX-0703-5: Apps created via the API now open correctly as placements

**Before**

Some apps created via [POST /v1/apps](/docs/apps/create) failed to open when their placement was invoked in Bitrix24 — instead of the app interface the user saw an app-resolution error.

**After**

Created apps now resolve correctly and open as placement widgets in Bitrix24. The create response is unchanged — the app is immediately ready for publishing and binding placements.

**Integrator impact**

No action required. Recreate a previously non-opening app (delete and create it again) — the new app opens correctly.

### NEW-0703-6: key deletion is blocked while an agent or bot is linked

[DELETE /v1/keys/:id](/docs/keys-auth) now returns `409` with code `KEY_HAS_LINKED_AGENT` when the key is the control key of a live AI agent or managed bot.

**Before**

Deleting such a key orphaned the agent and cascade-deleted the bot together with its token — the bot's Bitrix24 identity was lost irrecoverably, with no warning.

**After**

Response body: `{ success: false, error: { code: "KEY_HAS_LINKED_AGENT", message, details: { linkedAgentCount, linkedBotCount, agents: [{ id, name, status }] } } }`. Rebind the resources to another key or delete the agent/bot first; to restore access for an orphaned agent, use the "Restore access" action in the cabinet. The check runs before Bitrix24 synchronization — on a `409` the Bitrix24-side credentials are left untouched. Sibling of the existing `KEY_HAS_ACTIVE_SERVERS`.

### NEW-0703-7: Task change history and kanban stages

Two read-only endpoints added (scope `task`). `GET /v1/tasks/:taskId/history` returns a task's full change history in a single call: kanban stage moves, sprint and backlog moves, statuses and other events. Filter by event type via `?field=STAGE` (comma-separate several, e.g. `?field=STAGE,MOVE_TO_SPRINT`); sort via `?order=asc` or `?order=desc` (created-date ascending by default). Each entry carries `id`, `createdDate`, `field`, a `value` object with the previous and new value, and a `user` with the author id. `GET /v1/tasks/stages/:entityId` returns the current kanban columns of a workgroup (`N`) or personal plan (`0`).

### FIX-0703-8: bizproc-activities: clear error instead of "Wrong handler URL" when handler is missing

**Before**

POST /v1/bizproc-activities without a `handler` field (or with the handler URL mistakenly sent under `handlerUrl`) returned the opaque core error `422 BITRIX_ERROR: Wrong handler URL`.

**After**

`code`, `name`, and `handler` are validated before the Bitrix24 call: a missing `handler` now returns `400 MISSING_REQUIRED_FIELDS` with the message `Body field "handler" is required to create bizprocActivity`, pointing at the correct field name. Successful calls that include a valid `handler` are unaffected.

### FIX-0703-9: POST /v1/batch — update and delete responses are now normalized like create and get

**Before**

In the global batch call [POST /v1/batch](/docs/batch) an update sub-call returned its response in a raw wrapper (a nested object instead of the flat record), and a delete sub-call returned an empty array with no success signal. This diverged from create and get in the same endpoint and from single PATCH /v1/{entity}/:id, which return a flat normalized record.

**After**

An update sub-call returns the flat normalized record (camelCase fields) — like create, get and single PATCH. A delete sub-call returns a success signal of the form { id, deleted: true }.

### FIX-0703-10: POST /v1/{entity}/batch — create, update and delete work again for CRM entities

**Before**

A per-entity batch call to [POST /v1/{entity}/batch](/docs/batch) with a create, update or delete action for deals, contacts, companies, leads, quotes and invoices returned a per-item error "Could not find value for parameter {entityTypeId}", and the record was not created, updated or deleted. Single calls (POST /v1/{entity}) and the global POST /v1/batch worked on the same entities.

**After**

Per-entity batch create, update and delete for these entities now succeed — the same way single calls and the global batch endpoint do.

### FIX-0703-11: Omitting the model field in chat again falls back to the default model

**Before**

[POST /v1/chat/completions](/docs/ai/chat/completions) without a `model` field returned `400 no_default_model` on portals where no default model was explicitly set — even when a free model was available on the portal. Meanwhile `GET /v1/me` could report a `defaultModel` that could not be called.

**After**

When the `model` field is omitted, the request automatically uses the portal's first callable model — as the docs describe. `GET /v1/me` now always reports a callable `defaultModel`, the same one chat will use.

### FIX-0703-12: include of related entities again returns the entities themselves, not null

**Before**

`GET /v1/deals/:id?include=contact,company` returned `_included.company` = `null` and `_included.contacts` as relation metadata only (`sort`/`isPrimary`/`roleId`) without the entity's own fields, even though the deal referenced an existing company and contacts.

**After**

`_included.company` carries the full company object, and `_included.contacts` carries the full contact objects (`id`, `name`, …) alongside the relation metadata. The fix applies to `include` across CRM entities (`deals`, `leads`, `quotes`, and others).

### FIX-0703-13: deal search rejects an unknown filter field instead of silently returning every row

**Before**

[GET /v1/deals](/docs/entities/deals/list) and [POST /v1/deals/search](/docs/entities/deals/search) with an unknown filter field (a misspelled name, a field not in the schema) silently forwarded it to Bitrix24, which ignores unknown filter keys and returns the whole deal set with a `200`. A client that sent a filter with a typo in the field name got neither an empty result nor a `400`, but the full table — as if the filter had applied.

**After**

An unknown filter field is now rejected before the Bitrix24 call with a `400` and code `UNKNOWN_FILTER_FIELD`, listing the available fields in the message — the same behavior `contacts`, `companies`, `leads`, `quotes`, `invoices`, and `items` already have. Declared fields (including aliases such as `amount`), custom fields (`UF_CRM_*` and `ufCrm*`), and `id` work as before.

### FIX-0703-14: Catalog: listing and search return a clean 400 when iblockId is missing

**Before**

Listing and search [GET /v1/catalog-products](/docs/entities/catalog-products/list), [POST /v1/catalog-products/search](/docs/entities/catalog-products/search), [GET /v1/catalog-sections](/docs/entities/catalog-sections/list) and [POST /v1/catalog-sections/search](/docs/entities/catalog-sections/search) without `iblockId` in the filter reached Bitrix24 and returned a murky `422 BITRIX_ERROR` ("Field iblockId is not specified in the filter").

**After**

Catalog listing and search require `iblockId` in the filter — when it is missing they return `400 MISSING_REQUIRED_FILTER` with an example and never reach Bitrix24.

**Integrator impact**

No change needed — correct requests (with `filter[iblockId]`) work as before. Only the error code and its clarity changed for requests that already failed.

### FIX-0703-15: Contacts — real date fields createdTime/updatedTime instead of phantom createdAt/updatedAt

**Before**

[GET /v1/contacts/fields](/docs/entities/contacts/fields) advertised `createdAt` and `updatedAt`, but they never appeared in contact responses — Bitrix24 returns the dates under `createdTime`/`updatedTime`, and those are what the contact body carried. Meanwhile filter and `select` by `createdTime` (the name a client actually sees in the response) were rejected as an unknown field, while the phantom `createdAt` "worked" even though the field itself was never readable.

**After**

The schema declares the real keys `createdTime` and `updatedTime` (datetime, read-only): they appear in `/fields`, filter and `select` on them work, and the value is normalized to ISO-8601 in UTC. The phantom `createdAt`/`updatedAt` are no longer declared — filter or `select` on them returns `400 UNKNOWN_FILTER_FIELD`.

**Integrator impact**

Reads are unchanged — the `createdTime`/`updatedTime` keys were already in the response body, now also normalized. If you filtered or projected contacts by `createdAt`/`updatedAt`, rename them to `createdTime`/`updatedTime`.

### FIX-0703-16: Open-line config list now honors the limit parameter

**Before**

[GET /v1/openline-configs](/docs/openlines/config/list) and its search counterpart ignored `limit`: the underlying Bitrix24 method returns the whole set of configurations and the wrapper forwarded every row. The `hasMore` flag was wrong too — `false` even when records remained beyond the requested `limit`.

**After**

The response is clamped to `limit` on the wrapper side. `hasMore: true` when Bitrix24 returned more records than the requested `limit` (a next page exists), otherwise `false`. `total` is the record count in the current window.

**Integrator impact**

A request with `limit` now returns no more than `limit` records. Clients that relied on the whole set coming back regardless of `limit` will see a truncated list — use `offset` for the next page.

## 2026-07-02

### NEW-0702-1: filter and sort tasks by real status (realStatus)

`GET /v1/tasks`, `POST /v1/tasks/search` and `POST /v1/tasks/aggregate` now accept `realStatus` in `filter` (list and search also in `sort`) — filtering by the task's actual stored status: 1 — new, 2 — pending, 3 — in progress, 4 — awaiting control, 5 — completed, 6 — deferred, 7 — declined. Previously `filter[realStatus]` was silently ignored and the request returned the whole set.

Unlike `filter[status]`, which Bitrix24 treats as a virtual (meta) filter (values −1 overdue, −2 unviewed, −3 almost overdue) that does not match the `status` field value in the response, `realStatus` filters by the stored status. The field is read-only (change the status via `status`) and is used only in `filter`/`sort` — the response already exposes the task's real status in the `status` field.

### FIX-0702-2: creating an app reuses a failed slot with the same name

**Before**

Repeating [POST /v1/infra/servers](/docs/infra/servers/create) with the same `name` after a failed deploy created a new app slot. Failed slots piled up and were only removed by auto-cleanup after 7 days.

**After**

If the key owner already has a slot with the same `name` on the portal in `error` status (or one that was created but never received a deploy), the repeated call returns that same slot: its `id` is preserved, the error and build log are reset, and the status goes back to `provisioning` — deploy into it. Slots that never received any code are now removed by auto-cleanup after 24 hours instead of 7 days (slots with a failed build are still kept for 7 days together with their build log).

**Impact on integrators**

No request changes are required. If your flow re-created a slot with the same name after a failure, you will start receiving the previous `id` instead of a new one — this is expected: deploying into the returned slot works as usual. Slots owned by other portal users and running apps are never picked up for reuse.

### FIX-0702-3: read-only keys can no longer write through /v1/bots

**Before**

A read-only API key (`accessMode: READONLY`) could perform write operations through the bot endpoints ([POST /v1/bots](/docs/bots/management/create), sending and deleting messages, adding chat members, registering and deleting a bot, and others) — the call returned 200 instead of 403. Every other Bitrix24 proxy surface already blocked such writes.

**After**

A write through `/v1/bots/*` with a read-only key returns 403 with code `WRITE_BLOCKED_READONLY_KEY`. Read operations are unaffected, including fetching a message context ([GET /v1/bots/:botId/messages/:messageId/context](/docs/bots/messages/context)) and downloading a file ([GET /v1/bots/:botId/files/:fileId](/docs/bots/files/download)).

**Impact on integrators**

If your bot integration needs to write, switch the key to read+write mode in the /keys section.

### FIX-0702-4: folders include=storage now resolves

**Before**

`GET /v1/folders/:id?include=storage` (and the list form `GET /v1/folders?parentId=...&include=storage`) did not add `_included` to the response, even though `GET /v1/folders/fields` advertises `includable: true` for the `storage` relation.

**After**

The related storage now resolves: the response includes `_included.storage` with the storage record looked up by `storageId`. The relation is described in [GET /v1/folders/fields](/docs/entities/folders/fields).

### FIX-0702-5: PAGE_BACKGROUND_WORKER: bind no longer fails with 500

**Before**

`POST /v1/placements/bind` for the `PAGE_BACKGROUND_WORKER` placement filled in the `options.errorHandlerUrl` field Bitrix24 requires only when the call went through an OAuth session. When an app bound via a developer key or on a self-hosted portal, the field was not added and Bitrix24 answered 500 (`BITRIX_UNAVAILABLE`, "Field errorHandlerUrl is empty"), even though other placements bound fine.

**After**

For `PAGE_BACKGROUND_WORKER` the `options.errorHandlerUrl` value now defaults to `handler` regardless of the bind path. An explicit `options.errorHandlerUrl` still takes precedence. The response `options` field now reflects the effective value (with the defaulted `errorHandlerUrl`).

**Impact on integrators**

No action required — a call that previously returned 500 now succeeds.

### FIX-0702-6: POST /search reports missing required parameters with a clean error

**Before**

`POST /v1/{entity}/search` for entities whose Bitrix24 list method requires mandatory parameters did not check for them and forwarded the request to Bitrix24 as-is. A raw Bitrix24 error leaked out (`BITRIX_ERROR`, e.g. "Invalid value of parameter [ `$id` ]" or "required parameter `type` is not set"), whereas the equivalent `GET` list already returned a clear `400 MISSING_REQUIRED_PARAMS` in the same case. Affected [POST /v1/calendar-events/search](/docs/entities/calendar-events/search) (needs `type`), [POST /v1/files/search](/docs/entities/files) (needs `folderId`) and [POST /v1/folders/search](/docs/entities/folders) (needs `parentId`).

**After**

`POST /v1/{entity}/search` validates the required parameters before calling Bitrix24 — the same guard the `GET` list has had for a while. A missing parameter returns `400` with code `MISSING_REQUIRED_PARAMS` and the list of missing fields, with no call to Bitrix24. The required parameter can be passed inside `filter`, and the parent parameter (`folderId` for files, `parentId` for folders) may also be passed at the top level of the request body.

### NEW-0702-7: Research price in the key self-description and the required top-up in the 402 body

[GET /v1/me](/docs/keys-auth) now returns `cost` for every provider in the `webResearch.providers[]` block, mirroring the `webSearch` block. The field carries the research-mode price in Ꝟ (`cost.research`) and the currency (`cost.currency`), so an agent sees the deep-search price directly in the key self-description, without a separate call.

The `402` response on insufficient balance (`INSUFFICIENT_BALANCE`, as well as `BILLING_FROZEN`) on [POST /v1/search](/docs/search/run) and [POST /v1/research](/docs/search/research) now carries a `required` field — the amount in Ꝟ needed for the request. The existing `userMessage` and `hint` fields are unchanged.

Clients with strict `additionalProperties` schema validation need to account for the new response fields.

### NEW-0702-8: POST /v1/triggers/fire supports invoices (SmartInvoice)

The [POST /v1/triggers/fire](/docs/automation/triggers/fire) endpoint accepts a new `entityType` value — `invoice`. Pass `entityType: "invoice"` and the invoice `entityId` (returned by [GET /v1/invoices](/docs/entities/invoices)) to fire an automation trigger for a smart invoice. Existing values (`deal`, `lead`, `contact`, `company`, `quote`, `item`) keep working unchanged.

Previously firing a trigger for an invoice was impossible, and trying it through `entityType="item"` with `entityTypeId=31` was rejected with a message that led to a dead end. Now `item` with a reserved `entityTypeId` (including 31) points to the matching `entityType` — for invoices, that is `invoice`.

### NEW-0702-9: GET /v1/ai/usage returns transcribed audio duration per model

[GET /v1/ai/usage](/docs/ai/consumption/usage) now returns an `audioSeconds` field in the `byModel[]` block — the total number of audio seconds sent for transcription per model over the selected period. The field is populated for speech-to-text (Whisper) calls and is `0` for text models, where audio duration does not apply.

The field is additive — existing integrations keep working unchanged.

### BC-0702-10: categories: code and isDefault marked read-only (were phantom-writable)

> Old format supported until: 01.10.2026

**Before**

`GET /v1/categories/:entityTypeId/fields` advertised `code` and `isDefault` as writable (`readonly: false`), but writing them via `crm.category.add`/`update` was silently ignored (values not persisted, `200` returned).

**After**

Both fields are marked `readonly: true`. `/fields` now reports them as read-only, and an attempt to write `code` or `isDefault` returns `400 READONLY_FIELD` instead of silently dropping the data.

**What integrators must do**

Previously, sending `code`/`isDefault` in a `POST`/`PATCH` `/v1/categories/:entityTypeId` body was accepted (`200`, values silently ignored). Now such a request returns `400 READONLY_FIELD`. Remove `code` and `isDefault` from your category create/update request bodies — these fields are no longer accepted on write.

### BC-0702-11: telephony-lines: crmAutoCreate normalized to boolean and now listed in /fields

> Old format supported until: 01.10.2026

**Before**

`GET /v1/telephony-lines/fields` returned only `number`, `serverName`, `name`. The CRM auto-create flag leaked into list responses under the raw UPPER name `CRM_AUTO_CREATE` as a `"Y"`/`"N"` string — the only UPPER field among camelCase ones — and was absent from `/fields`. On write, camelCase `crmAutoCreate` was silently dropped.

**After**

The field is declared as `crmAutoCreate` (boolean). It now appears in `/fields`, comes back normalized (`true`/`false`) in `list` responses instead of raw `"Y"`/`"N"`, and is accepted as a camelCase boolean on `create`/`update` (the raw UPPER name is still accepted on write for compatibility). Clients reading `data[].CRM_AUTO_CREATE` should switch to `data[].crmAutoCreate` (boolean).

### NEW-0702-12: workgroups: aggregate operation and groupBy fields exposed

**Before**

`POST /v1/workgroups/aggregate` worked but was never advertised: the operation was missing from the machine index at `/v1/guide`, and `groupBy` returned `400` on any field (`Available: .`) because the aggregatable list was empty.

**After**

An aggregatable list is declared: `membersCount` (numeric sum/avg/min/max) plus categorical `active`, `isProject`, `ownerId` for grouping. The operation is now visible in `/v1/guide` and `/fields`, and `groupBy` over these fields works.

### FIX-0702-13: /fields: metadata completeness for doc-templates and bookings

**Before**

`GET /v1/doc-templates/fields` omitted `isDefault` and `productsTableVariant` even though they appear in list responses. On `GET /v1/bookings/fields` the mandatory `resourceIds` and `datePeriod` were not flagged `required`, so their obligatoriness was invisible in the schema.

**After**

`doc-templates`: `isDefault` and `productsTableVariant` are declared (read-only) — `/fields` now matches the responses. `bookings`: `resourceIds` and `datePeriod` are marked `required: true`, so the requirement is visible in `/fields`.

### FIX-0702-14: orders: /fields synced with responses, pseudo-key order removed, companyId filterable

**Before**

`GET /v1/orders/fields` carried a spurious pseudo-key `order` (an artifact of parsing `sale.order.getFields`) and omitted fields that actually appear in responses: `companyId`, `clients`, `dateMarked`, `personTypeXmlId`, `statusXmlId`, `version`. Because `companyId` was not in the schema, filtering by it failed with `UNKNOWN_FILTER_FIELD`.

**After**

`/fields` is now built from the schema: the `order` pseudo-key is gone and the six missing fields are declared (`companyId` — writable number; `clients` — read-only object returned by `get`; `dateMarked`/`personTypeXmlId`/`statusXmlId`/`version` — read-only). Filtering and sorting by `companyId` now work.

### FIX-0702-15: users: limit > 50 now honored, meta.hasMore is accurate

**Before**

`GET /v1/users?limit=500` returned only 50 records even though `meta.total` reported more. `meta.hasMore` was always `false`, so the documented `hasMore`-based pagination silently dropped everything past the first page.

**After**

The entity is backed by the legacy `user.get` method (no `.list` suffix), so the auto-paginator never engaged. A `paginateViaStart` flag was added (same as departments): for `limit > 50` it now walks pages via `start`, and `meta.hasMore` reflects whether more records actually exist.

### FIX-0702-16: POST /v1/batch rejects disabled write operations and forwards required list parameters

**Before**

A global [POST /v1/batch](/docs/batch) call with action create, update or delete for an entity whose operation is disabled (for example openline-configs — writes live behind dedicated routes) went straight to Bitrix24, bypassing normalization, and could silently create or modify a record. Separately: action list or search for an entity with required method parameters (for example calendar-events — type and ownerId) returned AUTO_PAGINATION_FAILED "missing required parameter", even though a direct list request with the same parameters worked. In addition, batch list for folders and files sent the parent folder under the name parentId or folderId, which the disk.folder.getchildren method ignores, so the list silently came back for the wrong folder; and batch list for calendar-events with a leftover filter key forwarded it to Bitrix24 with no error, so the method returned the whole calendar.

**After**

A disabled write operation in a sub-call is rejected with ACTION_NOT_SUPPORTED before any Bitrix24 call — the same as POST /v1/{entity}/batch. Required list parameters and the method's top-level parameters are forwarded to Bitrix24 under their original names, so batch list behaves like the direct list, and when they are missing a clear MISSING_REQUIRED_PARAMS is returned instead of a raw Bitrix24 error. For folders and files the parent folder is now renamed to the id key the method expects, so batch list comes back for the right folder. For entities whose method has no filter envelope (calendar-events), a leftover filter key is now rejected with UNSUPPORTED_FILTER before any Bitrix24 call — the same as the direct list.

**Integrator impact**

No action needed. If a batch sub-call previously relied on a disabled write operation running, switch it to the entity's dedicated route. For batch list on entities with required parameters (calendar-events), pass type and ownerId in the sub-call params. If a calendar-events batch list used filter, drop it or move it to the top-level parameters, otherwise the sub-call returns UNSUPPORTED_FILTER.

### NEW-0702-17: GET /:entity/fields now returns field label and description

The `GET /v1/{entity}/fields` response can now carry a human-readable short name `label` and an explanatory `description` per field — previously a field was described only by the `{type, readonly}` pair. This lets an AI agent or UI show a field's name and purpose without consulting the documentation. The text is returned in English. The keys were added for the following entities: Departments, Smart Processes, Storages, Folders, Files, Workgroups, Document Templates, Bookings, Calendar Events, Tasks, Requisites, Users. For Users the label resolution was additionally fixed: `GET /v1/users/fields` now returns the real Bitrix24 field names from `user.fields` instead of the technical codes. The change is additive: the new keys appear alongside the existing ones, and existing calls keep working unchanged.

### FIX-0702-18: a vibe:*-only key now issues instead of failing

**Before**

Issuing an API key via `POST /v1/keys` whose every requested scope is an internal Vibecode `vibe:*` scope (for example only `vibe:infra`), on a dev-key portal (self-hosted Bitrix24 or a connected cloud portal), was rejected with `502 DEVKEY_MINT_FAILED`. `vibe:*` scopes are never sent to Bitrix24, so the Bitrix24 webhook scope set came out empty and Bitrix24 rejected the mint, requiring at least one scope.

**After**

A `vibe:*`-only key now issues successfully. No Bitrix24 webhook is created for it — none is needed, such a key never calls the Bitrix24 REST — and the key works with Vibecode's internal features per its scopes.

**Impact on integrators**

No action required: the request that previously failed now returns the created key.

### FIX-0702-19: /v1/storages pagination — limit over 50 returns all records, meta.hasMore is correct

**Before**

`GET /v1/storages` with `limit` over 50 returned at most 50 records, and `meta.hasMore` was always `false` — even when the portal held more. A client requesting `?limit=50` against 489 storages saw `hasMore: false` and could not tell it needed to fetch the next page. Same on `POST /v1/storages/search` and in `/v1/batch`.

**After**

`limit` over 50 goes through auto-pagination (like every other list) and returns the requested number of records, and `meta.hasMore` equals `(offset + returned count) < meta.total` even for `limit` of 50 or less. The change applies to `GET /v1/storages`, `POST /v1/storages/search` and the `storages` list in `/v1/batch`.

Note: the correct `meta.hasMore` calculation for list responses with `limit` of 50 or less now applies to all entities (`GET /v1/{entity}` and `POST /v1/{entity}/search`), not only `storages` — previously `meta.hasMore` was always `false` on this path.

### FIX-0702-20: infra: Cyrillic in displayName on application create

**Before**

On [POST /v1/infra/servers](/docs/infra) with a Cyrillic `displayName`, the name could be stored as a run of question marks (`??????`) — an encoding corruption while sending to Bitrix24.

**After**

The name is transmitted as UTF-8 and Cyrillic is preserved correctly.

**Impact on integrators**

No action required. Cyrillic names are no longer mangled.

### FIX-0702-21: items: filtering by parentId<N> relation fields

**Before**

Filtering by a dynamic relation field (for example `parentId2` — the linked deal) on [GET /v1/items/:entityTypeId](/docs/entities/items/list) and [POST /v1/items/:entityTypeId/search](/docs/entities/items/search) was rejected with `400 UNKNOWN_FILTER_FIELD`, even though the field is present in `GET /v1/items/:entityTypeId/fields` and returned in responses.

**After**

Fields shaped like `parentId<N>` are accepted in the filter and forwarded to the request as-is. Finding a smart-process item linked to a specific parent entity now works directly through the items wrapper.

**Impact on integrators**

No action required. Requests that previously returned `400` now succeed.

### FIX-0702-22: smart processes: enumeration values and the userfield display name now persist

**Before**

On [POST /v1/items/:entityTypeId/userfields](/docs/userfields/smart-processes) an enumeration field (`userTypeId: enumeration`) was created but its value variants were not saved (the list came back empty), and a display name passed as a plain string stayed blank in the interface.

**After**

Value variants are accepted both as `enum` and as `list` and are saved correctly. A name passed as a plain string is automatically wrapped into a language map and fills the edit-form, list-column and list-filter labels.

**Impact on integrators**

No action required. The enumeration values and the name that were previously dropped silently now persist.

### FIX-0702-23: req-family: address and preset-field /fields now return camelCase keys

**Before**

`GET /v1/addresses/fields` and `GET /v1/requisite-presets/:presetId/fields/schema` returned the field description with raw UPPER_SNAKE_CASE keys (`TYPE_ID`, `ADDRESS_1`, `FIELD_NAME`, `IN_SHORT_LIST`), even though the data of these entities (`GET /v1/addresses`, the preset field list) already came back in camelCase — the schema did not match the real field names in the data.

**After**

Both endpoints normalize the description keys to camelCase (`typeId`, `address1`, `fieldName`, `inShortList`), like the rest of V1. The inner field descriptors (`type`, `isRequired`, `isReadOnly`, `title`) are unchanged.

**Impact on integrators**

The `/fields` response keys now match the field names in the data. A client that read the camelCase names from the data gets a consistent schema; no action required.

## 2026-07-01

### FIX-0701-1: Disk file upload accepts files larger than 1 MB

**Before**

[POST /v1/files/upload](/docs/entities/files/upload) rejected a request body larger than ~1 MB with `FST_ERR_CTP_BODY_TOO_LARGE`. The file is sent as base64 in the JSON body, and base64 inflates the size by roughly a third — so even a 1.1 MB file did not pass. There was no way to upload a larger file.

**After**

This route's body limit was raised to 70 MB — enough for a file of about 50 MB once base64 and the JSON envelope are accounted for (call recordings, typical attachments). Bitrix24 still enforces its own Disk file-size cap: exceeding it returns an error in the standard envelope. Files in the hundreds of MB need a separate upload path (multipart / presigned), which is not implemented yet.

### NEW-0701-2: server app icon: SVG upload, anonymous serve, favicon

You can now set an app icon for a server. `POST /v1/infra/servers/:id/icon` (multipart/form-data, field `file`, SVG only up to 256 KB, no scripts, event handlers or external references) uploads the icon; it is served anonymously at the stable `GET /api/server-icons/:id` and shown in the Bitrix24 app catalog. To make it the browser-tab favicon, add `<link rel="icon" type="image/svg+xml" href="<base-URL>/api/server-icons/:id">` to your app HTML at build time — after that, re-uploading the icon refreshes both the catalog and the favicon automatically. Format, requirements and procedure: [App icon](/docs/infra/app-icon).

### NEW-0701-3: The current-user profile reports administrator rights

[GET /v1/users/me](/docs/entities/users) now returns a working `isAdmin` field: `true` — the user is a portal administrator, `false` — not, `null` — could not be determined (a transient failure; the profile is still returned). Use it for server-side permission checks in your backend. `GET /v1/users/:id` and the user list still do not expose it — the verdict is available only for the current session user.

### NEW-0701-4: Extended static field contract in /v1/guide and a schema-discovery pointer in /v1/me

Each entity in the [GET /v1/guide](/docs/keys-auth) response gains a `data.entities[].fieldsDetailed` field — an extended static field contract: `type`, `readonly`, `required`, `createOnly`, and `enum` decoding (for example, the `status` and `priority` values on tasks). It is available with the `X-Api-Key` header alone, without a session, for mappings and code generation before a user session exists. The compact `fields` field is unchanged.

The `GET /v1/me` response for an authorization key without a user session (no `Authorization: Bearer` header) now carries a `schemaDiscovery` block — a pointer to where the static schema lives without a session (`/v1/guide`) and how to get live and custom fields (a Bearer session or a personal key). The `GET /v1/<entity>/fields` and `GET /v1/userfields/*` endpoints are unchanged.

### BC-0701-5: categoryId in the posts response is now an array of numbers

> Old format supported until: 01.01.2027

**Before**

[GET /v1/posts](/docs/feed/posts/list) returned `categoryId` with a type that depended on the number of a post's categories: `null` for none, a number (`11`) for one, a comma-separated string of IDs (`"5,7,9"`) for several. A typed client with a `categoryId: number | null` field worked on single-category posts but broke on posts with two or more.

**After**

`categoryId` is always an array of numbers `number[]`: `[]` for none, `[11]` for one, `[5, 7, 9]` for several. The type is uniform regardless of how many categories a post has.

**What integrators should do**

Read `categoryId` as an array: `post.categoryId.length` instead of a `null` check, `post.categoryId[0]` for the first category. The old "number or string" branch can be removed.

### FIX-0701-6: unified token and scope error codes across the Disk section

**Before**

Custom Disk operations — [POST /v1/files/:id/moveto](/docs/entities/files/moveto), `copyto`, [POST /v1/files/upload](/docs/entities/files/upload), [GET /v1/files/:id/download](/docs/entities/files/download) and the folder counterparts — returned `401 NO_TOKENS` when portal tokens were missing and `403 SCOPE_MISSING` when the `disk` scope was absent. The generated CRUD operations of the same section (list/get/create/update/delete) already returned `401 TOKEN_MISSING` and `403 SCOPE_DENIED` for the same conditions — so within one section a client saw two different codes for the same error.

**After**

All Disk operations return unified codes — `401 TOKEN_MISSING` and `403 SCOPE_DENIED`, matching the rest of the V1 API. The HTTP statuses (401 and 403) are unchanged.

**Impact on integrators**

If your code branched on the `NO_TOKENS` or `SCOPE_MISSING` strings on the moveto/copyto/upload/download operations, switch to `TOKEN_MISSING` / `SCOPE_DENIED` (or check the HTTP status). No change is needed otherwise.

### FIX-0701-7: CALL_CARD placement removed from the allowed list

**Before**

The `CALL_CARD` placement code was treated as valid: `POST /v1/placements/bind` passed it through validation and forwarded it to Bitrix24, and `GET /v1/placements/available` listed it. But no Bitrix24 module registers this placement, so `placement.bind` failed with an internal error that the platform surfaced as `INTERNAL_SERVER_ERROR`.

**After**

`CALL_CARD` is removed from the allowed list: it no longer appears in `GET /v1/placements/available`, and `POST /v1/placements/bind` with it is rejected up-front with a clear `VALIDATION_ERROR`, without calling Bitrix24.

**Impact on integrators**

Binding `CALL_CARD` never worked (it returned an opaque 500 error), so no working integration breaks. For the call-card apps panel, use the current placements from `GET /v1/placements/available`.

### FIX-0701-8: workday open/close/pause: the userId field is now applied

**Before**

The documented body field `userId` in [POST /v1/workday/open](/docs/workday/open), [POST /v1/workday/close](/docs/workday/close) and [POST /v1/workday/pause](/docs/workday/pause) was silently ignored: the operation always ran against the key's token owner, even when another employee was passed. The response was `success`, but the action affected the wrong user.

**After**

`userId` is translated to the Bitrix24 `USER_ID` parameter, so the operation runs against the specified employee (given admin or manager rights). A non-existent `userId` now returns a Bitrix24 error instead of a false success. An invalid `userId` (not a positive integer) is rejected as `400 INVALID_PARAMS`. This matches the already-working [GET /v1/workday/status](/docs/workday/status).

**Impact on integrators**

Callers that did not pass `userId` see no change — the operation still applies to the token owner. Callers that passed `userId` now get the correct action against the specified employee.

## 2026-06-30

### NEW-0630-1: POST /v1/cowork/deploy-key — get a project deploy key from Cowork/Code

The Cowork/Code key (`vibe:cowork`) is data-plane only and is blocked from the infrastructure control-plane with 403 `INFRA_FORBIDDEN_FOR_COWORK_KEY`. The new [POST /v1/cowork/deploy-key](/docs/cowork) endpoint lets an agent self-serve a separate project deploy key: call it with the same Cowork key, take the top-level `key` field from the response (a bare object, not wrapped in `data`), and use it as the `X-Api-Key` header for deploy/provision/exec under `/v1/infra/*`.

The returned key carries the `vibe:infra` + `vibe:storage` scopes (no `vibe:cowork`), expires in 7 days, and is bound to the Cowork key's owner and portal. Each call returns a fresh key and revokes the previous project key (exactly one is active). Requires the `vibe:cowork` scope and an active Cowork/Code subscription; rejection codes are 403 `INSUFFICIENT_SCOPE` / 403 `COWORK_NOT_ACTIVATED` / 503 `DEPLOY_KEY_DISABLED` / 503 `INFRA_DISABLED`.

### NEW-0630-2: Self-hosted placement now receives a one-time authorization code on appUrl

For an app with its own `appUrl` (outside Black Hole) opened as a placement, the platform now appends a one-time authorization code (`?code=...`) to the `appUrl` redirect instead of an internal gateway token. The app exchanges this code for a `vibe_session` via the existing `POST /v1/oauth/token` — `redirect_uri` must exactly match the configured `appUrl`. Previously such apps received a non-redeemable token and could not authorize the user.

### NEW-0630-3: New endpoint POST /v1/oauth/placement-session for self-hosted apps

A self-hosted app (on its own server, not on Black Hole) opened as a placement (iframe) in Bitrix24 can now exchange the Bitrix24 user token it received in the placement callback on its own handler for a `vibe_session`. The request `POST /v1/oauth/placement-session` with body `{ app_key, access_token, member_id, domain }` (optionally `refresh_token`, `expires_in`) is server-to-server — the session token never reaches the browser. The authorization section of the docs covers both placement topologies.

### FIX-0630-4: PATCH on an OAuth-app key's scopes: honest rejection instead of false access

**Before**

`PATCH /v1/keys/:id` adding a Bitrix24 scope to an OAuth-app key (`vibe_app_*`), and `PATCH /v1/apps/:id` widening an app's `scopes`, returned `200` and persisted the new scope set. But an OAuth app's scopes are fixed at issue time and such an edit never reaches the Bitrix24 side, so `GET /v1/me` then reported a scope Bitrix24 had not granted and the actual call was rejected.

**After**

Adding a Bitrix24 scope to an OAuth-app key or to an app is now rejected with `403 OAUTH_SCOPE_CHANGE_REQUIRES_REISSUE`. Removing scopes and changing `vibe:*` scopes still work. To gain a new scope, issue a new authorization key with the scopes you need.

### FIX-0630-5: a key with the `tasks` scope now actually unlocks Tasks methods

**Before**

A key issued with the `tasks` scope (plural — the spelling the UI picker offers) minted, on a dev-key portal, a webhook that Bitrix24 accepted but never bound to the Tasks REST methods. `GET /v1/me` reported `tasks`, yet Tasks method calls were rejected on the Bitrix24 side. The `task` scope (singular) worked.

**After**

On key issue and edit the scope set is canonicalised to the spelling Bitrix24 actually binds methods to (`tasks` → `task`), so the key unlocks Tasks methods regardless of the chosen spelling. Already-issued keys are not changed retroactively — re-issue the key.

### FIX-0630-6: a malformed FILES field on a timeline comment is now rejected with 400

**Before**

[POST /v1/timelines](/docs/entities/timelines/create) and `PATCH /v1/timelines/:id` accepted the `FILES` field in any shape and answered `200`/`201`. When the shape was anything other than an array of `[[fileName, base64Content]]` pairs — a flat array of strings or a single pair without the outer array — the comment was created but the file was attached as garbage (random name, unreadable content) or silently dropped, with no error.

**After**

A `FILES` value that is not an array of `[[fileName, base64Content]]` pairs is rejected before the Bitrix24 call with `400 INVALID_FILES_SHAPE` and a hint about the correct shape. An empty `FILES` (`[]`) and an omitted field are still accepted. The check applies to single `POST`/`PATCH` and to batch (`POST /v1/batch`, `POST /v1/timelines/batch`).

**Impact on integrators**

Callers passing `FILES` in the documented `[[fileName, base64Content]]` shape are unaffected. Callers relying on other shapes now get an explicit `400` instead of a silently broken attachment, and can fix the request.

### FIX-0630-7: empty bot and user fields now return null/[] instead of false/{}

**Before**

In bot-card responses ([GET /v1/bots/:botId](/docs/bots/management/get), [POST /v1/bots](/docs/bots/management/create), [PATCH /v1/bots/:botId](/docs/bots/management/update)) the unset fields inside `users[]` came back as the wrong primitive: the datetimes `lastActivityDate`, `mobileLastDate`, `desktopLastDate` were returned as boolean `false`, and an empty `phones` list was also `false`. The same `lastActivityDate` field in `GET /v1/users` came back as an empty object `{}`. As a result `new Date(lastActivityDate)` silently produced the epoch and `phones.map(...)` threw a type error.

**After**

An unset datetime is now encoded uniformly as `null` on every path, and an empty phone list as `[]`. A populated datetime is still an ISO string and a populated list is still an array.

**Impact on integrators**

No action required — the types are now correct. Code that relied on comparing empty values to `false` will stop matching: check the datetime against `null` and treat the phone list as an array.

**Affected endpoints:** [GET /v1/bots/:botId](/docs/bots/management/get), [POST /v1/bots](/docs/bots/management/create), [PATCH /v1/bots/:botId](/docs/bots/management/update), `GET /v1/users`

### NEW-0630-8: Relink an app's OAuth credentials without deleting it

A new endpoint [POST /v1/apps/:id/relink-oauth](/docs/apps) updates `bitrixClientId` and `bitrixClientSecret` on an existing application without deleting it. This is needed when the local OAuth application is recreated in the Bitrix24 account and its `client_id` changes: previously the only path was to delete the app (which broke the linked bot, openline and bindings) and create it anew.

Request body: `{ bitrixClientId, bitrixClientSecret }` (both required). The paired key, bot and bindings are preserved. If that `client_id` is already linked to another application — `409 OAUTH_CLIENT_ID_IN_USE`. It cannot be called with the OAuth-app's own key — `403 OAUTH_APP_KEY_CANNOT_RELINK` (use a personal key or the dashboard). After relinking, reinstall the app in the account — this restores the event subscription.

### NEW-0630-9: Web search: full page text, images, news mode, and research domain filters

**Before**

`POST /v1/search` accepted `include_raw_content` as a boolean flag, but the full text of the found pages did not reach the response. There were no parameters for news mode or for requesting images. `POST /v1/research` accepted `include_domains` and `exclude_domains` but silently dropped them.

**After**

`POST /v1/search` gained two new optional parameters: `topic` (`general` or `news`, default `general`) and `include_images` (boolean, default `false`). The boolean `include_raw_content` now actually returns the full text: each result gained a `rawContent` field (the full page text, size-capped). The response added top-level `images` (an array of objects with a `url` field) and `ignored_filters` (a string array — the passed filters the provider could not apply). These fields arrive in both the synchronous response body and the `done` streaming frame. The `X-Search-Filters-Ignored` header is kept and may now list `topic` and `include_images`. `POST /v1/research` now applies `include_domains` and `exclude_domains` (up to 20 each) for capable providers and reports overflow via `ignored_filters` in the `done` frame. Which capabilities a selected engine offers is returned by `GET /v1/search/providers`. Clients with strict schema validation via `additionalProperties` should account for the new response fields.

### NEW-0630-10: Read AI client-call transcripts via the API

The new [GET /v1/activities/:activityId/transcript](/docs/entities/activities/transcript) endpoint returns the ready-made AI transcript of a client call by the ID of its CRM Call activity. The method only reads an existing transcript — it does not trigger generation. It requires the `crm` scope. When there is no transcript for the call yet, the `data.transcription` field is `null` — a normal response, not an error.

## 2026-06-29

### FIX-0629-1: org structure node search now searches by name

**Before**

[POST /v1/humanresources/nodes/search](/docs/humanresources/nodes/search) proxied to `humanresources.node.list`: the node type was set inside `filter`, there was no name search at all, and a top-level `{ "type": ..., "name": ... }` body (or a request with no body) returned `400` or `500`.

**After**

The endpoint now wraps `humanresources.node.search`. Two fields are required at the top level of the body — `type` (`DEPARTMENT` or `TEAM`) and `name` (a substring of the name). Optional are `parentId` and `pagination.limit` (default 50, maximum 200). It returns nodes whose name contains `name`, in a flat `data` with `meta` (`total`, `hasMore`). The `filter`, `order` and `select` fields are no longer accepted.

**Integrator impact**

Send `{ "type": "TEAM", "name": "<substring>" }` at the top level instead of the former `{ "filter": { "type": "TEAM" } }`. To enumerate all nodes of a type without name search, use [GET /v1/humanresources/nodes](/docs/humanresources/nodes/list) with `?type=...`.

### NEW-0629-2: AI quota off-peak hours schedule

Added the `GET /v1/off-peak` endpoint — the off-peak (Time-of-Use) discount schedule for the AI quota. The response carries the price multiplier right now (`currentMultiplier`), the next window when it gets cheaper (`nextWindow`), a 24×7 grid by hour and weekday (`grid`), the current grid cell (`nowCell`), and the schedule timezone (`timezone`). The discount applies to quota-metered usage only — the quota drains slower during these hours; wallet pay-per-token charges are unaffected. The optional `model=<id>` parameter returns a specific model's schedule instead of the platform default. Requires the `vibe:ai` scope. While off-peak is not enabled, the response is `{ "enabled": false }`.

### BC-0629-3: vibe-search provider slug removed

> Old format supported until: 26.12.2026

**Before**

The `provider` field in [POST /v1/search](/docs/search/run) and [POST /v1/research](/docs/search/research) accepted the `vibe-search` slug — a separate platform engine added on 2026-06-06. It was also listed among the slugs in [GET /v1/search/providers](/docs/search/providers).

**After**

The `vibe-search` slug is removed. The platform search engine on every instance is `bitrix-search` — which upstream backs it is instance-dependent. A request with `provider: "vibe-search"` now returns `400 INVALID_REQUEST` (the value fails validation). research support for `bitrix-search` is likewise instance-dependent — see [GET /v1/search/providers](/docs/search/providers).

**What integrators should do**

If your request explicitly passed `provider: "vibe-search"`, replace it with `bitrix-search` or omit the `provider` field to use the instance default engine (shown by the `defaultProvider` field in [GET /v1/me](/docs/keys-auth)). The `vibe-search` slug was not the default engine on any production instance, so only integrations that hardcoded it are affected.

### FIX-0629-4: A null field value via POST /v1/batch no longer writes the string "null" into the field

**Before**

In a composite `POST /v1/batch`, a create or update with a field value of `null` (for example `{"entity":"deals","action":"update","entityId":123,"params":{"comments":null}}`) wrote the **literal string `"null"`** into the field.

**After**

The field receives an empty value, which Bitrix24 interprets by field type: text is cleared, numeric becomes `0`, a date is left unchanged. The literal string `"null"` is no longer written and no error is raised. This matches the behavior of a single `PATCH /v1/{entity}/:id` with `null`. The per-entity `/v1/{entity}/batch` path still skips a `null` field entirely (leaves the value unchanged for every type).

### FIX-0629-5: Search and list with a null filter now return more than 50 rows

**Before**

A `POST /v1/{entity}/search` or `GET /v1/{entity}` request with a filter on an empty value (for example `{"filter": {"closedDate": null}}`) and a `limit` above 50 returned at most 50 records, even though `meta.total` reported the real match count and `meta.hasMore` was `true`. Auto-pagination silently stopped after the first page, so the common "read while rows equal `limit`" loop got an incomplete result with no error.

**After**

Such a request now returns up to `limit` records, the same as with any other filter. A `null` filter value is treated as "field is empty" consistently across every page of the result set.

**Impact on integrators**

Clients that paged manually via `offset` in steps of 50 to work around the truncation no longer need to — up to 5000 records can be fetched in a single call.

### FIX-0629-6: port change and deploy auto-routing now work out of the box on new app servers

**Before**

A regular "Publish app" server booted its agent with a fixed port, so `PATCH /v1/infra/servers/:id/port` and the deploy auto-routing step returned `409 PORT_NOT_APPLIED` (`NO_SCANNER`), and the public URL served the Black Hole service page while the app listened on a non-default port.

**After**

New regular app servers boot with port auto-detection: a service on any port is reachable through the tunnel immediately, and port change plus deploy auto-routing succeed. Agent and galaxy-host servers are unchanged.

### FIX-0629-7: app install via /v1/apps returns a precise error code instead of the generic BOX_APP_INSTALL_FAILED

**Before**

On an OAuth-app install failure, [POST /v1/apps](/docs/apps/create) always returned `502 BOX_APP_INSTALL_FAILED`, with the full raw Bitrix24 response echoed into `error.message`.

**After**

The failure response is now classified: `403 B24_INSUFFICIENT_SCOPE` (the service integration lost its rights on the portal), `410 STALE_DEVELOPER_KEY` (access was changed or removed and cannot be auto-recovered), `502 RECOVERY_FAILED` (transient, retryable) or `502 DEVKEY_MINT_FAILED` (other). `error.message` no longer carries the raw Bitrix24 body — the diagnostic moves to the redacted `error.details.b24Body` field.

**Impact**

Existing "non-201 means install failed" handling keeps working unchanged. If your code branched specifically on `BOX_APP_INSTALL_FAILED`, add handling for the new codes above.

### FIX-0629-8: date-range search no longer returns empty for wide ranges

**Before**

`POST /v1/deals/search` (and likewise for leads, contacts, companies, quotes, invoices, items) with a date filter and a lower bound (`>=` / `>`) spanning more than 14 days returned `200` with an empty `data` and `meta.total: 0`, even when records existed in that range.

**After**

The request returns all matching records. `GET /v1/deals`, narrow ranges (≤ 14 days), and the `autoWindow: false` parameter were unaffected.

### NEW-0629-9: New error code CONNECTOR_APP_INSTALL_FORBIDDEN on app install

[POST /v1/apps](/docs/apps) on a self-hosted portal now returns `403` with code `CONNECTOR_APP_INSTALL_FORBIDDEN` when the Bitrix24 portal administrator has forbidden the user from installing applications. The `error.message` field carries a clear localized explanation pointing the user to ask their portal administrator. Previously this denial surfaced as a generic `502 CONNECTOR_APP_INSTALL_FAILED` with no cause; that code is still used for other install failures.

### NEW-0629-10: Transfer bot ownership to another key

Added [POST /v1/bots/:botId/transfer](/docs/bots/management/transfer) — moves a bot's ownership to another API key of the same Bitrix24 account and the same user (or an account admin). Resolves the case where a bot is orphaned after the app is recreated: the owning key is revoked and the bot's runtime stops working. Body: `{ "targetApiKeyId": "<id>" }`. The target key must be active, in the same account, with the `imbot` scope. After the transfer, verify the new key's B24 binding via `POST /v1/bots/:botId/reauth`.

## 2026-06-28

### FIX-0628-1: editing a key's scopes now applies them to the Bitrix24 webhook

**Before**

`PATCH /v1/keys/:id` with a `scopes` array saved the new set in Vibecode but, on self-hosted Bitrix24 portals, did not propagate it to the portal webhook. The webhook kept its old scope set, so a call to a freshly added scope was rejected by Bitrix24 (403) even though Vibecode already reported the key as carrying it.

**After**

A `scopes` change is now applied to the Bitrix24 webhook in the same request. If the sync cannot be performed, the key is not updated (Vibecode and Bitrix24 stay on the previous set) and the response carries an error code: `INVALID_SCOPES` (400) — the portal does not grant one of the requested scopes, `STALE_DEVELOPER_KEY` (410) or `RECOVERY_FAILED` (502) — the portal access key is invalid, `BOX_NO_DEVELOPER_KEY` (400) — the key owner has no portal access key, `DEVKEY_SCOPE_SYNC_FAILED` (502) — any other Bitrix24 rejection.

**Impact on integrators**

No action needed — scopes added via `PATCH /v1/keys/:id` now take effect immediately. If the response returns one of the codes above, the scope set was left unchanged: fix the cause and retry.

## 2026-06-27

### NEW-0627-1: Catalog product properties — read and manage the property schema

Added the [/v1/catalog-product-properties](/docs/entities/catalog-product-properties) section — definitions of a trade catalog's custom properties (id, name, type). List, get, create, update, delete, search, and the field reference are supported. A product's list-type properties come back in [/v1/catalog-products](/docs/entities/catalog-products) as fields named `propertyNNN`, where `NNN` is the property id. The new section maps that id to the property name and type. The `filter[iblockId]` filter scopes the selection to one catalog, with the id taken from [/v1/catalogs](/docs/entities/catalogs). Requires the `catalog` scope.

## 2026-06-26

### FIX-0626-1: catalog-prices: system fields priceScale, extraId, timestampX declared in the schema

**Before**

[GET /v1/catalog-prices](/docs/entities/catalog-prices/list) and [GET /v1/catalog-prices/:id](/docs/entities/catalog-prices/get) returned the `priceScale`, `extraId`, and `timestampX` fields, but they were not declared in the schema: they passed through without normalization (the `timestampX` field arrived in an offset format, e.g. `2024-06-17T16:53:24+05:30`) and were absent from the [GET /v1/catalog-prices/fields](/docs/entities/catalog-prices/fields) response.

**After**

The three fields are declared as read-only. They are now listed in [GET /v1/catalog-prices/fields](/docs/entities/catalog-prices/fields), and `timestampX` is normalized to ISO 8601 UTC (`2024-06-17T13:53:24.000Z`), consistent with the other datetime fields.

**Impact on integrators**

The instant in `timestampX` does not change — only its string representation does (UTC instead of a local offset). Clients that parse the value with a standard date parser keep working unchanged.

### FIX-0626-2: Server and app source download: the signed URL no longer returns 403

**Before**

`GET /v1/infra/servers/:id/sources/:versionId/download` and `GET /v1/apps/:id/sources/:versionId/download` returned `200` with a signed URL, but downloading from that URL failed with `403 AccessDenied` when the request used a personal key (`vibe_api_*`). Listing versions worked and the file was physically present in storage.

**After**

The signed URL is now bound to the object's actual storage location, so the download returns the archive content. The fix also covers snapshots whose storage key belongs to a different family (legacy app snapshots linked to a server).

**Impact on integrators**

The endpoint contract is unchanged — this restores the documented `200` + working signed URL behavior. No client-side changes are required.

### NEW-0626-3: api-bearer token refresh and Gateway rejection reason

The new endpoint [POST /v1/infra/servers/:id/access-tokens/:tokenId/refresh](/docs/infra/access-tokens/refresh) mints a fresh JWT (up to 10 minutes) for an existing `api-bearer` token — without creating a new record, and without consuming the active-token limit or the hourly mint limit. A long-running client (CI, AI agent) refreshes the token before `jwtExpiresAt` instead of minting a new one. A single revoke kills the original and all refreshed JWTs of one token.

The `401 BH_LOGIN_REQUIRED` response that the Gateway returns on an app subdomain when it rejects the `Authorization: Bearer` header now carries a `reason` field with the specific cause: `expired`, `signature`, `subdomain`, `type`, `revoked`, `malformed`, or `invalid`. The field is additive — existing clients ignore it.

## 2026-06-25

### FIX-0625-1: neutral provider, plan and region identifiers in the infrastructure API

**Before**

On the international (`.com`) surface, `GET /v1/infra/providers`, `GET /v1/infra/providers/:id/plans` and `GET /v1/infra/providers/:id/regions` returned provider, plan, disk-type and region identifiers in the underlying infrastructure's raw form rather than in the neutral brand namespace.

**After**

The same fields now use the neutral Bitrix Cloud namespace, matching the other segment: provider `bitrix-cloud`, plans `bc-small`/`bc-medium`/`bc-large`/`bc-xlarge`, `diskType: "network-ssd"`, regions `bc-eu-central`/`bc-eu-west`/`bc-us-east`/`bc-us-west`/`bc-ap-southeast`. The `name` and `country` fields stay human-readable (for example "Frankfurt (EU Central)", `DE`). The same values are returned in the `provider`/`plan`/`region` fields of `GET /v1/infra/servers` and `GET /v1/infra/servers/:id`.

**Integrator impact**

The standard flow needs no changes: an identifier read from the catalog is still passed to `POST /v1/infra/servers` as-is. Server creation accepts both the new ids (`bitrix-cloud`/`bc-small`/`bc-eu-central`) and the ids from the previous catalog, so existing integrations keep working. Only update code that compares response identifiers against hard-coded strings from the previous provider, plan, or region catalog.

### FIX-0625-2: offset inside /v1/batch sub-calls now pages through results

**Before**

In [POST /v1/batch](/docs/batch), `list` and `search` sub-calls silently ignored `offset` in `params`: Bitrix24 received an unknown `offset` key instead of `start`, so every page returned the same first set of records. Three `contacts.search` sub-calls with `offset` 0, 50 and 100 all returned the identical first page.

**After**

`offset` in a `list`/`search` sub-call is mapped to Bitrix24's `start` (matching the single `POST /v1/{entity}/search` endpoint). The same three sub-calls now return three distinct, non-overlapping pages. Single-endpoint behavior is unchanged.

### FIX-0625-3: Files and folders: deletedBy on non-deleted objects is now null

**Before**

[GET /v1/files/:id](/docs/entities/files/get), [GET /v1/files](/docs/entities/files/list) and the folder endpoints returned `deletedBy: 0` for a non-deleted object, although the field is declared as `number | null` with "`null` — the object is not deleted". The sibling field `deletedAt` correctly returned `null`, so two fields with the same contract behaved differently, and a `deletedBy !== null` check wrongly treated every active object as deleted.

**After**

`deletedBy` is normalized to `null` for non-deleted objects (Bitrix24 stores zero in the `DELETED_BY` column to mean "no user"; user id 0 does not exist). For deleted objects the field still carries the id of the deleting user.

**Impact on integrators**

The behavior now matches the documented `number | null` contract. Clients that checked `deletedBy === null` / `deletedBy !== null` now get the correct result for active objects.

### NEW-0625-4: Knowledge Base 2.0 reads — list bases, documents, tree, search

Read access to Knowledge Base 2.0: list the knowledge bases you can access (cursor pagination), get a knowledge base and a document by id (the document includes its Markdown content), the document tree of a knowledge base, and full-text search across documents. Scope `note`.

**Affected endpoints:** [GET /v1/note/collections](/docs/note/collections) (list), [GET /v1/note/collections/:id](/docs/note/collections) (one base), [GET /v1/note/collections/:collectionId/documents](/docs/note/documents) (tree), [GET /v1/note/documents/:id](/docs/note/documents) (document with Markdown), [GET /v1/note/documents/search](/docs/note/documents) (search by `query`).

### FIX-0625-5: Knowledge Base 2.0 methods (create, update, file upload) now work

**Before**

Creating and updating knowledge bases and documents ([POST /v1/note/collections](/docs/note/collections), [PATCH /v1/note/collections/:id](/docs/note/collections), [POST /v1/note/documents](/docs/note/documents), [PATCH /v1/note/documents/:id](/docs/note/documents)) returned `400` with a Bitrix24 validation error, and attachment upload ([POST /v1/note/documents/:documentId/files](/docs/note/files)) stored the file but did not return its id in `data.id`.

**After**

The methods work: create and update return `200`, and create responses for knowledge bases, documents, and files carry the id in `data.id`. Archive, delete, and file retrieval already worked.

### FIX-0625-6: /v1/me: storage supportedVisibilities are now uppercase

**Before**

[GET /v1/me](/docs/keys-auth) returned `supportedVisibilities: ["private","public"]` (lowercase) in the `storage` block, but the upload endpoints accept only `PRIVATE`/`PUBLIC` (uppercase). An agent copying the value from the manifest hit `STORAGE_INVALID_VISIBILITY`.

**After**

`supportedVisibilities` is returned as `["PRIVATE","PUBLIC"]` — exactly the values the `visibility` upload parameter accepts.

**Impact on integrators**

If a client took the `visibility` value from `/v1/me` and uppercased it itself, nothing changes. If it passed the value as-is, uploads now succeed without an error.

## 2026-06-24

### FIX-0624-1: galaxy app logs return container output

**Before**

[GET /v1/infra/servers/:id/logs](/docs/infra/deploy/logs) for a galaxy app (`kind=GALAXY_APP`) returned only the host system journal (`journalctl`), not the app container's own logs — you could not see the stdout/stderr of a crashed app.

**After**

For a galaxy app the endpoint reads the container's stdout/stderr (`docker logs`). The read is read-only: if the galaxy host is asleep or unreachable, the response is an empty `data.logs` plus `data.hint`, and the host is not woken. The `since` parameter for galaxy apps accepts only a duration (`10m`) or an RFC3339 timestamp — the human-readable `journalctl` forms ("1 hour ago") are allowed only for Black Hole servers.

### NEW-0624-2: GALAXY_APP_START_FAILED deploy error code

[POST /v1/infra/servers/:id/deploy](/docs/infra/deploy) for a galaxy app returns `502 GALAXY_APP_START_FAILED` when the app builds successfully but crashes or enters an OOM restart loop right after start. This is a distinct code from `GALAXY_APP_BUILD_FAILED` (a build error): it shows the build succeeded and the problem is at runtime (for example, exceeding the memory limit). The tail of the container logs is delivered in the `buildLog` field.

### FIX-0624-3: blocking server wake window raised to ~5 minutes

**Before**

A blocking wake — [POST /v1/infra/servers/:id/wake](/docs/infra/lifecycle/wake) with `?wait=true` and the auto-wake of a sleeping server on [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy) — waited for readiness (status RUNNING plus a connected tunnel) for up to about 3 minutes, then returned `504 WAKE_TIMEOUT`.

**After**

The main wait phase is raised from ~3 to ~5 minutes, and including the reboot phase the full ceiling before `504` is about 6.5 minutes. A deeply cold host (for example, a galaxy that has slept for several days) gets enough time to boot and connect instead of hitting a false timeout. The error code, response shape, and the proxy-side ceiling are unchanged.

**Impact on integrators**

If your client sets its own timeout for these calls, budget about 6.5 minutes instead of 3. Everything else is unchanged — you do not need to rewrite the integration.

### NEW-0624-4: placement and graduateFrom parameters on server create

[POST /v1/infra/servers](/docs/infra/servers/create) gained two optional parameters. `placement` — `auto` (default, unchanged behavior) or `dedicated`: on a portal with the `galaxies-only` placement model, the value `dedicated` creates a standalone virtual machine instead of a Galaxy application, passing the same checks as a normal server create — the `serverCreation` policy and the per-user server quota. `graduateFrom` takes the identifier of your Galaxy application (`kind=GALAXY_APP`): after the dedicated server is created, that application is deleted. `graduateFrom` is owner-scoped — a foreign or non-Galaxy identifier returns `404` and deletes nothing.

This is additive: without `placement`, or with `placement: "auto"`, the request behaves exactly as before.

In addition, when a Galaxy application fails with OOM, the response of [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy) with the code `502 GALAXY_APP_START_FAILED` now carries a structured hint `error.hint` with `recoveryAction: "graduate-to-dedicated-vm"` — how to re-create the application on a dedicated server via `placement: "dedicated"` and `graduateFrom`. The hint is added only when the failure cause is OOM (exceeding the container memory limit), not a plain crash.

### FIX-0624-5: server creation with code in source.content accepts archives up to 500 MB

**Before**

[POST /v1/infra/servers](/docs/infra/servers/create) with an inline archive in `source.content` (one-shot Galaxy app creation) returned `413 FST_ERR_CTP_BODY_TOO_LARGE` for archives above ~750 KB, even though the documented limit for `source.content` is 500 MB per request body. The route inherited the global 1 MB body limit.

**After**

The route accepts a request body up to 500 MB — matching `POST /v1/infra/servers/:id/deploy` and `POST /v1/infra/servers/:id/upload`. The documented limit now actually applies.

### NEW-0624-6: group requisites by tax IDs (INN/OGRN/KPP) in aggregate

[POST /v1/requisites/aggregate](/docs/entities/requisites/aggregate) now accepts `groupBy` on the requisite's string identifiers: `rqInn`, `rqKpp`, `rqOgrn`, `rqOgrnip`, `rqOkpo`, `rqVatId`, `rqResidenceCountry`, `rqCompanyName`, plus `presetId`, `entityTypeId`, `active`. Previously grouping by these fields returned `400 INVALID_PARAMS` with an empty list of available fields.

Grouping by `rqInn` is the fastest way to find duplicate requisites in a single call: groups with `count > 1` hold the repeated values. Existing calls (`groupBy` on `entityTypeId`/`presetId`) keep working unchanged. Numeric functions (`sum`/`avg`/`min`/`max`) remain unavailable on these string fields — they are for grouping only.

### FIX-0624-7: a sleeping server's availableActions includes wake/start; repair-status reports `running` immediately

**Before**

For a sleeping server with no tunnel (`SLEEPING` + `blackholeStatus: DISCONNECTED` — the normal state of a stopped server) the `availableActions` field in the `422 SERVER_WRONG_STATE` response and in `GET /v1/me` (`infra.unhealthyServers`) listed only `["repair","delete"]` — no obvious way to bring the server back. Separately: right after [POST /v1/infra/servers/:id/repair](/docs/infra/lifecycle/repair), a [repair-status](/docs/infra/lifecycle/repair-status) poll in the first milliseconds could return `{status:"idle"}`, and the poll loop exited prematurely.

**After**

`availableActions` for any sleeping server now contains `["wake","start","repair","delete"]` — both actions are genuinely accepted by the `/wake` and `/start` endpoints. And `repair-status` sets `running` synchronously when the repair starts, so the very first poll sees `running`, not `idle`. Existing calls keep working unchanged.

### FIX-0624-8: stage-history?entityType=invoice now returns smart-invoice history (31)

**Before**

[GET /v1/stage-history](/docs/entities-index)`?entityType=invoice` returned the history of the deprecated old invoice (entityTypeId 5, status-based: `statusId`/`statusSemanticId`). The current smart invoice (31) was reachable only under `entityType=new-invoice`. A client working with invoices via `/v1/invoices` (type 31) who queried history under `invoice` got a foreign, deprecated type.

**After**

`entityType=invoice` returns the current smart invoice's history (entityTypeId 31, stage-based: `stageId`/`stageSemanticId`/`categoryId`) — consistent with `/v1/invoices`. The `new-invoice` key is kept as an alias for 31 for backward compatibility, so nothing needs to change.

### NEW-0624-9: bitrix/embeddings text embeddings endpoint

An OpenAI-compatible [POST /v1/embeddings](/docs/ai/embeddings) endpoint converts text into vector representations (embeddings) for semantic search, clustering, deduplication, and finding similar CRM records. The `bitrix/embeddings` model is free and platform-provided — no provider key of your own is required. The `input` field accepts a string or an array of strings, and the response is returned in the raw OpenAI format: an `object` field set to `list`, a `data` array of objects shaped like `{ object: "embedding", embedding, index }`, and a `usage` block. The optional `encoding_format` (`float` or `base64`) and `dimensions` parameters are supported. The list of available models and their capabilities is at [GET /v1/models](/docs/ai/models/list), and the embeddings model has the `embeddings` capability set.

### FIX-0624-10: calendar event field types now match real responses

**Before**

[GET /v1/calendar-events/fields](/docs/entities/calendar-events/fields) declared `rrule` as `string` and `dateCreate` and `updatedAt` as `datetime`, while on read `rrule` comes back as an object and `dateCreate` and `updatedAt` come back as a string in the portal's regional format (not ISO 8601). The schema listed an `ownerType` field that responses never return. The `rrule` object carried internal keys `~UNTIL` and `UNTIL_TS`, and recurring-event list items carried an internal `RINDEX` key.

**After**

`/fields` declares `rrule` as `object` and `dateCreate` and `updatedAt` as `string`. The phantom `ownerType` field is removed from the schema. The internal keys `~UNTIL`, `UNTIL_TS`, and `RINDEX` no longer reach responses.

**Impact on integrators**

Documented fields did not change — a client that read only them keeps working. Use `from` and `to` (ISO 8601) for an absolute timestamp. Do not parse `dateCreate` and `updatedAt` with a fixed parser — their format depends on the portal's regional settings.

### NEW-0624-11: provisionReason field in the server responses

[GET /v1/infra/servers](/docs/infra/servers/list) and [GET /v1/infra/servers/:id](/docs/infra/servers/get) now return a provisionReason field with the value oom, crash or null — a structured reason for a galaxy app failure. Previously it was exposed only on the cabinet session routes, so the Vibecode API had to parse the free-text provisionError. The oom value signals to graduate the app to a dedicated server: create one with placement set to dedicated and graduateFrom. The field is optional and additive — existing integrations keep working unchanged.

### FIX-0624-12: the graduation signal fires for any galaxy app memory shortage

**Before**

A Galaxy app that ran out of container memory — both one that hit the limit and kept restarting at the edge and one that exhausts memory right at startup (for example, loading a big model) — was classified as an ordinary crash: [GET /v1/infra/servers/:id](/docs/infra/servers/get) returned provisionReason crash, and the [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy) 502 GALAXY_APP_START_FAILED response carried no graduation hint. Conversely, a non-OOM crash-loop (an unhandled exception) could be wrongly marked oom.

**After**

A real memory shortage in any shape — an instant boot-OOM or a grow-into-the-limit loop — now reliably yields provisionReason oom and an error.hint with recoveryAction graduate-to-dedicated-vm. Build errors and apps that never started (a broken start command) stay crash with no graduation.

**Impact on integrators**

No action needed: the field value and the hint now reflect the memory shortage more accurately. An agent can reliably detect provisionReason oom for any memory exhaustion and graduate the app to a dedicated server.

## 2026-06-23

### NEW-0623-10: push-delivery setup hint in event-subscription errors

The `400 NOT_OAUTH_APP` and `400 NO_USER_TOKEN` errors of [POST /v1/infra/servers/:id/event-subscriptions](/docs/infra/event-subscriptions) now include an `error.hint` field — a text instruction on how to get a server backed by an authorization key (`vibe_app_`) for push delivery: create an authorization key via `POST /v1/apps`, authorize the app on the portal, then create a new server under that key. There is no separate "migration" of an existing server from a regular key. The field is additive — existing clients are unaffected.

### FIX-0623-1: bizproc activity list

**Before**

[GET /v1/bizproc-activities](/docs/entities/bizproc-activities) returned each activity code as an object with per-character numeric keys — for example `{"0":"D","1":"i", …}` instead of the string `"DiskRead"`. The `Array.includes(code)` check did not work.

**After**

The endpoint returns activity codes as an array of strings, as documented.

### FIX-0623-2: duplicate-search response keys in camelCase

**Before**

`POST /v1/duplicates/find` returned the `data` object keys in upper case (`LEAD`, `CONTACT`, `COMPANY`), unlike the rest of the API which uses camelCase.

**After**

Keys come back in camelCase (`lead`, `contact`, `company`); the values (arrays of ids) are unchanged.

### FIX-0623-3: Scrum epic files field as an array of ids

**Before**

[GET /v1/scrum/epics/:id](/docs/scrum) returned the `files` field as a raw Bitrix24 UF object (with `VALUE_RAW`, `USER_TYPE_ID` and other internal metadata).

**After**

`files` is an array of attachment ids (`[417]`) or an empty array, consistent with the rest of the API.

### BC-0623-4: API key creation is admin-only

> Old format supported until: not applicable, the restriction takes effect immediately

**Before**

Any portal user could create an API key ([POST /v1/keys](/docs/keys-auth)).

**After**

Key creation is available only to portal administrators, others are rejected.

**What integrators should do**

Create keys under an account with Bitrix24 administrator rights.

### FIX-0623-5: Retry-After header on rate limiting

**Before**

On a `429` (rate limit exceeded) response the `Retry-After` header was not returned, so the integrator did not know when to retry.

**After**

The `429` response carries `Retry-After` with the interval in seconds. Use it as the pause before retrying.

### FIX-0623-6: a deleted server returns 404 again

**Before**

[GET /v1/infra/servers/:id](/docs/infra/servers/get) returned `200` with a full body and `status: "deleted"` for a soft-deleted server, although the documentation promises `404`. A client polling the endpoint and expecting `404` as deletion confirmation never received it.

**After**

The endpoint returns `404 NOT_FOUND` for a deleted server — the same as the [list](/docs/infra/servers/list) and [delete](/docs/infra/servers/delete), and as described in the documentation.

**Impact on integrators**

If your code relied on `200` with `status: "deleted"`, switch to checking for `404` (or for the server's absence from the [list](/docs/infra/servers/list)) as the deletion signal.

### NEW-0623-7: Universal Lists — full REST API

A new [Lists](/docs/lists) section (scope `lists`): programmatic access to Bitrix24 Universal Lists — the lists themselves, their fields, sections, and elements. 24 endpoints under `/v1/lists` over the `lists.*` methods.

A list is addressed by infoblock type (`iblockTypeId` — `lists`, `lists_socnet`, or `bitrix_processes`, default `lists`) and an identifier: a numeric path segment is treated as `IBLOCK_ID`, a non-numeric one as the symbolic `IBLOCK_CODE`. Fields, sections, and elements are available under nested paths.

If the Universal Lists module is not enabled on the portal, the call returns `409 LISTS_MODULE_NOT_ENABLED` — a sign the module is off, not an integration error.

**Affected endpoints:** `/v1/lists`, `/v1/lists/:iblockId`, `/v1/lists/:iblockId/fields`, `/v1/lists/:iblockId/sections`, `/v1/lists/:iblockId/elements`

### FIX-0623-8: smart-process relation isChildrenListEnabled flag

**Before**

The nested relation flag `isChildrenListEnabled` was accepted only as `true`/`false`. A `Y`/`N` value, like the other smart-process flags, was silently saved as disabled.

**After**

[POST /v1/smart-processes](/docs/entities/smart-processes/create) and [PATCH /v1/smart-processes/:entityTypeId](/docs/entities/smart-processes/update) coerce `Y`/`N` (and `1`/`0`, `yes`/`no`) to `true`/`false` for `isChildrenListEnabled` in relations.

### FIX-0623-9: deal custom field filter and select

**Before**

When filtering and selecting deal custom (UF) fields in the `UF_CRM_*` form, the field was rejected with `UNKNOWN_FILTER_FIELD` in the filter and silently dropped from `select`.

**After**

Deal custom fields are given in camelCase (`ufCrmCheckOut`) and work unchanged in filter and select.

**Affected endpoints:** [GET /v1/deals](/docs/entities/deals/list), [POST /v1/deals/search](/docs/entities/deals/search)

## 2026-06-22

### NEW-0622-1: deal-to-contacts relation

Manage a deal's contact set: read, add, replace the whole set, delete. PUT replaces the entire set at once. Scope `crm`.

**Affected endpoints:** `GET/POST/PUT/DELETE /v1/deals/:id/contacts` — [Deal contacts](/docs/entities/deals/contacts)

### BC-0622-2: model list contains only GA models

> Old format supported until: not applicable, experimental models were not part of the stable contract

**Before**

[GET /v1/models](/docs/ai/models/list) and the model list in `/v1/me` included experimental non-GA models.

**After**

The public list contains only GA models. Experimental ones are excluded from the list and rejected on call.

**What integrators should do**

Pick a model from the current `GET /v1/models` response, do not hardcode experimental model ids.

### FIX-0622-3: department auto-pagination

**Before**

[GET /v1/departments](/docs/entities/departments/list) returned only the first page when `limit > 50`.

**After**

Auto-pagination collects all departments into a single response.

## 2026-06-19

### FIX-0619-1: document creation

**Before**

[POST /v1/documents](/docs/entities/documents/create) returned `422` and did not create the document.

**After**

The endpoint creates a document from a template and returns the record.

### FIX-0619-2: warehouse auto-pagination

**Before**

`GET /v1/warehouses` and warehouse stock returned only the first page when `limit > 50`.

**After**

Auto-pagination collects every record into a single response.

**Affected endpoints:** [GET /v1/warehouses](/docs/entities/warehouses/list), [GET /v1/warehouses/:id/stock](/docs/entities/warehouses/stock)

### FIX-0619-3: list-type custom field values preserved

**Before**

When creating and updating a "list" (enum) custom field, the list values were lost.

**After**

List values are preserved on create and update.

**Affected endpoints:** [create](/docs/userfields/crm/create), [update](/docs/userfields/crm/update) a custom field

### FIX-0619-4: partial update of a basket item

**Before**

[PATCH /v1/basket-items/:id](/docs/entities/basket-items/update) did not perform a partial update of the item.

**After**

Partial update works, the `quantity` field is required in the body.

### FIX-0619-5: open-channel config filter and sort

**Before**

For [open-channel configs](/docs/openlines/config/list), filter and sort did not work for all fields, and values were not normalized on write.

**After**

Filter and sort respect the field schema, boolean values are coerced to the Bitrix24 format (`Y`/`N`) on write.

## 2026-06-18

### NEW-0618-1: grouping in deal aggregation

[POST /v1/deals/aggregate](/docs/entities/deals/aggregate) accepts `groupBy: "stageSemanticId"` — a breakdown by stage semantics (in progress, won, lost) for funnel analytics.

### NEW-0618-2: pagination and filter for stage history

`GET /v1/stage-history` supports pagination (`meta.total`, `meta.hasMore`) and a filter by entity type `entityTypeId`.

### FIX-0618-3: task time tracking does not reassign the author

**Before**

[PATCH /v1/tasks/:taskId/time/:id](/docs/entities/tasks/time) accepted a `userId` field, but Bitrix24 does not reassign the record author — the value was silently ignored.

**After**

The `userId` field is rejected with `400` — the author of a time-tracking record cannot be changed.

**Impact on integrators**

Do not pass `userId` when updating a time-tracking record.

### FIX-0618-4: calendar event time zone

**Before**

[PATCH /v1/calendar-events/:id](/docs/entities/calendar-events/update) could save the time in the Bitrix24 user's time zone rather than the event's own.

**After**

The event time zone is preserved on update.

## 2026-06-17

### NEW-0617-1: Knowledge Base 2.0 (note.*)

Knowledge base collections, documents, and attachments: create, update, archive, and delete knowledge bases and documents, plus attachment upload. Scope `note`.

**Affected endpoints (write methods):** [POST /v1/note/collections](/docs/note/collections), [POST /v1/note/documents](/docs/note/documents), [POST /v1/note/documents/:documentId/files](/docs/note/files), plus the paired `PATCH` and `DELETE`. Read methods are added in a separate entry.

## 2026-06-16

### NEW-0616-1: AI follow-up for completed calls

AI follow-up for completed calls. Scope `call`.

**Rolling out** — the methods ship in Bitrix24 update `call 26.600.0` and are not yet available on every portal. Until the update reaches a portal, the method returns `422 METHOD_NOT_YET_AVAILABLE` with the target version in the response — this is a sign of the rollout, not an integration error.

**Affected endpoints:** [POST /v1/calls/followups/list](/docs/calls/followup/list), [GET /v1/calls/followups/:callId](/docs/calls/followup/get)

### FIX-0616-2: transcription response format

**Before**

[POST /v1/audio/transcriptions](/docs/ai/audio/transcriptions) always returned a JSON object, even with `response_format=text`, `srt`, or `vtt`.

**After**

`text`, `srt`, `vtt` return the raw body as `text/plain`, SubRip, or WebVTT. `json` and `verbose_json` return a JSON object.

## 2026-06-12

### BC-0612-1: order payed field is read-only

> Old format supported until: not applicable, the field is now read-only

**Before**

`payed` was accepted in the order create and update body.

**After**

`payed` is read-only — it is rejected on write.

**What integrators should do**

Remove `payed` from the body of `POST /v1/orders` and `PATCH /v1/orders/:id`.

**Affected endpoints:** [POST /v1/orders](/docs/entities/orders/create), [PATCH /v1/orders/:id](/docs/entities/orders/update)

## 2026-06-11

### NEW-0611-1: Scrum API

Epics, linking tasks to epics, and reading the task chat. Scope `tasks`.

**Affected endpoints:** `/v1/scrum/epics`, `/v1/scrum/epics/:id`, `/v1/scrum/tasks/:taskId` — the [Scrum](/docs/scrum) section.

### NEW-0611-2: call rating on completion

[POST /v1/calls/:callId/finish](/docs/telephony/crm/finish) accepts a rating for the completed call and forwards it to Bitrix24.

## See also

- [API overview](/docs/quickstart)
- [Error codes](/docs/errors)
- [Batch requests](/docs/batch)
