For AI agents: markdown of this page — /docs-content-en/changelog/2026-07-30.md documentation index — /llms.txt
API changes: July 30, 2026
NEW-0730-1: withTotal in the list calls of a batch request
A list call inside POST /v1/batch accepts the withTotal parameter in params — the same one the single GET /v1/{entity} 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.
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 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 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.
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 with provider, name, plan, region (no source), wait for status: "running" and blackholeStatus: "CONNECTED", then POST /v1/infra/servers/:id/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 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. 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) 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 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) 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) 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 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) 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 and in data[i].error for POST /v1/{entity}/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 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} and POST /v1/{entity}/search with a limit above 50, and their list sub-calls in POST /v1/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}, on POST /v1/{entity}/search and on action: "list" calls inside POST /v1/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 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.