For AI agents: markdown of this page — /docs-content-en/changelog/2026-09-10.md documentation index — /llms.txt

API changes: September 10, 2026

← Changelog · September 2026

BC-0910-1: a network failure on the way to the provider arrives under its own code ai_provider_network

Old format supported until: not provided

Before

When the platform could not connect to the model cluster, or the connection dropped before its response, POST /v1/chat/completions, POST /v1/embeddings and POST /v1/audio/transcriptions answered 502 with the general code ai_provider_unavailable — the same code as an error the cluster itself answered with. The two cases could not be told apart by the response code.

After

The same 502 response carries the code ai_provider_network. The server_error type and retryability are unchanged. This response carries no providerStatusCode field: the cluster never answered. The code ai_provider_unavailable stays reserved for the cases where the cluster did answer with an error. The original status arrives in providerStatusCode whenever the cluster answered with an HTTP status — including a refusal to open the stream after the stream itself has already started. The field is absent where there was no status: the error arrived as a frame from the body of an already open stream (such a frame carries the field only under the code rate_limit_exceeded) or the failure happened while processing the cluster's response. In streaming mode the same code arrives in the error frame before data: [DONE], together with the retryable and retryAfter fields. Speech recognition reports a network failure under the same code ai_provider_network.

What integrators should do

A client that decides on a retry by the 502 status, the retryable field or the server_error type changes nothing. A client that recognises a network failure by the code ai_provider_unavailable adds the code ai_provider_network to that check. The previous code is not returned in this scenario from the release on: there is no transition period, because one response cannot carry two codes.

FIX-0910-2: AI provider errors now carry the pause from the provider's own header, in one shape for every stream

Before

When the model cluster answered 429, the pause handed to the client was taken from the text of the cluster's response, and with no number in the text a fixed ten seconds was used. The cluster's own Retry-After header was lost. In the streaming mode of the Cowork/Code fallback model a provider error arrived as the frame { "error": { "message": "fallback stream error", "type": "server_error" } } without the code, retryable, retryAfter and providerStatusCode fields, so a cluster refusal on a rate limit was indistinguishable from its internal error. A { "error": … } frame that the cluster sends inside an already open stream reached the client as an ordinary response fragment, and the call was journaled as successful.

After

The pause is taken from the cluster's Retry-After header. The synchronous rate_limit_exceeded response carries it in its own Retry-After header, the streaming one in the retryAfter field of the error frame before data: [DONE]. A number from the response text and the default value are used only when the header is absent. The error frame of the Cowork/Code fallback stream is built the same way as the frame of the main stream: the code, type, retryable, retryAfter and providerStatusCode fields are present under the same rules. An error frame that arrives inside an open stream becomes the platform's standard error frame: the response remains a stream, after the fragments already delivered a frame with code, type, retryable and retryAfter arrives, then data: [DONE], and the call is journaled as an error. A frame raised from the body of an open stream carries providerStatusCode only under the code rate_limit_exceeded: from the body the platform accepts the status 429 alone, and the other body errors arrive under ai_provider_unavailable without that field. A refusal by the cluster with an HTTP status while opening the stream arrives as an ai_provider_unavailable frame with providerStatusCode, exactly like the synchronous response.

Impact on integrators

Nothing needs to change. A client that already reads the Retry-After header and the retryAfter field receives a more accurate pause. A client of the Cowork/Code fallback stream can now tell refusals apart by code and providerStatusCode, exactly as in the main stream.

Affected endpoints: POST /v1/chat/completions, POST /v1/embeddings, POST /v1/audio/transcriptions.

FIX-0910-3: MCP tools mark API refusals as errors

Before

Outside entity tools, an API response with success: false could be returned as an MCP result without an error flag.

After

These responses carry isError: true, including local refusals and network errors with success: false. The diagnostic JSON text is preserved. Successful responses remain unchanged and omit isError.

Impact on integrators

MCP clients that honor isError now recognize these responses as tool errors rather than successful calls. Integrations that ignore this flag and check success in the JSON text continue to work as before: the JSON format and contents are unchanged.

FIX-0910-4: the QUEUE_TIMEOUT refusal message names the actual wait

Before

In a 429 QUEUE_TIMEOUT refusal the userMessage field named 30 seconds of waiting in the Bitrix24 queue. The number was baked into the message text and did not match the wait the platform actually applied.

After

userMessage names the wait that applied to this call. The refusal still arrives with the QUEUE_TIMEOUT code, the retryAfter field and the Retry-After header — take the retry delay from those, not from the text.

Impact on integrators

No code changes are needed: the response shape is unchanged. A client that sized its own timeout by this message now reads the real queue wait limit.

NEW-0910-5: external membership is visible in V1: an external value for access.via and an externalServers block in /v1/me

The access.via field of GET /v1/infra/servers/{id} has a new value, external (alongside owner, collaborator and galaxy-reference). It means the caller was invited to this server from ANOTHER Bitrix24 account: the code surface (deploy, exec, file upload, logs, reading and downloading sources, wake) is open and listed in access.allowedEndpoints, while the owner account has no dashboard for them at all. Such a row carries its own access._note, describing the boundary of the invitation rather than development-team membership — the former shared text sent an agent looking for an interface it is not entitled to. The other values — owner, collaborator and galaxy-reference — are unchanged.

GET /v1/me now carries a new externalServers section inside data.infra, next to collaboratorServers. It lists servers on other Bitrix24 accounts the key owner was invited to as an external collaborator: total, count and items with id, name, role, companyPortalDomain (the account the server belongs to) and expiresAt (null means no expiry); items is capped at 100 rows, and total greater than count means the list was truncated. There are two sections rather than one because the credential differs: a regular key does not reach a server on another account — every external membership has its own key, which the person manages in their own account under "External access". Neither section counts towards infra.limits; those machines are not theirs.

The section is OPTIONAL: the key appears only when cross-account collaboration is enabled for the Bitrix24 account of the company that owns the server, and at least one live external membership exists for it. An absent section means "none visible", not "none exist": a row also drops out of the response when the owning company has disabled collaboration on its side, even while the membership itself is still technically live. A direct call to GET /v1/infra/servers/{id} confirms access RIGHT NOW: a 200 means membership is live AND the owning company has cross-account collaboration enabled. A 403 EXTERNAL_COLLABORATOR_NOT_A_MEMBER means "no access right now" and does not distinguish between the two possible reasons — no membership, or the owner turned collaboration off: a client cannot tell them apart today.

Both changes are additive: no existing response field is touched, and a client unaware of them keeps working exactly as before.

FIX-0910-6: X-Vibe-User-Name on a catalog open now carries the employee card name

Before

Opening an app from the Vibecode catalog put the platform account name in the header: for some employees that is a login, an e-mail address, or Unknown. Opening the same app from its Bitrix24 tile delivered the employee card name.

After

Both paths deliver the employee card name saved when the app was authorized. With no saved name the chain is unchanged: account name, then e-mail address, then Unknown.

Impact on integrators

No client change is required. For some apps the header value changes towards the employee name. The name stays a display value: use X-Vibe-User-Id as the account identifier and as the basis for access checks.

FIX-0910-7: responses of a model hosted by a third-party provider name the public model id

Before

For a catalog model hosted by a third-party OpenAI-compatible provider under a public id, the model field of the POST /v1/chat/completions response and of every stream event echoed the provider's internal model name instead of the requested public id. Provider error text named the internal name as well.

After

The model field in the response and in every stream event equals the public model id the client sent in the request, as for every other catalog model. Provider error text names the same public id. A successful response stays successful; the envelope shape is unchanged.

Impact on integrators

A client that compares model in the response with the requested id now gets a match for these models too. An integration that parsed the internal model name out of model or out of error text must switch to the public id: the internal name is no longer returned.

BC-0910-8: GET /v1/me answers 401 for a deleted key instead of 200

Old format supported until: not provided

Before

GET /v1/me returned KEY_NOT_FOUND as the body {"success": false, "error": {"code": "KEY_NOT_FOUND", "message": "Key not found"}} under status 200 OK. That was the answer to a call made with a key deleted while the request was being processed. A revoked key does not get this error — it gets KEY_INACTIVE.

After

The same refusal arrives under status 401 with the same body. The body shape is unchanged.

What integrators should do

A client whose HTTP library throws on 4xx by itself (axios with the default validateStatus, ky, got, ofetch, Guzzle with http_errors) must change its code: it no longer reaches the response body, and an exception fires instead of the branch on the success field. Handle the 401 and issue a new key on it.

A client deciding success by HTTP status (res.ok, response.raise_for_status()) used to take a deleted key for a working one and parse the refusal body as a /v1/me response. It now receives a 401 — check that the error branch leads to issuing a new key rather than repeating the same request.

A client that reads the success field from the body and does not throw on 4xx (fetch with no status check) works unchanged.

BC-0910-9: a Bitrix24 credential refusal now arrives as 401, not 422

Old format supported until: not provided

Before

When Bitrix24 rejected the credentials a key calls the account with (a revoked or expired webhook), the call answered 422 BITRIX_ERROR and the account's own code arrived in the extra error.b24Code field. On that status the refusal looked like a request-data error, so clients retried it — on a live account that added up to roughly 2,300 useless retries a day.

After

The same refusal arrives as 401 PORTAL_CREDENTIALS_REJECTED, with a hint saying a retry will not help until the key is reconnected. The code arrives on any route that reads account data, and in a POST /v1/batch sub-error. Reference — Authorization, keys and rights.

What integrators should do

If you branch on 422 BITRIX_ERROR with error.b24Code equal to authorization_error or INVALID_CREDENTIALS, move that branch to 401 PORTAL_CREDENTIALS_REJECTED and reconnect the key (POST /api/keys/:id/reconnect) instead of retrying. Other error.b24Code values in 422 BITRIX_ERROR are unchanged.

FIX-0910-10: disk move and copy operations are now available in OpenAPI

Before

Five live disk operations were absent from /v1/openapi.json and from the generated API reference cards. Four of them — moving and copying a file and a folder — were described on documentation pages, while listing the contents of a storage root was described nowhere. An agent that learns the platform from the machine schema concluded that no such methods exist. The file download card carried a separate falsehood: its summary and description promised a link, while the method returns bytes.

After

OpenAPI and the API reference cards describe POST /v1/storages/{id}/children, POST /v1/files/{id}/moveto, POST /v1/files/{id}/copyto, POST /v1/folders/{id}/moveto and POST /v1/folders/{id}/copyto — with the disk scope, the request body and the full set of refusal codes. The GET /v1/files/{fileId}/download operation declares a binary response and is described as a byte stream rather than a link. Method behavior is unchanged, a successful response remains HTTP 200, and the existing error statuses and schemas are preserved.

BC-0910-11: a custom AI provider no longer follows redirects

Old format supported until: not provided

Before

If the base URL of a custom-openai-compat provider answered with an HTTP redirect (301, 302, 303, 307 or 308), the platform followed it and sent the request to the new address. This applied to key verification, model catalog fetching and model calls.

After

The platform does not follow the redirect and returns a refusal.

What integrators should do

Set baseUrl to the final API address the redirect points to. A common case is https:// instead of http://. Update an already saved key with PATCH /v1/ai/credentials/:id.

BC-0910-12: task, telephony and universal-list endpoints no longer ignore filter in silence

Old format supported until: not provided

Before

Several list endpoints answered 200 with a wider set than requested when the request carried a filter parameter, and the response gave no way to tell an applied filter from a discarded one.

The first cause: the two spellings of filter are written by the query-string parser into the same place, so the second one replaces the first. ?filter[AUTHOR_ID]=1&filter= left an empty value, which read as "no filter", and Bitrix24 was called without one: GET /v1/tasks/{taskId}/comments, GET /v1/calls/statistics, GET /v1/lists/{iblockId}/sections and GET /v1/lists/{iblockId}/elements. For the same reason a condition spelled with brackets deeper than two levels (?filter[a][b][c]=1) never reached the filter parameter at all.

The second cause: endpoints that have no filter neither read nor rejected the parameter — GET /v1/tasks/{taskId}/history, GET /v1/tasks/stages/{entityId}, GET /v1/tasks/{taskId}/checklist, GET /v1/lists and GET /v1/voximplant-lines.

After

A filter envelope from which at least one condition you spelled did not reach the parameter is rejected with 400 and the code INVALID_FILTER, before the Bitrix24 call. A condition is lost when both forms are mixed in one request (in either order), when two spellings address the same condition (?filter[a]=1&filter[a][b]=2), and when bracket nesting goes deeper than two levels — the query-string parser does not assemble that. The error text names which of these happened. Pass the whole filter in a single form, spell every condition once, and stay within two levels.

Endpoints that have no filter answer 400 with the code UNSUPPORTED_FILTER and name what to use instead: field for task history, sort and offset for the list of universal lists.

An empty ?filter= still means "no filter", and the response remains 200. Untouched on endpoints that do support a filter: a filter in a single form whose every bracket condition reached the parameter — all brackets or one JSON object — the named parameters field, sort and start, and a repeated ?filter=a&filter=b, where the parser keeps the last value — provided that value is NOT empty. An empty ?filter= standing last overwrites the previous condition, and such a request is refused.

Two caveats, so that list reads honestly. On endpoints that have no filter at all, any meaningful filter is refused, a single-form one included. And a request where the bracket form follows an empty ?filter= (?filter=&filter[ID]=1) used to answer 200 with the filter correctly applied, and is now refused as well: parameter names in a query string carry no values, so an empty spelling cannot be told apart from a non-empty one and both are refused — as has long been the case for entity list requests.

What integrators should do

  1. Collapse the filter into ONE form: either all brackets (?filter[ID]=1&filter[STAGE]=NEW) or one JSON object (?filter={"ID":1,"STAGE":"NEW"}) — on endpoints that accept both. ⚠️ GET /v1/calls/statistics accepts the BRACKET form only: a JSON object is forwarded to Bitrix24 as a string and does not become a filter. Do not send an empty ?filter= next to bracket conditions, even if such a request used to work.
  2. Drop bracket conditions nested deeper than two levels: the query-string parser does not assemble them, and they never reached Bitrix24 before either.
  3. On endpoints that have no filter at all, switch to the named parameters the error text lists: field for task history, sort and offset for the list of universal lists.

No support window is provided: the lost half of the conditions cannot be recovered, and answering 200 to a request whose filter was not applied is the very defect being fixed.

FIX-0910-13: two spellings of one filter field no longer drop a condition in silence

Before

A field has several accepted spellings: the schema name and the Bitrix24-native name (amount and OPPORTUNITY on deals), a different case. When two conditions in one filter resolved to the same Bitrix24 filter name, the request answered 200 through a defect with a condition lost: only one of the two applied, and which one was decided by the key order in the request. The result looked filtered even though half of the filter never applied. That answer could not be relied upon — the documentation never promised it. The same happened with two spellings of one operator, with paired date field names (updatedAt/updatedTime, createdAt/createdTime, where an entity declares one of the two and accepts the other as its alias) and with synonym pairs an entity declares as separate names: on leads those are amount/opportunity, stageId/statusId, currency/currencyId.

After

Such a request is rejected with 400 INVALID_DUPLICATE_FILTER_FIELD, and message names both conditions and the shared name they resolved to. A request with a single spelling of the field answers HTTP 200 as before — the successful response is preserved. Operators producing DIFFERENT names are unchanged too: { "amount": { "$gte": 1000, "$lte": 5000 } } is a range, not a duplicate.

The result decides, not the spelling, and the two pairs have DIFFERENT boundaries. The UF_ form together with the camelCase spelling of the same custom field folds into one name only on entities with the older naming style AND only for the spelling the platform converts: the letters-only one (ufCrmProjectCode) on every such entity, and the digit-suffixed one (ufCrm_1698325419) on requisites only. On the other entities a digit-suffixed pair travels as two names, is not refused, and Bitrix24 still drops the second condition silently. On camelCase-named entities and on CRM items those are different names on the wire and nothing is refused either. $ne together with $nin on one field produces a single ! prefix on EVERY entity except CRM items — including camelCase-named ones (catalog products, mail mailboxes, smart processes); on CRM items $nin gets its own !@ prefix and the pair passes.

The rule applies to lists, search, aggregation and both batch doors; inside a batch only that call is rejected and its neighbours still run.

Affected endpoints: GET /v1/{entity} and POST /v1/{entity}/search on entities with the standard filter parsing, POST /v1/{entity}/aggregate and its legacy twin GET /v1/{entity}/aggregate, POST /v1/batch and the POST /v1/{entity}/batch sub-calls, plus GET /v1/addresses with POST /v1/addresses/search. On these doors the filter is parsed by the same code, so the refusal arrives identically.

Individual routes with their own filter format parse it with their own code and are NOT part of this change: there a pair of spellings still answers 200 with a condition lost. Those include GET /v1/requisite-links with POST /v1/requisite-links/search, call statistics, lists, task comments and the open-lines routes; the list is not closed.

Why this is a correction and not a contract change: the former 200 on such a request was never promised by the documentation and was not reproducible — it applied one of the two conditions, and which one was decided by the key order. That answer could not be relied upon, so there is no support window for the old behaviour: there is nothing to support. A request with a single spelling of the field is unaffected.

NEW-0910-14: vibe balances for every Bitrix24 account in one export

A new GET /v1/platform/revenue/balances returns one row per Bitrix24 account, as a snapshot taken at read time. Each row carries balance (the same remainder the account sees), its composition paidRemaining + grantRemaining, the accrued shortfall accruedShortfall, overdraftLimit, billingMode, frozen, plus portalId, portalDomain, portalStatus, portalKind and accountId. The figures reconcile: balance = paidRemaining + grantRemaining − accruedShortfall.

The request takes no time window, only limit and cursor; the snapshot time arrives as capturedAt next to data. An account with no tranches is returned as a zero row instead of being skipped, so the export describes the whole account population. This is the only surface exposing the granted remainder: GET /v1/platform/revenue/topups covers purchased tranches, so an account with no purchases is absent from it and the welcome allocation and granted bonuses are not shown.

The method is opened by a dedicated revenue:balances scope — a key holding revenue:read gets 403 INSUFFICIENT_SCOPE on it. The existing exports are unchanged and keys with existing scopes keep working with no edits.

NEW-0910-15: companies expose their linked contacts through `include=contact`

The company entity declares a contact relation, so include=contact is now accepted on GET /v1/companies/{id}, GET /v1/companies and POST /v1/companies/search. Every record carries _included.contacts, an array of full contact records, each extended with the binding fields sort, isPrimary and roleId; a company with no linked contacts gets an empty array. Such a request used to answer 400 INVALID_INCLUDE, because companies declared no relations at all and the list of available include values was empty.

The relation also opens the binding sub-routes: GET /v1/companies/{id}/contacts lists the bindings, POST /v1/companies/{id}/contacts adds one, PUT /v1/companies/{id}/contacts replaces the whole set, DELETE /v1/companies/{id}/contacts/{contactId} unlinks one. Requests without include answer exactly as before.

FIX-0910-16: a boolean userfield flag is no longer dropped silently

Before

The userfield properties multiple, mandatory, showFilter, showInList, editInList and isSearchable are stored by Bitrix24 as the characters Y and N. Sent as booleans, they applied only half the time — within ONE request body: showInList: true and editInList: true were switched on, while showFilter: true and isSearchable: true were silently saved as off. The response was a success either way: 201 on create, {"updated": true} on update. Re-sending the same flags as booleans changed nothing: the ones that were off stayed off.

None of the six flags were declared in the API description: create listed only fieldName, userTypeId and label, and update described its body as an empty object. There was nowhere to learn the working value form.

After

All six flags are accepted both as booleans and as the strings "Y" / "N"; the platform converts either form before calling the account. The canonical forms remain a boolean and "Y" / "N"; on top of those the platform accepts the lenient spellings "y", 1, "1" to switch a flag on and "n", 0, "0" to switch it off. All of them used to travel to the account verbatim — the enabling ones did not switch the flag on and now they will; the disabling ones switched it off before and still do, so nothing changes for them. Unrecognised values pass through unchanged, as before.

Take note if your integration already sends these flags as booleans. Some of them used to do nothing and you may have grown used to that — now all of them work. Two switch-ons were measured: showFilter: true and isSearchable: true used to leave the flag off and will now turn it on. Enabling isSearchable is not cosmetic: the field's values go into the account's full-text search and start surfacing in global search.

Watch mandatory: true as well — once on, it blocks creating records without that field, including through other integrations on the account. Whether it took effect from a boolean before this change was not measured, so assume it will take effect now.

If your integration sends showFilter: "E" — the earlier table on the fixed-CRM pages called that a "mask" — the behaviour changes: such a write used to leave the filter OFF, and now it turns it ON. Among the MEASURED string values this is the only switch-over, and it is deliberate: "E" is the form the filter arrives in on a read, and sending it back must be safe.

If your integration sends "I" or "S" following our earlier table, such a write does not turn the filter on — neither before nor now. Send true to turn it on.

Check what you send before you upgrade.

The flags are now declared in the API description for both create and update. The reading quirk of showFilter is documented separately: an enabled filter comes back as E, which is Bitrix24's storage form rather than an error. The values "I" and "S" are not accepted on write and switch the filter off, so enable it with true, "Y" or an "E" you read back. The fixed-CRM reference used to describe those letters as filter modes and used them in its examples; a measurement across four field types did not confirm that, and the pages are corrected.

The round trip is closed separately: an "E" you read can now be sent straight back — the platform reads it as "on". Previously the ordinary read-modify-write cycle handed that "E" back to the account and the filter switched off silently under a success response.

The change covers the fixed CRM types — deals, leads, contacts, companies, quotes and requisites. Smart-process custom fields travel a different Bitrix24 contract and are not affected by this change.

FIX-0910-17: V1 responses return X-Request-Id

Before

The documentation asked clients to include X-Request-Id in support tickets, but API responses did not contain this header.

After

Every /v1 response produced by the backend contains a server-generated X-Request-Id. The value from a failing response can be included in a ticket together with the request time.

Impact on integrators

Existing requests require no changes. A client may save the new header for diagnostics but does not have to process it.