# API changes: July 29, 2026

[← Changelog](/docs/changelog) · [July 2026](/docs/changelog/2026-07)

### FIX-0729-1: task comments are no longer refused over key permissions

**Before**

[POST /v1/tasks/:taskId/comments](/docs/entities/task-comments/create) could return `403` on accounts with the new task card, quoting the account's own "insufficient scope" message — even with the `task` permission present on the key and neighbouring task calls answering `200` in the same second. The wording was a dead end: it read as "grant the app access to tasks", while the permission set is fixed when the key is issued, not by anything the user can change.

**After**

The key is now granted the comment permission in both spellings the old and the new Bitrix24 routers expect, so the call goes through. Keys issued earlier get their permission set pushed to the account on the first such refusal and the request is retried — no manual step. If the account still refuses, the `403` now states the real cause (the key's webhook carries a narrower permission set than the key itself) and points at re-issuing the key, instead of repeating the account's message.

**Impact on integrators**

No action required. A client that treated this `403` as a permanent error now receives `201`.

### FIX-0729-2: access changes for an app on a shared host now reach how it is shown in Bitrix24

**Before**

For an app on a shared host (`kind=GALAXY_APP`) embedded into the Bitrix24 interface, a change to the access list did not always reach the display. A user whose access had been revoked could keep seeing the app in its embedding slot — while access to the app itself was already closed.

This covered switching the access policy and editing the list via [PATCH /v1/infra/servers/:id/access-policy](/docs/infra/access/access-policy), [POST /v1/infra/servers/:id/access](/docs/infra/access/access-add) and [DELETE /v1/infra/servers/:id/access/:accessId](/docs/infra/access/access-delete).

**After**

An access change now reaches the display: the app disappears from the Bitrix24 interface for those who lost access, and appears for those who were granted it.

**Impact on integrators**

No action required. Request bodies, responses and error codes are unchanged — only the observable effect of the call is different. Apps on a dedicated virtual machine are not affected: there the display already followed access.

### FIX-0729-3: agents and bots no longer idle-sleep

**Before**

[PATCH /v1/infra/servers/:id/sleep](/docs/infra/lifecycle/sleep) accepted any `sleepAfterMinutes` value, including servers created for an agent or bot (`createdVia` agent or bot).

**After**

For a server with `createdVia` agent or bot and a non-null `sleepAfterMinutes`, the endpoint responds `400` with code `AGENT_IDLE_SLEEP_FORBIDDEN`. The value `null` (never idle-sleep) is still accepted.

**Impact on integrators**

A sleeping agent or bot stops polling Bitrix24 and will not wake on a new message, so the previous value never worked — there is nothing to change in a working scenario. To save on a schedule, use Scheduled Wake.

### BC-0729-4: the session in Authorization must belong to the app from X-Api-Key

> Old format supported until: 29.07.2026

**Before**

A session (`vibe_session_*`) is issued to one app on one Bitrix24 account, but `/v1/*` never checked that binding. The authorization key of app B accepted a session issued to app A, and the request then ran with B's key rights — a foreign session opened data access through another app's key.

**After**

`/v1/*` verifies that the session in `Authorization: Bearer` belongs to the same app and the same Bitrix24 account as the key in `X-Api-Key`. A mismatch returns `403` with code `SESSION_APP_MISMATCH`. An app authorization key whose app is unlinked or deleted no longer accepts a session. Personal `vibe_api_*` keys are not affected by this change: they never read a session from `Authorization` — neither before it nor after.

Calls without `Authorization` (key only) are unaffected. A session presented with the key of its own app works as before.

**Impact on integrators**

Make sure both headers refer to one app: `X-Api-Key` must be the authorization key of the app that issued the session via `POST /v1/oauth/token`. If your service serves several apps, keep each key-and-session pair together and never source them separately.

The check is active from the release of this change, with no transition period: it closes data access through a foreign session. The `403 SESSION_APP_MISMATCH` error is described in the error reference.

### FIX-0729-5: an app on a personal key gets X-Vibe-Authorization again

**Before**

An app hosted in Black Hole on a personal key (`vibe_api_*`) lost the `X-Vibe-Authorization` header about a minute after the placement opened. The first requests carried a session, then it disappeared and never came back — not on a page reload, not on reopening the app — only a fresh placement open helped, and again only for a minute.

The cause: the session a placement issues to a user is bound to the app through its address (`appUrl`), but it was only ever recovered through the `server → key → app` chain. A personal key carries no app, so recovery answered "server not found" and the Gateway remembered that refusal. `X-Vibe-User-Id` kept arriving throughout, so from the app's side it looked like "the user is there but the token is gone".

**After**

When the key owning the server carries no app, the app is resolved by its address instead: among the apps of the same Bitrix24 account created by that key's owner, the one whose `appUrl` points at exactly this app address. The session is recovered and the header keeps arriving for the whole session lifetime.

Apps on an authorization key (`vibe_app_*`) behave as before — they do have the `server → key → app` chain, and it stays authoritative.

**Impact on integrators**

Nothing to change. If your app worked around this by reopening the placement or by caching the token itself, those workarounds can go.

### NEW-0729-6: a management key whose owner account is pending erasure now returns 503

**Before**

The account freeze that applies while a data-erasure request is pending covered the Vibecode
dashboard and ordinary app keys, but not management keys: an owner whose account was pending
erasure kept issuing, rotating and deleting keys through `/v1/keys`.

**After**

A management key whose owner account is pending data erasure returns `503` with code
`ACCOUNT_PENDING_ERASURE` and a `Retry-After: 3600` header — the behaviour ordinary app
keys have had for a while. Cancelling the erasure request makes the key work again with no
re-issue needed.

### FIX-0729-7: galaxy app deploy verifies reachability and returns steps

**Before**

A successful galaxy app deploy via [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy) returned `success: true, status: "running"` with no `data.steps[]` and without verifying that the app actually answered over HTTP. A container that started but did not listen on its port still reported success — you could not tell a working deploy from a broken one. Also [GET /v1/infra/servers/:id](/docs/infra/servers/get) showed `runtime: null` and the default port for such an app — the deployed runtime and port were not persisted.

**After**

The success response carries `data.steps[]`: a `{ step: "build", status: "ok" }` step plus, when the probe ran, a `{ step: "healthcheck", status: "ok" | "warning", httpCode, healthPath }` step. `status: "ok"` means the app answered 2xx/3xx on `data.appUrl`; `warning` means it answered 4xx/5xx (still reachable, the deploy passed). A container that started but did not answer over HTTP on its port now fails honestly with `502 GALAXY_APP_START_FAILED` (the same family as a crash after start — the response keeps a `buildLog` tail) instead of reporting success. The optional `healthPath` field (default `/`) in the deploy body sets the probe path. `GET /v1/infra/servers/:id` now reflects the deployed `runtime` and port.

**Impact on integrators**

An app that answers over HTTP on the deploy port is unaffected. An app that starts but does not begin answering within the probe window gets `502 GALAXY_APP_START_FAILED` instead of a false success — make sure it listens on the port you deployed with and that `healthPath` returns a response. The `build` step in `data.steps[]` and the `runtime`/port persistence in `GET` are available immediately; the HTTP probe itself (the `healthcheck` step and `502 GALAXY_APP_START_FAILED`) is rolling out — it is enabled gradually on the platform, so until it is active a deploy behaves as before (no probe).

### FIX-0729-8: servers wake up after the debt is cleared on postpay accounts too

**Before**

A postpay account that went negative down to its overdraft limit had its servers stopped and tagged as billing-frozen. Topping the balance back up made the API return 200 again, yet the servers stayed off: `POST /v1/infra/servers/{id}/wake` kept refusing (`SERVER_WAKE_BLOCKED`), and only support could clear the tag. The same scenario already worked on prepay accounts.

**After**

As soon as the balance is no longer negative, the tag is cleared and the servers are woken automatically — identically on prepay and postpay. A partial top-up that leaves the balance negative re-opens the API but keeps the servers off: they come back once the debt is fully cleared. Servers stopped for other reasons (expired access, a manual stop) are left untouched.

### FIX-0729-9: deploy no longer reports a false success after a hardened-unit rollback

**Before**

A deploy on `POST /v1/infra/servers/:id/deploy` could report success (`healthcheck:ok`, `hardening:warning`) while actually serving the response of a **foreign** process holding the app's port. This happened when the hardened unit failed, the deploy automatically rolled back to the plain unit, but the rollback did not free the port — and on the very first check the foreign port holder answered `200`. The deploy reported success even though the new version never took the port and the previous version kept running in production.

**After**

If the same process that blocked the hardened unit still holds the port after the rollback (the reverted unit never took it), the deploy ends with `healthcheck:error` and an explicit message that the port is still held, instead of a false `healthcheck:ok`. A normal rollback, where a new instance of the app has taken the port, still succeeds.

### FIX-0729-10: metadata cache now covers Bitrix24 account data and field schemas

**Before**

The cache for [GET /v1/statuses](/docs/entities/statuses/list) was documented as personal-key scoped, and `GET /v1/{entity}/fields` schemas were not covered in the caching section. A client could not tell from the docs which repeated requests return `X-Cache: HIT` or how to request a fresh field schema.

**After**

[GET /v1/statuses](/docs/entities/statuses/list) is documented as a 5-minute Bitrix24 account cache. `GET /v1/{entity}/fields` is documented as a 5-minute field-schema cache scoped by Bitrix24 account, authorization key, entity, path parameters, request parameters, and response language. `Cache-Control: no-cache` bypasses the cache for these reads, and `/fields` also supports `refresh=true`.

**Integrator impact**

No code changes are required. Repeated metadata reads put less load on the Bitrix24 account queue, and the `X-Cache` and `X-Cache-Bypass-Reason` headers show whether the cache was used.

### NEW-0729-11: delta of recent dialogs through the updatedAfter parameter

[GET /v1/chats/recent](/docs/chats/discovery/recent) accepts `updatedAfter` — an ISO 8601 instant from which changed dialogs should be returned. This is a separate response mode: `data` arrives as a flat array of dialogs, and `meta` carries `mode` with the value `delta` and `returned` with their count.

The page size in this mode is owned by the server — one page of up to 200 dialogs is read. A passed `limit` does not affect it and comes back in `meta.requestedLimit` together with the applied `meta.appliedLimit`. When the delta could not be confirmed complete — Bitrix24 reported more dialogs beyond the returned page, or the response shape could not be parsed — `meta` carries `truncated` with the value `true`: in that case do not move `updatedAfter`, read the full list using the paged mode with the `lastMessageDate` cursor. The number of rows returned is not a completeness signal.

The boundary is inclusive — a dialog whose `dateUpdate` equals the given instant is included. The date has to carry an explicit offset or `Z`: a value such as `2026-06-29 10:00:00` reads differently depending on the server time zone and is rejected with `400 INVALID_PARAMS`. The same code rejects `updatedAfter` combined with `offset` or `lastMessageDate` — paged navigation and the delta use different cursors.

### NEW-0729-12: a transient 503 at the platform edge now says how long to wait

When every backend replica is momentarily unreachable — during a galaxy application redeploy, for instance — the platform edge answers `503 SERVICE_UNAVAILABLE` on the `/api/` and `/v1/` prefixes. The body carried only the human-readable "Retry in a few seconds" and no machine-readable retry signal, so a client could not tell a seconds-long gap from a permanent outage and either failed the job or retried blindly.

That response now carries the `Retry-After: 5` HTTP header and a `retryAfter: 5` field inside the `error` object — next to `code` and `message`, exactly as the platform already does for its own transient 503. Existing calls are unchanged: the `SERVICE_UNAVAILABLE` code and the status stay put, and the header plus the field are added. The message text now also points at the header — you still should not parse it, branch on `error.code`. The full code list is at [/docs/errors](/docs/errors).

The same edge block also answers a read timeout from the backend. There the request DID reach the backend and may still be running, so for non-idempotent operations re-read the entity before retrying.

Worth stating what this does NOT do: it does not remove the reason the backend upstreams became unreachable. It makes the error honest and machine-readable so a client waits and retries correctly.

### BC-0729-13: batch sub-calls reject a sort when the Bitrix24 method cannot do one

> Old format supported until: 29.07.2026

**Before**

The single entity list and its `POST /v1/departments/search` already answered `400 INVALID_SORT_FIELD` when the Bitrix24 method behind the list accepts no ordering. A sub-call of the global `POST /v1/batch` had no such check: the same sort went to the method, the method discarded it, and the sub-call returned success with an unsorted list. The same query therefore behaved differently on the two surfaces — refused through the single route, silent through the batch one.

**After**

A `POST /v1/batch` sub-call with action `list` or `search` now runs the same check and answers `400 INVALID_SORT_FIELD` under that sub-call's `errors` entry, while the remaining sub-calls run as usual. The refusal is raised before the Bitrix24 call. Both spellings are checked — `sort` and `order`. Two entities are affected: `departments` and `telephony-lines`. Storages are NOT — their method can sort, and their refusal is lifted by a separate entry in this release.

**What this means for integrators**

If a sub-call to one of those two passed a sort, drop it — it never applied and the list came back in Bitrix24's own order. If you need a specific order, sort the returned list on your side. Sub-calls without a sort, as well as `limit`, `offset`, `select` and filtering, work exactly as before. There is no parallel support for the old behaviour: the old behaviour was the parameter being silently ignored, so there is nothing to keep.

### BC-0729-14: `defaultOperatorData` on Open Channels is an object now, and empty means `null`

> Old format supported until: 27.01.2027

**Before**

The field was declared an array, and an unset value was coerced to `[]`. The real type is different: Bitrix24 returns an object shaped `{ "NAME": …, "AVATAR": … }` when default operator data is set. So [GET /v1/openline-configs](/docs/openlines/config/list) and `GET /v1/openline-configs/:id` promised an array in `fields` while sending an object whenever the value was populated.

**After**

The field type is `object`; an unset value arrives as `null` rather than `[]`. A populated value arrives as an object, as it already did. The neighbouring `kpiFirstAnswerList` and `kpiFurtherAnswerList` are genuine string arrays and are still coerced to `[]`.

**What integrators should do**

Code that measured or iterated this field (`defaultOperatorData.length`, `.map`, `.forEach`) will break on `null` — switch the check to `if (config.defaultOperatorData) { … }` and read the object's fields directly. If you built against `fields` and expected an array, re-read the new `object` type.

### BC-0729-15: telephony lines: writing `serverName`, sorting, filtering and offset no longer fail silently

> Old format supported until: 29.07.2026

**Before**

`serverName` was declared a plain writable field, but Bitrix24 neither stores nor returns it: [POST /v1/telephony-lines](/docs/telephony/lines/create) with that field answered `201` while the value vanished, and a `PATCH` of the same field hit an error from Bitrix24 itself. Ordering, filtering and offset behaved the same way: the Bitrix24 method behind this list accepts no input parameters at all, so `?order[number]=desc`, `?name=…`, `?filter[number]=…`, `?offset=50` and the same values in the body of `POST /v1/telephony-lines/search` were dropped and the list came back `200` — looking sorted, filtered and paged while it was none of those. A sub-call of `POST /v1/batch` lost the sort the same way.

**After**

All four are now an explicit error raised before the Bitrix24 call. Writing `serverName` returns `400 READONLY_FIELD`; any sort returns `400 INVALID_SORT_FIELD`; any filter returns `400 UNSUPPORTED_FILTER`; a non-zero offset returns `400 UNSUPPORTED_OFFSET`. The filter refusal is raised on the list, in search, in `POST /v1/telephony-lines/aggregate` and in sub-calls of both batch endpoints — the global `POST /v1/batch` and `POST /v1/telephony-lines/batch`; the sort and offset refusals are raised on the list, in search and in a sub-call of the global batch (aggregate reads neither parameter). In the global `POST /v1/batch` the refusal arrives under that sub-call's `errors` entry while the remaining sub-calls run as usual; if every sub-call is refused the request answers `400` and the per-sub-call breakdown stays in `errors`. `POST /v1/telephony-lines/batch` differs: a filter-contract violation rejects the WHOLE batch with a single `400` naming the sub-call index, and no sub-call runs. The `serverName` field itself stays visible in `GET /v1/telephony-lines/fields` marked read-only, so its meaning is still discoverable.

**What this means for integrators**

If you sent `serverName` on create or update, drop the field — the value was never stored anyway. If you relied on sorting, filtering or offset, none of them ever applied; the list of an application's external lines arrives whole in a single page, so sort, filter and page it on your side. A plain list without those parameters, plus `limit` and `select`, works exactly as before. There is no parallel support for the old behaviour: the old behaviour was the parameter being silently ignored, so there is nothing to keep.

### NEW-0729-16: the storages list can be sorted

Sorting by storage fields works on [GET /v1/storages](/docs/entities/storages/list) and in [POST /v1/storages/search](/docs/entities/storages/search): `?sort=-id`, `?order[name]=desc` and the same values in the search body. A leading minus means descending.

The fields that change the order are `id`, `name`, `entityType`, `entityId`, `rootFolderId` — each was verified on a live account to return different results ascending and descending. The method also accepts `code` and `module` without an error, but on the accounts probed every storage carries the same value in those columns, so ordering by them changes nothing — do not rely on them as a sort.

Previously any sort of storages was rejected with `400 INVALID_SORT_FIELD`. That refusal was a mistake: it had been inferred from the description of the Bitrix24 method, while the method does accept and apply an ordering — verified on a live account, where ascending, descending and unsorted results all differ. If you worked around the refusal by sorting the list on your side, that workaround can go; it keeps working either way.

The ordering also became stable. `id` is appended to your sort as a final key, and a list with no sort now arrives ascending by `id` — previously it arrived in the account's unspecified order. This is not cosmetic: the list is served 50 records at a time, and when sorting by a non-unique field (a name, say) a row sharing that value could land on two pages at once, or drop out of the result entirely, at a page boundary. The order is now total and unambiguous, which is also what makes paging over it repeatable. If you already sorted by `id`, your direction is preserved — no second key is added.

Storages are paged 50 records at a time, so when sorting, ask for the volume you need in one call (`limit` up to 5000) rather than walking pages by hand.

### FIX-0729-17: restart a galaxy app via /reboot

**Before**

For a galaxy app (`kind=GALAXY_APP`), [POST /v1/infra/servers/:id/reboot](/docs/infra/lifecycle/reboot) had no working path: the call returned `422 VM_MISSING` (the container has no virtual machine of its own), and the only way to recover a stuck app was to delete it — which wipes the persistent `/data` volume.

**After**

`/reboot` restarts the app container as a self-recovery kick — the persistent `/data` volume is preserved. The call is accepted in the `running` or `error` status and returns an advisory verdict: `restarted` (the container was restarted) and `healthy` (the container came up and stopped restarting — a container check, not the app's HTTP response), plus a `hint` field when `healthy: false` about how to deploy a fixed version. The restart does not clear the error state — a crash-looping app is authoritatively reset by redeploying its source via [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy). New error codes for this path: `GALAXY_APP_REBOOT_USE_AGENT_CONTROLS` (409 — an agent- or bot-backed app is managed from its own controls), `GALAXY_APP_BUSY` (409 — another command is running on the host, the response carries `Retry-After`), `GALAXY_HOST_UNREACHABLE` (502 — the host is unreachable). Rebooting a regular server is unchanged.

### NEW-0729-18: totalDefault — the meta.total default on the API key itself

An API key gained a `totalDefault` setting: whether list calls made with this key request a count when the request itself passed no `withTotal`. A value of `true` means send `meta.total`, `false` means do not, and `null` means inherit the platform default. Every key starts at `null`.

The setting is edited in the dashboard on the keys page and through [PATCH /v1/keys/:id](/docs/management-keys) with the `totalDefault` field (a `vibe_live_` management key). Rotating a key through `POST /v1/keys/:id/rotate` preserves the setting, just as it preserves the access mode. A change is written to the audit log.

The value in force is visible in [GET /v1/me](/docs/keys-auth/me) — the `totalDefault` block shows the whole chain: `key` (the key setting), `platform` (the platform default), `effective` (what applies when a request passes no `withTotal`) and `source`, telling you where the effective value came from.

The setting is for cases where changing integration code costs more than flipping a key once: it sets the default for every list call made with that key at a stroke. The `withTotal` request parameter overrides it per call.

### NEW-0729-19: meta.nextAfterId — the next-page cursor when sorting by id

Responses of [GET /v1/{entity}](/docs/entity-api) and [POST /v1/{entity}/search](/docs/entity-api) gained an optional `meta.nextAfterId` field — the identifier of the last returned record, as a string.

The field arrives when three conditions hold at once: the entity has a numeric identifier and supports cursor paging, the request sort is strictly `id` ascending, and `meta.hasMore` is `true`. On the last page the field is absent — there is nowhere left to go. Today the conditions are met by deals, leads, contacts, companies, quotes and smart-process items.

You pass the value back through the same filter cursor paging already used: `filter[>id]=<nextAfterId>` with the sort `id` ascending. No new request parameter appeared — the field only saves you from reading the identifier out of the last row by hand.

This kind of paging does not depend on an offset and does not get more expensive towards the end of a collection, so for walks of tens of thousands of records it is preferable to a growing `offset`.

### NEW-0729-20: withTotal — a list call can decline the count

List calls gained an optional `withTotal` parameter. On [GET /v1/{entity}](/docs/entity-api) it is a query parameter with exactly two accepted values — `true` and `false`; on [POST /v1/{entity}/search](/docs/entity-api) it is a body field with a boolean value. Anything else reads as "the parameter was not passed", and no error is raised.

`withTotal=false` asks the platform not to count. Where that request can be honoured, no count is ordered from Bitrix24 and `meta.total` is absent from the response. Where the count cannot be avoided, the parameter has no effect and `meta.total` arrives as before. So check whether the field is present instead of assuming it.

Page by `meta.hasMore` — it is derived from page fullness and carries a "read while `hasMore`" loop to the end whether or not a count happened. When the sort is strictly `id` ascending, the response also carries `meta.nextAfterId`, which you pass back in `filter[>id]`.

If the parameter is absent, the value comes from the API key setting, and failing that from the platform default. The value in force right now, and the whole chain behind it, is shown by the `totalDefault` block in [GET /v1/me](/docs/keys-auth/me).

When you genuinely need an exact count, ask for it directly: `POST /v1/{entity}/aggregate` with the `count` function returns the number in one call without fetching any records. Do not emulate a counter by walking the collection page by page — that is dozens of calls instead of one, and the most expensive way to learn a single number.

### FIX-0729-21: meta.hasMore in lists is derived from page fullness, and meta.total may lag by up to a minute

**Before**

`meta.hasMore` in [GET /v1/{entity}](/docs/entity-api) and [POST /v1/{entity}/search](/docs/entity-api) responses was derived from `meta.total`: "there is more" meant "`offset` plus the returned rows is below the overall count". While the count was recomputed on every call, that matched reality.

**After**

The platform stops asking Bitrix24 to recount on every repeated call with the same key and query — counting is disproportionately expensive for the account. That has two observable consequences.

`meta.hasMore` on such responses is derived from page fullness: a full page means "there may be more", a short page means the list has ended. A "read while `hasMore`" loop still always reaches the end. When the collection size is an exact multiple of `limit`, the last step returns an empty list — that is the normal end-of-list signal.

`meta.total` stays a number and stays in place, but becomes informational: it may lag by up to a minute, so the number of returned rows can exceed it.

**Impact on integrators**

Nothing to change if you page by `meta.hasMore` — that is the recommended way. If your code treats `meta.total` as an exact loop bound, or asserts that the returned rows never exceed it, switch to `meta.hasMore`. For an exact count at request time use `POST /v1/{entity}/aggregate` with the `count` function.
