# API changes: July 30, 2026

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

### NEW-0730-1: withTotal in the list calls of a batch request

A list call inside [POST /v1/batch](/docs/batch) accepts the `withTotal` parameter in `params` — the same one the single [GET /v1/{entity}](/docs/entity-api) takes. `withTotal: false` means "the count is not needed": no count is ordered from Bitrix24, and `data.totals.<id>` and `meta.<id>.total` are absent from the response. The paging bound is still `meta.<id>.hasMore`.

The applicable scope is worth knowing before the parameter reaches your code. It takes effect on calls with `action: "list"` — both on those whose `limit` is at most 50 and whose `offset` is zero (those are the calls that travel to Bitrix24 as a single command of the batch) and on those whose `limit` is above 50. On `action: "search"` and in the single-entity batch `POST /v1/{entity}/batch` the parameter is inert — the count is ordered as before and `total` arrives.

The rule for whether `total` is present is the same as on a single call: when no count was ordered and the page came back shorter than a full one, the exact number still arrives — the page itself proves it.

On a call that did not request a count, `meta.<id>.hasMore` is derived from the fullness of the page rather than from the absent number: a full page means more records remain, a short page means the collection ended. A "read while `hasMore`" loop reaches the end without a `total`; when the collection size is an exact multiple of the page size, the last call returns an empty list — the normal end-of-collection signal, not an error.

Along with this, a call that did not request a count no longer receives `data.totals.<id>` and `meta.<id>.total` on a positioned page either — that is, at an `offset` above zero. The single endpoints behave the same way: the count must not blink in and out within one walk.

### FIX-0730-2: meta.total arrives on a short page even when no count was ordered

**Before**

When a list call did not request a count, `meta.total` was always absent from the response — including where the number was already known exactly. A page shorter than the requested `limit` means the collection ended there, so the record count equals the number of returned rows, yet the field still did not arrive.

**After**

In that case `meta.total` does arrive and carries the exact number. The rule is simple: `total` appears only on a call with `offset = 0`, and only when the page came back shorter than requested. An empty result is a number too: `"total": 0`.

An explicit `withTotal=false` in the request still means "there will be no `total` field" — the promise about that parameter has not changed. The `totalDefault` setting on the API key and the platform default do not forbid the exact `total` derived from a short page: no such promise was made about them. Hence a consequence worth knowing up front: two identical requests from two different keys can return responses of different shape.

The rule also applies to list calls inside [POST /v1/batch](/docs/batch).

**What this means for integrators**

Nothing to change. `meta.total` stays an optional field: check whether it is present in a given response instead of assuming it. The bound of a paging loop is still `meta.hasMore` — arithmetic over `meta.total` was never fit for that, before or after this change.

### FIX-0730-3: free tier no longer gets 402 PLAN_NOT_ALLOWED_ON_TRIAL when deploying into a galaxy

**Before**

`POST /v1/infra/servers` with `{ name, source, runtime, start }` on a galaxy-placement account returned `402 PLAN_NOT_ALLOWED_ON_TRIAL` (`details.allowedPlans: ["bc-micro"]`) even though `GET /v1/me` advertised `capabilities.servers.create.available: true` with that same `bc-micro`. The request carried no plan at all: the platform provisioned the shared galaxy host on its own (non-`bc-micro`) plan, and that internal choice was checked against the same plan whitelist as a user-created VM. Every galaxy app also consumed the single free-tier slot, so a second deploy hit `402 TRIAL_PORTAL_LIMIT`.

**After**

One-shot create-and-deploy works on the free tier. The shared galaxy host occupies the single VM slot and the platform picks its plan (the segment's minimal stable plan), while app containers on that host consume no slot — their number is bounded only by host capacity. The plan list in `capabilities.servers.create.limits.allowedPlans` now governs only a VM you create yourself; `deployment.galaxyApp._rules` (the `QUOTA` rule) states this explicitly. Free-tier limits for `placement: "dedicated"` are unchanged.

### FIX-0730-4: /v1/me no longer advertises access-token endpoints while the feature is off

**Before**

The `data.infra.preview` block of [GET /v1/me](/docs/keys-auth/me) always carried the `mintUrl`, `listUrl`, `revokeUrl` and `refreshUrl` addresses — including when access tokens are switched off on the platform and all four endpoints answer `503 FEATURE_DISABLED`. The neighbouring `data.capabilities.servers.preview` and `data.deployment.preview` blocks of the same response reported `{"available": false, "reason": "FEATURE_DISABLED"}`, so a single response contradicted itself.

**After**

`data.infra.preview` now follows the same signal as its two neighbours. While the feature is on, `"available": true` comes alongside the addresses, and while it is off the block is `{"available": false, "reason": "FEATURE_DISABLED"}` with no addresses. The `data.api._rules` entry about checking a deployment through `api-bearer` mode names that check and the fallback — the `healthcheck` and `tunnel_routing` steps of the [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy) report.

**Impact on integrators**

A client that read the addresses from `data.infra.preview` unconditionally now gets the block without them while the feature is off. Check the `available` field before using the addresses — calls to them already answered `503 FEATURE_DISABLED`.

### FIX-0730-5: the X-Vibe-User-Id header no longer mixes identifier namespaces

**Before**

The `X-Vibe-User-Id` header the platform sets on every request to an app was documented as a numeric Bitrix24 user ID. On some sign-in routes it carried an internal Vibecode identifier with no marker at all, or the value `0` — a placeholder meaning "visitor not identified". An app had no way to tell those apart from a real ID. The dangerous case is an internal identifier that starts with digits: Bitrix24 coerces the string to a number, so `47a4cff2-…` became `47` — a valid ID of an unrelated employee. An app that passed the header into `DIALOG_ID` of `im.message.add` delivered a private message to the wrong person.

**After**

The header value is now always one of two shapes: a digits-only string — the Bitrix24 account user ID — or a value with an explicit prefix when the visitor has no ID in the account: `net_` for a Bitrix24 Network user outside the account, `share:` for an anonymous visitor following a guest link, `vibe:` for a Vibecode user whose account ID could not be resolved. The value `0` is never sent: when the identity is unknown, the header is simply absent. For a visit through the Vibecode dashboard the platform now resolves the real account user ID and sends that, where an internal identifier used to arrive.

If your app passes the header into Bitrix24 method parameters (`USER_ID`, `DIALOG_ID`, `RESPONSIBLE_ID`, and the like), check that the value consists of digits only. A prefixed value means the visitor has no ID in the account and must not be forwarded to Bitrix24. For logs, analytics, and your own ACL the header is usable in any shape. Full contract — [What arrives in the app](/docs/infra/app-runtime).

### BC-0730-6: a galaxy is no longer created on the free Bitrix24 plan

> Old format supported until: 30.08.2026

**Before**

A Bitrix24 account on the free plan with Galaxy mode enabled got a galaxy for its apps. A one-shot `POST /v1/infra/servers` carrying `source` deployed the app as a container on a shared host, and the `deployment` block of `GET /v1/me` described the galaxy contract.

**After**

On the free plan a new galaxy is not created. While the account has no galaxy, every app deploys to a dedicated virtual machine and a create carrying `source` returns `400 SOURCE_AT_CREATE_GALAXY_ONLY`. In that state `GET /v1/me` omits `deployment.galaxyApp`, sets `deployment.primary` to `standalone`, and states the reason in the new `deployment.placementNote` field.

An account that already has a galaxy keeps deploying into it — only creating a new one is restricted. A commercial Bitrix24 plan lifts the restriction entirely.

**What integrators should do**

Deploy in two steps, like any dedicated server: [`POST /v1/infra/servers`](/docs/infra/servers/create) with `provider`, `name`, `plan`, `region` (no `source`), wait for `status: "running"` and `blackholeStatus: "CONNECTED"`, then [`POST /v1/infra/servers/:id/deploy`](/docs/infra/deploy/deploy) with `source`, `runtime`, `start`. Detect the placement model from `GET /v1/me` — by whether the `deployment.galaxyApp` block is present, not from the account's mode.

### FIX-0730-7: POST /v1/apps reports the plan requirement clearly on a self-hosted portal

**Before**

Installing an app on a self-hosted portal whose Bitrix24 plan does not grant the required access made [POST /v1/apps](/docs/apps/create) return an opaque `502 CONNECTOR_APP_INSTALL_FAILED`: the cause was never reported to the caller, and the code itself promised a temporary failure — a retry looked reasonable while it could never help.

**After**

A plan-access denial is now recognised on a self-hosted portal too: `POST /v1/apps` returns `403` with a code that names the cause — `INT_TARIFF_REQUIRED`, meaning the account needs a commercial Bitrix24 plan — together with a readable message. An account on the plan-based access model gets it, and so does an account where REST itself is unavailable. Other connector install failures are classified as before.

**Impact on integrators**

No action required, successful calls are unchanged. If you handled `502 CONNECTOR_APP_INSTALL_FAILED`, also handle `403 INT_TARIFF_REQUIRED` and prompt the user to upgrade the Bitrix24 plan of the account: this denial is terminal, retrying will not help.

### NEW-0730-8: new 429 TIMEOUT_QUARANTINE refusal: a method that stopped answering is paused

When the same method fails to answer your Bitrix24 account within the call timeout several times in a row, Vibecode stops sending requests to it and answers `429` with the code `TIMEOUT_QUARANTINE`, a `Retry-After` header and an `error.retryAfter` field. The pause covers the "account + method" pair: other methods keep working as usual, and it does not depend on which key made the call.

This is a Vibecode-side refusal, not a Bitrix24 limit: the request never reached the account, so nothing changed — a retry is safe even for write methods. The pause lifts itself: one call is let through periodically as a probe, and the first successful answer lifts it immediately. No manual step is needed, and there is no endpoint to lift it.

The response now carries a machine-readable `error.scope` field: on this refusal it is `"portal"` — the pause is shared by EVERY key on the account, third-party integrations included. On the neighbouring `OPERATION_TIME_LIMIT` refusal (Bitrix24 paused a method that exhausted its operating-time budget) it is `"apiKey"` — there only the calling key is paused. The same two `error.scope` values already arrive on the feedback-quota refusal `FEEDBACK_QUOTA_EXCEEDED`, so the vocabulary is shared. The distinction answers "fix my own code or wait alongside the account" without parsing the error text. Next to it comes `error.hint` with the action to take, in English. Both codes are now documented in the [error reference](/docs/errors). The number of other keys, their names and the volume of their failures never appear in the response — that is other clients' data.

What to do: wait out the `Retry-After` delay and retry with a random extra delay added — do not spin the retry in a loop. Do not shorten the retry interval either: while the pause holds, one call every 5 minutes is let through as a recovery probe, and an aggressive retry occupies that slot itself — the method then stays closed for the whole account longer than if you had simply waited. If the method keeps not answering, make the call lighter: fewer fields in `select`, a smaller page, a narrower filter or a narrower date range. A heavy request is precisely why the account cannot answer in time — the pause lifts on the first call the account manages to complete.

The code can arrive on any call Vibecode proxies to Bitrix24 under a Bitrix24 method name: on single reads and writes as a `429` with the header, and on the batch sub-calls Vibecode runs as separate requests (search, and list with a `limit` above 50) inside a `200` response, as a `data.errors[<id>]` entry shaped `{ "code": "TIMEOUT_QUARANTINE", "message": …, "retryAfter": …, "scope": "portal", "hint": … }` — the envelope bundles different calls, so it carries no per-sub-call `Retry-After` header and the delay arrives as a field instead. In a single-entity batch ([POST /v1/{entity}/batch](/docs/batch)) the refusal likewise arrives inside a `200`, but as an element of the `data` array: `{ "error": { "code": "TIMEOUT_QUARANTINE", "message": …, "retryAfter": …, "scope": "portal", "hint": … } }`. The [POST /v1/batch](/docs/batch) envelope itself is never paused: it bundles different methods, and its own delay says nothing about which of them stopped answering. The bot event poll ([GET /v1/bots/{botId}/events](/docs/bots)) is likewise never paused — it has its own answer to a timeout, carrying a hint about restoring the subscription. In a date-windowed search ([POST /v1/{entity}/search](/docs/entity-api)) the refusal arrives either as the same `429` or — when some windows were read — in `meta.windowErrorSample.code` alongside a `200`, which signals an incomplete result set.

The guard is enabled gradually, account by account, so not everyone will see this code yet.

### FIX-0730-9: the OPERATION_TIME_LIMIT refusal in a batch now arrives with its own code and delay instead of a generic one

**Before**

Bitrix24 pauses a method that exhausted its operating-time budget for about 5 minutes, and Vibecode refuses such calls up front while the pause still holds. On a single call that refusal arrived as a `429` with the code `OPERATION_TIME_LIMIT`, a `Retry-After` header and the `retryAfter` and `scope` fields. Inside a batch the same refusal lost both the code and the delay: on the [POST /v1/batch](/docs/batch) sub-calls Vibecode runs as separate requests (search, and list with a `limit` above 50) it arrived as `data.errors[<id>]` under the generic code `AUTO_PAGINATION_FAILED`, and in a single-entity batch ([POST /v1/{entity}/batch](/docs/batch)) as `data[i].error` with the code `CALL_FAILED` and the text `Internal error`. The response was a `200`, so there was nothing to tell a paused method apart from an internal fault, and the retry delay never arrived at all — leaving you to retry blindly against a method the account keeps closed.

**After**

Both batch surfaces now return the same refusal a single call does: `{ "code": "OPERATION_TIME_LIMIT", "message": …, "retryAfter": …, "scope": "apiKey", "hint": … }` — in `data.errors[<id>]` for [POST /v1/batch](/docs/batch) and in `data[i].error` for [POST /v1/{entity}/batch](/docs/batch). The envelope bundles different calls, so it carries no per-sub-call `Retry-After` header — the delay arrives as the `retryAfter` field. The `scope` field is `"apiKey"`: the "your key + this method" pair is paused, other methods keep working, and other keys on the account may still call the same method. The contrast is `TIMEOUT_QUARANTINE` with `scope: "portal"`, where the pause is shared by the whole account. The localized `userMessage` field is absent from a `200` envelope on every refusal, so it is absent here too. The limitation is gone from the [error reference](/docs/errors) as well.

**Impact on integrators**

Nothing to change: the codes narrowed from generic to specific, and fields were added. If your code branched on `AUTO_PAGINATION_FAILED` or `CALL_FAILED` to catch a paused method, `OPERATION_TIME_LIMIT` and the delay in `retryAfter` are now the way to do it — wait the delay out and retry, rather than spinning the retry in a loop.

### NEW-0730-10: the machine-readable schema now describes 51 endpoints that worked but were missing from it

All of them answered before — but a client that builds calls from `/v1/openapi.json` (a code generator, an AI agent, our own reference) could neither see nor discover them.

Product rows — single-row access and its field schema: `GET|PATCH|DELETE /v1/{deals,leads,quotes,invoices}/{id}/products/{rowId}` and `GET /v1/{deals,leads,quotes,invoices}/{id}/products/fields`. The same for smart processes: `/v1/items/{entityTypeId}/{id}/products/{rowId}` and `.../products/fields`.

Field schemas: `GET /{entity}/fields` appeared for 22 entities for which Bitrix24 exposes no schema method (orders, documents, document templates, payments, basket items, order statuses, catalogs, catalog sections and prices, product properties, bookings, calendar events and workgroups, departments, sites and pages, business-process templates/activities/robots, telephony lines, open-channel configs, org-structure nodes). The response there is the field set declared by the wrapper, without custom (UF) fields: the operation description says so outright, so nobody expects more.

Open Channels: `GET|POST /v1/openline-configs`, `PATCH|DELETE /v1/openline-configs/{id}`, `POST /v1/openline-configs/search` and the read-only `POST /v1/openline-configs/batch` (batch serves `list`, `get`, `fields` only — writes go through the operations listed above). The list carries its real envelope: `total` is the size of the returned window, not of the whole set, so bound a paging loop by `hasMore`.

Bookings: `GET /v1/bookings` and `POST /v1/bookings/search`. The date window (`dateFrom`, `dateTo`) is documented as required — without it the Bitrix24 method would silently return an empty list, so the wrapper refuses the call instead.

Lead conversion: `POST /v1/leads/{id}/convert` — the operation description states that it is not idempotent (a repeat call creates a second set of entities).

**Affected endpoints:** `GET /v1/openapi.json`

### FIX-0730-11: the BYOK credential verification error no longer contains the key itself

**Before**

When a provider rejected a credential check and echoed the submitted key back in its message, that text reached the caller and was stored in the lastError field returned by GET /v1/ai/credentials. Only sk-… keys, URLs and user:password@host pairs were masked, so keys from other providers landed in the response and in the field verbatim — readable by any member of the account.

**After**

The submitted key and the proxy URL are removed from the error text by value, whatever their format: the responses of POST /v1/ai/credentials, PATCH /v1/ai/credentials/{id}, POST /v1/ai/credentials/{id}/test, and the stored lastError all carry a redaction marker instead. Error codes and statuses are unchanged.

### FIX-0730-12: a multi-page list returns the start of the result set instead of an error when the count fails

**Before**

A multi-page list call — [GET /v1/{entity}](/docs/entity-api) and [POST /v1/{entity}/search](/docs/entity-api) with a `limit` above 50, and their list sub-calls in [POST /v1/batch](/docs/batch) — started with a record count. Counting a collection costs Bitrix24 disproportionately more than returning a page of it, and when the count failed (a timeout, a request limit, an account error) the call returned an error as a whole — with not a single record, even though the first page had already arrived.

The `withTotal=false` parameter had no effect on such calls: the count was ordered anyway and `meta.total` arrived.

**After**

The count runs only where it cannot be avoided, and its failure no longer cancels the response. When records were fetched but the count failed, a `200` arrives with a contiguous start of the result set: `meta.hasMore` is `true`, `meta.total` is absent, and `meta.pageErrorSample` carries the `code` and `message` of the reason. Two new `code` values — both Vibecode codes, not Bitrix24 ones:

- `PAGE2_COUNT_FAILED` — the count failed: a timeout, a request limit or an account error.
- `LAZY_COUNT_NO_PROGRESS` — the walk was stopped: the next page brought no new record at all, even though the collection holds more than has been returned.

In a batch request the same thing arrives in `data.meta.<id>`: `hasMore` is `true`, `pageErrorSample` is filled in, and `total` together with `data.totals.<id>` are absent — the count was not taken, and the number of returned rows is not a substitute for it.

Alongside that, `withTotal=false` started taking effect with a `limit` above 50 — on [GET /v1/{entity}](/docs/entity-api), on [POST /v1/{entity}/search](/docs/entity-api) and on `action: "list"` calls inside [POST /v1/batch](/docs/batch): `meta.total` now arrives on no outcome at all. The applicable scope for `action: "search"` inside a batch and for the single-entity batch `POST /v1/{entity}/batch` is unchanged. Without that parameter the count arrives as before.

A caveat about load: above a `limit` of 50 this parameter is about the shape of the response, not about its cost. The platform still needs the count to plan the walk, so the call does not get cheaper, and the exact number a short first page hands over for free is discarded. The parameter cancels the count only when `limit` is at most 50.

**What this means for integrators**

Check how your code learns that a response is incomplete. The error itself used to be the signal: a retry loop on `429` and `5xx` fired by itself. Incompleteness now travels inside the body of a successful response — an error handler will not fire, and the `Retry-After` header that came with a `429` is not part of such a response.

The signal of incompleteness is `meta.pageErrorSample` next to `meta.hasMore`. There are two ways to resume, and the order between them is not arbitrary.

By cursor — the first choice. `meta.nextAfterId` is passed back as `filter[>id]`, the offset stays zero, and the continuation takes the same path again — the one that does not always request a count. The cursor does not arrive everywhere: only for entities that support a cursor walk, and only when the sort is strictly `id` ascending.

By offset — the fallback. The new `offset` is the original one plus the number of returned records, but a call with a non-zero `offset` goes down the counted path and orders exactly the count that has just failed. After `PAGE2_COUNT_FAILED` that is very likely the same timeout, so retry with a pause rather than in a tight loop.

[GET /v1/tasks](/docs/entities/tasks/list) has no cursor — tasks run without a cursor walk, so `nextAfterId` never arrives in their responses. For them the offset is the only way to resume, with every caveat above.

The bound of a paging loop is still `meta.hasMore`, not arithmetic over `meta.total`: the field was optional before this change too.

### FIX-0730-13: the spec declares create-mandatory fields on create, and stopped demanding them on update

**Before**

Creating through POST /v1/bizproc-robots, POST /v1/bizproc-activities and POST /v1/folders was rejected without code, name and handler (name for a folder), while the spec declared those fields optional: a client or an SDK generated from it was refused on its very first call. The same cause had a second face: the request-body description was shared between create and update, so wherever the mandatory fields WERE declared — POST /v1/activities, POST /v1/documents, POST /v1/bizproc-templates — PATCH demanded them too, although the API accepts a partial update of a single field.

**After**

The requirement is declared on the create operation rather than in the shared body description. POST lists the fields the runtime refuses to go without; PATCH does not require them and accepts a partial update, as it always did in practice. Behaviour is unchanged — the description changed, and it now matches both operations. The same fields are marked required on the two other descriptive surfaces as well: GET /v1/folders/fields with its counterparts and GET /v1/guide.
