For AI agents: markdown of this page — /docs-content-en/changelog.md documentation index — /llms.txt
Vibecode API Changelog
Vibecode API change history: new features, fixes, and breaking changes. Entries are ordered newest to oldest.
Entry prefixes
- NEW — a new capability: a new endpoint, a new optional field or parameter, a new error code in a new scenario. Existing client requests keep working.
- FIX — corrected behavior. The response changes to the correct one, no client action required.
- BC — a breaking change. Requires client action. The old format is supported for the stated period, then discontinued.
Entry code format: {TYPE}-{MMDD}-{N}, where MMDD is the publication date and N is a running number within that date.
Some NEW entries are marked rolling out: the method shipped in a specific Bitrix24 update and is not yet available on every portal. Until the update reaches a portal, the call returns 422 METHOD_NOT_YET_AVAILABLE with the target version — this is a sign of the rollout, not an integration error.
2026-09-10
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.
- POST /v1/ai/credentials and PATCH /v1/ai/credentials/:id answer
400 base_url_redirect. - POST /v1/ai/credentials/:id/test still answers HTTP 200 with
valid: false, anddatanow carries acodefield with the valuebase_url_redirect. - POST /v1/ai/credentials/:id/fetch-models answers
400 base_url_redirectinstead of a response withPROVIDER_LIST_MODELS_UNAVAILABLE. - POST /v1/chat/completions with a model of such a provider answers
400 ai_provider_redirect_blocked, and in streaming mode an error frame with the same code arrives. Retrying the request will not help.
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
- 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. - Drop bracket conditions nested deeper than two levels: the query-string parser does not assemble them, and they never reached Bitrix24 before either.
- On endpoints that have no filter at all, switch to the named parameters the error text lists:
fieldfor task history,sortandoffsetfor 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 portal in one export
A new GET /v1/platform/revenue/balances returns one row per portal, as a snapshot taken at read time. Each row carries balance (the same remainder the portal 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 portal population. This is the only surface exposing the granted remainder: GET /v1/platform/revenue/topups covers purchased tranches, so a portal 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.
2026-09-09
BC-0909-1: Audio transcription retains subscription billing when Cowork/Code is disabled
Old format supported until: not provided
Before
When Cowork/Code was disabled, requests to POST /v1/audio/transcriptions using an already-issued vibe:cowork key bypassed subscription quota, even with an active subscription and an audio price configured for an available model. Exhausting subscription quota did not prevent a successful HTTP 200 response.
After
Disabling the product does not change billing: with an active subscription and a configured per-minute or per-call audio price, usage counts toward its quota, just like chat. When a quota window is exhausted, the request receives HTTP 402 cowork_quota_exhausted with a Retry-After header. Without an active subscription, the existing billing path is preserved without drawing on subscription quota.
Integration changes
Handle HTTP 402 cowork_quota_exhausted even while the product is disabled: wait for the quota window to reset according to Retry-After or upgrade the subscription tier. The key and request format do not need to change.
NEW-0909-2: Connector catalog method identifiers support Unicode
GET /v1/onec/tools and POST /v1/onec/tools/{method}/call support method identifiers containing Unicode letters, combining marks and numbers, plus ._-. The limit is 100 UTF-16 code units. Pass the exact catalog identifier in the URL using encodeURIComponent(method): case and Unicode representation are preserved. Existing ASCII identifiers continue to work.
BC-0909-3: Universal lists: an absent list answers "not found", and a bad offset answers with an error
Old format supported until: not provided
Before
The same absent list answered differently depending on which address was called: the element
list, the field list, the field-type set, a single element, a single field and element files all
answered 422 with a Bitrix24 message — an "something went wrong" error rather than "no such
list". A read-then-create-if-missing flow broke on that.
Paging through those same lists accepted any offset. start=-5, start=abc, start=1.5 and
start=1e2 were not refused: the offset was either silently dropped or applied distorted (1e2
became 1, so the second record arrived instead of the hundredth). The response was 200, so a
mistake in the calling code stayed invisible — while the neighbouring parameters of the same
request, iblockTypeId and sort, are refused with a clear message.
After
An absent list answers 404 with error code LIST_NOT_FOUND on the six addresses where
Bitrix24 reports it with a machine code. An offset is accepted only as a non-negative integer in
plain notation; anything else is refused with 400 and error code INVALID_PARAMS, and the
message names the parameter that did not fit. Both start and offset are checked, even when
the precedence between them is won by the first.
What integrators should do
If your code treated a 422 from the list collections as "the list is gone", switch it to 404.
What is recognized is Bitrix24's machine code, not the message text, so the answer does not
depend on the account language. The code itself was measured on the international platform: if
your account reports an absent list differently, the recognition does not fire and the answer
stays as it was.
If the offset came from an external source and could arrive negative or fractional, an error now
arrives instead of a page: fix the source rather than the walk. One input deserves a separate
mention because it used to be understood CORRECTLY: ?start=+5 — a plus in a query string
decodes to a space, and the old parsing read that as 5. It is now refused. If your builder
encodes with a plus, drop it: ?start=5.
Separately, about writing parameters with square brackets. ?start[]=7 and
?iblockTypeId[]=lists used to pass: the platform joined such a list into a single value and
carried on. That is now refused — brackets mean a structure, and these parameters do not take
one. If your request builder adds brackets to every list by habit, send these three parameters
as a plain value: ?start=7, ?offset=7, ?iblockTypeId=lists. Bracket shapes with a name inside
(?start[x]=1) used to break the request with an internal error; now they get a clear
refusal instead.
One more parameter belongs to the same row — the returned field set: ?select[x]=1 used to
break the request with an internal error, and now the set is simply not applied and the full
field list arrives; empty entries such as ?select[]=&select[]=NAME no longer travel to the
account as an empty string.
The same applies to the request body when creating a section or an element:
"iblockSectionId": [5] used to be read as the number five and placed the record under
that parent; the value is now dropped and the record lands at the root. Send a number, or a
string holding one.
The infoblock type in the body narrowed the same way but answers differently:
"iblockTypeId": ["lists"] used to pass (the list was joined into a single value), and now
400 with error code INVALID_IBLOCK_TYPE arrives on the eight addresses that read this
parameter from the body at all: creating and updating a list, a field, a section and an
element. Deletions do not take it and are untouched. Send a string: "iblockTypeId": "lists".
What this does not change
Reading the sections still answers with a complaint about a wrong infoblock type, and reading the list itself still answers with a permission refusal when the list id is numeric. With a symbolic id that same address already answered "not found" before this change: there Bitrix24 returns an empty result rather than a refusal. Bitrix24 reports an absent list on those addresses in the same words it uses for a genuine permission refusal and a genuinely wrong type, so recognizing them as "not found" would hide real refusals.
BC-0909-4: phantom contacts field removed from companies, leads, and deals
Old format supported until: not provided
Before
Field schemas and company, lead, and deal responses could contain contacts, although the field could not be read reliably from Bitrix24. An explicit select: ["contacts"] was accepted with a 200 response.
After
contacts is no longer published in field schemas or returned in records. An explicit select: ["contacts"] without * or UF_* is rejected with 400 UNKNOWN_SELECT_FIELD. Requests for all fields through * or UF_* still succeed, but contacts is removed from the response.
What integrators should do
Remove contacts from explicit select lists. Use contactIds for company contact relations, and contactId plus contactIds for leads and deals.
Affected endpoints: GET /v1/companies/fields, GET /v1/leads/fields, GET /v1/deals/fields, read and write operations under companies, leads, and deals, POST /v1/batch, POST /v1/{entity}/batch.
FIX-0909-5: a self-hosted portal now issues a key that needs no webhook
Before
On a self-hosted portal whose owner has no developer key, issuing or rotating a key carrying
only vibe:* scopes (for example vibe:infra + vibe:storage) answered
400 BOX_NO_DEVELOPER_KEY, even though no incoming webhook is registered on the portal for
such a key at all. It affected POST /v1/keys, POST /v1/keys/{id}/rotate and the personal
key issued for a Cowork application.
After
A scope set with no Bitrix24 scope is checked before the self-hosted guard: the key is issued,
webhookUrl stays empty and the portal is not called. The 400 BOX_NO_DEVELOPER_KEY refusal
stays in force for sets carrying at least one Bitrix24 scope — such a key does get a webhook,
and nothing but the owner's developer key can remove it. Rotation adds one more condition: the
previous row must carry no webhook. A key with no Bitrix24 scope that still has one on record
answers with the same 400 — nothing can remove that webhook without the developer key, and a
successful answer would hide that the old access keeps working.
FIX-0909-6: replacing an application key no longer hands out an already-expired secret
POST /v1/cowork/applications/{id}/key copied the lifetime of the replaced key onto the new one
verbatim. When that lifetime had already ended, the owner received a secret that passed no request
at all, while the expired key it replaced was given another 24 hours and started working again.
Before
An application whose key expired three days ago. The response is 201 with issued: "rotated",
key.expiresAt holds the same past date, and previousKey.graceUntil points 24 hours ahead of the
replacement.
After
The response is still 201 with issued: "rotated". key.expiresAt is counted afresh from the
lifetime configured on the portal (the same value GET /v1/cowork/applications/defaults reports as
keyExpiresInDays), so the new key works. previousKey.graceUntil never exceeds the replaced key's
own expiry: on an expired key it stays in the past, and the 24-hour grace does not revive it. A
lifetime that is still running, and the absence of one, are carried over as before — a key with no
expiry stays without one, a live expiry is repeated verbatim.
The same correction to the grace period applies to POST /v1/keys/{id}/rotate: rotating a key whose
lifetime had ended no longer grants the replaced key another 24 hours of service.
FIX-0909-7: a public numeric address in baseUrl and proxyUrl is no longer refused as private
Before
POST /v1/ai/credentials and PATCH /v1/ai/credentials/{id} carrying a public numeric address in
baseUrl or proxyUrl — http://203.0.113.10:8080, say — answered 400 with code
BASE_URL_PRIVATE or PROXY_URL_PRIVATE. Storing such a credential was only possible by writing
the same address as a domain name.
After
That request now succeeds, exactly as it does for a domain name. A private numeric address still
answers 400 with the same codes: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16,
100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 0.0.0.0/8, and for IPv6 ::1, ::ffff:,
fc00::/7 and fe80::/10. For numeric addresses only, that list now also covers the reserved
ranges a numeric address could never get through before: 192.0.0.0/24, 198.18.0.0/15,
224.0.0.0/4, 240.0.0.0/4 (including 255.255.255.255), and for IPv6 fec0::/10, ff00::/8
and 100::/64. For domain names the set of refused ranges is unchanged: a name pointing into any
of those reserved ranges keeps working, and a request carrying a domain name still succeeds as
before. The exception is addresses that wrap IPv4 in an IPv6 form (::a.b.c.d, NAT64
64:ff9b::/96, 6to4 2002::/16): those are refused the same way whether they are written as
numbers or a domain name points at one.
FIX-0909-8: the spec declares the 429 refusal of endpoints that carry their own rate limit
Before
Twenty-eight operations carry their own rate limit and answer 429 RATE_LIMITED once it is spent, while the public GET /v1/openapi.json spec did not declare that response for them. A client generated from the spec treated 429 as an undescribed status and built no retry branch at all. Affected: POST /v1/batch, GET /v1/bots/:botId/events, GET /v1/chats/recent, POST /v1/chats/events/subscribe, GET /v1/connect/authorize, POST /v1/connect/token, POST /v1/connect/revoke, GET /v1/app/blueprints/:slug, GET /v1/me/sources, POST /v1/feedback/attachments, GET /v1/platform/coupons/campaigns, GET /v1/platform/coupons/campaigns/:slug, GET /v1/workday/records and fifteen infrastructure endpoints — POST /v1/infra/servers, GET /v1/infra/servers/:id/ssh, POST /v1/infra/servers/:id/reboot, PATCH /v1/infra/servers/:id/sleep, PATCH /v1/infra/servers/:id/port, POST /v1/infra/servers/:id/wake-schedules, PATCH /v1/infra/servers/:id/wake-schedules/:scheduleId, DELETE /v1/infra/servers/:id/wake-schedules/:scheduleId, POST /v1/infra/servers/:id/sources, POST /v1/infra/servers/:id/exec, POST /v1/infra/servers/:id/upload, POST /v1/infra/servers/:id/icon, GET /v1/infra/servers/:id/logs, POST /v1/infra/servers/:id/deploy and POST /v1/infra/servers/:id/unstick. Separately, POST /v1/platform/coupons/issue did declare its 429, but named it with the code RATE_LIMIT_EXCEEDED, which the platform never sends.
After
Every one of these operations declares its 429 in the spec, and coupon issuance now names the real code RATE_LIMITED. The description sends you to the x-ratelimit-limit response header for the value in force and to Retry-After for the retry delay. No figure is written into the spec, deliberately: the header is the only source that cannot drift from the configured value. For POST /v1/infra/servers, POST /v1/infra/servers/:id/upload and POST /v1/infra/servers/:id/deploy a second code is named on the same status — DEPLOY_BACKEND_BUSY, which arrives when every slot for an inline request body is taken, refuses that one body rather than the caller, and carries Retry-After: 30. General information about limits stays on the limits page.
Impact on integrators
Neither the limits nor the behaviour of the endpoints changed, and no request needs editing. A client generated from the spec before this change is worth regenerating — 429 handling then appears where the refusal used to arrive as an undescribed status. Check the code-matching branch on coupon issuance if it was written against the spec: the server answers RATE_LIMITED.
FIX-0909-9: an expired OAuth user token returns an authentication error
Before
When an application could not refresh an expired user token, the Bitrix24 expired_token rejection was returned by the V1 API as 422 BITRIX_ERROR, which incorrectly indicated invalid request data.
After
A single V1 API call returns 401 TOKEN_EXPIRED with a hint to re-open the application from the Bitrix24 menu. In a batch request, TOKEN_EXPIRED appears in the individual call's error while the overall HTTP response remains 200. See API errors.
FIX-0909-10: the token endpoint's rate-limit refusal is now machine-readable
Before
When the platform-edge rate limiter fired, POST /v1/connect/token returned an HTML page and set no Retry-After header. An application could neither parse the body nor learn how long to wait. For the device code sign-in this is the only rate-limit refusal a live poll actually reaches.
After
The same refusal now arrives with a body in RFC 6749 form — {"error":"slow_down","error_description":"..."} — and with a Retry-After header naming the minimum pause in seconds. The form matches the one the endpoint already declares for its other refusals, so an off-the-shelf OAuth library parses it with no extra work. The response status stays 429, so a rate-limit refusal is still distinguishable from the authorization states, which arrive with code 400. Existing calls keep working.
Important: this route has two rate limiters and their bodies differ. Only the platform-edge limiter answers in the RFC 6749 form described above. The endpoint's own limiter answers with the same 429 status but in the general API envelope — { "success": false, "error": { "code": "RATE_LIMITED", "message": "..." } } — so a branch of the shape «on 429, parse the body and compare error against slow_down» recognises only half the refusals and takes the other half for an unknown error. Tell the two apart by the X-RateLimit-Limit header: the platform-edge refusal does not carry it, the endpoint's own refusal does. The Retry-After value is a minimum pause, not a promise that the next request is accepted, so an application needs to grow its own pause as well. Both refusals and both bodies are described in Partner Connect.
FIX-0909-11: model reasoning for Cowork no longer appears in final text
Before
When Cowork did not send an explicit reasoning setting, a model could return internal working text as a regular part of the final answer.
After
POST /v1/chat/completions applies the model-declared default reasoning
setting to keys with the vibe:cowork scope. Reasoning stays in its separate channel, while distinct
final answer text is preserved. The response remains HTTP 200.
Impact on integrators
No client changes are required. Calls without the vibe:cowork scope retain their previous behavior.
BC-0909-12: box top-ups now check out at the licence's own till
Old format supported until: not provided
Before
A box licence issued outside the installation's default region was pointed at its single default till: no other box till existed, so the platform deliberately resolved such a licence to the default region. The purchase did not go through there anyway — the till rejects a licence key from another region — so the path ended in a dead end with no explanation.
After
Where a country's own box till is open, checkout goes to that country's site in its own currency — prices, currency and the checkout URL come from the licence region's block. While a country's catalogue has no box items, top-up for such clients is refused with a clear message instead of pointing them at a till that would reject the purchase anyway. Once the items appear, checkout switches on by itself — no release needed.
Additionally, on installations whose catalogue carries more than one currency: a payment whose currency differs from the order's is now held for review instead of silently closing the order. Where the catalogue has a single currency this does not apply.
GET /v1/cowork/subscription/preview returns the licence country's currency for such a licence.
What to do
Read the currency from the response instead of assuming the default one: currency on packages and at the response root may now be the licence country's, and null while top-up is unavailable there (topUpAvailable: false). Amounts are in that currency's minor units — converting them as if they were the default currency yields wrong numbers.
Handle the BOX_TOPUP_NOT_AVAILABLE refusal on POST /api/billing/topup-init and the unavailable flag in the catalogue: they are returned while the country's catalogue has no box items. Previously this case returned a checkout URL for the default till — the purchase did not go through there anyway, so it can no longer be treated as a working one.
2026-09-08
FIX-0908-1: warehouses no longer substitute a different id, and refuse an unsupported filter out loud
Before
A warehouse id in the request path was read leniently: GET /v1/warehouses/12.5 was not rejected but quietly became 12, so a read, an update or a delete landed on a different, genuinely existing warehouse and looked like a success. 12abc, 1e2, 007, an id with a space and an id beyond integer precision behaved the same way, and the same lenient reading applied to productId and limit. The filter[] parameter on the warehouse list, on warehouse stock and on the stock totals was not read at all: any condition — an invented field name as well as a real one — was silently ignored and the full list came back. In the API description seven warehouse operations and the aggregation operation declared success as their only outcome, and warehouse deletion was declared as a 200 response although it answered an empty 204.
After
A warehouse id, productId and limit are accepted only in canonical form — digits only, no sign, no leading zero, no fraction, no exponent, and within integer precision. Anything else is refused with 400 INVALID_PARAMS before Bitrix24 is called, so a typo in the id can no longer reach someone else's object. A filter[] passed to the three endpoints above is refused with 400 UNSUPPORTED_FILTER listing the keys received, instead of a full list that looked filtered. Requests with a canonical id and without filter[] work as before, and their successful 200 response is unchanged. The API description now carries the real refusal codes (400, 401, 403, 404, 422) for the warehouse operations and for aggregation, and warehouse deletion is declared as 204 — the response it was already sending.
BC-0908-2: A value of the wrong type in a write field is refused instead of silently corrupting data
Old format supported until: not provided
Before
A field declared as a number in the entity schema accepted any string: {"amount":"one hundred"}
on POST /v1/deals answered 201 while the amount was stored as
zero — Bitrix24 casts a non-numeric string to 0. On update this erased an already-stored
amount: a PATCH with an unparsable value answered 200 and zeroed the field. Every other
numeric field in the registry behaved the same way (sort on catalogs and products), boolean
fields accepted any word, and the color string field on order statuses was silently truncated
at the database column width.
After
Such a value is refused with 400 and error code INVALID_PARAMS before any call reaches
Bitrix24: a numeric field accepts a JSON number or a numeric string using . as the decimal
separator; a boolean accepts true/false and the recognized string forms ("yes"/"no",
"y"/"n", "1"/"0", "true"/"false"); a string field with a declared length limit
accepts a value within that limit. The priority and status fields on tasks accept only the
values listed in their enumeration.
What integrators should do
Send monetary amounts as a number or as a string using a dot: 1234.56, not "1234,56" — a
comma decimal separator is refused with an explicit error rather than quietly reinterpreted.
Review places where values arrive from external systems as strings: such a request used to
answer with success while the data was lost, and now returns an error naming the field.
FIX-0908-3: Paging through recent dialogs no longer loses or repeats records
Before
GET /v1/chats/recent forwarded the page size and offset to
Bitrix24 as they were, where they bound an internal table join rather than the dialog count. A
page could return fewer records than requested while promising another one; the last record of a
page arrived with its chat and last-message fields empty; and neighbouring windows overlapped, so
a offset += size walk returned some dialogs twice and missed others.
After
The page is read with headroom and sliced on the Vibecode side: a window holds exactly the number of fully populated records requested, under-populated rows are not returned, and the has-more flag is computed from the window actually served. A window past the end of the list comes back empty and no longer promises another page. A request that names no page size now returns 50 records — the size used to be Bitrix24's to choose and is now stated explicitly.
A caveat about deep windows: the overlap protection holds while offset plus page size stays at or below 190 — the window together with the headroom for under-populated rows has to fit inside one Bitrix24 page, and that page is 200 records. Past that the offset is forwarded to Bitrix24 and, on very long lists, neighbouring windows may overlap again — a limitation of the method itself, not of the platform.
What integrators should do
Nothing: the requests are unchanged and the response is now honest. If your code de-duplicated dialogs by hand, that workaround is no longer needed.
BC-0908-4: Fields that Bitrix24 assigns itself are now declared read-only
Old format supported until: not provided
Before
Five fields were declared writable and accepted with 200/201, but Bitrix24 never stored
them — the value stayed empty, and the response gave no way to tell that apart from a
successful write: the origin identifiers on sales pipelines, the owner module on
document templates, and the problem flag with its
reason on payments when updating.
After
These fields are declared server-assigned: a write attempt is refused with 400 and error code
READONLY_FIELD before any call reaches Bitrix24, and the field description states plainly that
the platform sets the value. The payment problem flag and its reason are still accepted when
creating a payment — only the update path, where the value was lost, is closed. The pipeline
origin identifiers were not declared in the schema at all and were picked up as writable; they
are declared now and refused.
What integrators should do
Remove these fields from an update request body. The platform stamps the template module itself, the pipeline origin identifiers are never persisted, and the payment problem flag should be set when the payment is created.
FIX-0908-5: search and aggregate answer with an error on a wrongly typed parameter instead of silent emptiness
Before
In the body of POST /v1/{entity}/search, the limit and select fields accepted a value of any type. A non-numeric limit (for example "abc") produced HTTP 200 with an empty record list and meta.hasMore: true at the same time — a client paging while the platform promises more went into an endless loop, receiving neither a record nor an error. A select value that was neither a string nor a list of strings turned into a field name such as "999" or "[object Object]", travelled to Bitrix24 and came back as HTTP 502 with the code BITRIX_UNAVAILABLE — the platform reported its own unavailability where the fault was in the client request.
In the body of POST /v1/{entity}/aggregate, the groupBy field was checked only when it arrived as a string or a list of strings. A number, a boolean or an object was dropped silently: the answer came back as HTTP 200, without the groups key and without a warning — the client asked for a breakdown, received the overall total, and had no way to notice.
The meta.total key of list answers carried a fabricated number on pages beyond the collection, growing together with the offset: on a portal holding five storages, GET /v1/storages?offset=100 answered total: 100, and GET /v1/storages?offset=1000 answered total: 1000.
After
A wrongly typed limit is refused with INVALID_LIMIT, a wrongly typed select with INVALID_SELECT_TYPE, and a wrongly typed groupBy with INVALID_PARAMS; all three refusals arrive as HTTP 400 and name the type that came in. A numeric limit behaves as before, including the string spelling of a number ("50"), while null, an empty string and an empty list still mean "parameter not supplied" and are not errors.
The meta.total key is withheld when the page is empty and the offset is above zero: nothing can vouch for a count there, and the description of the field already warns that the key may be absent at a non-zero offset. Wherever the page is non-empty or the offset is zero, meta.total arrives as before, and the response remains HTTP 200.
FIX-0908-6: task favorites, comments and time entries answer honestly
Before
Adding a task to favorites and removing it from them answered 200 with success: true even for a task that does not exist: Bitrix24 confirms that action for any identifier while saving nothing. The 404 TASK_NOT_FOUND promised by the API description never arrived.
The task comment list ignored offset on a request without a filter and with a sort by identifier — the same page came back at any value — and meta was counted over raw chat messages. On a task whose slice held only system notifications the answer was data: [] together with total: 1 and hasMore: true, so a while (hasMore) offset += limit walk never finished.
Addressing a checklist item or a time entry that does not exist answered 422 BITRIX_ERROR carrying the internal Bitrix24 exception text (TASKS_ERROR_EXCEPTION_#512; …; 512/TE/ITEM_NOT_FOUND_OR_NOT_ACCESSIBLE), and on the time route that text spoke about a checklist.
The from and to parameters of GET /v1/task-time were not validated: ?from=notadate silently returned the whole range with status 200, as if no period had been requested.
After
Before adding to or removing from favorites the service checks that the task exists and is accessible to the key, and answers 404 TASK_NOT_FOUND when it is not. On a task that exists the response remains 200. Rate limiting and an authorization failure are not substituted by "not found".
The comment list honours offset on every read path, and meta.hasMore and meta.total are counted over comments rather than raw chat messages: a slice of only system notifications reads further, and hasMore: false with total: 0 means there are no comments.
A missing checklist item and a missing time entry answer 404 NOT_FOUND — without the internal Bitrix24 text and in terms of the requested resource. The code is declared in the API description for every method that addresses a single record.
An unparseable from or to answers 400 INVALID_PARAMS before Bitrix24 is called; YYYY-MM-DD and ISO 8601 are accepted. An empty value still means no filter. The API description of GET /v1/tasks/{taskId}/comments now declares the limit and offset it actually reads.
BC-0908-7: Lead conversion stamps backlinks and answers in the standard read format
Old format supported until: not provided
Before
POST /v1/leads/{id}/convert answered 200, but the deal, contact
and company it created carried no link back to the lead they came from: filtering deals by lead
returned nothing, and a deal could not be traced to its source. The response itself came in raw
Bitrix24 shape — upper-case underscored keys and stringified numbers — the only such response
among the routes of this section: the platform read the created records through legacy methods
whose field names did not match the schema, so the values bypassed normalization.
After
Every created record gets a link to the source lead, and the lead itself gets links to the contact and company that were created (only to those actually created). The response is read through the same method as an ordinary read of a deal, contact or company, so its shape matches GET /v1/deals/{id} and its neighbours — field names in the platform's usual style, numbers as numbers.
What integrators should do
If your parsing of the conversion response was written for the raw Bitrix24 shape, switch it to the ordinary read format used by every other route of this section. The backlinks only add data and break nothing: filtering deals by lead now finds the created deal.
BC-0908-8: a payment currency that disagrees with the order is no longer swapped silently
Old format supported until: not provided
Before
Creating a payment accepted any currency with 201, while
Bitrix24 stored the payment in the currency of its order — the requested one was dropped
without a word. The field description meanwhile promised a free choice with a pointer to
GET /v1/currencies, so the discrepancy only surfaced during reconciliation.
After
When currency is passed and does not match the currency of order orderId, the request is
refused with 409 and error code CURRENCY_MISMATCH before any call reaches Bitrix24; the
message names both currencies and the order. A matching currency is still accepted, so a
payment read and sent back whole keeps working. The same rule applies to
batch payment creation. When the order currency cannot be read, the request is
let through rather than refused.
What integrators should do
Either omit currency entirely to inherit the order currency, or send exactly the one the
order carries: GET /v1/orders/:id shows it.
NEW-0908-9: the standalone server auto-sleep policy is now visible through the API
POST /v1/infra/servers returns the saved data.sleepAfterMinutes for every successful create, reuse, and idempotency replay. This is a response field, not a new create parameter. A successful standalone deploy returns the same policy snapshot in data.sleepAfterMinutes, while SSE returns it as sleepAfterMinutes in the done event. For a regular standalone server that is not covered by agent/bot idle-sleep protection, a numeric timeout with no enabled recurring wake windows appends an entry starting with AUTO-SLEEP POLICY: to warnings[] with two choices: a wake window for a task that finishes before the timeout, or a deliberate always-on mode. A window only wakes the server and does not keep a background process running. The existing successful slept: false, reason: "WAKE_IMMINENT" outcome of POST /v1/infra/servers/:id/sleep-now is now documented for an imminent wake window. The HTTP 200 response itself is unchanged.
BC-0908-10: writing an amount to a smart-process type without product rows no longer answers with a false success
Old format supported until: not provided
Before
POST /v1/items/{entityTypeId} and PATCH /v1/items/{entityTypeId}/{id} accepted opportunity and isManualOpportunity on any smart-process type and answered 201/200. When the type has product rows disabled (isLinkWithProductsEnabled: false), Bitrix24 does not store those fields — the amount was silently lost while the caller saw success and moved on.
After
The platform compares what was requested against the re-read record — it already re-read it before responding, so no additional Bitrix24 calls were added. When the request EXPLICITLY asked for manual mode (isManualOpportunity: true) and did not get it, the answer is 422 AMOUNT_NOT_APPLIED: error.details.unappliedFields lists the fields that were not applied, and data carries the actual state of the record that was already created or updated. An amount sent WITHOUT that flag is not checked: from the response it is indistinguishable from a legitimate recalculation from the product rows, which works as documented — so those requests answer exactly as before. The check does not touch deals, leads, invoices or quotes — those always have product rows.
FIX-0908-11: the host from galaxyId is now available for reading
Before
The Galaxy application creation response already contained galaxyId, but GET /v1/infra/servers did not show that shared host, and GET /v1/infra/servers/:id answered 404 NOT_FOUND to the same API key.
After
Both GET operations return a limited host projection with access.via: "galaxy-reference". It contains only safe read fields. Host management and application operations still require a resource managed by the current key.
Impact on integrators
Requests require no changes. A client using the newly visible shared host should check access.via: galaxy-reference means read-only access.
NEW-0908-12: key issuance now says whether the key got access to Bitrix24 data
POST /v1/connect/token now returns an optional b24_credentials field, and
GET /v1/cowork/me the same object as b24Credentials. It answers whether the issued key can
read Bitrix24 account data: ready: true — the rights work, ready: false — they do not, and
a reason from a closed set comes with it (WEBHOOK_NOT_CONFIGURED, WEBHOOK_MINT_FAILED,
WEBHOOK_MINT_REFUSED_BY_PORTAL, INT_TARIFF_REQUIRED, VIBE_SCOPES_ONLY), plus an
upgradeUrl for the reason that names a paid plan. The 401 TOKEN_MISSING body already
carries the same set, so a client parses it with one branch of code.
Why it exists. A key is issued even when the account refuses to connect it to its own data:
the key still works for AI calls, while every request for account data answers
401 TOKEN_MISSING. Until now a client only found that out by hitting it, and could not tell
a missing right from a network failure. The state now arrives together with the key and can be
re-read at any time from GET /v1/cowork/me.
The field is additive: the other fields and the response status are unchanged, and requests that ignore it behave exactly as before. It is absent where the question does not apply — for example an app key, whose account tokens are stored separately.
FIX-0908-13: workday history is returned while a day is still open
Before
GET /v1/workday/records answered 502 BITRIX_UNAVAILABLE whenever the page contained a
record of a workday that was not finished yet. Such a record carries no end time, no duration
and no approval flag, and the endpoint required those fields to be filled in, rejecting the
whole page — including the closed days that arrived in the same response. In practice every
employee currently at work got an error.
After
The record of an unfinished day is returned as is, together with the rest of the page: the
HTTP 200 response is unchanged and the closing fields arrive exactly as Bitrix24 sent them.
The checks that keep a wrong answer from passing silently are untouched: the page is still
rejected when a record belongs to a different employee or carries no valid start time, which
is what tzOffset is derived from. No client action is required.
NEW-0908-14: agents are available for Open Channel binding
Added GET /v1/agents: the method returns agents owned by the API-key owner and the bitrixBotId value accepted by welcomeBotId in an Open Channel configuration. agentId is not used for a Hermes agent; queue continues to contain human operator IDs.
NEW-0908-15: department include in user records
The include=department parameter adds an _included.departments array to a user record with the full records of every department referenced by departmentId. A user without a department returns an empty array. The request requires the user and department scopes.
Affected endpoints: GET /v1/users, GET /v1/users/:id, POST /v1/users/search
NEW-0908-16: labels and descriptions for every basic field
In GET /v1/{entity}/fields responses, every declared basic field now has a localized label and description: Russian for Russian-language portals and English for international portals.
NEW-0908-17: protection against writing over newer sources
POST /v1/infra/servers/{id}/sources accepts an optional X-Parent-Version: v<N> header —
"this is the version I edited". If the latest version has moved on by the time the write
lands, it is refused with SOURCE_VERSION_CONFLICT (409) and
error.details.latestVersionId names the current one (null when the server has no live
versions at all). A malformed header value is 400 INVALID_VERSION_ID. Without the header the
endpoint behaves exactly as before.
"Current" here means the newest LIVE version — the same one that heads the version list and can
be downloaded, so the version named in the refusal can always be fetched and the write retried.
Deleted versions do not count, though their numbers are never reused. Deduplication is no
exception: an archive byte-identical to an existing version is refused with 409 too if the
head has moved on.
The guarantee runs in a single direction: your write will not land on top of a newer version. The reverse half does not exist — sources saved from the browser come from the deploy auto-save, which has no parent to declare, so a later browser deploy can still save over yours. The refusal is decided in the same place the version is created, so two uploads declaring the same parent cannot both win, and the refused archive does not stay in storage.
The POST /v1/cowork/deploy-key response gained two fields about the move onto the fresh key:
repointTruncated — not everything moved, the remainder goes on the next call, and
applicationSlotBlocked — an application card stayed on its previous key because the fresh
key's slot is taken by another card. Previously a partial outcome was visible only in the
platform's own journal.
Affected endpoints: POST /v1/infra/servers/{id}/sources, POST /v1/cowork/deploy-key
NEW-0908-18: replace an application key in one call, and see its mask on the card
An application can be added to Cowork/Code from the catalog — created earlier, on another machine, or before the Code tab existed. Such an application has no raw key and none can be recovered: the platform stores only a hash. There was nothing to publish with.
The new POST /v1/cowork/applications/{id}/key brings an
application key into working order in one call. An empty personal-key slot gets a key issued,
an occupied one gets it replaced; issued (minted or rotated) says which happened. The raw
key is returned once, as it is on application creation. Idempotency-Key is required, and a
replay answers rawApiKey: null with KEY_NOT_REPLAYABLE. If the request failed AFTER a key was issued
(a 500 naming orphanKeyId), that Idempotency-Key is spent for good: repeating it answers
IDEMPOTENCY_KEY_ALREADY_USED rather than minting a second key. Read the card, then retry
with a NEW Idempotency-Key. One case apart: when the key was issued but the card could
not be read afterwards, the answer is still 201 with the secret, application arrives null,
and warningCodes carries APPLICATION_CARD_UNAVAILABLE. Read the card with
GET /v1/applications/{id} — there is no reason to lose the only copy of the secret over it.
The previous key is not revoked immediately: it gets a one-day expiry, so a publish already in
flight finishes. previousKey.graceUntil names the moment it stops authenticating. The request
body accepts syncServerEnv: true — the platform then swaps the key in the deployed server's
environment and restarts the application, and envSync reports the outcome: updated, no such
line in the file, server asleep, restart failed, and so on. Without it the application keeps
the old key and starts getting refusals a day later.
Refusals arrive as distinct codes rather than a generic 400: NOT_APPLICATION_OWNER — the
application belongs to someone else; KEY_ROTATE_OAUTH_APP_KEY, KEY_ROTATE_SYSTEM_KEY,
KEY_ROTATE_LIVE_OAUTH_GRANT, KEY_ROTATE_NOT_ACTIVE — the key in the slot cannot be
replaced, the code says why; KEY_ROTATE_KEY_VANISHED — the key was removed while the request
was in flight; KEY_LIMIT_REACHED — the key quota is spent;
APPLICATION_KEY_REPLACE_IN_PROGRESS — a replacement for this application is already running.
One replacement per application at a time, whatever Idempotency-Key the second request carries:
otherwise both would issue a key and the slot would go to whichever finished last. A replacement
abandoned by a crashed request stops blocking after two minutes.
On an empty slot the issue goes through the platform-wide access gate, like every other
key-issuing door: an account without access gets a 402. A rotation deliberately does not —
it carries the previous key's rights forward and creates none, and an owner whose access has
lapsed must still be able to wind their affairs down.
The application card gained a key block: slot (auth or api — which key is shown),
present, prefix, suffix, status, expiresAt, lastUsedAt, rotatable and
rotateBlockedReason (whether replacement works and why not), affectsShownKey (whether it
touches the very key shown). The mask is assembled client-side from prefix and suffix.
The rotatable flag reports what the platform ALREADY
knows, and neither side of it is a guarantee: false means "a refusal is known, and here is
its reason" (rotateBlockedReason, which covers access as well as key state), true means "no
known refusal". Driving the button off it is convenient — grey it out on false and show the
reason — but do not turn false into a dead end: the refusal may already be gone while the
card has yet to learn of it (on the lag and on the two gates outside the flag, see below).
Judge by the door's answer, not by the flag alone. The block is ALWAYS there; someone the application was merely shared with
receives empty fields (present: false) and rotatable: false with reason NOT_MANAGER —
which means "not for you", not "there is no key". On an EMPTY slot, where the call would issue a key, the flag also covers the issuing
gates: ISSUANCE_BLOCKED (the account has no platform access), INFRA_DISABLED
(infrastructure is switched off), KEY_QUOTA_REACHED (the key quota is spent) and
READONLY_POLICY (the account issues read-only keys only). About the CALLING key the field
speaks the door's own codes, and they repeat on EVERY card of the response: INSUFFICIENT_SCOPE
(the key does not carry the vibe:cowork scope) and COWORK_HARNESS_KEY_FORBIDDEN (the key was
issued for an external agent). Accuracy is one-sided by design: false on an empty slot is read
off the access state the platform already knows, so an account whose Bitrix24 commercial plan
changed seconds ago may still show false until that state refreshes. TWO Cowork/Code gates stay
OUTSIDE the field and can still refuse a call made on a true: the platform-wide kill switch
(503 COWORK_FEATURE_DISABLED) and the state of your Cowork/Code seat (403
COWORK_NOT_ACTIVATED, which GET /v1/cowork/state already reports). Neither is a fact about the
card itself, so neither is repeated per card — handle the door's refusal instead of reading true
as a guarantee.
Affected endpoints: POST /v1/cowork/applications/{id}/key,
GET /v1/applications, GET /v1/applications/{id}
NEW-0908-19: read the sprint list, active sprint, and a sprint by ID
The Vibecode API adds GET /v1/scrum/sprints for reading visible sprints, GET /v1/scrum/sprints/active for reading a project's current sprint, and GET /v1/scrum/sprints/:id for reading one sprint by ID. The response contains its name, dates, status, and project link. If no sprint is active, the active-sprint endpoint returns data: null.
BC-0908-20: a paused Cowork/Code seat no longer opens Bitrix24 past the account plan
Old format supported until: not provided
Before
A Cowork/Code desktop key and an agent key opened the Bitrix24 REST API on an account whose plan does not carry REST access, regardless of whether the seat itself was in force. A paused seat kept that ability until its key was revoked — about a month.
After
The ability follows the state of the seat. The bypass applies only to a seat in the ACTIVE state, including a seat with a scheduled cancellation, until the end of its paid term. For a paused, cancelled or parked seat the key is served by the ordinary account rule: where the Bitrix24 plan already carries REST access nothing changes, and on a plan without it the calls to Bitrix24 get the same account refusal as any other application.
What integrators should do
Resume the Cowork/Code subscription, or move the account onto a paid Bitrix24 plan. The current state of the seat arrives in the subscription.state field of the GET /v1/cowork/state response.
NEW-0908-21: ERP module tool catalog is available through the client API
GET /v1/onec/tools returns the latest validated portal tool catalog together with loaded, stale, version and update-time metadata. Each tool contains method, title, description and inputSchema. The endpoint requires the vibe:onec scope, an enabled portal connector and the RU segment, but it does not require a mapping for the current user. Before a catalog is loaded, the response explicitly contains loaded: false, an empty tools array and null revision and update-time fields.
POST /v1/onec/tools/{method}/call now accepts nested JSON values in filters: up to 200 array items, 50 keys per object, 1024 characters per string, depth 6 and 2000 nodes across the whole filters object. The keys __proto__, prototype and constructor are rejected at every level. The server continues to assign user, portal and requestId. Execution remains asynchronous: a successful request returns 202, and its result is read through GET /v1/onec/operations/{operationId}.
FIX-0908-22: the sources registry no longer advertises a door that answers 403 to the calling key
Before
GET /v1/me/sources derived reachableViaApi from ROW
reachability alone — "is the server alive, is the app not deleted" — and never looked at the KEY
that called. The doors its pointers lead to are key-scoped and reject an OAuth application key
whenever the server belongs to someone else.
In practice: the listing enumerates snapshot owners per USER, not per key, so calling with
your own application's key returned your servers created with PERSONAL keys carrying
reachableViaApi: true and a working-looking listEndpoint — while
GET /v1/infra/servers/{id}/sources answered that very key
403 NOT_AUTHORIZED at that very address.
The same lie appeared on application rows: the author of applications A and B, calling with the
key of B, saw the row of A with a working pointer whose door answers
403 SOURCE_APP_ID_MISMATCH.
Now
reachableViaApi answers one question: will the CALLING KEY be admitted through the drill-in. A
row whose door that key will not open carries reachableViaApi: false with listEndpoint and
latestDownloadEndpoint set to null. Both kind branches of the listing (server and
legacy-app) compute it with the same predicate the door itself uses.
The row itself does NOT disappear from the listing: a false promise was withdrawn, not visibility.
The owner still sees that snapshots exist and whose they are via user.id.
What integrators should do
Check reachableViaApi before following listEndpoint or latestDownloadEndpoint. Both fields
were already declared string | null and already arrived empty for an orphaned server and a
soft-deleted application, and reachableViaApi: false was already a documented value for those
same two cases — so a client that honoured the contract needs no change. If you get false where
you expected true, you called with an application key: repeat the call with a personal key
(your own or the server owner's) or with a portal administrator's key. The same holds for an
application row — an application key opens only its own application.
What this does NOT change
Access. No door started or stopped admitting anyone: the access rule is unchanged, only the
listing stopped promising access that was never there. Collapsing the doors of one server onto a
single ownership model is a separate breaking change and ships as its own entry — and so do the
publish and deploy refusal hints, because there the same address is withdrawn TOGETHER with the
hint.requiredAction field, which is a documented-field removal.
2026-09-07
FIX-0907-1: redeeming a coupon on your own portal is now refused with a dedicated code
Before
POST /v1/cowork/coupon/redeem looked only at the coupon, the campaign and the seat. It did not
care who was redeeming or whose portal it was: a platform employee holding the coupon-issuing
right could mint a code and activate it on a portal they stand behind — the plan was granted at
the platform expense, and the response was a plain HTTP 200.
After
Redemption is refused with COUPON_SELF_PORTAL (HTTP 409) when the portal is backed by a
platform employee holding the coupon-issuing right — the redeemer themselves, or any portal
member, API key owner or server owner on it. A successful redemption is not affected: on a
portal with no such people the HTTP 200 response is unchanged, body included, and no integration
work is required. The refusal does not consume an attempt against the code-guessing counter —
the cause is the portal's composition, not a wrong guess. The code is listed in the endpoint's
response table in the documentation.
NEW-0907-2: self-description now says whether infrastructure is stopped for non-payment
The GET /v1/me and GET /v1/cowork/state
responses carry a new infraState block: frozen tells whether servers, deploy and
storage are stopped for non-payment, reason is the reason code (DEBT or null), and
topupUrl is the dashboard top-up address or null.
The block answers a question that had no readable source before: what exactly is stopped for an account in debt. The strings inside the block are machine-readable, and the human-facing text is up to the client.
Note that what the field means depends on whether infrastructure-debt scoping is enabled
for the account (the FIX in this same release). Until it is, a negative balance refuses
V1 calls broadly, GET /v1/cowork/state included; only GET /v1/me stays readable in that
mode, being exempt from the freeze. There infraState.frozen means "almost everything is
stopped". Once it is enabled the field means exactly what it
says: infrastructure is stopped while calls within the plan monthly quota keep working.
The block is additive, existing fields are unchanged.
FIX-0907-3: fixed pagination for business process activity and automation rule lists
Before
A request with offset=50 returned the same codes as a request with offset=0. The response remained successful with HTTP 200.
After
offset skips the specified number of codes in the full list. meta.total reports the full list size, while meta.hasMore reports whether codes remain after the current page. The same behavior applies to list sub-calls for these entities in the global batch request, including both total channels. The response still returns HTTP 200, and OAuth authorization requirements are unchanged.
Impact on integrators
No action is required. Integrations using offset now receive the requested page instead of a repeated first page; the response format and authorization requirements are unchanged.
Affected endpoints: GET /v1/bizproc-activities, GET /v1/bizproc-robots, POST /v1/batch.
FIX-0907-4: infrastructure debt no longer disables what is paid for separately
Before
A negative wallet balance returned 402 ACCOUNT_FROZEN on almost every V1 call — only
self-description, the guide and the support conversation were exempt — including calls
that never touch the wallet: proxying to your own Bitrix24, Cowork subscription
state, the model list. Debt for a virtual machine disabled AI granted by the Bitrix24
plan.
After
The account freeze applies to what the wallet pays for: servers, storage, deploy, search and research on platform credentials, spending beyond the monthly quota, deploy-key issuance and application creation. A call within the plan monthly quota, a paid Cowork subscription period and REST proxying to your own portal go through whatever the wallet state is.
Note the behaviour sits behind the wallet-debt-scoped-to-infra feature flag; with the
flag off the responses are unchanged.
FIX-0907-5: an oversized numeric CRM ID is rejected before Bitrix24 is called
Before
Numeric CRM IDs were checked for shape only: any digit string without leading zeros passed the guard. This affected regular record :id values in paths (/v1/deals/{id}, /v1/contacts/{id} and similar entities), as well as positive smart-process entityTypeId values and related CRM operations. An ID beyond the safe integer range reached Bitrix24, where it could be rounded to a different value and some crm.item methods answered it with a raw PHP error instead of a clear refusal.
After
Such IDs are rejected as invalid parameters before Bitrix24 is called. The check covers regular record IDs on path and batch surfaces, as well as smart-process entityTypeId values, dynamic parameters and related CRM operations. The boundary is the safe integer: 9007199254740991 is still accepted, while 9007199254740992 and anything longer is rejected. The ID 0 remains valid wherever it was already allowed, such as the main deal pipeline.
FIX-0907-6: lead conversion respects the selected deal pipeline
Before
POST /v1/leads/{id}/convert ignored the categoryId parameter, so the new deal was placed in the default pipeline.
After
When categoryId is provided, the new deal is created in the selected pipeline immediately. The value 0 still selects the default pipeline.
A non-negative integer string remains accepted for compatibility and is normalized to a number. Other value shapes remain ignored and leave the deal in the default pipeline; null and an omitted parameter are equivalent.
NEW-0907-7: Bitrix24 employee id of the app author in /v1/apps responses
Responses of the apps family now carry two new fields: authorBitrixUserId — the
numeric Bitrix24 employee id of whoever created the app — and
authorBitrixUserIdSource, telling where that id came from. authorBitrixUserId
is the same identifier the id field of GET /v1/users
returns, so it is the join key between the two responses: an app previously carried
only authorId, a Vibecode platform user identifier, with nothing to match the
creator against an employee card.
Values of authorBitrixUserIdSource: member — the id comes from the author's
confirmed membership of this Bitrix24 account and is safe to link; snapshot — the
id comes from a value captured when the app was created, which is best-effort and
may point at a different employee than the app's current author; null — the id is
unknown, and authorBitrixUserId is null as well. A registry that must not be
wrong should link to an employee card only on member. On self-hosted accounts and
on accounts with microservice credentials the snapshot value is never returned:
there the captured value has no identity-confirmed origin. Confirmed membership
still works on such accounts, so the id there is either member or empty.
The author's name is deliberately NOT returned by the apps responses, which narrows
the original request — it asked for the name as well. Personal data does not travel
in a response every account key can read, the key embedded in a deployed
application's code included; the name is fetched by authorBitrixUserId from
GET /v1/users, where the user permission gates it.
Existing requests keep working unchanged: the fields are additions, nothing was removed or renamed.
Affected endpoints: GET /v1/apps, GET /v1/apps/:id, POST /v1/apps, PATCH /v1/apps/:id, POST /v1/apps/:id/publish, POST /v1/apps/:id/unpublish, POST /v1/apps/:id/relink-oauth
BC-0907-8: companies field removed from the contacts API
Old format supported until: not provided
Before
The companies field was advertised by GET /v1/contacts/fields and accepted in select for contact read operations. Bitrix24 did not return a value for this field, so a successful response could omit the companies key.
After
The companies field is no longer advertised and is always removed from contact responses. Requesting it in select is rejected with 400 UNKNOWN_SELECT_FIELD.
What integrators should do
Do not request companies. Use the companyIds field for related company identifiers.
Affected endpoints: GET /v1/contacts, GET /v1/contacts/:id, POST /v1/contacts/search, POST /v1/contacts/aggregate, POST /v1/contacts/batch, GET /v1/contacts/fields.
FIX-0907-9: multipart upload replaces a large object under its existing key
Before
POST /v1/storage/objects/multipart/create returned 409 STORAGE_KEY_EXISTS when a live app object already occupied the same key. Content larger than 10 MB therefore could not be replaced without changing the key. For a personal key, the advice to use multipart led to the same failure.
After
For an existing app object, the request returns 200 and opens a replacement session with the existing objectId. Reads return the old content until POST /v1/storage/objects/multipart/complete publishes the new bytes; a successful response confirms the new version. A confirmed POST /v1/storage/objects/multipart/abort before publication preserves the old version. If complete returns 502 STORAGE_BUCKET_ERROR, publication may have happened: that response does not identify the current version, the session remains active, and object deletion stays blocked until a platform administrator resolves it. Object visibility does not change.
POST /v1/storage/objects/multipart/create returns 409 STORAGE_KEY_EXISTS for an existing live object owned by a personal key and does not open an upload session. Delete the object first, then start a new multipart upload under the same key. This operation is non-atomic: the object is unavailable between deletion and successful completion of the new upload.
Impact on integrations
App files larger than 10 MB can be updated by multipart upload under the existing key. Multipart create, complete, and abort may return the retryable 409 STORAGE_KEY_CONFLICT when another request is changing the same key or session state changed. For create and complete, retry the same operation. A 409 from abort during finalization requires retrying complete with the same parts; if complete returns 502 STORAGE_BUCKET_ERROR, do not infer the current version and contact a platform administrator. If abort returns 502 STORAGE_BUCKET_ERROR, keep retrying abort until session release is confirmed. DELETE /v1/storage/objects/{key} returns 409 STORAGE_MULTIPART_IN_PROGRESS while a multipart session is active: complete or abort the session first, accounting for the finalization case above.
FIX-0907-10: moving a deal/lead to a nonexistent stage now returns an error instead of a silent success
Before
POST /v1/deals/{id}/move, POST /v1/leads/{id}/move, PATCH /v1/deals/{id} and PATCH /v1/leads/{id} answered 200 success:true with the full deal/lead object even when the requested stageId (or a deal's categoryId) did not exist in the reference list — Bitrix24 silently ignored the value, the stage stayed unchanged, and the caller could not tell a real move from a rejected one without manually comparing the data.stageId field.
After
The response remains 200 when the requested stage or pipeline exists and is applied — behavior for valid values is unchanged. When the requested value was not applied (Bitrix24 accepted the call but the actual stage stayed the same), the response is 422 STAGE_NOT_APPLIED with details in error.message: what was requested and what is actually there after re-reading the record.
FIX-0907-11: The placement-bind note says trial, not demo
Before
The apps.bindPlacements capability note in GET /v1/me still called Bitrix24 trial
access a demo, while the refusal it describes has always been named after a trial.
FIX-0904-7 settled the latin terminology on the word trial everywhere else, so this
string was the last live disagreement of its kind. The same term also survived in two
documentation articles — the one on starting Bitrix24 trial access, and the plan name
reported by GET /v1/me.
After
The note says trial, and the documentation uses the same term. Refusal codes, statuses and response fields are unchanged, and a successful response stays successful. The note is not localised and is served identically on every installation.
Impact on integrators
None for logic. A client matching this note by substring should re-check the comparison.
FIX-0907-12: the spec declares every server mode-switch response
Before
The public spec GET /v1/openapi.json declared only 200, 400, 403 and 404 for PATCH /v1/infra/servers/:id/mode. A client generated from the spec treated the remaining outcomes as impossible, although the endpoint returned them: the exhausted-balance refusal, the server-role refusal, a parallel-request conflict and two network-policy failures.
After
The spec gains the responses the endpoint does return: 401, 402 ACCOUNT_FROZEN and OPEN_MODE_REQUIRES_COMMERCIAL, 409 SERVER_NOT_RUNNING, AGENT_NOT_CONNECTED, MODE_SWITCH_SEALED_ROLE and CONFLICT, 502 IPTABLES_FAILED, PROVIDER_NOT_CONFIGURED, GATEWAY_UNREACHABLE and TUNNEL_NOT_FOUND, 503 SECURITY_GROUP_ATTACH_FAILED and a refusal whose code starts with GATEWAY_TIMEOUT. The 400 description gains the codes SAME_MODE, NO_SUBDOMAIN and MODE_SWITCH_STANDALONE_ONLY. The same codes now appear in the endpoint page's error table: before this change it named none of the three state refusals — server not running, agent not connected, server without a subdomain. The code PROVIDER_ERROR is removed from the endpoint page's error table: a mode switch does not return it. Status 429 is deliberately left undeclared — the endpoint carries no limiter of its own, and the platform-wide edge limit is documented on the limits page rather than declared per route. Endpoint behaviour is unchanged: the successful response remains HTTP 200 with the same body, no request needs changing, and the edit touches the declaration and the documentation only.
NEW-0907-13: `/v1/cowork/me` and `/v1/cowork/state` now name the portal the key is bound to
Both Cowork/Code self-info endpoints gained a new portal field:
{
"portal": { "id": "8f3c…", "domain": "acme.bitrix24.com" },
"tier": "PRO",
"state": "ACTIVE"
}
The field answers which account the rest of the body is about. A Cowork/Code seat
is defined on the pair «key owner + account», and a key is bound to a single
account for good, so for a person with several accounts the app reports the tier
and quota of one account while the browser shows another. Until now the domain
arrived exactly once, when the code was exchanged for a key
(POST /v1/connect/token), and could not be asked for again.
portal.id is always present — it is taken from the key itself. portal.domain
is null only in the degenerate case where the account row is already gone. Every
other field of both responses is unchanged, and requests written before the field
existed keep working without edits.
BC-0907-14: a partial lead conversion failure now answers 422, not 200
Old format supported until: not provided
Before
POST /v1/leads/:id/convert creates the deal, contact and company as separate records one after another. When only some of them were created, the response still arrived with status 200: the success field was false, while error, code and details sat at the top level of the body, and error was a string rather than an object. A client branching on the HTTP status or reading error.code took a partially written CRM state for a success.
After
A partial failure answers 422 with code CONVERSION_FAILED in the usual error envelope: error is an object carrying code and message. The outcome of every requested operation moved into error.details — one key per deal, contact and company, each with a success flag and either the id of the created record or the Bitrix24 refusal text. The top-level error string and code are gone from the response. A lead that could not be read answers 404 with code ENTITY_NOT_FOUND in the same envelope. A successful conversion still answers 200 with a data field.
What to do
Branch on the HTTP status and on error.code, not on the success field and not on the top-level code; read per-record outcomes from error.details rather than from details. Records already created are still not rolled back and the lead keeps its status, so before repeating a call check what already landed in CRM: a repeat creates a second set of records. There is no support window for the former 200 response: it reported success where some CRM records had not been created, so keeping it would mean going on passing a failure off as a success.
FIX-0907-15: root-type validation on entity create, update, batch and search
Before
A JSON literal null as the request body crashed POST /v1/{entity}, PATCH /v1/{entity}/{id} and POST /v1/{entity}/batch with 500 INTERNAL_ERROR; that status was not declared for any of the three routes. A JSON array or a JSON string as the body of POST /v1/{entity} created a real record with empty or auto-generated fields and answered 201, as if the request had been valid. On PATCH /v1/{entity}/{id} a JSON string passed the empty-body check and answered 200 without actually changing anything. On POST /v1/{entity}/search an array body answered 400 INVALID_FILTER_SHAPE with a message that misnamed the type actually sent (reporting a function instead of an array); a bare scalar body (a number, a string, a boolean) was treated as "no filter" and answered 200 with the unfiltered record list.
After
A well-formed body (a JSON object — for /search, including an empty {}, which is the legitimate "no filter" request) is handled unchanged. The response still remains 201 on create and still remains 200 on update, batch and search — none of that changed. A body whose JSON root is not an object (null, an array, a string, a number, a boolean) now answers 400 on all four routes, and no record is created or modified: EMPTY_CREATE_BODY on create, EMPTY_UPDATE_BODY on update, INVALID_BATCH_ACTION on the per-entity batch route, INVALID_REQUEST on search. The search error message now names the type actually sent instead of an internal implementation artifact.
FIX-0907-16: an absent record answers 404 for warehouses, catalogs, order statuses, basket items and smart processes
Before
One and the same scenario — "no record with this id" — answered with different codes inside a
single product area. GET /v1/order-statuses/{id} and GET /v1/basket-items/{id} returned
422 BITRIX_ERROR, while the neighbouring orders, invoices, payments and quotes returned 404 at
the same step. GET /v1/catalogs/{id}, GET /v1/warehouses/{id} and the product-property pages
behaved the same way, even though catalog products and prices already answered 404. A
delete-confirmation check based on the response code silently failed on those paths.
Separately, on a portal whose interface language is not Russian, PATCH and DELETE for
/v1/smart-processes/{id} with a nonexistent entityTypeId answered 400 INTERNAL_ERROR — a code
the DELETE contract does not even declare. On a Russian portal the same call correctly answered
404 SMART_PROCESS_NOT_FOUND, and GET was correct in every language.
After
An absent record answers 404 on all of the paths above. GET, PATCH and DELETE for
warehouses, catalogs, order statuses and basket items return 404 ENTITY_NOT_FOUND; the portal
text in the message field is unchanged, only the classification of the response is. The same
404 now arrives on the product-property and property-value pages.
PATCH, DELETE and POST /v1/smart-processes/batch answer 404 SMART_PROCESS_NOT_FOUND
regardless of portal language, exactly as GET has done for a long time. A client can rely on one
existence code across the whole Vibecode API instead of keeping per-area and per-language
exceptions.
2026-09-06
FIX-0906-1: a free Bitrix24 plan now answers key issuance terminally, not with a "retry later"
Before
A cloud account on a free Bitrix24 plan could get 502 CONNECTOR_REST_UNAVAILABLE with
details.retryable: true on key issuance, whenever its Marketplace access read as in
force. The answer promised that a retry would help. Bitrix24 shuts REST off entirely on
a free plan, so a retry never did.
The activation.marketTrial.available flag in GET /v1/cowork/state has always read
false on this segment: the Marketplace trial is a subscription-region product, so
POST /v1/cowork/activate-market-trial never had a one-time trial to spend here.
After
The same case answers terminally with the refusal this segment already uses for a plan
that does not carry access — INT_TARIFF_REQUIRED (403) with a userMessage naming the
remedy, a paid Bitrix24 plan. The code is not new; what changed is the set of states it
comes in: previously only "access paid for but not in force", now any access state on a
plan read as free. A retry without changing the plan returns the same thing.
activation.marketTrial.available keeps its region_not_supported reason on this
segment, and POST /v1/cowork/activate-market-trial together with
POST /v1/portals/{id}/activate-market-trial keep refusing exactly as before — that
half of the change is region-gated above the plan check and does not reach here. An
account on the Bitrix24 demo plan is not affected either: that plan does not shut REST
off, so it keeps the answer it had. An account whose plan could not be read behaves as
before.
BC-0906-2: PATCH /v1/infra/servers/:id/mode rejects opening a server with a sealed role
Old format supported until: not provided
Before
For a server whose role (a pool member, a service machine) requires a sealed network policy, PATCH /v1/infra/servers/:id/mode with mode: "OPEN" responded 200: the server did switch to open mode, and the issued SSH password worked.
After
Such a request is now rejected with 409 MODE_SWITCH_SEALED_ROLE; the server's mode is left unchanged and no password is issued. The rejection applies where the server's network policy is backed by the additional layer for that server; where it is not backed, behavior is unchanged.
What integrators should do
Clients that expected 200 for servers with a sealed role must handle 409 MODE_SWITCH_SEALED_ROLE as a terminal rejection: open mode is not available for such a server, and retrying will not help. For other servers (not a sealed role), 200 and password issuance are unchanged.
NEW-0906-3: PATCH /v1/infra/servers/:id/mode may return a network-policy switch failure
PATCH /v1/infra/servers/:id/mode enforces a server's network policy with one more layer — outside the guest machine, on top of the protection already in place inside it. If switching the server's network policy fails, the endpoint responds 503 SECURITY_GROUP_ATTACH_FAILED; the server's mode is left unchanged, and retrying the request makes sense. This rejection is only possible where the additional layer is supported for a given server; where it is not supported, the endpoint's behavior is unchanged.
FIX-0906-4: PATCH /v1/infra/servers/:id/mode closes the server firewall when the connection drops mid-switch
Before
PATCH /v1/infra/servers/:id/mode performs the switch by running commands on the server itself. If the connection was lost partway through that sequence — a timeout, a dropped tunnel, an unreachable agent — the endpoint returned an error, but the server could be left with its firewall already open while its mode stayed BLACKHOLE: the opening command had time to run, and nothing was left to close it again. The error, meanwhile, reported that the switch had been aborted.
After
Such a drop now triggers closing the firewall back before the error reaches the client: previously nothing was attempted on this path, and a server left open while its mode said BLACKHOLE stayed that way until someone intervened.
That closing travels over the same connection that has just dropped, so it is an attempt, not a guarantee: if the server is unreachable altogether it does not arrive either, and the discrepancy remains. The response does not report this — the outcome of the attempt is visible in the platform log.
One case is deliberately left out: if the server had already been closed and it was the mode write that failed, the firewall is NOT reopened — undoing a completed closure because of a write failure would be the more dangerous choice. The server is then closed while its mode is reported as OPEN.
What integrators should do
Response codes are unchanged, and retrying the request still makes sense. In the exception described above, retrying with mode: "OPEN" is rejected as 400 SAME_MODE — the stored mode is already OPEN; access is restored by switching to BLACKHOLE and back to OPEN.
BC-0906-5: a platform integration key now dies together with its issuer's authority
Old format supported until: 31.12.2026
Before
A /v1/platform/* key lived until someone revoked it by hand. Revoking a team
member's platform tier, blocking them, or accepting an account-erasure request
left the key alone: the revenue export channel and coupon batches kept working
on behalf of a person the platform itself no longer admits.
After
The key is revoked automatically at the moment its creator loses platform
authority — tier revocation, a block, or an accepted erasure request. Requests
made with such a key receive 401. The remaining platform administrators get an
email listing the revoked keys.
If the channel is still needed, issue a new key from an active team member:
POST /api/platform/integration-keys. There is no way to check the channel state
in advance — the email is the notification — so integrations should treat 401
as "a new key is required" rather than as a transient error.
2026-09-05
FIX-0905-1: chat with a Cowork/Code key works while the product is disabled
Before
POST /v1/chat/completions returned HTTP 503 with the lowercase code cowork_feature_disabled in the response body for an already-issued key carrying the vibe:cowork scope when Cowork/Code was disabled at the platform level.
After
The model call continues through the Cowork/Code subscription and still checks subscription activity, model availability, and quota. The switch continues to close Cowork/Code product interfaces and operations. Integrators do not need to change anything.
FIX-0905-2: the key-issuance refusal now separates an unread plan from a free one
Before
An international account whose plan Vibecode could not read was refused key issuance with 402 and the code INT_VIBE_PLUS_REQUIRED (or INT_TARIFF_REQUIRED), advising a paid plan. That advice could be wrong — the account might already be paying — because the decision was made off an empty plan code, and the code is empty both for a read free account and for one whose licence could not be requested.
After
The two states are now told apart. When the plan code is absent AND the last licence probe failed, the answer is 403 with the code PORTAL_TARIFF_UNREADABLE: the plan was not read, and buying one does not change that. details.requiredTariffs is empty — no plan clears this refusal — while details.upgradeUrl and alternatives[0].url point to support. The previous codes keep their own state: an account with a plan read as free still gets 402 and the previous copy.
2026-09-04
NEW-0904-1: asynchronous ERP tool calls and operation status
A separate Vibecode deployment, not this one, gains POST /v1/onec/tools/{method}/call and GET /v1/onec/operations/{operationId}: a tool call executed asynchronously by the account's ERP connector module and tracked as a durable operation with a status URL. The codes ONEC_USER_NOT_MAPPED and ONEC_OPERATION_GONE belong to that deployment's contract.
Nothing changes on this deployment and no action is required. The ERP connector is not part of the API served here: both URLs answer 404 for every account, whatever its configuration, no setting exists that could enable them, and neither code above is reachable here. No new field and no new branch appear in the contract you consume here.
NEW-0904-2: the Supports BitrixMobile flag at application registration
POST /v1/apps accepts an optional mobile: boolean field, defaulting to false. When true, the platform reports the Supports BitrixMobile flag to Bitrix24 at application registration on the account, and the application becomes visible in the mobile client. The mobile field now appears on the application object too — in the creation response, application data, the list and relink. Applications created earlier carry false. The flag is set only at creation — PATCH /v1/apps/:id does not accept mobile. When the account registers the application through a path that cannot carry the flag, the application is created with mobile: false and the creation response carries a warnings line starting with mobile:. Existing requests without mobile keep working. Details — Create an application.
BC-0904-3: the self-hosted refusal code is renamed to SELFHOSTED_NOT_AVAILABLE
Old format supported until: not provided
Before
A self-hosted Bitrix24 account on the international installation was refused with
the code INT_BOX_PARTNER_REQUIRED. The message explained that access comes with
a partner licence, that the key carried no confirmed partner mark, and asked the
customer to re-register the module on the portal. The button pointed at the
connect documentation: details.upgradeUrl and alternatives[0].url carried the
address of /docs/connect-self-hosted-bitrix24.
After
The same refusal arrives with the code SELFHOSTED_NOT_AVAILABLE. A client that
branches on the code must replace the string — the old code is no longer returned
on any surface.
The rest of the response is unchanged: the status is still HTTP 402,
details.requiredTariffs is still empty (no plan purchase clears this refusal),
and alternatives keeps the same members in the same order.
The message and the button address changed. The message no longer names the
reason and no longer asks for anything to be done on the portal: self-hosted is
not available yet, access is being opened gradually, and the account's servers and
data are kept as they are. details.upgradeUrl and alternatives[0].url now
carry a mailto: support address — the only address where this is resolved.
FIX-0904-4: trial deployment hints account for galaxy host eligibility
Before
GET /v1/me promised one-shot deployment whenever any running or sleeping galaxy host existed. If that host could not accept the app, POST /v1/infra/servers rejected the request and its hint suggested two-step creation, which then hit the trial portal limit.
After
GET /v1/me shows the one-shot path only when a known host appears eligible and explicitly marks the prediction as advisory. POST /v1/infra/servers remains authoritative; when an ineligible host occupies the trial limit and trial enforcement is active, the response no longer recommends an unavailable two-step create.
Impact on integrators
Check deployment.galaxyApp before a one-shot create and follow error.hint after a refusal. Before using the two-step path, also check capabilities.servers.create.available and choose a plan from capabilities.servers.create.limits.allowedPlans in the same response.
FIX-0904-5: a catalog card is no longer published without its portal binding
Before
Publication could complete with b24CatalogSync.status set to SYNCED even though the catalog card
had no binding to the sender. In that state, the new-version notification did not arrive in the
application chat and no reason was displayed.
After
When calling POST /v1/infra/servers/:id/b24-catalog/publish,
such a card is no longer created. For a paired portal, publication completes only after the card's
binding to the sender has been confirmed. Until it is confirmed, b24CatalogSync.status stays other
than SYNCED, pendingOp stays ADD, and lastError names the reason. The response is still HTTP
200, and a successful publication returns exactly what it returned before.
Impact on integrators
Requests do not need to change. If an integration monitors the asynchronous status,
pendingOp=ADD together with a status other than SYNCED means that the card has not been
published yet. Wait for the status to change or show the lastError value to the user.
FIX-0904-6: expiresAt of presigned URLs now equals their signature lifetime
Before
POST /v1/storage/objects/multipart/create computed parts[].expiresAt from the requested lifetime — 24 hours for every part — and GET /v1/apps/{id}/sources/{versionId}/download and GET /v1/infra/servers/{id}/sources/{versionId}/download from a fixed 30 minutes; the redirect link of GET /v1/storage/objects/{key} was documented as valid for 10 minutes. The currently disabled POST /v1/storage/objects answers 503 STORAGE_PRESIGNED_UPLOAD_DISABLED and computed expiresAt the same way. The URL signature could expire earlier, and storage answered 403 while expiresAt was still in the future.
After
expiresAt in these responses is taken from the URL signature itself and equals its lifetime. It may be shorter than the 24-hour multipart session and shorter than 30 minutes for download URLs; the redirect link may also expire before 10 minutes. Nothing changes for the disabled POST /v1/storage/objects: it still answers 503, and once enabled its expiresAt will also come from the signature. The response remains HTTP 200, the field format is unchanged.
Impact on integrators
Schedule uploads and downloads by the expiresAt from the response and start as early as you can, not by the uploadId session lifetime and not by the documented 30 minutes. Clients that already relied on expiresAt change nothing.
FIX-0904-7: the latin locales call Bitrix24 trial access a trial
Before
The latin locales called Bitrix24 trial access a demo, while Bitrix24 itself names it a trial on the international installation. The mismatch reached the contract too: GET /v1/me returned Demo period in data.tariff.name, and the INT_TARIFF_REQUIRED refusal told the reader to activate a paid plan "or its demo". Within a single activation flow neighbouring messages disagreed with each other: one said trial, the next said demo about the very same access.
After
The plan value and the refusal text in the latin locales are settled on the word trial. GET /v1/me returns Trial period for that plan, and the INT_TARIFF_REQUIRED refusal now offers a paid plan "or its trial". A client comparing this field by string should re-check the comparison: the codes, statuses and response fields themselves are unchanged, and a successful response stays successful. The Russian locale keeps its own wording.
This entry covers the contract: the plan value and the refusal text. At publication time the apps.bindPlacements capability note of the same endpoint and the documentation articles still said demo, and they were translated later, in FIX-0907-11.
FIX-0904-8: batch currencies sub-call no longer returns meta.total 0 next to a non-empty data
Before
POST /v1/batch with a currencies list sub-call published data.meta.<id>.total: 0 and hasMore: false while data.results.<id> held records. The Bitrix24 method crm.currency.list reports envelope total: 0 next to a non-empty result, and the batch door accepted that literal zero as the collection size. A client paging by hasMore under a limit smaller than the catalog read the first window and treated the set as exhausted, losing the tail.
After
On a counted sub-call the envelope zero no longer beats the measured set size: data.meta.<id>.total and data.totals.<id> report the size before the client-side window, and hasMore is computed as offset + returned < total, staying true while a tail remains. This matches the single endpoints, which already normalized this method behaviour. An uncounted sub-call (params.withTotal: false, a negative params.start) still gets no total, and a genuinely empty catalog still returns total: 0. The response remains HTTP 200.
NEW-0904-9: a partner application can revoke its own key
A new POST /v1/connect/revoke endpoint follows RFC 7009: the application sends client_id, client_secret (public clients send none) and token, and exactly the presented key is killed. Its slot in the user's key limit for that Bitrix24 account is freed immediately, and requests with the key start answering 401 KEY_INACTIVE.
A 200 also comes back when there was nothing to revoke — for an unknown token, for a key issued to another application and for an already revoked one — so the call is idempotent and cannot be used to probe whether someone else's keys are alive. A missing client_id or token gives 400 invalid_request; an unknown client or a wrong secret gives 401 invalid_client. A key issued before the client was deactivated can be revoked as well.
The endpoint is announced in the discovery document /.well-known/oauth-authorization-server through revocation_endpoint and revocation_endpoint_auth_methods_supported. Until now a key issued to an application could only be revoked by the user in the "Connected apps" section, by the application owner deleting the client outright, or by a platform administrator; all three keep working as before.
BC-0904-10: offset in the statuses and deal-categories references works at any depth
Old format supported until: not provided
Before
GET /v1/statuses returned the same records for offset=0, 50,
100 and 300. The Bitrix24 method behind this reference answers with the whole collection in
one response and applies no navigation, while the wrapper cut that response from the beginning —
the client received the first page in place of the fiftieth. At an offset that was not a multiple
of 50 the window repeated with a period of 50: offset=130 returned the same records as
offset=30.
An offset walk did terminate, but it collected duplicates and never reached the end of the
reference: out of 267 records only about 50 were reachable through the list, while meta.total
reported an honest 267. There was no error at any step — every response came back with status 200.
GET /v1/deal-categories and list sub-calls inside
POST /v1/batch behaved the same way.
A list sub-call inside a batch with NO explicit limit also behaved differently from the same
list issued as a single request: a single GET /v1/statuses
returned 50 records by default, while the batch sub-call returned the entire reference — all 267
records in one response.
After
The [offset, offset + limit) window is computed on the Vibecode side over the full set, in the
order the core returned. offset=50&limit=5 yields records 51 through 55, a limit above 50 no
longer truncates the tail, meta.total equals the size of the filtered set, and meta.hasMore
turns false on the last page. An offset walk terminates and covers every record exactly once.
The response remains 200.
A list sub-call inside a batch now follows the same default as a single request: with no explicit
limit it returns the first 50 records and hasMore: true, not the whole reference.
Sorting and filtering are still performed by Bitrix24, so the sort parameter behaves as before.
The same behaviour applies to POST /v1/statuses/search and to
list sub-calls inside POST /v1/batch.
Impact on integrators
Loops that previously re-read the same records and never reached the end of the reference now return the full selection — they need no code change.
One case does require a code change: a POST /v1/batch sub-call listing statuses or
deal categories with no explicit limit. It used to return the whole reference; it now returns
the first 50 records. There is no error — the response comes back with status 200 and
hasMore: true — so the remaining records are lost silently unless they are requested.
What to do: either set limit explicitly, or read the reference page by page, advancing offset
by the page size until meta.hasMore becomes false. The second option is preferable — it does
not depend on the size of the reference and works on any portal.
BC-0904-11: Workday history now returns the time-zone offset
Old format supported until: not provided
Before
GET /v1/workday/records required only timeman. Records did not contain a required tzOffset, so a client could not reliably obtain the employee's local time.
After
The endpoint requires timeman and one of user_brief, user_basic, or user. Every record contains a required tzOffset: seconds east of UTC calculated for the startTime instant using the historical rules of the current IANA TIME_ZONE identity in the employee profile. The value is not proof that this zone was assigned when the record was created. The startTime and endTime strings are unchanged. A non-empty page returns 502 BITRIX_UNAVAILABLE if the current profile zone or its offset cannot be determined reliably; the API cannot detect a zone reassignment after record creation and does not promise a 502 for it.
What integrators should do
Reissue existing keys with timeman and one user-family scope, then process the required tzOffset in the GET /v1/workday/records response.
NEW-0904-12: asynchronous accounting-system calls under a mapped user
POST /v1/onec/tools/{method}/call queues an operation under the current user's active mapping and requires the vibe:onec scope. The body specifies only the method, columns, filters, and pagination: the server takes the user identifier and name from portal settings, with no client override or fallback account. The 202 response contains operationId, while status and result are available through GET /v1/onec/operations/{operationId} to the same portal, user, and key.
BC-0904-13: Entity read calls now have a per-portal rate limit
Old format supported until: not provided
Before
Entity reads — list (GET /v1/deals and the same call on every entity), search (POST /v1/deals/search), aggregate (POST /v1/deals/aggregate, including the legacy GET …/aggregate), field definitions (GET /v1/deals/fields), related records (GET /v1/deals/{id}/contacts, …/activities), product rows (GET /v1/deals/{id}/products) — and the per-entity batch (POST /v1/deals/batch and the same on every entity) accepted requests without a rate limit — while the global POST /v1/batch already carried one. One client reading a list more than roughly ten times per second slowed lists and search for every portal on the same instance, up to request timeouts. A HEAD request to a list was served as a full GET: the data was read in full and only the headers were returned.
After
Every such read is capped at 300 requests per minute per portal; all API keys of one portal share one limit, and each entity and each operation is counted separately. On exceeding it the Vibecode API answers 429 RATE_LIMITED with a Retry-After header; the body carries no delay — read it from the header. The current limit value arrives in the x-ratelimit-limit header. The per-entity batch is capped tighter — 30 requests per minute per portal, the same as the global POST /v1/batch: one such request fans out into hundreds of Bitrix24 calls. The HEAD method on these paths is no longer served by the read handler — use GET with limit=1 instead. Successful responses, error codes and payload formats are unchanged.
What integrators should do
Handle 429 on every entity read and on the batch the same way as on /v1/search and /v1/batch: wait for the delay in Retry-After and retry. If the limit triggers regularly, read less often and in larger pages (limit up to 5000 per call), cache results on your side and do not run identical reads in parallel. If you probed list availability with HEAD, use GET with limit=1 instead. Make batches larger rather than more frequent: one request takes up to 500 items.
NEW-0904-14: invoices support include=deal
GET /v1/invoices/:id, GET /v1/invoices, and POST /v1/invoices/search now accept include=deal. The related deal from parentId2 is returned in _included.deal. When no relation exists, the value is null.
2026-09-03
NEW-0903-1: `GET /v1/models` exposes the reasoning control declaration
Every model in GET /v1/models and GET /v1/models/{model} now carries a reasoning field: null when no declaration is set, otherwise an object { map, default, budgetTokens } — map translates a platform step (none|low|medium|high|max) into the model's native mode, default names the step the model applies when no reasoning parameter is sent, budgetTokens — reasoning token-budget support. The field is additive: the shape of capabilities is unchanged and existing requests keep working as before. Details — /docs/ai/models/list.
NEW-0903-2: reasoning control in chat completions
POST /v1/chat/completions accepts reasoning_effort, reasoning and chat_template_kwargs, normalizes them to the five Vibecode platform steps (none, low, medium, high, max) and applies the nearest step the model supports — rounding down and never disabling reasoning without an explicit request. The response carries a reasoning field (requested, applied, native), the REASONING_EFFORT_ADJUSTED, REASONING_NOT_SUPPORTED, REASONING_CANNOT_BE_DISABLED, REASONING_BUDGET_NOT_SUPPORTED, TEMPERATURE_OVERRIDDEN_BY_REASONING warnings in warnings and the X-Reasoning-Applied, X-Reasoning-Native, X-Reasoning-Warnings headers — in streaming mode the headers are the only channel. Without the parameter in the request the body sent to the model is unchanged and the model's behavior does not change. Reasoning tokens are billed as output tokens at the same rate. Details — /docs/ai/chat/completions.
FIX-0903-3: invalid reasoning parameter values are rejected
Before
Unknown reasoning_effort, reasoning, chat_template_kwargs fields were silently dropped, and a request with a typo in the value ran as if the parameter were absent — the response was HTTP 200.
After
A value outside the vocabulary (none, minimal, low, medium, high, xhigh, max), a non-positive reasoning.max_tokens or a non-object chat_template_kwargs is rejected with 400 invalid_request in the OpenAI envelope { "error": { "message", "type", "code" } }; an explicit null in any of the three fields is accepted as "not set"; for a valid request the response remains HTTP 200.
FIX-0903-4: GET /v1/me no longer declares region as required
Before
In the GET /v1/me response, the server-create description for requests without source marked region as required and included it in the required-fields list. As a result, clients using self-discovery could require a region even though the server-create contract already allowed it to be omitted.
After
GET /v1/me marks region as optional and explains that the platform uses the selected provider's default region when it is omitted. Only self-discovery changed: GET /v1/me remains HTTP 200, while the existing behavior and HTTP 201 response of POST /v1/infra/servers without region are unchanged.
Integrator impact
Clients that build requests from GET /v1/me may stop treating region as required. Existing requests that explicitly supply a region need no changes.
FIX-0903-5: direct BOX calls honor certificate trust
Before
On a BOX portal with a trusted self-signed certificate, windowed search, file downloads, and some key operations could fail with a TLS error while regular API calls worked.
After
All these calls use the trust setting only for the exact BOX portal address. Cloud and OAuth calls, and download redirects to an external address, keep strict certificate verification.
BC-0903-6: an app key without an employee session reads only app-shared files by name
Old format supported until: not provided
Before
GET /v1/storage/objects/{key} made with an app key and no Authorization header returned an
employee file when exactly one object existed under that logical name. It worked only because the name
was one per app, and the behaviour could not be relied upon: as soon as more than one object held the
name, the choice became arbitrary.
After
Such a request reads only the app-shared file, from the release onwards — there is no transitional mode. If the name is held solely by per-employee files, the
response is 404 STORAGE_OBJECT_NOT_FOUND. The same narrowing applies to HEAD and to delete-by-name.
The GET /v1/storage/objects listing is unchanged.
What integrators should do
If you read or deleted an employee file with an app key and no employee session, pick one of two: pass
that employee's session in the Authorization: Bearer header, which makes their file reachable; or take
the object identifier from the GET /v1/storage/objects listing and address it directly
(GET /v1/storage/objects/{objectId}), a route the narrowing does not touch.
FIX-0903-7: employees of one app can save files under the same name
Before
A logical file name was one per app. If a portal employee saved avatar.png, a second employee of the
same app got 409 STORAGE_KEY_OWNED_ELSEWHERE on a direct upload and 409 STORAGE_KEY_EXISTS on a
multipart one — even though the files would land at different addresses, because a per-employee file
address includes the employee identifier. The refusal never expired: the name stayed taken until the
first employee deleted the file.
After
The name is claimed per employee. The second employee gets 200 and their own object with its own
identifier; a repeated upload by the same employee still replaces that employee's own file. An
app-shared file and a per-employee file share one namespace, so a request that finds the name already
held by an object of the other ownership kind still gets 409 STORAGE_KEY_OWNED_ELSEWHERE.
FIX-0903-8: a multipart upload distinguishes who holds the logical name
Before
POST /v1/storage/objects/multipart/create answered 409 STORAGE_KEY_EXISTS for a taken logical name regardless of who held it — both when the object was yours and when it belonged to the other ownership kind: an app-shared file versus a per-employee file. The response could not tell those cases apart, although the actions that resolve them differ.
After
When the name is held by an object of the other ownership kind, a multipart upload answers 409 STORAGE_KEY_OWNED_ELSEWHERE — the same way a direct upload has long done. Every other taken-name case still answers 409 STORAGE_KEY_EXISTS, and the response status is unchanged in all cases.
What this means for integrators
No action is required: a refusal stays a refusal with the same 409 status, only the code in the body changes. If your client branches on the specific code of a multipart upload, add a branch for STORAGE_KEY_OWNED_ELSEWHERE — it means the name is held by an object of the other ownership kind and cannot be freed by switching employees.
BC-0903-9: smart process stage history by numeric entityTypeId, query parameters are now strict
Old format supported until: not provided
Before
GET /v1/stage-history accepted only four named types — deal, lead, invoice and
new-invoice. A smart process has no name, so its stage history could not be requested at all:
?entityType=128 answered 400 INVALID_ENTITY_TYPE without ever calling Bitrix24.
The sibling parameters (ownerId, typeId, categoryId, limit, offset, createdAfter,
createdBefore, stageId, stageSemanticId, statusId, statusSemanticId) meanwhile were
accepted in forms that answered SUCCESSFULLY:
?entityType=deal&ownerId[]=5 returned 200 and the history of owner 5, exactly as if the
value had arrived as a scalar. A repeat ?entityType=deal&ownerId=5&ownerId=6 returned 200,
silently taking the last value. A repeat with identical values
?entityType=deal&limit=50&limit=50 returned 200 and a correct result set.
After
entityType accepts a smart process numeric entityTypeId — ?entityType=128. The identifier
comes from GET /v1/smart-processes. Smart processes are stage-based, exactly like deals:
stageId, stageSemanticId and categoryId apply, and the response carries the same fields.
A numeric identifier that already has a named key (1, 2, 31) is refused and steered to that
key. Contact (3), company (4), quote (7) and the legacy invoice (5) have no stage history
and are refused as well. An identifier absent from the account returns 400 INVALID_ENTITY_TYPE
rather than a Bitrix24 error.
Every parameter listed above is accepted exactly ONCE and only as a single string value. All three
forms from the Before block now answer 400 INVALID_PARAMS — the bracket ones (?ownerId[]=5,
?ownerId[x]=1) and the plain repeat (?ownerId=5&ownerId=6) alike, including when the repeated
values are identical, and a MIXTURE of spellings (?ownerId[]=6&ownerId=5) — that last one answered
200 keeping only 5 of what was asked for. The bracket form ?ownerId[x]=1 used to answer 500.
The numeric parameters (ownerId, typeId, categoryId, limit, offset) are additionally
accepted only as an integer written in full: no sign, exponent, leading zeros or suffix.
?ownerId=1e3 returned 200 and the history of owner 1 instead of 1000, ?typeId=2abc the
history of type 2, and ?ownerId=abc dropped the owner filter altogether and answered WIDER than
asked. All three now answer 400 INVALID_PARAMS.
The entityType forms are tightened too: entityType[]= and entityType[x]= returned 500
instead of 400; a repeated entityType=a&entityType=b silently took the last value and is now
refused; entityType=constructor and entityType=__proto__ answered 200 with an empty result
instead of a refusal. The INVALID_ENTITY_TYPE message now names the numeric form too, not only the
four names.
The same class is closed in POST /v1/duplicates/find and POST /v1/triggers/fire: a body with a
non-string type or entityType — number, object or array alike — returned 500 instead of the
documented 400. In triggers/fire the same now applies to entityId and triggerId: an object in those fields
either crashed the request into a 500 or reached Bitrix24 as a meaningless value and answered 200
without firing anything. A MULTI-value array in entityId reached Bitrix24 glued into one string,
addressed no record and fired nothing — that is a 400 now. A ONE-value array worked ([5] was read
as 5 and the trigger fired) and keeps working. Numbers are still accepted in both fields and reach
Bitrix24 unchanged — a numeric triggerId that worked keeps working. A non-string entityType in duplicate search is now refused rather than ignored.
Both routes also answer 400 to a request sent with no body and no Content-Type header, where
they used to answer 500.
What integrators should do
Send every query-string parameter once and as a scalar value: ?ownerId=5 rather than
?ownerId[]=5 and rather than the repeat ?ownerId=5&ownerId=6. If the filter is assembled in a
loop, check that a key cannot be appended to the query string twice — a stray repeat used to pass
unnoticed and now answers 400 INVALID_PARAMS naming the parameter in the error text.
The old form is not kept accepted for a transition period: there is no support window and the refusal applies as soon as the update ships, so a client-side change ships together with the update rather than after it.
Clients that already send parameters as single scalars need no change.
FIX-0903-10: one request no longer pauses a Bitrix24 method with its own sub-calls
Before
When one request to the Vibecode batch and looping API methods repeatedly timed out on the same Bitrix24 method, it could trigger a 15-minute pause for that method by itself. Remaining sub-calls returned TIMEOUT_QUARANTINE, and the pause affected other requests from the same portal.
After
All sub-calls of one request now count as one attempt for each portal-method pair. Separate requests can still trigger the protective pause after the configured number of consecutive timeouts. Successful and error response formats are unchanged.
Impact on integrators
No client changes are required. The fix covers POST /v1/batch, POST /v1/{entity}/batch, GET /v1/tasks/:taskId/comments, POST /v1/tasks/:taskId/comments, POST /v1/tasks/:taskId/comments/batch, GET /v1/task-time, and GET /v1/timeline-logs.
BC-0903-11: an employee photo that cannot become a file is refused on update
Old format supported until: not provided
Before
On employee update, a personalPhoto value Bitrix24 cannot read as file content reached the portal as a command to remove the current photo. The literal false behaved that way, and so did the booleans true and false, the number 0, a string with fewer than two base64-alphabet characters — !!! or ===, for example — an inline [file name, base64] pair whose content is one of those, and any other structure whose second value is missing, null or unreadable, the nested {fileData: [...]} and an explicit null or a structure in the content element included. The call answered with success after the deletion had happened, while the earlier refusal covered only an empty or whitespace-only string.
After
Such values are refused with INVALID_PARAMS before the Bitrix24 call on all three update surfaces, exactly like the empty string. A value with two or more base64 characters still travels to the portal: it does become a file, and Bitrix24 itself decides whether that file is usable. On CREATE in POST /v1/users the new refusal does not apply.
What integrators should do
Do not put placeholders such as false, 0, true or empty strings into personalPhoto to mean "change nothing" — omit the field instead. To remove a photo, call DELETE /v1/users/:id/personal-photo.
NEW-0903-12: a command that removes an employee photo
The employee photo now has a command of its own for removal — DELETE /v1/users/:id/personal-photo. It removes the profile photo and answers 200 with the fields id, personalPhoto: null and removed: true. The call is idempotent: for an employee with no photo it succeeds as well. The operation is irreversible, Bitrix24 deletes the file itself, so uploading the same image again yields a new URL. The user scope is required, a read-only key does not run the command, and the rights decision belongs to Bitrix24 — on a refusal it answers 403 UPDATE_FAILED and the photo stays in place.
Writing the personalPhoto field removes the photo on no update surface, and null is not a way to do it either. On PATCH /v1/users/:id and in the per-entity batch, null is accepted and ignored, while the global POST /v1/batch refuses it with INVALID_PARAMS, because the sub-call encoder would turn it into an empty query-string value — the same remove-the-photo command. That difference between surfaces is now documented deliberately, and the behaviour of the calls did not change. To replace a photo you do not need a separate removal: send the [file name, base64] pair in personalPhoto to POST /v1/users or PATCH /v1/users/:id.
BC-0903-13: business process activity and robot creation returns a string code
Old format supported until: not provided
Before
POST /v1/bizproc-activities and POST /v1/bizproc-robots returned HTTP 201 with boolean data.id: true. This value could not be used as the code in PATCH or DELETE.
After
The same requests still return HTTP 201. For the regular Bitrix24 response true, data.id contains the string code submitted at creation; when Bitrix24 explicitly returns CODE, that value wins. The identifier can be used for subsequent PATCH and DELETE requests.
What integrators should do
Change the data.id type for these two responses from boolean to string and use the returned value as the code for updates or deletion.
BC-0903-14: re-uploading with a personal key now refuses where the object state requires it
Old format supported until: not provided
Before
Uploading again onto an occupied key with a personal developer key or in a server-owned context
always returned HTTP 200 and created a second row, so object-state checks were never reached.
After
The repeat now goes through the same state ladder as an app-bound key, and three situations return a
refusal. A different visibility on the repeat returns 400 STORAGE_VISIBILITY_MISMATCH: omit the
field or pass the stored value, which is named in the message. A multipart upload (Path C) onto an
occupied address returns 409 STORAGE_KEY_EXISTS before the session is created. A body with no
Content-Length onto an occupied address returns 409 STORAGE_REPLACE_REQUIRES_LENGTH.
409 STORAGE_UPLOAD_PENDING and 409 STORAGE_MULTIPART_IN_PROGRESS also become reachable — for
these keys they never fired before.
FIX-0903-15: re-uploading with a personal key replaces the file instead of creating a second object
Before
A personal developer key and a server-owned context created a SECOND object at the same physical
address when the same key was uploaded again. The first object's bytes were overwritten by the
second, its size was billed twice, and a read or delete by logical name could hit either row. The
dangerous-content-type gate for PUBLIC objects was evaluated against the declared visibility, so
text/html slipped in under a live public object.
After
Uploading again replaces the content of the existing object: object.id, createdAt, key,
physical address and visibility are preserved, while sizeBytes, sha256, contentType and
contentUpdatedAt are updated. The response remains HTTP 200. An eligible soft-deleted object is
revived exactly as it is for an app-bound key. The dangerous-content-type gate is evaluated against
the visibility of the row that was found, before any byte is written, so text/html can no longer
land under a live PUBLIC object.
FIX-0903-16: Node.js 20 deploy no longer depends on the package repository on a prepared image
Before
The Node.js 20 preparation step always contacted the external system package repository, even when every base package was already installed. An unavailable repository left the step without updates for a long time and then failed it.
After
A prepared image with the base packages does not contact the repository. When a package is still required, the network wait is bounded; the streaming response periodically reports elapsed time during lengthy preparation. The terminal response shape and successful status are unchanged.
BC-0903-17: currency sorting rejects a field Bitrix24 cannot sort by
Old format supported until: not provided
Before
GET /v1/currencies and POST /v1/currencies/search accepted any field name in sort and order. The Bitrix24 method crm.currency.list silently replaces an unknown key with its default order, so the answer came back 200 with a list in an order the client had not asked for and with no sign of an error. The same applied to the sort and order keys of an entity=currencies sub-call in POST /v1/batch.
After
Three fields are accepted: sort, id and fullName. Any other name — the non-existent bogus as well as the declared amount — returns 400 UNKNOWN_SORT_FIELD before Bitrix24 is called, and the message lists the accepted names. The documented ?order[sort]=asc is unchanged.
What integrators should do
Three classes of change, each needing its own check.
First: a request with an unknown field name now gets 400 instead of 200. Code that relied on such a request "just working" will now see an error — that is the fix, but it has to be handled.
Second: the fields amount, amountCnt, base, formatString, decimals, decPoint, thousandsSep, lid, dateUpdate and lang also answer 400. They never sorted anything: the platform method replaced the order with its default, so the successful answer was untrue.
Third, and quietest: ?sort=id and ?sort=fullName now really change the row order while staying 200. Both used to fall back to the default order silently. A paged walk over those fields via offset will return different pages than before the update, and there is no sign of it in the response status — re-check such walks.
Affected endpoints: GET /v1/currencies, POST /v1/currencies/search and the entity=currencies sub-call in POST /v1/batch — both the sort and the order key. For other entities the batch order key still reaches Bitrix24 as-is.
BC-0903-18: include accepts only resolvable relations
Old format supported until: not provided
Before
The site relation on GET /v1/pages/:id, requisite on GET /v1/companies/:id and GET /v1/contacts/:id, and quote on GET /v1/deals/:id were advertised as available. A request with such an include returned HTTP 200 but did not add the relation to _included. The same happened to deal, contact, and company on GET /v1/quotes/:id, contact and company on GET /v1/invoices/:id, and section on GET /v1/products/:id.
After
Quote, invoice, and product relations are returned in _included as an object or null when the foreign key is empty. site on pages, requisite on companies and contacts, and quote on deals are no longer advertised. Requesting these names returns 400 INVALID_INCLUDE.
What integrators should do
For a page, read siteId and request GET /v1/sites/:id. For a company, use GET /v1/requisites with entityTypeId=4 and entityId=<companyId> filters. For a contact, use it with entityTypeId=3 and entityId=<contactId>. For a deal, read quoteId and request GET /v1/quotes/:id.
BC-0903-19: input array in POST /v1/embeddings capped at 64 strings
Old format supported until: not provided
Before
POST /v1/embeddings accepted an input array of any length — there was no limit on the number of strings in one request.
After
The input array accepts at most 64 strings. A longer array is rejected with 400 invalid_request before anything is charged and before the model is called. Split a larger request into parts of 64 strings.
NEW-0903-20: speech-to-text error texts no longer name the recognition engine
The English texts of the ai_provider_timeout and ai_provider_unavailable errors returned by POST /v1/audio/transcriptions no longer name the recognition engine; codes, statuses and the transcription contract on the international platform are unchanged.
FIX-0903-21: a Vibe+ demo now opens access on .com
Before
An account holding a Vibe+ demo kept getting INT_VIBE_PLUS_REQUIRED: granting
the demo does not change the account's plan code, and the access verdict read
only that code. The capabilities.servers.create slot in /v1/me stayed
available: false, so retrying the action hit the same refusal.
After
A live demo opens access in trial mode, under the same limits as a Marketplace demo. A purchased Vibe+ plan on top of a live demo still grants full access, and an expired demo grants none.
NEW-0903-22: two new promo code refusal codes: Bitrix24 plan restriction
A promo code campaign can now narrow its audience by Bitrix24 plan. The restriction is a list of allowed plans; an empty list means there is no restriction — every campaign and promo code issued earlier behaves exactly as before, and a request that knows nothing about the new codes loses nothing.
When the list is set, both public promo code methods report the new codes: POST /v1/cowork/coupon/preview returns them in the reason field when valid=false, and POST /v1/cowork/coupon/redeem answers 409. There are two codes, and they must not be conflated.
COUPON_TARIFF_NOT_ELIGIBLE — the account plan has been read and is not on the list. The refusal is final: the code will not start working on this account by itself, so retrying the request is pointless.
COUPON_TARIFF_UNKNOWN — the restriction applies, but the platform has not established the account plan yet (this happens on a freshly connected account). The promo code is intact, the attempt does not consume the redemption attempt limit, and retrying later is the right action.
In both cases the promo code stays unredeemed, and the existing refusal codes are unchanged.
Important: for preview the response status is the same as before — 200 with valid=false — but the reason field now also carries these two values. A client that matches reason against a known set must add branches for them: COUPON_TARIFF_UNKNOWN falling into a default "the promo code is invalid" branch shows a final refusal where the right action is to retry later. The attempt-limit behaviour of a check is the same as that of a redemption: COUPON_TARIFF_UNKNOWN does not spend it, COUPON_TARIFF_NOT_ELIGIBLE does.
2026-09-02
BC-0902-1: Documents expose the PDF transformation status
Old format supported until: not provided
Before
The isTransformationError, transformationErrorCode, transformationErrorMessage, transformationCancelReason, and pullTag fields were not declared in the documents contract. If Bitrix24 returned them through GET /v1/documents/:id, the Vibecode API passed their values without normalization. The same fields in POST /v1/documents and PATCH /v1/documents/:id bodies were not rejected by the local read-only field validation.
After
The five fields are declared as nullable response-only fields. isTransformationError is returned as boolean | null, and the other four fields are returned as string | null. Empty strings in the string fields become null. A write request containing any of these fields returns 400 READONLY_FIELD.
What integrators should do
Do not send these five fields in document create or update bodies, including when resubmitting a previously received object. Accept null when reading the fields and use pullTag to subscribe to transformation status updates.
BC-0902-2: The mode-switch address in the access-mode refusal is no longer fixed
Old format supported until: not provided
Before
The WRITE_BLOCKED_READONLY_KEY access-mode refusal always returned details.switchUrl as
"/keys", and the machine schema pinned that value with enum: ["/keys"]. The keys section lists
personal keys only, so for the two other kinds the path was a dead end: neither an application
auth key nor a management key is ever listed there, by construction — neither for the owner
nor for the Bitrix24 account administrator. The holder was given the address of a page that
carries no switch for their key.
After
details.switchUrl points at the page that switches the mode for that particular key: the
keys section "/keys" for a personal key, the Applications page "/applications" for an
application auth key, the management keys section "/management-keys" for a management key.
The message text names the same address as the field — they are built together and cannot
disagree. The value enumeration is gone from the schema: this is a UI
path, not a protocol constant, and pinning a literal made the contract unsatisfiable once the
page moved. The field is still always present and still required — the value changed, not its
presence.
What integrators should do
Read switchUrl from the response instead of hardcoding "/keys". Clients that generate
types from the schema no longer receive the literal type '/keys' — regenerate your types,
otherwise value validation will fail.
FIX-0902-3: a stream that never started ends with a retryable error for models from every provider
Before
Stream setup for bitrix/* models already made a single attempt on one shared budget, while for models from every other provider the platform silently repeated the request and waited for another full budget. The client received nothing meanwhile — up to two full budgets of silence — and then saw a generic provider failure that does not tell a stalled stream setup apart from any other unclassified error.
data: { "error": { "code": "ai_provider_unavailable", "type": "server_error", "retryable": true, "retryAfter": 6 } }
data: [DONE]
After
Stream setup makes a single attempt on one shared budget for models from EVERY provider. If the headers do not arrive within it, the wait no longer doubles and the stream ends with the same stream_idle_timeout event as a stream that went silent mid-response — naming the stream itself as the thing that stalled.
data: { "error": { "code": "stream_idle_timeout", "type": "server_error", "retryable": true, "retryAfter": 7 } }
data: [DONE]
The wait no longer doubles invisibly and the reason is named precisely — now across the whole model set, not only bitrix/*.
NEW-0902-4: booking resources lookup in V1
GET /v1/booking-resources is now available — it returns the booking resources available to the key and closes the gap in the first-booking flow: POST /v1/bookings requires a non-empty resourceIds, and there was no way to learn valid identifiers through V1 alone.
The booking scope is required. A key in read-only mode works — the method is a read. The typeId and searchQuery filters are optional and may be combined, and any other parameter is rejected with 400 INVALID_PARAMS.
The catalogue is collected in full, so there are no pagination parameters: meta.total equals the length of data, and meta.hasMore is always false. Each record carries exactly four fields — id, name, typeId and isMain. The description field is withheld: it is free-form portal text and is not needed to pick a resource.
Completeness is guaranteed for a catalogue that did not change during the request — Bitrix24 provides no snapshot of the collection. An account with more than 500 resources gets 502 BITRIX_RESULT_TOO_LARGE instead of a truncated list; the same two filters narrow the lookup.
NEW-0902-5: money-in export marks revoked orders and returns the recognizable amount
In the per-payment export GET /v1/platform/revenue/money-in/payments every row now carries a revokedAt field — the package revocation date in UTC ISO-8601 format, or null for a live order. A revoked order's row does not disappear from the export and its amounts do not change: the money did arrive on the paymentDate, and cash-in still reconciles with the bank statement.
In the aggregate GET /v1/platform/revenue/money-in two nested tiers now travel alongside the previous sums. The revoked tier is the revoked part of the same values (ordersCount, grossCashRub, vatRub, netRubExclVat, vibesCredited), and the recognized tier is the same values minus the revoked part, that is the recognizable amount. Top-level fields do not change their values and remain the cash-in for the period, so existing integrations keep working with no changes.
A revocation arrives as a separate event and may happen after the period is closed, so on a repeated export a previously exported row may turn out to have revokedAt filled in. The set of rows stays the same, and the orderId key is stable.
NEW-0902-6: device-flow accepts a device identifier
POST /v1/connect/device/authorize accepts an optional device_id — a stable identifier of the client installation (up to 128 characters; letters, digits, _.:-). It takes effect for the Cowork/Code application: when the same device connects again, the previous key is revoked instead of sitting in the list next to the new one. Other clients may send the parameter, but it does not yet affect how their keys are issued.
The parameter is optional, and behaviour without it is unchanged — every connection issues its own key and earlier ones stay active. Several distinct devices for one person keep working as before: their identifiers differ, so their keys never displace each other. A value that does not match the format is ignored — the connection still succeeds, only the replacement is lost.
NEW-0902-7: new INT_BOX_PARTNER_REQUIRED refusal code for self-hosted accounts on the international installation
On the international installation platform access for a self-hosted account can require a
confirmed partner (NFR) licence. A self-hosted account without that confirmation receives
HTTP 402 with the new code INT_BOX_PARTNER_REQUIRED — on server, agent and managed-bot
creation, and on key issuance. The refusal body is shaped like its neighbours in this family
(userMessage, alternatives, hint), but details.requiredTariffs is empty: moving to
another Bitrix24 plan does not clear this refusal, and the mark is read when the connector
module registers. The same code appears in the reason of the capabilities.servers.create slot of
GET /v1/me, and the documentation address in its alternatives[].url. The requirement is off by default and is switched on by
a platform administrator; while it is off, no account sees this code and responses stay
unchanged.
NEW-0902-8: stable account identifier in the key self-description
GET /v1/me returns a new field, portalId — the stable identifier of the Bitrix24 account the key is bound to. It arrives in the same request, next to the existing portal and owner, and is present for vibe_api_ and vibe_app_ keys.
Unlike the domain in the portal field, this identifier survives a rename or a move of the account. Use it whenever local data is split per account: an account key built from the domain stops matching the previous one after a move, and the data of the same account ends up in a new empty store.
In a successful response the field is never empty: a key with no account bound never reaches this response, it gets 401 NO_PORTAL. Management keys vibe_live_ do not get the new field — they are not bound to a single account and still return the list of accounts in portals.
NEW-0902-9: server creation answers 409 `REISSUE_IN_PROGRESS` while the application authorization key is being re-issued
POST /v1/infra/servers has a new refusal code REISSUE_IN_PROGRESS with status 409. The Vibecode platform answers with it when the application card owner is re-issuing its authorization key at that moment: no server is created, because it would land on a key that is being revoked in the same second.
The refusal is transient — repeat the request once the re-issue finishes. The successful response and every other refusal code are unchanged, and requests that worked before keep working.
FIX-0902-10: Open Channels dashboard methods are now available in OpenAPI
Before
Six public Open Channels statistics methods were described on documentation pages but were absent from /v1/openapi.json and the generated API cards.
After
All six methods are available in OpenAPI and API cards with the imopenlines scope, parameters, and response schemas. Runtime behavior is unchanged: a successful response remains HTTP 200, and the existing error statuses and schemas are preserved.
NEW-0902-11: support code in an unrecognized key issuance refusal
When Bitrix24 refuses to issue a key for a reason the platform does not
recognize, the response now additionally carries error.details.incidentCode —
a six-character support code. The platform writes the same code to the portal
journal next to the refusal breakdown, so quoting it is enough for support to
find the entry.
The field is optional and added to the existing response body: the HTTP status,
error.code and error.details.reason are unchanged, and clients need to do
nothing.
NEW-0902-12: the Performance Review section is available through /v1/performan/review
Six endpoints are now available under the new performan scope. Reads: GET /v1/performan/review/campaigns, GET /v1/performan/review/self-reviews, GET /v1/performan/review/peer-reviews, GET /v1/performan/review/manager/questions, GET /v1/performan/review/manager/reviews. Every list but campaigns requires the campaignId parameter; manager cards also accept an optional revieweeUserId filter. The response is { "success": true, "data": [...], "meta": { "nextCursor": ... } }. Pagination is cursor based: limit (1..200, 50 by default) and afterCursorId, taken from meta.nextCursor.id of the previous page. A limit outside that range answers 400. These methods have no offset parameter and no total-count field.
Every list is scoped to the calling user: their campaigns, their cards and the manager relations where they are the reviewer. An empty response means "nothing for this user", not "nothing on the account".
Write: POST /v1/performan/review/manager/answers with a body of { "relationId": 4, "answers": [...], "isAutosave": true, "expectedStateHash": "..." }. The isAutosave field defaults to true, which saves a draft; pass false to finalise the review, which moves the card to the completed status and cannot be undone. The expectedStateHash value is returned only by a write, never by a read method, so the first call goes without it. When the state changed since it was read, the answer is 409 with the PERFORMAN_STATE_CONFLICT code: re-read the card and repeat the request with the fresh value. A relation that belongs to somebody else and a relation that does not exist both answer 403. A read-only key gets 403 WRITE_BLOCKED_READONLY_KEY on the write.
The section is only available on accounts where the Performance Review module is installed and the surface is enabled for them: elsewhere all six addresses answer 404 and are absent from that account's GET /v1/openapi.json. The performan scope is withheld from the default set for the same reason.
A Cowork key gets the scope reactively: if it has not been granted yet, the first request to any of the addresses answers 409 PERFORMAN_SCOPE_JUST_GRANTED and grants the right on the spot — retry the same request and it will succeed. A read-only key does not get the right this way: the reactive grant is a write, so such a key is refused with 403 and its owner adds the scope in the dashboard instead.
A choice question cannot be answered through the API: no read method exposes option ids, and the question list returns a question without its options.
GET /v1/openapi.json now carries a rate limit per caller address. The precise cap is returned in the x-ratelimit-limit response header — read it there instead of hard-coding a value. Exceeding it answers 429 RATE_LIMITED. The cap is generous and normal reading does not reach it: the spec is served with an ETag, so a discovery client or an SDK generator gets a 304 instead of the body on a repeat fetch.
The spec body depends on the request's key: with a key of a portal the performan surface is enabled for, it carries the performan paths and the performan scope in the catalogue; without a key it does not. The response is therefore marked Vary: Authorization, X-Api-Key (the key is read from either header), and with such a key it is served as Cache-Control: private rather than public. Do not reuse one stored spec file across different keys.
FIX-0902-13: the Bitrix24 plan refusal on key issuance and app installation now states its cause
Before
When Bitrix24 refused issuance with FEATURE_NOT_AVAILABLE_ON_CURRENT_PLAN (the portal plan does not include Vibecode), the answer carried nothing actionable on ANY issuance endpoint — both where a key is issued (POST /v1/keys → 502 CONNECTOR_KEY_ISSUE_FAILED) and where an app is installed (POST /v1/apps → 502 CONNECTOR_APP_INSTALL_FAILED). The body held no human-readable text and the cause stayed in an internal field only, so clients read the refusal as a transient platform failure and retried. Retrying never helps: the gate lifts only when the Bitrix24 plan changes.
HTTP 502
{ "success": false, "error": { "code": "CONNECTOR_KEY_ISSUE_FAILED", "message": "connector refused to issue the key" } }
After
The refusal is now resolved against the portal region. Where access is on sale, the answer is 402 with a plan paywall code and human-readable text: INT_VIBE_PLUS_REQUIRED, carrying details.requiredTariffs: ["vibe+"] and a details.upgradeUrl pointing at the plan page inside the portal itself. Where there is nothing to offer (a self-hosted portal, a portal already on a paid plan, an unrecognised region), the answer stays 502 but gains its own code CONNECTOR_PLAN_REQUIRED and a non-empty userMessage instead of an empty body.
HTTP 402
{ "success": false, "error": { "code": "INT_VIBE_PLUS_REQUIRED", "message": "This action requires a Vibe+ plan on your Bitrix24 account.", "details": { "requiredTariffs": ["vibe+"], "upgradeUrl": "https://example.bitrix24.com/online/?feature_promoter=limit_why_pay_tariff_vibe" } } }
What integrators should do
Successful responses are unchanged. A client that branches on the error code now receives a terminal class instead of a transient one and can stop retrying: none of the new codes clears on a retry. A client that only looked at the status keeps working — the operation ended in an error before and still does.
FIX-0902-14: a refusal on the consent page returns the user to the partner application
Before
The user approved access, hit a key issuance refusal — no subscription, an unsupported region, the key limit used up — and stayed on the consent page. Nothing arrived at redirect_uri: the partner learned neither of the attempt nor of its reason.
After
The refusal now carries a return address: the page shows a "Back to the app" button, and redirect_uri receives error=access_denied (temporarily_unavailable when Bitrix24 is down), the machine-readable reason in error_description and the original state. If the registered return address carried a code parameter of its own, a refusal strips it — a refusal never arrives mixed with the marks of success. Refusals before the confirmation behave the same way — an expired consent link and a broken CSRF token, including on the "Decline" button.
2026-09-01
FIX-0901-1: a permission refusal on the sprint board arrives as 403, not as a platform error
Before
A key whose user has no access to the scrum project got 422 BITRIX_ERROR for the sprint board columns — the same class the Vibecode platform uses to report a Bitrix24 business error. The code gave no way to tell "no rights" from "something is temporarily off on the account", so the request was retried even though the state is stable and a retry changes nothing.
HTTP 422
{ "success": false, "error": { "code": "BITRIX_ERROR", "message": "Access denied", "b24Code": "0" } }
After
GET /v1/scrum/sprints/{sprintId}/stages and POST /v1/scrum/sprints/{sprintId}/stages answer a permission refusal with 403 BITRIX_ACCESS_DENIED — the code the common error reference already describes. It reads as "do not retry, grant the user access to the scrum project instead".
HTTP 403
{ "success": false, "error": { "code": "BITRIX_ACCESS_DENIED", "message": "Access denied" } }
Successful responses are unchanged: the column list still arrives with HTTP 200 and a created column with HTTP 201. The answer for a missing sprint is unchanged too — it stays 404 ENTITY_NOT_FOUND.
BC-0901-2: an empty employee photo is refused on update
Old format supported until: not provided
Before
On employee UPDATE, an empty or whitespace-only string in personalPhoto reached Bitrix24 as a command to remove the current photo. The single PATCH /v1/users/:id returned 200 success:true even though the photo was removed.
After
Such values are refused with INVALID_PARAMS before the Bitrix24 call. The single PATCH /v1/users/:id returns 400 success:false; batch routes keep their existing error envelopes. The global POST /v1/batch also refuses personalPhoto: null: its sub-call encoder turns null into an empty query-string value, the same remove-the-photo command. On PATCH /v1/users/:id and in the per-entity batch, null gets no new refusal. To leave the photo unchanged, omit personalPhoto. On CREATE in POST /v1/users, an empty string does not get the new refusal; the working inline pair and separate employee-invite validation are unchanged.
FIX-0901-3: rate limit descriptions now name the replica share, not just the total cap
Before
The descriptions of POST /v1/feedback in the GET /v1/guide directory, in the feedback.limits block of the GET /v1/me response and in the GET /v1/openapi.json schema named the limit as a number — "5 requests per minute". Requests are served by several backend replicas and each one holds its own share of the limit, so the client received less than promised and the X-RateLimit-Limit header disagreed with the description. The same mismatch was present on the help pages for submitting a ticket and uploading an attachment, and in the schema for five operations of the Cowork section.
After
The descriptions name the number and explain next to it that the cap is divided across the replicas, while the value actually in force arrives in the X-RateLimit-Limit header. For the Cowork operations in the schema, the 429 refusal description is aligned with the form already used on the help pages. A single shared 429 row on the feedback page is split in two — creating a ticket and uploading an attachment carry different counters.
Impact on integrators
The limit values did not change and the behaviour is the same as before. A client that relied on the number from the description was refused earlier than expected — the source of the value in force is now named explicitly: read X-RateLimit-Limit from the response.
BC-0901-4: task comment updates reject read-only fields
Old format supported until: not provided
Before
On a legacy card,
PATCH /v1/tasks/:taskId/comments/:id
could return HTTP 200 when a client sent a comment object from GET with an
updated message, forwarding read-only fields to Bitrix24. An item in
POST /v1/tasks/:taskId/comments/batch
could similarly finish with success:true.
After
A single PATCH containing a read-only field returns HTTP 400
READONLY_FIELD before Bitrix24 is called. For a successful legacy-card
comment update containing only {message}, the response remains HTTP 200; the
existing 410 GONE remains unchanged on the new card.
In the custom batch, the top-level response remains HTTP 200 with
success:true; the rejected item gets success:false and
error:READONLY_FIELD, is not sent to Bitrix24, and does not stop valid sibling
items.
What integrators should do
For a single PATCH, build a new body containing only {message}; do not send
response fields such as id, taskId, authorId, createdAt, or their aliases
back to the endpoint. In a custom batch item, keep lowercase id as the comment
selector and send only message beside it; remove an additional ID and all
other read-only fields. Clients that already send only documented writable
fields need no changes.
FIX-0901-5: the 401 TOKEN_MISSING error now names the reason on every V1 route
Before
The reason a personal key had no Bitrix24 webhook was named only on entity routes
(/v1/deals, /v1/tasks and so on) and in POST /v1/batch. Every other
family — chats, lists, mail, scrum, notes, Open Channels, bots, userfields and the rest —
answered with the flat text "API key has no tokens configured." and no error.details
field. The same broken key got an explanation on /v1/deals and a text that implied
nothing on /v1/chats.
After
The 401 TOKEN_MISSING body is built the same way on every V1 route: a personal key gets
the reason in error.details.reason plus the text naming the required action, an app key
gets a pointer to the missing OAuth step. The response remains HTTP 401 and the set of
codes is unchanged.
Impact on integrators
Read error.details.reason — that is the contract, not error.message. The message text
on these routes has changed, so code comparing it with the literal "API key has no tokens
configured." stops matching; that parsing was unreliable before as well. The reasons are
listed in the GET /v1/me response under b24Credentials.
FIX-0901-6: direct upload can be repeated after object deletion
Before
After a successful DELETE /v1/storage/objects/{key}, a new direct POST /v1/storage/objects/upload with the same key returned 409 STORAGE_KEY_DELETED until the object was physically purged 30 days later.
After
For an app object in the COMPLETED state without a multipart session, direct upload returns HTTP 200, preserves the same id and visibility, and writes new content. A deleted public link with that id becomes available again with the new bytes. This does not restore the previous content.
Path B remains disabled and returns 503 STORAGE_PRESIGNED_UPLOAD_DISABLED without a URL. Path C, deleted PENDING objects, and objects with a multipart session continue to return 409 STORAGE_KEY_DELETED. DELETE itself and subsequent reads without a new eligible direct upload continue to return 410 STORAGE_OBJECT_DELETED during the retention period.
Impact on integrations
No request changes are required. After deleting an eligible object, send the new bytes with a regular direct upload using the same key.
FIX-0901-7: a transient image-registry outage no longer marks a galaxy app broken
Before
POST /v1/infra/servers with a source field that hit 502 GALAXY_BASE_IMAGE_UNAVAILABLE — or ran the host out of disk — flipped the slot to status=error, even though the same refusal on POST /v1/infra/servers/:id/deploy left the status untouched and the error text promised the slot, its container and its /data volume were intact. Re-sending the deploy never cleared that error: GET kept reporting a broken slot, and the app stopped holding its host awake.
After
Both transient refusals leave the slot at status=provisioning; the cause stays readable in provisionError of GET /v1/infra/servers/:id. Re-sending the same deploy to a slot broken by such a cause clears error at the start of the attempt. An abandoned slot is still failed by the platform after ~20 minutes, but it keeps its real cause instead of the generic DEPLOY_INCOMPLETE. The error.hint copy, the /v1/me checklist and the docs no longer promise a "2–3 minutes" window: the registry outage can last an hour.
BC-0901-8: Regular chat-bot blocks key deletion
Old format supported until: not provided
Before
Deleting an API key could delete its linked regular chat-bot together with the token required for normal portal unregistration.
After
Deleting a key with a regular chat-bot, including a disabled Bot Platform row, returns 409 KEY_HAS_LINKED_AGENT. Take numeric bitrixBotId from details.bots in the 409 response, then unregister the bot through DELETE /v1/bots/:botId or transfer it to another key.
FIX-0901-9: file uploads and AI requests now answer 429 under memory pressure
Before
POST /v1/files/upload (up to 70 MB), POST /v1/note/documents/{id}/files (up to 40 MB),
POST /v1/chat/completions (up to 30 MB) and POST /v1/audio/transcriptions (audio up to 25 MB)
accepted any number of concurrent large bodies. Several parallel uploads at the ceiling could drop
requests without a response — other requests in flight at that moment included.
One more observable difference: a request with a missing or invalid key on these routes could get
400 (unparsed JSON), 413 (size) or 415 (content type) — an answer about the body before the
answer about the key.
After
Concurrent bodies over 1 MiB are bounded by their total size. A request over the bound gets 429
with code LARGE_BODY_BACKEND_BUSY and a Retry-After: 5 header, emitted before any Bitrix24
call, so it carries no side effects — and identically whether or not Content-Length is
declared:
{
"success": false,
"error": {
"code": "LARGE_BODY_BACKEND_BUSY",
"message": "Too many large request bodies are being processed. Retry in a few seconds.",
"retryAfter": 5
}
}
On the OpenAI-compatible POST /v1/chat/completions, POST /v1/ai/chat/completions and
POST /v1/audio/transcriptions (+ POST /v1/ai/audio/transcriptions) the envelope matches the
rest of that surface — no success, lowercase code:
{
"error": {
"message": "Too many large request bodies are being processed. Retry in a few seconds.",
"type": "rate_limit_exceeded",
"code": "large_body_backend_busy",
"retryAfter": 5
}
}
A request with a missing or invalid key now gets 401 on all of these routes — the key is checked
before the body is read. Successful requests and bodies under 1 MiB are unaffected; a retry after
5 seconds succeeds.
Important: on transcriptions the bound is counted from the declared Content-Length. An upload without one
(Transfer-Encoding: chunked) is not counted against the shared total and is bounded only by its
own 25 MB per-file ceiling.
FIX-0901-10: creating a Hermes agent no longer fails because of the startup payload size
Before
Creating a Hermes agent failed right after the server step. The virtual machine was never created, the agent stayed in the error state with an incident code, and the server field on the card stayed empty. Retrying produced the same result. The cause was the cloud startup payload growing past the size limit the cloud allows for such payloads.
After
Internal comments are no longer shipped to the cloud together with the install script, so the startup payload fits well within the limit again and the virtual machine is created. The install script itself and every other delivery path for it are unchanged. In addition, a size check now runs before the cloud is called: should the payload ever grow past the limit again, no cloud request is sent at all and Vibecode returns the usual provider error with an incident code.
NEW-0901-11: the business-process editor is available through the API
A new /v1/workflow-designer/* section works with templates of the new business-process editor in the editor's own language: blocks, connections, a catalog of block types, graph validation, drafts and publication. It is not the same as /v1/bizproc-templates, which reads and writes template rows with the graph as an opaque blob.
The bizprocdesigner scope is required — a separate one from bizproc. Add it to the key and re-issue the key.
Writing takes two steps. POST /v1/workflow-designer/templates/{id}/draft saves a draft: the running business process keeps executing its published version, so a draft can be shown to a person, rewritten or thrown away. POST /v1/workflow-designer/templates/{id}/publish makes the draft the working version — irreversibly, and it deletes every draft of the template, other people's included. Publication requires the draftFingerprint from GET /v1/workflow-designer/templates/{id}, so that exactly the graph you read and showed the person is the one published; a draft someone rewrote in the meantime is refused instead of quietly replacing it.
Start with GET /v1/workflow-designer/capabilities: it answers whether the editor is available to this portal and this key, and names the reason when it is not (DESIGNER_NOT_SUPPORTED — the portal is not updated to a version that serves the editor through the API, or the module is absent, DESIGNER_UNAVAILABLE — switched off or not in the plan, SCOPE_DENIED — the key lacks the scope, DESIGNER_REQUIRES_WRITE_KEY — the key is read-only and the operation writes, DESIGNER_PROBE_FAILED — the check did not complete and the editor state is unknown). These states cannot be told apart from a refusal of the methods themselves, which is why the check is a call of its own.
A read-only key is served by this section: the graph, the block catalog, the document fields, the portal instructions and validate all read. DESIGNER_REQUIRES_WRITE_KEY arrives only on the three writing operations — creating a template, saving a draft and publishing one.
Affected endpoints: GET /v1/workflow-designer/capabilities, POST /v1/workflow-designer/templates, GET /v1/workflow-designer/templates/{id}, GET /v1/workflow-designer/templates/{id}/brief, GET /v1/workflow-designer/templates/{id}/blocks, GET /v1/workflow-designer/templates/{id}/blocks/{blockId}, GET /v1/workflow-designer/templates/{id}/document-fields, POST /v1/workflow-designer/templates/{id}/validate, POST /v1/workflow-designer/templates/{id}/draft, POST /v1/workflow-designer/templates/{id}/publish
BC-0901-12: deleting a server now deletes the agent bound to it
Old format supported until: not provided
Before
DELETE /v1/infra/servers/:id deleted the server only. The agent that ran on it stayed alive on the Vibecode platform: its state still read as running, its bot stayed registered on the Bitrix24 portal, its key stayed valid, and the agent pointed at a server that no longer existed.
After
The agent bound to the server is deleted together with it. The agent row is marked deleted, the bot is removed from the Bitrix24 portal, the agent key is revoked, and the application card is removed from the Bitrix24 catalog. A successful delete still answers HTTP 200 with the body {"success": true}.
One new outcome appears. If another request was changing that same agent concurrently, the answer is 409 AGENT_DELETE_CONFLICT in the usual rejection shape {"success": false, "error": {"code": "...", "message": "..."}}. In that case the server is not deleted either: it keeps running and keeps being billed, and the request has to be repeated.
What integrators should do
An integration that expected the agent to survive a server delete and be reused has to be reworked: create the agent again after creating the new server. Treat AGENT_DELETE_CONFLICT as retryable — the server is intact, so repeat the same request. Deleting a server with no agent and deleting a server of kind GALAXY_APP behave as before.
2026-08-31
FIX-0831-1: a server whose agent never came up is no longer reported as running
Before
When the on-boot agent install aborted, the VM still reached running and the server stayed
there. The server card and the server list returned status: "running" with provisionError
and provisionErrorCode both empty — nothing in those responses told a working server from a
broken one, and billing kept accruing.
{ "status": "running", "blackholeStatus": "NONE", "provisionError": null, "provisionErrorCode": null }
After
Such a server moves to error with an explanation and a new
provisionErrorCode: "AGENT_NEVER_CONNECTED", and its billing is closed out.
{ "status": "error", "blackholeStatus": "NONE", "provisionError": "The server booted, but its agent never connected — …", "provisionErrorCode": "AGENT_NEVER_CONNECTED" }
Impact on integrators
Nothing to change: error is an existing status and no field was added. Worth knowing:
- A readiness wait now terminates —
runningwithblackholeStatus: "NONE"no longer lasts forever. If you poll forrunning+CONNECTED, add an exit onerror. - Recovery is
POST /v1/infra/servers/:id/repair, which reinstalls the agent over SSH. Recreate the server only if repair does not help. POST /v1/infra/servers/:id/starton such a server answers422with the codeAGENT_NEVER_CONNECTED. The status did not change: this machine already got a422from/start(SERVER_WRONG_STATE, since it counted asrunning) — the code changes, not the status. Starting it fixes nothing and erases the explanation, which is why it is refused. A client that restarts failed servers on a schedule must read the code and call repair instead.- Three lifecycle operations on such a server turn from a success into a refusal, because they
require
running:POST /v1/infra/servers/:id/stopandPOST /v1/infra/servers/:id/rebootanswer422with the codeSERVER_WRONG_STATE, andPOST /v1/infra/servers/:id/sleep-nowanswers400withNOT_RUNNING. Stopping such a machine is not needed either: billing is closed out with the verdict, so a scheduled stop saves nothing on it. The working actions arerepairandDELETE /v1/infra/servers/:id— the same pair the response lists inavailableActions. - The verdict applies to newly created servers and never touches ones already running. The
platform checks with the gateway first, so a live tunnel whose notification was lost is
healed rather than failed. If the agent connects later, the server returns to
runningon its own and both fields are cleared.
FIX-0831-2: key issuance on a free plan now names the cause instead of "retry in a minute"
Before
When Bitrix24 refused REST because the portal's Bitrix24 plan does not permit it, key issuance and app installation answered 502 CONNECTOR_REST_UNAVAILABLE with the text "Bitrix24 did not issue the key. Retry in a minute". Retrying never helped: the plan gate does not clear on its own, and the response never said what did block the call.
After
The same situation answers 402 with a dedicated paywall code. The userMessage field states that a paid Bitrix24 plan is what unlocks the action. Successful responses are unchanged and still return HTTP 200, and every other refusal reason still answers 502 CONNECTOR_REST_UNAVAILABLE.
FIX-0831-3: search and research through the Linkup provider
Before
Every POST /v1/search or POST /v1/research call with provider: "linkup" was rejected by the provider: the platform did not send the required outputType field. For the same reason a Linkup key could not be connected — adding a BYOK key returned INVALID_CREDENTIAL with the text HTTP 400 even for a working key.
After
Linkup calls go through, and connecting a BYOK key reflects the real state of that key: a working key is stored, a non-working one is rejected.
In /v1/search the answer field is filled when include_answer: true and is null when include_answer: false; in that case the page text arrives in results[].content. The results[].published_date field is always null for Linkup — the provider returns no publication date, and the capability matrix in GET /v1/search/providers now reports that honestly.
FIX-0831-4: seamless dashboard sign-in from a signed Bitrix24 form
Before
Opening the dashboard from the Bitrix24 app catalog launched the platform address in a new tab. The user landed on the sign-in screen and then on the account picker, even though they had just clicked a button inside their own Bitrix24 account.
After
The licence-signed channel gained a second transition surface, cabinet. The Bitrix24-side module posts a signed form carrying BX_VIBE_SURFACE=cabinet to the new endpoint POST /microservice/open-cabinet (the trailing-slash variant is accepted too), and the user arrives in the dashboard already signed in, with no sign-in screen and no account picker.
Surface fields: BX_VIBE_AUD — the platform address the form is addressed to (mandatory, compared verbatim); BX_USER_EMAIL — the Bitrix24 user's email; BX_VIBE_NETWORK_STATE — the network-state hint, self-hosted only; BX_VIBE_TARGET — an optional relative path inside the dashboard, /dashboard by default. The signed field set and its order, the requirement on each field and the refusal codes are in the integration contract.
The session cookie is not set in response to a cross-site request: a successful response is a page on the platform domain with an auto-submitting form that exchanges a one-time code for the cookie same-site at POST /api/cabinet/handoff and forwards the user to the destination path. The code lives 120 seconds and is spent exactly once.
The issued session is an ordinary one but is bound to the Bitrix24 account it came from: switching the active account is refused, and the account listing serves only that one. Two-factor verification, terms consent, account access mode and blocks behave exactly as on an ordinary sign-in.
The surface sits behind a switch and is off by default: while it is off the endpoint answers 404. The app surface (POST /microservice/open-app) is unchanged.
FIX-0831-5: opening an application from the Bitrix24 catalog always acts as an account
Before
A Bitrix24 employee with no Vibecode account opened an application on the strength of the portal's signature alone: the platform issued a one-time init code carrying an empty Vibecode user id, and the application received an identity backed by no platform record at all. On a self-hosted portal such an employee opened as a guest, with the name and departments read out of Bitrix24; on cloud the platform asked the portal user.get before the redirect to confirm the employee was live.
After
Every open now has an account, and __init always carries a non-empty user. On cloud the account is created from the signed BX_NETWORK_USER_ID and BX_USER_EMAIL. On a self-hosted portal the employee confirms the identity themselves: the platform serves a server-rendered screen for the code mailed to the address from the signed form, and on a correct code the same request links the Bitrix24 member to the Bitrix24.Network profile and opens the application. The outbound user.get before the redirect is gone.
The e-mail code is now sent AND checked by Bitrix24.Network itself, through two trusted operations. The first takes the address: the Network finds or creates the profile behind it and mails the code as its ordinary e-mail confirmation. The second takes the same address plus the code and returns the profile for the confirmed address. The platform mails nothing and stores no code. Code lifetime and attempt count are the Network's; the platform keeps its own send-rate caps on top.
BX_USER_EMAIL moved out of the cabinet surface fields into the common part of the signed set: both platforms send it on both surfaces, right after BX_NETWORK_USER_ID (or after BITRIX_USER_ID when there is no network id) and before BX_VIBE_SURFACE. The field is optional — a form without it is served exactly as before, so an older module keeps working with no changes. The successful response is still a 302 to the application address carrying __init.
The account-creating branches sit behind a platform switch that is off by default: while it is off, an open with no account answers with a refusal page linking to the ordinary sign-in, and no partial profiles are left behind.
The "Authorize the application" page is reworked: one button instead of a button plus a quiet link, and once the application is authorized the page forwards to it by itself — no closing the tab and reopening the application from the catalog. The iframe therefore has to allow allow-popups. The e-mail confirmation screen opens no new tab: the code field unfolds behind a button on the same page, and a loader covers the card while the platform checks the code and connects the application. The address can no longer be changed there — only the one from the signed form is confirmed, and the single alternative is signing in on the platform yourself, through a link on that same screen. The full field set, the e-mail confirmation screens and the continuation endpoints are in the integration contract.
FIX-0831-6: Batch create of telephony lines returns the addressable number in results[].id
Before
POST /v1/telephony-lines/batch with action: "create" answered 200, but results[].id carried Bitrix24's internal numeric row id. It could not be used in PATCH or DELETE /v1/telephony-lines/:number — the line is looked up by number, so the request answered 422. The single POST /v1/telephony-lines already returned the correct number, so the two doors into the same create disagreed.
After
results[].id is the same number passed in that item's number field, and the same value the single POST /v1/telephony-lines returns. It comes back verbatim, including numbers with + and other characters. The response is still 200 and no other field changed.
FIX-0831-7: signing back into Cowork on a fee-free seat no longer fails
Before
Cancelling a fee-free Cowork subscription made every later desktop sign-in fail with
403 COWORK_SUB_CANCELLED: the cancelled seat was never revived, and a key is only issued
for an active one. There was no way out from inside the app — this path has no browser
route back to a fee-free seat — so the person stayed locked out for good.
After
A zero-price seat is revived at sign-in and the key is issued. A paid seat still answers
403 COWORK_SUB_CANCELLED / COWORK_SUB_PAUSED: resuming it is a payment, and that stays
a deliberate action in the dashboard.
FIX-0831-8: `TOKEN_MISSING` for a personal key now names the webhook mint failure
Before
When a personal key (vibe_api_*) had no Bitrix24 webhook because the portal had refused
the key owner the right to create incoming webhooks, or because the last mint attempt had
failed, the 401 TOKEN_MISSING response still reported a generic reason (for example
WEBHOOK_NOT_CONFIGURED) and pointed at GET /v1/me, without explaining that no webhook
was coming or who needed to act.
After
In these two scenarios error.details.reason now returns WEBHOOK_MINT_REFUSED_BY_PORTAL
(a portal administrator must act) or WEBHOOK_MINT_FAILED (the platform retries on its
own), and error.message names the cause and the addressee explicitly. For
WEBHOOK_MINT_REFUSED_BY_PORTAL the reconnect advice (POST /api/keys/:id/reconnect) is no
longer included — the platform mints the webhook automatically once a portal administrator
opens the right to create incoming webhooks, and retrying reconnect will not help. The
response code, HTTP status and error shape are unchanged.
Impact on integrators
Handle WEBHOOK_MINT_REFUSED_BY_PORTAL and WEBHOOK_MINT_FAILED as distinct
details.reason values alongside the already-documented ones — do not recreate the key or
loop on reconnect for these reasons; wait for the portal administrator or the platform's
automatic retry.
A separate note on the message text. error.message now branches on the reason and on
whether reconnect is available for the key — and this affects more than the two new
reasons. A key with no Bitrix24 scopes (VIBE_SCOPES_ONLY) gets its own wording, and keys
the platform would refuse reconnect for (cowork keys, app keys, keys bound to a server or
to a live agent) get advice to create a new key instead of advice to reconnect. The
details.reason values in those scenarios are unchanged. If your code matches on
error.message as a string, switch to details.reason: the message text is not a
contract and does change.
FIX-0831-9: vibe top-up is open to self-hosted accounts on the international installation
Before
A self-hosted account on the international installation always got the
BOX_TOPUP_NOT_AVAILABLE refusal. The subscription preview
GET /v1/cowork/subscription/preview returned topUpAvailable: false for such
an account, and the package catalogue came back empty.
After
A self-hosted account on the international installation tops up just like a
cloud one — it gets the package catalogue, a payment link and
topUpAvailable: true in the subscription preview. The
BOX_TOPUP_NOT_AVAILABLE refusal remains only where top-up is closed to
self-hosted accounts in the account's region. The response format is unchanged
and the successful response remains HTTP 200.
2026-08-30
BC-0830-1: Presigned single-PUT upload is temporarily disabled
Old format supported until: not provided
Before
Path B minted a presigned PUT URL for single-PUT uploads and published it through /complete.
After
Path B (URL creation and completion) now fails closed with 503 STORAGE_PRESIGNED_UPLOAD_DISABLED. Use direct upload (Path A) or multipart upload (Path C) for new writes; legacy PENDING reservations are neither published nor automatically released by GC.
BC-0830-2: an application rename no longer reports success without confirmation from Bitrix24
Old format supported until: not provided
Before
On a developer-key rename PATCH /v1/apps/{id} removed the previous menu-item binding and created a new one. When Bitrix24 rejected the new binding after a confirmed removal, the response was 502 BITRIX_PARTIAL_REBIND and the item stayed removed.
On the cloud OAuth path the same method behaved differently in another case: when Bitrix24 confirmed neither the removal of the previous binding nor the new one, the response was 200 OK with a warning and the new name was saved. The menu item either disappeared from the account or kept its previous label, and a repeated request no longer rebound it, because the platform considered the name current.
After
On a developer-key rename the platform makes one attempt to restore the previous label. Successfully restored placements are listed in the new error.restored field and need no manual repair. The error.unbound field lists the placements to check on the account: those left removed, plus those whose removal Bitrix24 never confirmed. The new error.bitrixCodes and error.bitrixStatuses fields carry sanitised diagnostics for every rejected call.
The cloud OAuth path now answers 502 BITRIX_PARTIAL_REBIND and does NOT save the new name when Bitrix24 confirmed neither the removal nor the new binding.
What integrators should do
Treat 502 BITRIX_PARTIAL_REBIND as "the name was not saved" and repeat the request — a retry fires the rebind again. The former 200 OK did not mean success in this scenario. Only the placements in error.unbound need manual restoration through POST /v1/placements/bind.
FIX-0830-3: for `bitrix/*` models a stream that never started also ends with a retryable error
Before
When the upstream of a bitrix/* model sent no response headers, the platform silently repeated the request and waited for another full budget. The client received nothing meanwhile — up to two full budgets of silence — and then saw a generic provider failure that does not tell a stalled stream setup apart from any other unclassified error.
data: { "error": { "code": "ai_provider_unavailable", "type": "server_error", "retryable": true, "retryAfter": 6 } }
data: [DONE]
After
Stream setup for these models makes a single attempt on one shared budget. If the headers do not arrive within it, the wait no longer doubles and the stream ends with the same stream_idle_timeout event as a stream that went silent mid-response — naming the stream itself as the thing that stalled.
data: { "error": { "code": "stream_idle_timeout", "type": "server_error", "retryable": true, "retryAfter": 7 } }
data: [DONE]
The wait no longer doubles invisibly and the reason is named precisely. Models from other providers are not affected yet: their stream setup still takes two attempts and ends with the generic ai_provider_unavailable.
2026-08-29
FIX-0829-1: importable field flag in guide
Before
In GET /v1/guide, the detailed data.entities[].fieldsDetailed contract did not indicate whether a field could be set during import.
After
For such fields, data.entities[].fieldsDetailed now includes importable: true. The response remains HTTP 200, and the compact data.entities[].fields map is unchanged.
NEW-0829-2: a link to a public app expands into a card
A link to an app in PUBLIC mode reached a messenger or Bitrix24 chat as a bare subdomain address. The preview fetcher got the same page a browser did, and there was nothing for it to show.
A link to an app in PUBLIC mode expands into a card with a title, a description and an image. The data comes from the app's catalogue entry; with no catalogue title the server name is used, with no icon the platform image. The gateway assembles the card itself: the app receives no request, and a sleeping server is not woken. Under any other access policy there is no card — the preview fetcher gets a neutral page with no title, description or image. A card that has already been shown cannot be recalled: the receiving side caches it, so switching the policy to a private one closes off new requests only. More — Authorization in a Black Hole app.
BC-0829-3: lead filters now support stage semantics
Old format supported until: not provided
Before
GET /v1/leads, POST /v1/leads/search, and POST /v1/leads/aggregate with a stageSemanticId filter returned UNKNOWN_FILTER_FIELD. A stageSemanticId field sent in create, update, import, entity batch, or global batch was ignored.
After
The filter accepts P (in progress), S (success), and F (failure). Sorting by stageSemanticId is also available. The Guide, OpenAPI, and reference describe the field as a read-only string. groupBy: stageSemanticId remains unsupported. Create and update return READONLY_FIELD, import returns IMPORT_ITEM_VALIDATION, entity batch returns BATCH_ITEM_VALIDATION, and global batch returns READONLY_FIELD for the corresponding call.
What integrators should do
Use stageSemanticId only for reads, filters, and sorting. Remove the field from create, update, import, entity batch, and global batch bodies. Choose another field for grouping.
FIX-0829-4: mailbox and org-structure node list calls work in the batch APIs
Before
A {"entity": "mail-mailboxes", "action": "list"} sub-call in POST /v1/batch answered
ERROR_METHOD_NOT_FOUND, and the same list in POST /v1/mail/mailboxes/batch answered
CALL_FAILED. humanresources-nodes behaved the same way. As a standalone request,
GET /v1/mail-mailboxes returned the same list with data.
After
Both batch APIs return the records. Such a sub-call runs as a separate request to the
portal at any limit and spends its own rate-limit quota; neighbouring calls on other
entities still travel in one batch. The record count on these two entities always
arrives, even with withTotal: false. The get action on them still does not answer
with data inside a batch — read the record through its own route. Details are in the
"Known specifics" section of the "Batch operations" page.
Separately, the message text of a failed sub-call changed across all batch calls, not just
for these two entities. It used to carry the platform's internal diagnostic; now only text
that came from Bitrix24 is published, and everything else gets a fixed Internal error.
Portal queue overflow, queue wait timeout and portal timeout no longer fall into that generic
code: they arrive under their own QUEUE_OVERFLOW, QUEUE_TIMEOUT, BITRIX_TIMEOUT,
ERROR_LOOP_DETECTED, RATE_LIMITED, OPERATION_TIME_LIMIT and TOKEN_REFRESH_FAILED
codes, all but the last with a retryAfter field, so they can be told apart from an
internal failure. The response
stays 200 and the codes of the other sub-errors are unchanged.
The same rule applies when the whole batch request fails rather than one call: the text
comes from the Bitrix24 response, and is fixed otherwise. The status, the code and the
bitrixError field are unchanged — bitrixError arrives whenever Bitrix24 really
answered, even when its answer carried no text.
Single endpoints also stopped returning internal text in one case: a
TOKEN_REFRESH_FAILED refusal now carries a fixed message — a fragment of the portal's
response could previously end up in it. The code and the status are unchanged.
2026-08-28
FIX-0828-1: stopping a server is no longer billed as a full running hour
Before
After a server was stopped via POST /v1/infra/servers/{id}/stop, the transition to
sleep was not recorded in the status journal. When the hourly charge for that hour ran
late, it relied on the last known record — running — and billed a full hour at the
running rate, including the time the machine was already stopped. The endpoint response
itself was correct; the discrepancy showed up only on the invoice.
After
The transition is recorded immediately, and the hour is billed as it actually happened: running time up to the stop, sleeping time after it. The response shape and status codes are unchanged, and the HTTP 200 response is preserved.
FIX-0828-2: a late upload through a valid URL preserves the object
Before
If an upload through a signed URL started more than one hour after issuance, its reservation could be deleted before the URL expired. A later completion returned 404, while the uploaded bytes remained without an API object.
After
An empty reservation remains until the maximum signed URL lifetime expires. The successful completion response remains unchanged; a late upload through a still-valid URL remains linked to its API object.
FIX-0828-3: GET /v1/guide returns the paging rules as a single paginationCanon block
Before
The paging and record-counting rules were repeated inside every entity of the response. The operations.search.paginationStability fields and the operations.search.params.withTotal description carried the same text for every entity.
After
The same text arrives once, as a top-level paginationCanon block. The wording is carried over verbatim — no rule was rewritten or shortened. The same-named fields inside the entities are still present and carry a pointer to the matching paginationCanon entry.
The one exception is operations.search.paginationStability.counting. It still arrives per entity with its own text, because how to obtain an exact count depends on whether that entity offers the aggregate operation.
Impact on integrators
Calls work exactly as before and no response field was removed. If your code read the rule text out of the entity fields, read it from the paginationCanon block instead. On a full set of scopes the response is roughly 37 percent smaller.
FIX-0828-4: task comment filter and sort accept camelCase field names
Before
GET /v1/tasks/:taskId/comments accepted only raw Bitrix24 field names in filter and sort. The names authorId and createdAt, in which the same fields arrive in the response, returned 400 UNKNOWN_FILTER_FIELD or 400 INVALID_SORT_FIELD.
After
The filter and sort parameters accept camelCase names alongside raw ones: id, authorId, authorName, createdAt, and sort additionally accepts authorEmail. Card-type limits are unchanged: filtering by authorName and sorting by authorName or authorEmail are still accepted only on the old card and return 400 on the new one. When filter names one field twice in different spellings of the name or of the equality sign, the response is 400 INVALID_FILTER. Unknown names still return 400.
Impact on integrators
Filtering and sorting can use authorId and createdAt — the same names in which these fields arrive in the response. Existing requests with raw field names require no changes.
BC-0828-5: Deploy outcomes can be confirmed after a dropped connection
Old format supported until: not provided
Before
If the gateway connection dropped during a deploy, the client received neither the outcome nor the operation identifier. A repeated deploy could encounter EXEC_BUSY without answering the main question: whether the first deploy completed.
After
POST /v1/infra/servers/:id/deploy stores an observable standalone or galaxy deploy outcome, while GET /v1/infra/servers/:id/operations returns recent operations started by the current API key even when the terminal response carrying operationId was lost. For GATEWAY_UNREACHABLE, GATEWAY_CONNECTION_TERMINATED, GATEWAY_STREAM_ERROR*, GATEWAY_TIMEOUT*, TUNNEL_NOT_FOUND, and a post-drop Galaxy interruption, the outcome is stored as unknown, and error.hint requires reconciling the operation, server, and logs first. On standalone this applies equally when the transport throws and when the gateway reports a timeout/error frame in-band during exec or upload/download; that unconfirmed outcome carries error.retryable: false and does not trigger an internal retry. After confirmed EXEC_BUSY, a standalone server may use /unstick only when no operation is still running; tenant recovery is unsupported for a shared Galaxy host, so wait and contact support if the refusal persists.
What integrators should do
When error.retryable: false, do not resend the same deploy: read GET /v1/infra/servers/:id/operations, the server state, and logs first. Retry only after a durable failed outcome when the response hint explicitly permits the same request. Do not delete or recreate the slot while the outcome is unknown.
FIX-0828-6: task comment creation returns the correct numeric ID
Before
On the new task card, POST /v1/tasks/:taskId/comments and POST /v1/tasks/:taskId/comments/batch with action: create could return id: null for a found message when its numeric identifiers arrived as strings. With multiple matches, string comparison could select the wrong ID.
After
Both endpoints return the found message's numeric id and compare matching IDs as numbers. The single POST response remains HTTP 201. The batch response remains HTTP 200, and a successfully created item remains success: true.
If the search does not find the message or cannot identify it unambiguously, id: null remains a valid successful result: the comment has already been created, and retrying the request may create a duplicate.
Impact on integrators
No client changes are required. Continue supporting number | null and do not treat null as a failed creation.
BC-0828-7: incomplete public uploads are no longer anonymously accessible
Old format supported until: not provided
Before
For a PUBLIC object in the PENDING state, GET /v1/public-storage/{portalId}/{objectId} returned 302, while HEAD /v1/public-storage/{portalId}/{objectId} returned 200. The file became accessible without authentication before the upload was completed.
After
Both requests return 404 while the upload remains in the PENDING state. Anonymous access is available only for a PUBLIC object whose upload is complete.
What integrators should do
After uploading the file, call POST /v1/storage/objects/complete and publish the anonymous link only after a successful response.
FIX-0828-8: MCP returns the Drive file content instead of a network error
Before
The download action of manage_file parsed the GET /v1/files/:fileId/download response as service JSON, although that request returns file bytes. On any real file the agent got success: false with code NETWORK_ERROR and an Unexpected token … message — the first bytes of the already downloaded file presented as a connection failure. The action had no working answer at all.
After
The action returns content with the file content in base64, plus contentType, size and filename — mirroring how upload accepts content. Files above 10 MiB are refused with code FILE_TOO_LARGE, and the message names the way to get such a file: a request to the same address with the same API key. Error responses stay the usual platform envelope: a missing file is ENTITY_NOT_FOUND, not NETWORK_ERROR. The generic call_api, which only reads JSON envelopes, now refuses known non-JSON routes up front with NON_JSON_RESPONSE_UNSUPPORTED instead of making a request it cannot parse. It refuses the browser entry points of the OAuth and connect flows the same way, with REDIRECT_ROUTE_UNSUPPORTED: they answer with a redirect, and the request used to travel there with the API key and then follow that redirect to an address the platform does not choose. The path is reduced to its canonical form before the check, so spelling it with .. no longer slips past the refusal, and a full address in place of a path is refused with INVALID_PATH. The download action now requires the file id to be a positive whole number: a fractional value used to return another file's content as a successful answer. The REST request itself still returns binary data, so ordinary clients work as before.
FIX-0828-9: concurrent direct uploads of one object are coordinated
Before
Concurrent POST /v1/storage/objects/upload calls for the same physical app address could mix file content and its metadata.
After
Path A direct uploads with an app-bound key run one at a time. After a turn is acquired, the response remains HTTP 200. If safe write ownership is not confirmed within 60 seconds or the concurrency limit, the request receives 409 STORAGE_KEY_CONFLICT before writing to object storage. Retry the entire request.
The guarantee applies only to Path A with an app-bound key. It does not extend the behaviour of personal keys (appId = null) or Paths B/C.
Impact on integrators
The successful response format is unchanged. On STORAGE_KEY_CONFLICT, retry the original request in full.
FIX-0828-10: create and update response specifications reflect the identifier
Before
The OpenAPI specification and response examples for create and update operations promised a full entity where the API returns only data.id.
After
OpenAPI and the documentation describe the actual response with data.id. The HTTP 201 status for create, the HTTP 200 status for update, and API runtime behavior are unchanged.
Affected endpoints: POST /v1/bizproc-activities, PATCH /v1/bizproc-activities/:code, POST /v1/bizproc-robots, PATCH /v1/bizproc-robots/:code, POST /v1/bizproc-templates, PATCH /v1/bizproc-templates/:id, POST /v1/calendar-sections, PATCH /v1/calendar-sections/:id, POST /v1/telephony-lines, PATCH /v1/telephony-lines/:number.
Impact on integrations
Existing requests require no changes. Regenerated clients now see the actual successful response shape.
FIX-0828-11: commands in a galaxy app are no longer refused before dispatch
Before
POST /v1/infra/servers/{id}/exec on a galaxy app answered 502 with code EXEC_FAILED and a message about safe container execution not being supported. The refusal hit every app and depended neither on the command nor on the state of the container: the request never reached the container at all.
After
The command runs in the app container, and the successful response remains HTTP 200 with the exitCode, stdout, stderr and duration fields — the same shape as on other servers. The CONTAINER_NOT_READY code for a stopped or missing container is returned where the platform can confirm that state; where it cannot, the reason arrives in the stderr of the result and the response itself is still HTTP 200.
FIX-0828-12: Node.js 20 runtime templates install more reliably
Node.js 20 in node20* templates is now installed more reliably when external network access is unavailable.
Before
The template performed an unbounded NodeSource installation. On a network failure, Node.js 18 could be installed, and the deploy then failed only with a generic version-check error.
After
The NodeSource installation uses bounded network attempts and an integrity check. If it fails, its package source is temporarily isolated, pre-existing configuration is preserved, and a verified Node.js 20 archive is used as a fallback; runtime identifiers and the deploy API stay unchanged.
FIX-0828-13: A ZIP deploy no longer asks you to change the archive format when the server is busy with another command
Before
While another command was running on the server, POST /v1/infra/servers/:id/upload with a zip and extract: true, and a galaxy-app deploy from a zip, reported a failed unzip install (UNZIP_PREFLIGHT_FAILED / a build failure) and advised re-packing the archive as .tar.gz.
After
A short-lived busy command channel is retried by the server. If the channel is still busy, the response is 409 with code EXEC_BUSY (on a galaxy-app deploy — GALAXY_APP_BUSY), retryable: true, and a Retry-After header. Do not change the archive format. A real failure to install unzip is still 502 UNZIP_PREFLIGHT_FAILED and the .tar.gz advice.
Impact on integrators
Retry using Retry-After. Do not re-pack the archive because of EXEC_BUSY / GALAXY_APP_BUSY. The UNZIP_PREFLIGHT_FAILED branch is unchanged.
FIX-0828-14: a payment with an inflated vibe count is held instead of rejected
Before
A payment.paid event whose metadata.tokens was ABOVE the catalog count received
HTTP 400 with code TOKENS_MISMATCH. The rejection was terminal: no retry follows
for that event, while the payer has already been charged — the credit never happened
and the payment surfaced in no review queue.
After
Such an event is accepted as held: HTTP 200 with body
{ "ok": true, "held": true, "reason": "TOKENS_ABOVE_CATALOG", "vendor_reason": "catalog-drift-over" }.
No Vibe credits are added — the payment enters the review queue, the same way the
opposite-direction TOKENS_BELOW_CATALOG hold does. The TOKENS_ABOVE_CATALOG value
of reason is new; the two directions carry separate dictionary names because an
overpayment and an underpayment owe different amounts.
The downward direction (TOKENS_BELOW_CATALOG) is unchanged. An unreadable count is
still rejected with BAD_TOKENS.
FIX-0828-15: a pending-deletion owner's key no longer calls Bitrix24 methods through V1
Before
A portal owner's key with scheduled self-deletion could still call some Bitrix24 methods through V1. The restriction did not cover every generated and handwritten wrapper, REST 3.0 method, direct call, and batch request.
After
All V1 methods that use a portal-bound APP/OAuth key to send requests to Bitrix24 return 503 user_self_deletion_pending with Retry-After before dispatch. This includes GET /v1/lists, GET /v1/calendar/settings, GET /v1/chats/recent, GET /v1/workday/status, GET /v1/applications, POST /v1/apps, tariff-refresh and trial-activation methods, and POST /v1/batch.
For a pending-deletion owner's READONLY key, this 503 takes precedence over WRITE_BLOCKED_READONLY_KEY. A fully authenticated active owner's key still receives the READONLY rejection, while earlier authentication and account-state errors keep their existing codes.
V1 metadata methods, including plain GET /v1/me without refresh=tariff, the MANAGEMENT control plane, and methods that operate only on Vibecode platform data are not frozen by this change.
Impact on integrators
No integration changes are required. On a 503 response, retry no earlier than the Retry-After value.
BC-0828-16: include now requires the scope of the related entity
Old format supported until: not provided
Before
The include parameter did not check whether the key held the scope of the entity it pulled in. Only the entity you were reading was checked. A key holding just tasks could therefore read a full employee record through GET /v1/tasks/{id}?include=responsible, while the same key was refused on /v1/users.
After
Both sides of the relation are checked. When the scope of the related entity is missing, the request is refused with code SCOPE_DENIED and HTTP 403, and the message names the missing scope and the relation that needed it.
This affects relations whose two sides require different scopes: responsible and creator on tasks and owner on workgroups require user, while catalog on product sections requires catalog. Relations that stay within one scope — preset on requisites or parentSection on product sections, for example — are unchanged.
If your integration uses such a relation, add the missing scope to the key: the scope set is editable on the key itself, there is no need to reissue it.
Affected endpoints: every endpoint that accepts include — read by identifier, list reads and POST /search. The mechanism itself — including related records.
NEW-0828-17: include for requisites, workgroups and product sections
Three reference entities got their relations, so the include parameter is now declared and works on them. Requisites gained preset, the requisite preset that defines the field set. Workgroups gained owner, the record of the employee who owns the group. Product sections gained two at once: catalog, the trade catalog the section belongs to, and parentSection, the parent section, which comes back empty for a top-level section.
The related record still arrives in the _included field next to the record itself, on list reads, on read by identifier and in the POST /search body. The mechanism limits are unchanged: at most three relations per request, and on a selection of more than 200 records relations are not resolved and the answer is marked includeSkipped.
The other reference entities still do not declare include, and that is a decision rather than an omission. Bitrix24 returns deal pipeline stages as a string code instead of a reference-record identifier, so there is nothing to join them by. On a reference record the pipeline number is meaningful only together with the CRM object type, and one and the same number belongs to a deal pipeline and to a smart-process pipeline at the same time. On a document template the file identifier is not addressable through Drive methods, and the numerator is not exposed as a separate entity in the API. The workgroup member list is served by its own endpoint rather than through include.
Affected endpoints: GET /v1/requisites, GET /v1/workgroups, GET /v1/product-sections, their read by identifier and POST /search. The mechanism itself — including related records.
FIX-0828-18: select combined with include no longer empties the relation on list and search
Before
When a list request or POST /search carried both select and include and the foreign key was not named in select, the related record came back empty. GET /v1/deals?select=id,title&include=company returned an empty _included.company, even though the same relation resolved without select. Read by identifier was unaffected.
After
The relation is resolved before the answer is narrowed to the requested fields, and the foreign key is added to the internal request on its own. The public answer is not widened by it: it still carries only the fields you named, plus the _included block.
Affected endpoints: list reads and POST /search on any entity that declares relations. The mechanism itself — including related records.
NEW-0828-19: the current-user profile is now in the specification and the reference
The method GET /v1/users/me already worked, but it was not declared in GET /v1/openapi.json, so it reached neither the API reference nor the endpoint map. A client checking against the specification did not find the method and concluded it did not exist.
The operation is now described: scope user, the same response shape as GET /v1/users/:id plus a tri-state isAdmin field. The behaviour of the method itself is unchanged.
isAdmin is null when the check could not be completed. The profile is still returned in full, so gate on isAdmin === true rather than on a negation.
FIX-0828-20: `topUpAvailable` for a self-hosted account reflects the self-hosted checkout
Before
GET /v1/cowork/subscription/preview answered topUpAvailable: true for a self-hosted account whenever sales were open in its region at all. Self-hosted accounts buy through a separate checkout that opens separately, so the flag could promise a top-up that the very next order request refused.
After
The flag is computed from the availability of the self-hosted checkout in the account's region. The HTTP 200 response is unchanged and carries the same fields; for cloud accounts the value did not change. While the self-hosted checkout is closed, the answer is topUpAvailable: false with currency: null, and an attempt to start a top-up answers with code BOX_TOPUP_NOT_AVAILABLE.
FIX-0828-21: the auth key now gets the scopes the portal actually grants
Before
When an application's personal API key carried no Bitrix24 scopes, the platform had
no credential to ask the portal with, and the auth key was issued with crm +
placement only. An app that needed tasks, chat or disk hit silent permission
errors, and an issued key cannot be widened — only re-issued.
After
Scopes are probed over the channel the platform manages the portal with, so the set no longer depends on whether a particular application key has a webhook. The auth key receives the intersection of the portal's available scopes with the supported ones, as designed. When there is nothing to ask with, behaviour is unchanged and issuance is not blocked.
FIX-0828-22: an employee photo can be written through the API again
Before
The personalPhoto field is declared a string, so any array or object in it was refused with
400 INVALID_PARAMS — and the [file name, base64] array is the only shape Bitrix24 accepts.
On the entity routes no shape could write the photo: a base64 string and a URL answered 422,
batch routes 400. It only travelled through the POST /v1/users/invite wrapper.
After
On write, personalPhoto takes the file inline: an array of exactly two non-empty strings —
the file name and its base64 content — on POST /v1/users and PATCH /v1/users/:id. Reading
the field is unchanged, it still returns the photo URL. Other shapes stay refused on purpose:
the nested {"personalPhoto": {"fileData": [...]}} is accepted by the portal with success yet
clears the photo. Batch routes refuse the photo: a batch sub-call travels as a query string under the
global body cap, and past its length limit the value would be truncated while still
answering success. The POST /v1/users/invite wrapper accepts the same shape — it translates
the field name and validates the pair separately — but its body limit is still the global one
(1 MiB against the 40 MiB of the single routes), so send a real photo through
POST /v1/users or PATCH /v1/users/:id.
2026-08-27
NEW-0827-1: aggregation marks a partial total next to the number itself
Before
POST /v1/{entity}/aggregate with sum / avg / min / max computes over the first 5000 records matching the filter. The incompleteness marker lived in data.meta.truncated alone, while data.aggregates.amount looked like { "sum": 1234567 } — a plain number with nothing attached. A client reading only the total received an understated result under code 200 with nothing to branch on: data.count meanwhile reported the full number of records.
After
When the selection is truncated, every field object in data.aggregates and in groups[].aggregates additionally carries truncated: true — { "sum": 1234567, "truncated": true }. The same condition adds a warning with code AGGREGATE_TRUNCATED to data.meta.warnings. The sum / avg / min / max values stay numbers, nothing wraps them into an object. On a complete selection the response does not change at all: the field object carries no truncated key and no meta.warnings appears. The 5000 ceiling and data.count behave as before.
Grouping gains one more field — groups[].truncated: on a truncated answer the group object itself carries this marker next to its counter. The marker says the answer is a sample; whether the group counter is exact is shown by meta.aggregatePath — on the ordinary walk groups[].count is computed over the slice that was read, while under the stage-count mode (meta.aggregatePath: "fanout") it comes from a separate probe and is exact. On a count-only grouping it is the only marker next to a number: a count expression produces no field object, and both aggregates bags come back empty. data.count and data.meta.totalRecords stay exact at any size and never carry it.
Alongside the incompleteness marker the response now always reports the size of the gap. Previously meta.recordsShortfall arrived only when the truncation was NOT caused by the ceiling: on a selection above 5000 records the response raised truncated: true with no gap-size field at all. That case is the most common one: on a pipeline of 20,000 deals a client saw "this answer is a sample" and zero as the size of what was missing. Now every reason for truncation carries a quantifier next to the marker — meta.recordsShortfall (how many records never reached the numbers) or meta.pageErrorSample (when the slice was cut short by a sub-page error). On a complete selection neither key appears in the response, as before.
A false alarm is removed at the same time. The truncation marker used to be raised from the comparison "more than 5000 records in total", which is only an indirect sign that the read will be cut. On entities where the limit never reaches Bitrix24 — pages and sites (landing.*), Open Channels configs — nothing is cut and every row is read. Such a response still declared itself a sample. The marker is now raised from the fact: fewer rows read than promised, then and only then. A fully read selection of any size counts as complete again.
This also closes a gap in grouping deals by stage: when a stage walk returned fewer records than its probe had counted (usually because of access rights), the response used to arrive with data.meta.truncated: false and looked complete. Such a response now raises truncated honestly — and with it comes both the marker next to the number and the warning.
BC-0827-2: runtime installation order and nginx startup have changed
Old format supported until: not provided
Before
During a redeploy, the runtime was installed after the previous app version had been stopped. A failed installation could leave the app stopped. For static, php83, and php83-mysql, package installation could implicitly start and enable the system nginx.service, and a request could rely on that process.
After
POST /v1/infra/servers/:id/deploy installs the runtime before stopping the app and replacing its files. If installation fails, the previous app is not stopped. Installing nginx for static, php83, and php83-mysql does not start the system nginx.service or leave it newly enabled. The existing service enablement state is preserved.
What integrators should do
For static, php83, and php83-mysql, make sure the command in the start field starts the process that listens on the application port inside app.service. Do not rely on an implicit start of the system nginx.service. Requests using other runtimes do not need to change.
NEW-0827-3: unfinished BOX account reason and employee identity transfer between accounts
The BOX account card with an unfinished connection (accessPending: true) in the GET /api/portals response now carries a reason in the optional accessPendingReason field: transfer_requested — the employee already has a pending request to move their link to this Bitrix24 account from another Vibecode account, incomplete — the connection is simply unfinished. Such requests are managed by the new /api/box-transfers/* routes: the current holder lists their pending requests, opens one via the email link, and approves or declines the transfer.
FIX-0827-4: unfinished BOX account connection is visible in the list
Before
GET /api/portals silently hid a BOX account whose connection was unfinished.
After
The account card is returned with the new accessPending: true field.
FIX-0827-5: user invitations preserve all profile fields
Before
POST /v1/users/invite accepted ten writable profile fields in the regular Vibecode API format but forwarded them without converting their names. The user was created while the photo, external ID, time zone, and personal details remained empty.
After
POST /v1/users/invite converts these fields to the Bitrix24 format. Clients do not need to change their requests, and the previous Bitrix24 field format keeps working.
FIX-0827-6: single comment lookup finds a comment with a string identifier
Before
GET /v1/tasks/:taskId/comments/:id could return 404 NOT_FOUND when Bitrix24 returned the found message identifier as a string. The same comment was still present in the task comment list.
After
The endpoint normalizes the numeric identifier from the Bitrix24 response and returns the found comment with numeric id and authorId values.
Impact on integrations
No client changes are required.
FIX-0827-7: contact select accepts typed email addresses again
Before
POST /v1/contacts/search and a contact sub-call in POST /v1/batch rejected emailWork, emailHome, and emailMailing with UNKNOWN_SELECT_FIELD, even though contact responses already contained these values. GET /v1/contacts/fields did not list these names.
After
Both operations accept the three names in select and return a successful response with the selected values. GET /v1/contacts/fields describes them as nullable read-only fields.
FIX-0827-8: tariff headers are returned for infrastructure requests
Before
Responses from the /v1/infra/* endpoint family did not contain X-Tariff-Checked-At or X-Tariff-Is-Commercial, even when the tariff check had completed successfully.
After
Responses from /v1/infra/* contain tariff headers under the same rules as /v1/me: the time of the last successful check and the commercial-tariff indicator are returned when the corresponding portal data is available. The response status and body are unchanged.
Impact on integrators
No request changes are needed. A missing header once again means that the corresponding tariff-check data is unavailable, rather than that the infrastructure route was skipped.
NEW-0827-9: array element schemas are available in field contracts
GET /v1/orders/fields, GET /v1/basket-items/fields, and GET /v1/guide now return itemSchema for arrays with a declared element shape. The schema recursively describes type, readonly, nullable, properties, and nested itemSchema values. Clients can optionally read the new field, and existing integrations continue to work without changes. The separate items key continues to contain the raw Bitrix24 value directory for enumeration fields.
FIX-0827-10: lists without a total no longer look complete too early
Before
If Bitrix24 did not return the total record count, a list request with a limit above 50 stopped after the first page. A full window of up to 50 records could also look like the last page because its size was treated as the collection size.
After
The platform continues reading after each full page and stops at a short page or the request limit. When the collection end is not yet proven, the total remains unknown and meta.hasMore or truncated reports that more records may exist.
Impact on integrations
No request changes are required. Requested windows larger than one page are no longer silently truncated; GET /v1/mail/messages now always includes a boolean truncated signal and omits an unknown total instead of returning null.
Affected endpoints: GET /v1/humanresources/nodes, GET /v1/mail/mailboxes, GET /v1/mail/messages.
NEW-0827-11: the self-description now points at the Cowork/Code promo code docs
The GET /v1/guide and GET /v1/me responses now name the desktop application's promo code pair. The cowork section of guide.ts carries the new docs.couponPreview and docs.couponRedeem pointers to the POST /v1/cowork/coupon/preview and POST /v1/cowork/coupon/redeem pages, and the /v1/me rules gained an item about them: which key class is required, how the check differs from the redemption, and why accessGranted: false deserves a screen of its own.
The endpoints themselves behave as before — what is new is that an integrator or an agent finds their description without reading the changelog.
FIX-0827-12: the Cowork/Code rate limits no longer quote a number the client never receives
Before
The GET /v1/guide reference, the GET /v1/me rules and the Cowork/Code documentation pages quoted rate limits as a concrete number: 20 requests per minute for POST /v1/cowork/coupon/preview, 10 for POST /v1/cowork/coupon/redeem, 30 for GET /v1/cowork/subscription/preview, 5 for DELETE /v1/cowork/key, 3 per five minutes for POST /v1/cowork/deploy-key, 30 for GET /v1/cowork/applications/defaults and 6 for POST /v1/cowork/applications. None of those numbers ever reached the client: the cap is divided across the platform's processes, and the x-ratelimit-limit header returned 7, 4, 10, 2, 1, 10 and 2 respectively. For deploy-key, key and the create-application wizard the two channels of one endpoint contradicted each other — the page promised one thing while the header returned another.
After
The GET /v1/guide reference, the GET /v1/me rules and the section's documentation pages now name the x-ratelimit-limit response header as the source of the current value and ask you not to hard-code a number in client code. The endpoints themselves did not change and the response is unchanged — what was corrected is the description that diverged from it. A client that already read the header changes nothing.
What this entry does NOT cover. The machine schema GET /v1/openapi.json still quotes numbers for five operations of the section — subscription/preview, deploy-key, applications/defaults, applications and key — and those numbers diverge from the header exactly as the others did. A client generated from the schema must still read x-ratelimit-limit rather than the value in the operation description. One promise in the schema is accurate: the "three requests per hour" on POST /v1/cowork/activate-market-trial, where the cap is multiplied by the process count before the division.
FIX-0827-13: telephony line creation returns a usable key
Before
POST /v1/telephony-lines returned an internal numeric identifier. It could not be used in the path for updating or deleting the created line.
After
The data.id field contains the created line number. This value can be used in subsequent update and delete requests.
Impact on integrations
No action is required. New create responses immediately contain an addressable key.
NEW-0827-14: time entry lists explicitly reject unsupported filter envelopes
Before
An unsupported filter in GET /v1/task-time and GET /v1/tasks/:taskId/time could return status 200 with a broader result set than the client expected.
After
A non-empty, bracket, JSON, or repeated filter returns 400 UNSUPPORTED_FILTER. For requests with the named userId, taskId, from, and to parameters, the response remains HTTP 200 and their behavior is unchanged.
FIX-0827-15: PUBLIC app robots.txt is available to crawlers
Before
Exact GET and HEAD requests to /robots.txt always received the gateway's local denial, even when the app had accessPolicy=PUBLIC, was running, and served its own file.
After
With accessPolicy=PUBLIC and a live tunnel, exact GET and HEAD requests to /robots.txt return the app response: 200 text/plain with a complete non-empty GET body no larger than 1 MiB (HEAD has no body), or 304. For every other policy, an unavailable app, or any invalid response, the gateway still returns local 200 text/plain with Disallow: /.
Impact on integrators
A PUBLIC app can now control indexing through its own /robots.txt file. No change is needed for non-public apps: denial remains fail-closed by default.
FIX-0827-16: entity filters are no longer lost on list calls
Before
list_entities could send filters as ordinary query parameters. This made a field named sort collide with the sorting directive, while openline-configs could return the full list instead of a filtered result. On envelope-based entities, a filter could also be lost in GET /v1/{entity}/aggregate, POST /v1/batch, and POST /v1/{entity}/batch.
After
list_entities sends fields through filter[...], separately from sorting. Every V1 list path applies the envelope declared by the entity, so a non-empty filter reaches Bitrix24 and a non-matching filter returns an empty result instead of the full collection. The custom bookings list accepts the same bracket form for its required dateFrom/dateTo window while preserving the existing flat form.
Impact on integrations
No request changes are required. Previously over-broad successful responses now match the supplied filter.
FIX-0827-17: the application access list contains portal employees only
Before
GET /v1/infra/servers/{id}/access returned every audience row in users[]. The element's id field is declared required, yet a grant issued to someone outside the server's portal has no portal number at all — such an element would arrive with an empty id, breaking any client that reads the field as required.
After
users[] contains only rows carrying a portal number. Rows addressed by a network identifier are excluded from this list — they live in a different identifier space, and the two must never be mixed. On today's data the response is unchanged: no such rows exist yet. The same rule applies to the matching dashboard screen.
FIX-0827-18: the `connect` step of repair status no longer fails for an agent that did connect
Before
Server repair counted the connect step as complete only when the version of the agent that came
up matched the version set in platform settings. If the agent connected on a different version, the
tunnel was up and blackholeStatus read CONNECTED, yet
GET /v1/infra/servers/{id}/repair-status returned status: failed, step: connect and
error: "Agent did not connect".
After
The connect step completes once the agent connects to the Gateway — exactly as the step order is
described on the operation page. The installed agent version is still returned in
data.agentVersion and no longer affects the repair outcome.
2026-08-26
FIX-0826-1: socnetGroupId now works on the nested resources of workgroup lists
Before
The socnetGroupId parameter was honoured only on the lists themselves — GET /v1/lists and GET /v1/lists/{iblockId}. The nested resources accepted it and dropped it silently: GET /v1/lists/{iblockId}/elements, fields, sections, property files and every delete reached Bitrix24 without the workgroup id when iblockTypeId=lists_socnet. Bitrix24 looked the infoblock up outside the workgroup, failed to find it and answered with a wrong-infoblock-type error, so the client saw a 422 even though it had supplied the group.
After
socnetGroupId reaches Bitrix24 from every endpoint of the /v1/lists family: in the query string on reads and deletes, in the request body on creates and updates. The elements, fields and sections of a workgroup list come back exactly as they do for an ordinary list.
Impact on integrators
No request changes are needed. If a workgroup-list traversal was built on direct lists.* calls because of this defect, it can now be assembled from the platform endpoints.
BC-0826-2: exec into a Galaxy app answers CONTAINER_NOT_READY instead of a false success
Old format supported until: not provided
Before
POST /v1/infra/servers/{id}/exec on a Galaxy app whose container was not up yet answered 200 with success: true, while carrying exitCode: 1 and the container engine's own text in the stderr field. The command had not run, the answer looked successful, and a caller had to string-match the error text to learn why.
After
Such a call answers 502 with the code CONTAINER_NOT_READY and a hint that leads to deploying the application. Container engine details no longer travel in the response. Clients that expected 200 must handle CONTAINER_NOT_READY as a signal to deploy the application first and then retry the command. No string matching is needed: the code names the cause. If the host agent does not yet support safe container_exec, the platform does not dispatch a legacy command and answers 502 EXEC_FAILED; retry after the host agent is upgraded. Successful commands on a supported agent version are unchanged.
FIX-0826-3: 404 responses for Bitrix24 method names now point to the V1 route
Before
Requests to GET /v1/catalog.product.list, GET /v1/crm.deal.list, GET /v1/crm.user.list, and GET /v1/crm.deal.search returned the generic 404 ROUTE_NOT_FOUND response with only a pointer to GET /v1/guide. An agent had to find the replacement among the V1 routes on its own.
After
These requests still return 404 ROUTE_NOT_FOUND and do not call the Bitrix24 method, but error.details now contains the BITRIX_METHOD_AS_PATH reason, the original method name, a suggestedEndpoint object with the correct HTTP method and path, and a guide object pointing to GET /v1/guide. The hints lead to GET /v1/catalog-products, GET /v1/deals, GET /v1/users, or POST /v1/deals/search.
Impact on integrators
Do not build V1 paths from Bitrix24 method names. When error.details.reason = BITRIX_METHOD_AS_PATH, retry the request using suggestedEndpoint and read guide for the parameter requirements.
FIX-0826-4: Neutral offset in the English createdDate description
Before
GET /v1/task-time/fields used a timezone offset example specific to one region for createdDate.
After
The English description uses the neutral +00:00 offset.
Impact on integrators
No action is required. Only the example in the field description changed.
NEW-0826-5: creating an application from the Cowork/Code desktop — two new endpoints
An application on the Vibecode platform can now be created straight from the desktop, with no trip to the dashboard for a key.
GET /v1/cowork/applications/defaults returns everything the wizard needs before it asks the person anything: the presets for "what portal data does the application need", the access mode the account gives new keys by default, the remaining key quota and the server parameters for the later POST /v1/infra/servers call. The endpoint only reads and creates nothing. A preset carries an identifier and a set of scopes but no labels: the platform owns what a preset consists of, while the client localises the wording by the stable identifier. The placement field has three values rather than two: galaxy-preferred is the ordinary verdict for an account whose policy allows both placements, and it is also where galaxy is demoted for an account on the free tier. When creation is switched off by a platform setting the endpoint answers 200 with available: false instead of a refusal, so the wizard hides its menu entry up front instead of hitting a refusal after the name has been typed.
POST /v1/cowork/applications creates a personal application and mints its personal key, which is returned once. No server is created: a personal application has none by definition — it is grown by publishing, and the server then attaches itself to the card. The card in the response carries the same set of fields the applications catalog returns, so the client still models a single "application" object. Some of the signs are not filled in by this endpoint — read pinning, saved source versions and the running operation from the catalog.
The Idempotency-Key header is required. A repeat with the same body answers 201, the header Idempotent-Replayed: true and rawApiKey: null — the raw key is stored irreversibly and is never re-issued. A lost key cannot be re-minted with the same Cowork/Code key — the response carries no identifier for the issued key, so store it immediately and, if it is lost, create the application again with a new header value. The same header with a different body answers 409 IDEMPOTENCY_KEY_BODY_MISMATCH; the fingerprint is taken over a normalised body, so re-ordered fields and re-ordered scopes still replay.
The Bitrix24 scopes are required and have no default. The issued key carries infrastructure and storage rights plus the Bitrix24 scopes that were asked for, and deliberately does NOT carry AI or web-search rights: an application created by this endpoint does not spend the platform wallet on AI.
Both endpoints require the vibe:cowork scope and an active Cowork/Code subscription. A subscription key issued for third-party agent software is not admitted to them — 403 COWORK_HARNESS_KEY_FORBIDDEN.
FIX-0826-6: work schedule answers with a clear error when the required id is missing
Before
GET /v1/workday/schedule without the required id was forwarded to Bitrix24 and returned 422 BITRIX_ERROR carrying the Bitrix24 text about a missing parameter. The response named neither the parameter in a readable form nor this endpoint, and the machine-readable description did not declare id as required, so a generated client would build such a call itself.
After
A request without id is refused before Bitrix24 is contacted and returns 400 MISSING_REQUIRED_PARAMS, stating that the schedule id is needed, not a user id. The machine-readable description declares id as a required query parameter.
Impact on integrators
A request with id behaves as before. A call made without id now returns 400 instead of 422 — the response was already an error, so working requests need no changes.
NEW-0826-7: an app is no longer granted the `vibe:infra` scope on portals with infrastructure disabled
Before
POST /v1/apps with vibe:infra in the scope list created the app and its key
with that scope regardless of whether infrastructure was enabled on the portal.
POST /v1/apps
{ "title": "My app", "scopes": ["crm", "vibe:infra"] }
HTTP 201
{ "success": true, "data": { "scopes": ["crm", "vibe:infra"] } }
After
When the account admin has turned server management off, a request carrying that
scope returns 403 INFRA_DISABLED_FOR_PORTAL and no app is created.
POST /v1/apps
{ "title": "My app", "scopes": ["crm", "vibe:infra"] }
HTTP 403
{
"success": false,
"error": {
"code": "INFRA_DISABLED_FOR_PORTAL",
"message": "Infrastructure is disabled on this portal — the vibe:infra scope cannot be granted"
}
}
The successful path is untouched: when infrastructure is enabled, the response remains HTTP 201 for the same request. A request without that scope keeps working on a closed account too — the scope is simply not appended by default, and no error is returned.
NEW-0826-8: the `vibe:infra` scope can no longer be added to an app by editing its scopes
Before
PATCH /v1/apps/:id with vibe:infra appended to the scope list wrote that scope
onto the app's paired keys regardless of whether infrastructure was enabled on the
portal. That path handed a key server access on accounts where creating such a key
is refused.
PATCH /v1/apps/{id}
{ "scopes": ["crm", "vibe:infra"] }
HTTP 200
{ "success": true, "data": { "scopes": ["crm", "vibe:infra"] } }
After
When the account admin has turned server management off, adding the scope
returns 403 INFRA_DISABLED_FOR_PORTAL and neither the app nor its keys change.
PATCH /v1/apps/{id}
{ "scopes": ["crm", "vibe:infra"] }
HTTP 403
{
"success": false,
"error": {
"code": "INFRA_DISABLED_FOR_PORTAL",
"message": "Infrastructure is disabled on this portal — the vibe:infra scope cannot be granted"
}
}
The successful path is untouched: when infrastructure is enabled, the response remains HTTP 200 for the same request. Editing an app that already carries the scope keeps working on a closed account too, and removing the scope is always allowed.
NEW-0826-9: managing a galaxy server is blocked while galaxies are disabled for the account
Before
The galaxy flag was checked on creation only. An already created galaxy server —
POST /v1/infra/servers/:id/wake, PATCH /v1/infra/servers/:id/sleep and the
other mutating operations — could still be managed after the account admin had
turned galaxies off.
POST /v1/infra/servers/{id}/wake
HTTP 200
{ "success": true }
After
While galaxies are disabled for the account, mutating operations on such a server
return 403 GALAXY_DISABLED.
POST /v1/infra/servers/{id}/wake
HTTP 403
{
"success": false,
"error": {
"code": "GALAXY_DISABLED",
"message": "Galaxy feature is not enabled for this portal"
}
}
When galaxies are enabled, the response remains HTTP 200. Reading (GET), status
refresh (POST /v1/infra/servers/:id/refresh) and server deletion stay available
at all times: leaving the pilot and cleaning up is possible after the switch-off
too. Regular servers are not affected.
NEW-0826-10: the `vibe:infra` scope is no longer granted on portals with infrastructure disabled
Before
POST /v1/keys and PATCH /v1/keys/:id granted the vibe:infra scope
regardless of whether infrastructure was enabled on the portal — availability
was checked by the dashboard UI only.
PATCH /v1/keys/{id}
{ "scopes": ["crm", "vibe:infra"] }
HTTP 200
{ "success": true, "data": { "scopes": ["crm", "vibe:infra"] } }
After
When the portal admin has turned server management off, adding the scope
returns 403 INFRA_DISABLED_FOR_PORTAL.
PATCH /v1/keys/{id}
{ "scopes": ["crm", "vibe:infra"] }
HTTP 403
{
"success": false,
"error": {
"code": "INFRA_DISABLED_FOR_PORTAL",
"message": "Infrastructure is disabled on this portal — the vibe:infra scope cannot be granted"
}
}
The successful path is untouched: when infrastructure is enabled, the response remains HTTP 200 for the same request. Editing a key that already carries the scope keeps working even on a closed portal: the gate looks at the scope being added, not at its presence in the request body. Removing the scope is always allowed. On key creation without an explicit request the scope is simply not appended by default — no error is returned.
NEW-0826-11: scope requirements map in the key self-description
GET /v1/me now returns a scopeRequirements field inside data.api — a map of the key's scopes. Until now the only way to learn a key's boundary was to make the call: the request went out and came back with a 403 SCOPE_DENIED refusal naming the missing scope. The list of available entities showed only what was open, while what was closed was simply absent, and absence could not tell "no such entity in the product" from "it exists but your key cannot reach it".
The granted field lists the scopes the key holds, split between Bitrix24 and Vibecode. The missing field lists the absent ones and, for each, which entities and which paths it would open, plus howToObtain — how to get it: Bitrix24 binds scopes to a key when the key is issued, so the only way to gain one is to reissue the key. Path lists are cut at five items and the remainder is reported in unlocksRemaining next to a truncated flag. The unobtainable field lists scopes a personal webhook key can never carry. The aliases field shows spellings Bitrix24 treats as one scope.
The coverage field declares the completeness of the map itself: entities is always complete, while paths is partial for now — some addresses still lack a machine-checked verdict, so the path lists under-report. Absence of an address from unlocks does not prove the address needs no scope: if a call still answers with a refusal, trust the refusal, not the map. Scopes the platform mints itself and a user cannot obtain go to unobtainable, not into missing with impossible advice.
The map describes the platform-side scope check only, as _note warns. It does not promise the call will succeed: a scope granted in the Vibecode account after the key was issued shows up in the map, but Bitrix24 does not honour it and answers BITRIX_ACCESS_DENIED. Refusals by key kind, ownership, read-only mode, account freeze and plan are separate axes and are not described in the map.
Refusals changed in neither code nor text: 403 SCOPE_DENIED still names the missing scope. This adds a pre-flight check, it does not replace the refusal.
FIX-0826-12: bracket-form sorting of task comments, `?sort[field]=direction`
Before
GET /v1/tasks/{taskId}/comments?sort[id]=desc answered 500 INTERNAL_ERROR. The bracket sort form is accepted by /v1/tasks and /v1/deals, and the flat spelling of the same request — ?sort=id:desc — already worked here too, so the failure looked arbitrary. Every bracket sort failed, including an unsortable field: ?sort[createdAt]=desc also answered 500, while the flat ?sort=createdAt returned a clear 400 INVALID_SORT_FIELD listing the sortable fields.
After
The bracket form is parsed like the flat one and goes through the same field-name check. ?sort[id]=desc sorts by ID and answers 200; the flat ?sort=id:desc is unchanged — its response remains HTTP 200. An unsortable field in brackets answers 400 INVALID_SORT_FIELD with the same field list as the flat form, and the error text names the field — Unsupported sort 'createdAt:desc'. A sort value that cannot be read unambiguously (a repeated parameter, mixed flat and bracket forms, nested or malformed brackets, an array, or more than one colon) also answers 400 instead of being silently reinterpreted or dropped. The same bracket-shaped sort on the task-checklist and platform-portals lists now returns a controlled 400 instead of 500. This parameter no longer produces 500 in any form.
FIX-0826-13: waking an app on a failed Galaxy host now ends with an explicit error
Before
POST /v1/infra/servers/:id/start and POST /v1/infra/servers/:id/wake could appear successful for a Galaxy app even after the shared host guest OS had been classified as unable to boot. The client kept waiting for a startup that could never finish.
After
Both requests now return HTTP 422 with code GUEST_NOT_BOOTING for that Galaxy app. The client can stop waiting immediately and prompt the user to replace the failed host.
FIX-0826-14: Bitrix24 registration failures no longer expose the vendor's internal text
Before
When Bitrix24 refused registration while issuing a key or an application, its
internal wording was passed straight through in error.message — for example
Cannot register webhook: Failed to register webhook in portal. That text is
always English, is not covered by the documentation, and could change on the
Bitrix24 side at any time. One family of these failures (incoming-webhook
registration) also returned the Bitrix24 code directly instead of a Vibecode one.
After
error.message now carries Vibecode wording, and the details of the Bitrix24
reply stay in the logs. A webhook registration refusal is classified the same way
as an application registration refusal and returns REST_REGISTRATION_FAILED.
The response gains error.details.incidentCode — a six-character support code
that lets support locate the log record. HTTP statuses and error codes are
unchanged, so no client action is required.
2026-08-25
FIX-0825-1: an application card now gets its server on galaxy portals too
Before
An application created without a server and then deployed on a galaxy portal stayed in the Applications section with no server: GET /v1/applications/{id} returned server: null indefinitely. Linking the container to an already existing card depended on a setting that is off by default, so the card and the container lived apart.
After
The container is linked to the already existing card regardless of that setting, and GET /v1/applications/{id} returns the server right after the deploy. The setting still governs only the creation of a NEW card for an app created on a shared host — its behaviour is unchanged.
BC-0825-2: a read-only key no longer writes on platform endpoints
Old format supported until: not provided
Before
The read-only key mode filtered Bitrix24 calls but did not cover platform V1 endpoints.
A read-only key could still manage servers (DELETE /v1/infra/servers/{id},
POST /v1/infra/servers/{id}/stop|start|reboot|wake), deploy and run commands on a server,
write to storage (POST /v1/storage/objects), submit feedback (POST /v1/feedback), manage
search and AI credentials, and call endpoints that spend credits.
After
Any write on a platform V1 endpoint made with a read-only key returns
403 WRITE_BLOCKED_READONLY_KEY before the operation runs. Endpoints that proxy a call to
Bitrix24 are unchanged: there the decision comes from the per-method classifier, so reads
that carry a request body (POST /v1/deals/search, a read-only POST /v1/batch, and
similar) are not affected.
There are two exceptions. POST /v1/apps — a read-only key can still create an application
and its paired key IN read-only mode, but cannot issue a read-write key.
DELETE /v1/infra/servers/{id}/lock — releasing a stuck lock does not change server state
and stays available: the recoveryAction field of an EXEC_BUSY response points to it.
If your application needs these operations, switch the key to read-write mode on the
/keys page. You can check the current mode and whether server creation is available via
GET /v1/me: for a read-only key the response now carries a
writeRestriction field — the refusal code, the scope it applies to, and the address of the
access-mode page. It exists because the response names writing endpoints in some thirty
places (storage, feedback, source-storage and cowork hints); those are addresses, not
permissions, and the field says so outright. Per-operation availability stays in
capabilities.
FIX-0825-3: /v1/me no longer reports app creation as unavailable to a read-only key
In the GET /v1/me response the capabilities.apps.create slot came back with available: false and the reason WRITE_BLOCKED_READONLY_KEY under a read-only key. That was wrong: POST /v1/apps refuses such a key only when it asks for an app and paired key in read+write mode, while an app in READ-ONLY mode is created by the same key and answers 201.
The slot is now reported as available, with the restriction spelled out in its note field. A client that branches on capabilities — which is what /v1/me exists for — no longer skips an operation that works.
BC-0825-4: a Cowork desktop key can no longer change OAuth application registrations
Old format supported until: not provided
Before
A key carrying the system vibe:cowork grant could register an OAuth application on the Bitrix24 account (POST /v1/apps), edit it (PATCH /v1/apps/{id}), delete it (DELETE /v1/apps/{id}) and re-point it at other credentials (POST /v1/apps/{id}/relink-oauth). The same key was already barred from infrastructure, the source depot and catalog publishing, so the restriction was incomplete.
After
All four operations answer 403 INFRA_FORBIDDEN_FOR_COWORK_KEY, with details.deployableKeys listing the owner's usable keys. On POST /v1/apps the refusal comes before the body is checked, so an invalid body also gets this code rather than VALIDATION_ERROR. Reads of the family (GET /v1/apps, GET /v1/apps/{id}) are unchanged.
What to do
Run these operations under an ordinary application key: the vibe:cowork grant is platform-minted and meant for data only. Candidate keys arrive in details.deployableKeys of the same response — name, prefix and trailing characters, never the secret. There is no support window for the old behaviour: the vibe:cowork grant is not user-grantable, never appears in the key dialogs, and its only holder does not call these operations.
BC-0825-5: re-uploading a file now replaces its content instead of failing with 500
Old format supported until: not provided
Before
Uploading to a logical key that was already taken returned 500 with code P2002 from
POST /v1/storage/objects/upload. The bytes in storage had already been overwritten, while the
object sizeBytes and sha256 still described the previous version — so the size shown in listings
and billing did not match the actual file.
After
Re-uploading to the same key replaces the content: 200, the same object.id (previously issued
links keep working), and sizeBytes, sha256, contentType and the new contentUpdatedAt field
match the new bytes.
This applies to an app-bound key. With a personal developer key that has no app binding,
re-uploading still creates a new object with its own id — unchanged behaviour, fixed separately.
When the object cannot be replaced in place, a 409 with a meaningful code is returned instead of
500: STORAGE_KEY_DELETED (the object is deleted and holds the name until the purge),
STORAGE_MULTIPART_IN_PROGRESS (a multipart upload is in progress),
STORAGE_UPLOAD_PENDING (the key holds an unfinished presigned reservation),
STORAGE_KEY_OWNED_ELSEWHERE (the name belongs to another object of this app),
STORAGE_KEY_CONFLICT (the object changed while the upload was in flight — retry).
What you need to do
- The
visibilityfield cannot change visibility while replacing: omit it or pass the current value. Any other value returns400 STORAGE_VISIBILITY_MISMATCH, and the current value is named in the message. Such a request used to return500while still replacing the bytes — so a client that ignored the error was in fact publishing updates and will stop doing so after this change. - A presigned URL (
POST /v1/storage/objects) is no longer minted for an existing object:409 STORAGE_KEY_EXISTS. Its Content-Type is unsigned, so content can only be replaced by a direct upload. An unfinished reservation of your own is reused instead —200with the sameobject.idand a fresh URL; such a URL cannot change the reservation visibility, so a differing value returns400 STORAGE_VISIBILITY_MISMATCH. - A multipart upload (
POST /v1/storage/objects/multipart/create) on a taken key also answers409instead of500:STORAGE_KEY_EXISTS,STORAGE_KEY_DELETED,STORAGE_MULTIPART_IN_PROGRESS,STORAGE_UPLOAD_PENDINGorSTORAGE_KEY_CONFLICT(a concurrent request created the object). Replacing an object through a multipart upload is not supported — use another key. - Responses now carry
object.contentUpdatedAt— when the content last became current. It isnullfor objects written before this change.
BC-0825-6: the binding and author fields of a timeline comment no longer look editable
Old format supported until: not provided
Before
GET /v1/timelines/fields returned entityType, entityId and authorId as ordinary writable fields. PATCH /v1/timelines/{id} with any of them answered 200 and success: true, yet the value did not change — reading the record back showed the previous one. Meanwhile id and createdAt in the same output were honestly refused with 400 READONLY_FIELD, so the field reference told the truth only selectively. An integrator or an AI agent concluded it had changed the author or moved the comment to another record, while nothing had changed at all.
After
The three fields are marked immutable in the field reference, and an attempt to write them is refused with 400 READONLY_FIELD instead of a silent success. The reasons differ per field, and the reference now distinguishes them:
entityTypeandentityId— available on create only: a comment is bound to its record at the moment it is added, and it cannot be moved to another one;authorId— read-only: Bitrix24 derives the author from the credentials the call is made with, so the value cannot be set on update or on create.
comment stays writable — it is the only field the Bitrix24 update accepts.
What integrators should do
Drop entityType, entityId and authorId from the body of PATCH /v1/timelines/{id} — they were never applied, and now a request carrying them returns 400 READONLY_FIELD. Check creation separately: POST /v1/timelines carrying authorId also moves from "201, value ignored" to 400 READONLY_FIELD, because the author cannot be set on create either. entityType and entityId stay required and accepted on create. If your code treated the successful answer as proof that the author had changed or the comment had moved, that expectation was already unmet before this change: the value stayed as it was. The comment text still updates through a normal PATCH with the comment field. Set the binding at creation time through POST /v1/timelines; the author cannot be changed — make the call under the account you need.
No support window for the previous behaviour is provided, deliberately: the previous behaviour was the defect — it silently discarded the value that was sent, and keeping it for a period would mean prolonging a silent data loss.
BC-0825-7: deploy base version is required when the top saved source version belongs to somebody else too
Old format supported until: not provided
Before
POST /v1/infra/servers/{id}/deploy required baseVersionId only on a server that currently had a live development team. If the owner added a teammate, the teammate deployed their own version, and the owner later removed them from the team, the requirement disappeared along with the last team member — and the next deploy silently overwrote the saved work.
After
baseVersionId is also required when the top version saved in the server's source depot was saved by somebody other than the caller deploying now — regardless of whether the server currently has a team. The error is unchanged: 409 BASE_VERSION_REQUIRED naming the current version.
What integrations must do
Send baseVersionId on every deploy. A client that already does needs no changes. A client that relied on "no team means no label needed" will get 409 BASE_VERSION_REQUIRED on a server holding somebody else's saved version — including after an application ownership transfer, where the top version was saved by the previous owner. Handle it the way the team case is already handled: read the current version number from the error body and repeat the deploy declaring it as the base. The old behaviour is not kept for any period: it is exactly what caused other people's work to be lost.
NEW-0825-8: a server's development-team member now sees its application in the catalog
A member of a server's development team now sees the application bound to that server in GET /v1/applications (arriving with viewerState: "shared") and can read its card via GET /v1/applications/{id} — previously the card answered 403 FORBIDDEN, because access was checked only by ownership and by the server's access policy, without considering team membership.
FIX-0825-9: include is advertised only for entities with relations
Before
OpenAPI and the MCP reference advertised the include parameter for list, get, and search on every entity. For entities without available relations, a request with this parameter returned 400 INVALID_INCLUDE with an empty list of available relations.
After
OpenAPI advertises include only for entities with available relations, while the MCP reference directs clients to check the capability through discover or get_fields first. Runtime validation and the 400 INVALID_INCLUDE response for an unsupported include are unchanged.
Impact on integrations
Client generators no longer receive an unsupported include from OpenAPI, while MCP agents get explicit guidance to check the capability first. The common optional MCP key remains available. Existing valid requests continue to work unchanged, while previously unsupported requests receive the same 400 INVALID_INCLUDE response.
BC-0825-10: Inline archive cap on the deploy body narrowed to 96 MB
Old format supported until: not provided
Before
POST /v1/infra/servers/:id/deploy with code in inline source.content, POST /v1/infra/servers/:id/upload with inline content, and POST /v1/infra/servers with the source field at creation accepted a body up to 500 MB. A body over the cap was refused with PAYLOAD_TOO_LARGE.
After
The body of these three requests is capped at 96 MB. The unit is the HTTP body itself, not the archive: source.content / content is base64, which runs about a third larger than the raw bytes, so a 96 MB body corresponds to roughly a 72 MB archive. A body over the cap is refused with 413 INLINE_SOURCE_TOO_LARGE — a new code, replacing the former PAYLOAD_TOO_LARGE on these three endpoints. The refusal is decided from the Content-Length header before the body is read, so it is deterministic — re-sending the same body fails identically. It saves no traffic: the whole body is uploaded first, and the refusal arrives once the upload has finished. The error envelope carries error.hint with reason, recovery, recoveryAction and note fields — a ready recovery recipe.
The multipart (multipart/form-data) form of POST /v1/infra/servers/:id/deploy falls under the same cap conditionally. The file part streams into storage — and keeps its former 500 MB archive limit — only when three conditions hold at once: the caller reaches the server as its direct owner, not through another application's card or a management key; the server is either not a galaxy app, or both link-based-deploy settings for galaxy apps are turned on for the caller; and the source storage feature is enabled, both platform-wide and for that portal. If even one condition fails, streaming does not kick in and the form accepts the archive the same buffered way as the base64 fields above — under the same 72 MB archive cap, with the same 413 INLINE_SOURCE_TOO_LARGE code and error.hint.
The former PAYLOAD_TOO_LARGE code has not gone away: it still applies at the edge nginx layer on /v1/ (500 MB cap) and on every other platform route with its own limit — nothing was renamed.
Unchanged:
source.url— still accepted up to 500 MB; the platform downloads the archive from the link itself;source.versionId— deploying an already-saved version, up to a 500 MB archive;- the source-version save cap on POST /v1/infra/servers/:id/sources and
POST /v1/apps/:id/sources— 500 MB, untouched.
What integrators should do
Send an archive larger than 72 MB through the versioned path in two calls — it works the same way for a personal key (vibe_api_*) and for an OAuth-app key, and for the server's direct owner these two calls are all it takes:
POST /v1/infra/servers/:id/sources # raw archive bytes, Content-Type: application/gzip, --data-binary
POST /v1/infra/servers/:id/deploy # {"source":{"versionId":"vN"}}
If, on the other hand, the server is reached through an application card or a management key rather than by its direct owner, those two calls take a third one: {versionId} may answer SOURCE_VERSION_REQUIRES_APP or SOURCE_VERSION_NOT_FOUND — in that case fetch a signed link at GET /v1/infra/servers/:id/sources/vN/download and deploy from it instead: {"source":{"url":"<link>"}}. This same path is also the way around the multipart form's conditional cap — a versioned deploy never buffers the whole archive, so its 500 MB limit is unconditional.
The cap was narrowed at the observable-behavior level: a body of several hundred megabytes in base64 form (and a multipart archive for which at least one of the conditions above did not hold) had to be held in memory whole for the duration of the request, and such a request could abort without a response, taking concurrent requests on the same process down with it.
NEW-0825-11: `GET /v1/me` now names the archive equivalent of the inline cap as its own field
Before
The deployment.limits block carried the inline body cap in a single uploadInlineMax field. Its unit is the HTTP body, while source.content and content travel as base64, so the archive size that fits into that body was left for the client to work out.
After
A sibling field uploadInlineMaxArchive was added — the same cap expressed as an ARCHIVE size: three quarters of uploadInlineMax, because base64 runs about a third larger than the raw bytes. The field is added to the response and removes nothing from it: uploadInlineMax stays where it was and means what it meant, so a client that ignores the new field needs no changes. Both values arrive as strings with a unit — for example 96MB and 72MB.
What integrators should do
Nothing is required. If you were converting the body cap into an archive size yourself, read uploadInlineMaxArchive instead — it is rendered from the same constant as the 413 INLINE_SOURCE_TOO_LARGE refusal, so it cannot drift from the actual behaviour.
BC-0825-12: POST /v1/apps now honours the portal policy for who may create apps
Old format supported until: not provided
Before
The public route only checked the legacy list-based mode. A portal whose Bitrix24 admin had
limited app creation to admins, or disabled it entirely, still let any personal API key create
an app — while the same action in the dashboard answered 403.
After
POST /v1/apps answers 403 with code APP_CREATION_RESTRICTED when the
portal policy does not allow the caller to create apps. The list-based mode behaves as before:
a member on the list creates the app, everyone else gets 403. On portals where creation is
open to all, nothing changes.
What integrators should do
A key issued on a restricted portal will start receiving 403 APP_CREATION_RESTRICTED where
it previously got 201. Ask the Bitrix24 admin to grant the app-creation right, or create apps
with a key that already holds it.
FIX-0825-13: Node.js 20 runtime setup no longer repeats completed steps
Before
When redeploying a Node.js 20 runtime through POST /v1/infra/servers/:id/deploy, the platform reinstalled Node.js and pm2. An unavailable npm registry could consume the entire step budget and finish without a precise cause.
After
The platform skips installation when Node.js 20 and pm2 are already available. If pm2 is absent, its installation has time and retry limits, and an error or timeout stops deployment with an explicit diagnostic.
Impact on integrators
Repeat deployments finish faster and require no integrator changes. A pm2 installation error is now immediately visible as the cause of an unsuccessful deployment.
FIX-0825-14: placement bind answers 400 for an over-long iconName instead of a reasonless 502
Before
POST /v1/placements/bind accepted an options.iconName of up to 255 characters. Bitrix24 refuses anything longer than 50, so the request travelled to the account and came back as 502 BITRIX_UNAVAILABLE carrying "Failed to register placement on Bitrix24 via dev key". Which field was at fault could not be told from the answer.
After
The schema bound now matches the Bitrix24 bound: a value longer than 50 characters is refused up front, and the 400 names the options.iconName field. Values that bound before keep binding — anything longer than 50 was never accepted by the account, and the default fa-cube icon sits well inside the bound.
2026-08-24
BC-0824-1: activity type can no longer be changed after creation
Old format supported until: not provided
Before
PATCH /v1/activities/:id accepted the typeId field. The value never reached storage: an activity type is fixed at creation, so it was discarded. A request carrying typeId as its only field ended up with an empty field set and returned the opaque refusal Fields is not specified., which gives no hint that immutability of the type is the reason. A request that also carried at least one writable field answered with success, so the caller believed the type had changed.
After
typeId is declared as a field that is set only on creation. PATCH /v1/activities/:id carrying it returns 400 READONLY_FIELD and names the field. Both batch update operations answer the same way. POST /v1/activities still accepts the field, where it stays mandatory. In the GET /v1/activities/fields output, typeId now carries the createOnly marker.
What integrators should do
Remove typeId from the body of PATCH /v1/activities/:id. If the type genuinely has to change, create an activity with the required type and delete the previous one — the type of an existing activity cannot be edited. The createOnly marker in the GET /v1/activities/fields response lets you tell such fields apart before sending a request.
FIX-0824-2: paid accounts no longer receive an error asking them to pay again
Before
POST /v1/apps could return a 403 error asking a paid account to upgrade when application installation through Bitrix24 failed.
After
When Bitrix24 confirms that the plan is paid, the method returns the technical error 502 CONNECTOR_REST_UNAVAILABLE with a safe reason and no upgrade prompt. A confirmed unpaid plan still returns a 403 plan error.
Impact on integrations
Requests require no changes. The technical error can be retried, while the plan error requires an upgrade.
BC-0824-3: the deployment block changes shape for Belarusian accounts along with their access model
Old format supported until: not provided
Before
An account licensed in Belarus landed in the limited-access bucket under three conditions at
once: it held a demo subscription, it held no paid subscription, and it had no history of
commercial payments. Its Bitrix24 plan — paid or free — did not affect membership, whereas an
account that had ever paid before did not land in the bucket at all. In
GET /v1/me such an account received a deployment block with no
galaxyApp sub-block, with primary = standalone and with a placementNote field
explaining that no galaxy host of its own is created for it and that an app must be deployed
in two steps. A one-shot create carrying source answered
400 SOURCE_AT_CREATE_GALAXY_ONLY.
After
The plan-based model has no limited-access bucket at all, so a Belarusian account leaves it in
both directions at once: on a paid or demo Bitrix24 plan it gets full access, on a free plan it
is denied. In both cases the deployment block changes shape the same way — provided galaxy
placement is open to the account and it does not yet have a galaxy of its own (both carve-outs
are named below): the
deployment.galaxyApp sub-block appears, the deployment.placementNote field disappears, and
deployment.primary switches from standalone to galaxyApp. For an account with full
access a one-shot create carrying source starts working.
What integrators should do
Detect the placement model by the PRESENCE of the deployment.galaxyApp sub-block, exactly as
the response reference prescribes. Rewrite any branch keyed on the presence of placementNote
or on the value of primary: both fields move with the account's access model, not only with
its plan. Read the permission to create a server separately, from
capabilities.servers.create: the deployment block describes the placement CONTRACT and is
present for a denied account too, while
POST /v1/infra/servers answers such an account with 402.
Two carve-outs where the response shape does not change at all. The first is an account with
galaxy placement switched off: neither field is present before or after. The second is an
account that already has a galaxy: placementNote explained the absence of a galaxy of its
own, so the owner of a live galaxy host received galaxyApp and no placementNote while in
the bucket too. The change reaches an account at the moment it is moved onto the plan-based
model, so both response shapes occur side by side across accounts.
NEW-0824-4: the Bitrix24 plan becomes the access condition for accounts licensed in Belarus
Vibecode admits an account to the infrastructure either by its Bitrix24 plan or by a subscription, depending on the licence region the account belongs to. Accounts licensed in Belarus — served by a separate Vibecode deployment, not by this one — move onto the plan-based model: a paid or a demo Bitrix24 plan grants access there, and an account with neither is declined. The move is staged, account by account.
Nothing changes on this deployment and no action is required. Access here has always been
granted by the Bitrix24 plan alone: a commercial plan grants full access, a demo plan grants
a limited trial, and on a free plan POST /v1/infra/servers —
like an app publish, a wake of a sleeping machine or the issuance of a new key — is still
declined with 402 and INT_TARIFF_REQUIRED, which GET /v1/me also
reports in capabilities.servers.create.reason. No new field, no new denial code and no new
branch appear in the contract you consume here.
FIX-0824-5: the server-creation hint in the key response no longer leads nowhere
Before
An account whose access is granted by a Bitrix24 plan rather than a subscription received a
capabilities.servers.create slot in GET /v1/me carrying a generic
wording: servers are available on commercial plans or during an active trial. That was wrong
twice over. Such an account is not entitled to the trial — the entitlement is computed on the
subscription model — and the address where a plan is chosen was not named at all. Separately,
for accounts licensed in Belarus the alternatives[].url field pointed at the subscription
checkout inside the account itself, and would have kept pointing there after the account moved
onto the plan-based model, where a subscription no longer resolves the denial.
After
The slot names the working paths and the address: a paid or demo Bitrix24 plan, and for
Belarus a paid subscription as well. The text in capabilities.servers.create.userMessage
now matches the 402 body that POST /v1/infra/servers
returns for the same account — one denial code no longer says different things on two
surfaces. For accounts licensed in Belarus that have moved onto the plan-based model,
alternatives[].url points at the plan-selection page instead of the subscription checkout.
Impact on integrators
No action required: a text field and a URL changed value, the response structure is unchanged.
The fix reaches Kazakhstan and Uzbekistan at once, and Belarus as each account is moved onto
the plan-based model. If you render your own copy instead of userMessage, review it: the
promise of a trial is wrong for these accounts.
FIX-0824-6: the denial text for Kazakhstan and Uzbekistan no longer offers a subscription
Before
On the deployment that serves Kazakhstan and Uzbekistan an account on a free plan was refused with the subscription-model text and was asked to activate a subscription that is not sold in those countries. The action named in the text could not be performed, and the working paths to access — a paid or a demo Bitrix24 plan — were not named at all, nor was the address where a plan is chosen.
After
Such an account is now refused with the plan-based code of its own region, and the text
names both working paths and the address. The subscription brand is gone from it. The
correction reaches every surface where an access denial is visible to the client: server
creation (POST /v1/infra/servers), an app publish, a wake of
a sleeping machine and the issuance of a new key, as well as
capabilities.servers.create.reason in GET /v1/me.
Nothing changes on this deployment and no action is required here. Access on this surface is
granted by the Bitrix24 plan alone, a free-plan denial is still answered with 402 and
INT_TARIFF_REQUIRED, and neither the request nor the shape of the response changes. The
regional denial codes corrected above are reachable only on the deployment named there.
BC-0824-7: bot re-authorization is now explicit and safe
Old format supported until: not provided
Before
POST /v1/bots/:botId/reauth could clear any disabled bot state, while 410 BOT_DISABLED did not tell clients whether automatic recovery was allowed.
After
410 BOT_DISABLED includes boolean error.details.reauthAllowed. Call POST /v1/bots/:botId/reauth automatically only when reauthAllowed=true; the server emits that signal only for writable bots disabled by authentication failures and enables it gradually. A manual call also remains the credential probe for an active bot after ownership transfer. Other disabled states now fail immediately with 409 BOT_REAUTH_NOT_ALLOWED; a concurrent state change returns 409 BOT_REAUTH_STATE_CHANGED. Update clients that called re-authorization for every BOT_DISABLED response.
FIX-0824-8: galaxy application deploy warns when environment variables are reset
Before
A successful /deploy without user env recreated the galaxy application without previous variables but did not report that in the response.
After
The response includes a warnings[] entry asking callers to send the complete env on every deploy. A development-team member's deploy, where the owner's variables are preserved, receives no false warning.
Impact
The deploy status and behaviour are unchanged; clients should read the existing warnings[] array.
NEW-0824-9: task dependencies are now available through the API
Before
The Bitrix24 task card shows a "Related tasks" block, but there was no way to read those links through the API: no dependency fields in the task list, none in a task read by id, nothing about them in the task field description, and no dedicated endpoint. A dashboard that needed the dependency graph could not build it.
After
Two read-only endpoints were added. GET /v1/tasks/:taskId/dependencies returns the dependencies of one task as an array of id-and-title pairs, sorted by id. POST /v1/tasks/dependencies/bulk takes up to 50 tasks and answers per task: successes in results, failures in errors, counters in meta.
The bulk call answers 200 even when every sub-call failed — partial failure is the whole point of it, so what has to be read is errors, not only results. Duplicate ids collapse silently and the requested counter counts unique ids, so "requested" always equals "succeeded" plus "failed".
Boundaries, stated explicitly
Only predecessors are returned — the tasks the requested one depends on. Bitrix24 exposes no reverse direction through the API, so a dashboard builds it by inverting the pairs it collected. The link type is readable by no portal method, so it is absent from the response entirely: an empty or guessed value in a contract is worse than a missing field.
Links added through the Gantt chart are invisible to these endpoints: the portal keeps them separately and returns them from no read method. Telling "there are no links" apart from "there are links, in the invisible store" is impossible here and in raw REST alike.
A task carrying more dependencies than the endpoint returns gets an explicit refusal rather than a truncated list: the response is either complete or a refusal.
Impact on integrators
There are no breaking changes — both routes are new. Writing dependencies did not get an endpoint of its own: the set is still changed through the DEPENDS_ON field on task update, and the semantics there are a full replacement of the set rather than an addition.
FIX-0824-10: international bot access keeps the Bitrix24 plan model
Before
The international instance already governed account access through the Bitrix24
plan model. Bot permission failures returned 403 BITRIX_ACCESS_DENIED.
After
There is no change to that .com contract: bot access stays on the international
plan model. GET /v1/me?refresh=tariff continues to refresh the account's
Bitrix24 plan state after an upgrade.
What integrators should do
Keep handling international access through the existing plan errors. No new bot
error branch is required on .com.
BC-0824-11: opening a workday no longer confirms an expired state
Old format supported until: not provided
Before
POST /v1/workday/open returned 200 success:true when Bitrix24 left the
workday in the EXPIRED status. No new workday was opened.
After
An unchanged EXPIRED status is returned as HTTP 409 with success:false and
the WORKDAY_EXPIRED code. The instructions say to read status for the same
user, close the expired day using its timeStart date and a report, and then
retry open.
What integrators should do
Handle success:false and HTTP 409 as an explicit signal that the workday was
not opened; resolve the previous day's state using the response hint first.
NEW-0824-12: calendar events can be linked to CRM elements
A calendar event now carries a crmFields field — the link between a meeting and CRM elements: deals, leads, contacts and companies. The field is readable and writable, is declared in GET /v1/calendar-events/fields, and is available in select.
The value is an array of typed references: D_<id> deal, C_<id> contact, L_<id> lead, CO_<id> company. On create and update the value must be an array; [] clears every link, and omitting the field from the body keeps the stored ones. Clearing with an empty array works on POST and PATCH; in a batch request an empty array is refused rather than silently succeeding — a batch sub-call cannot carry one. On POST /v1/batch that is INVALID_PARAMS in data.errors under the call's id; on POST /v1/calendar-events/batch it is 400 BATCH_ITEM_VALIDATION for the whole batch, with the name INVALID_PARAMS and the item index inside message. An event with no links reads back as [] — the empty value never arrives as null or an empty string, so a crmFields.length check is always safe. An unknown prefix, or a reference to a record that does not exist, is rejected.
Affected endpoints: GET /v1/calendar-events, GET /v1/calendar-events/:id, POST /v1/calendar-events/search, POST /v1/calendar-events, PATCH /v1/calendar-events/:id, GET /v1/calendar-events/fields.
FIX-0824-13: an application on a galaxy host that does not boot can be deleted without a live tunnel
Before
DELETE /v1/infra/servers/{id} on a galaxy application answered 502 with the code
GALAXY_HOST_UNREACHABLE whenever the host was offline, whatever the reason. When the host guest
operating system does not boot at all (the provisionErrorCode field of the server record equals
GUEST_NOT_BOOTING), the tunnel will never come back, so retrying helped neither a minute later
nor a day later. Deleting the host itself was blocked too: it answered 409 with the code
GALAXY_HAS_APPS, because its applications formally remained alive. The owner ended up in a closed
loop, still holding a quota slot with a machine that no longer runs.
After
Once the platform has proven that the host guest operating system does not boot, an application on
that host is deleted without contacting the host: DELETE /v1/infra/servers/{id} answers 200, the
application record is closed, and its access tokens, domain and catalog item are released. After
deleting the applications one by one, the owner deletes the host itself with the regular call — the
GALAXY_HAS_APPS refusal no longer appears. The GALAXY_HOST_UNREACHABLE code keeps its previous
meaning for every other case: the host is temporarily offline and retrying makes sense.
Important: the does-not-boot mark alone does not guarantee the 200: before deleting without
contacting the host, the platform also confirms through the gateway that no live tunnel
exists. That check is fail-safe — an unreachable gateway, an answer without a connection
list, or a host that came back meanwhile all keep the answer at 502. Clients need
no changes — the retry loop for this state simply stops being necessary.
BC-0824-14: an unknown select field name is refused with 400 on almost every entity
Old format supported until: 18.02.2027
Until now such a request answered
200with the requested field simply missing from the records — so the "old format" here means an incomplete answer, not a working one.
Before
A field name the entity does not have went into the selection silently. The request ran, the
field was absent from the records, and the explanation sat in meta.warnings — where nobody
looks. A typo in select therefore looked like a successful request with mysteriously
incomplete records:
GET /v1/deals?select=id,titel → 200, records carry only id
Only calendar events answered such a name with an error.
After
The request is refused before the Bitrix24 call — 400 with the UNKNOWN_SELECT_FIELD code
and the list of accepted names in the message. That list is the precise answer to "what may I
ask for".
It applies to the entities whose field set has been verified against Bitrix24: deals, contacts,
companies, leads, quotes, activities, addresses, smart processes, products and product sections,
catalogs with their products and sections, orders and order statuses, statuses, currencies,
calendar sections, files, folders, storages, departments, workgroups, document
templates. Everywhere else the behaviour is unchanged — a warning in meta.warnings.
A separate class is a name the entity DOES have but never returns: GET /v1/{entity}/fields
shows such a field with notReturned: true. In this cohort that is, for example, currencyId
on products and sort on product sections. It is refused with a 400 too, but under its own
SELECT_FIELD_NOT_RETURNED code and a message that names the reason. The separate code is
deliberate: UNKNOWN_SELECT_FIELD claims the name does not exist, while the entity's own field
catalogue publishes it — a client that honestly took the name from there would otherwise read
the answer as "the catalogue is lying".
Only the name is checked. Custom fields (UF_*, ufCrm*) are accepted as before on every
entity; product properties are each accepted on THEIR OWN entity: PROPERTY_295 on products,
property295 on catalog products, where their numbers are assigned by the portal; the other
spelling is not understood by the method and the gate rejects it. A * (or UF_*) value still
means "return every field", and a typo next to it causes no refusal — a warning arrives instead.
In both batch calls — the global one and the per-entity one — the selection refuses only its own
sub-call while neighbouring ones still run.
What integrators should do
Check the field names in your select against the list in the error message or against the
GET /v1/{entity}/fields schema. A request that used to "work" but returned records missing
some of the requested fields will now answer 400 naming exactly which name was not found —
which was its original mistake.
FIX-0824-15: the `datePeriod` field schema is now visible in `GET /v1/bookings/fields`
Before
The datePeriod field was described as "type": "object" with no nested schema. The shape of the object could not be learned from the contract itself — a client would send {} or a date string, get 422 BITRIX_ERROR, and could only guess the required keys from the error text.
After
Fields of type object with a known nested shape now carry a properties key holding a recursive schema of the nested keys. For datePeriod that is from and to, each with timestamp (number, Unix seconds) and timezone (string, an IANA zone name). The same key now appears for requisiteLink in GET /v1/orders/fields, and on both surfaces at once — fieldsDetailed in GET /v1/guide carries it too, so the shape is visible without portal tokens. Arrays of objects still carry no element schema. The change is additive: existing field-description keys stay in place, and create-time validation was not tightened.
NEW-0824-16: Scrum kanban stages and sprint listing in the API
Four operations for working with the columns of a Scrum board. Stages belong to a sprint rather than to a workgroup — different sprints of one project hold independent sets of columns — so the collection is addressed through the sprint.
GET /v1/scrum/sprints returns the sprints of a project. It is the only way to obtain the sprintId the other three operations need; without the groupId parameter it returns every sprint the key can see.
GET /v1/scrum/sprints/:sprintId/stages returns the columns of a sprint, converting id, sort and sprintId to numbers. POST /v1/scrum/sprints/:sprintId/stages creates a column and returns it re-read in full, including the defaults Bitrix24 substitutes. PATCH /v1/scrum/stages/:stageId renames, recolours and reorders a column.
Platform-side validation closes the places where Bitrix24 answers with success and stores something else: a name longer than 255 characters, a colour longer than six characters and a type outside NEW, WORK, FINISH now get a clear refusal instead of a silent substitution. A colour may be sent with a leading #, which is stripped.
Moving a column to another sprint is not exposed: sprintId in the request body is rejected. Updating a stage that does not exist, or that belongs to a project the key cannot access, answers STAGE_NOT_FOUND_OR_NO_ACCESS — the two cases cannot be told apart, because Bitrix24 answers both identically.
FIX-0824-17: ACCOUNT_FROZEN is now documented on server creation
The 402 response of POST /v1/infra/servers listed only the plan, trial and Marketplace-subscription refusals. It omitted ACCOUNT_FROZEN, the code the platform answers to a portal whose wallet is frozen, even though the endpoint already returned it: the balance check runs ahead of the handler, and server creation is not exempt from it.
Because of the omission, a client generated from /v1/openapi.json treated the code as impossible and dropped a refusal that has a clear remedy — top up the balance. Endpoint behaviour is unchanged; only the description was.
2026-08-23
BC-0823-1: OpenAPI no longer offers create-only fields in PATCH
Old format supported until: not provided
Before
Fields accepted by the API only when creating a record appeared in GET /v1/openapi.json as ordinary PATCH body properties. A client generated from that schema could send them on update and receive 400 READONLY_FIELD.
After
For entities with create-only fields, PATCH uses a separate input schema without those fields. Other entities keep using their existing PATCH input schema. In both cases, the POST input schema preserves the create contract.
What integrators should do
Regenerate the SDK from the new OpenAPI schema and use the type referenced by each PATCH requestBody. Affected entities use UpdateInput; other entities keep Input. Remove fields allowed only during creation from update payloads; POST continues to use Input.
NEW-0823-2: several employees can work on one application's code
A server owner can now assemble a development team: portal employees work with the server using their own personal keys, with no key handover and no server rebinding. The Developer role gets deploy, exec, upload, logs and the source depot; the Admin role additionally manages the machine: lifecycle (stop, start, reboot, sleep schedule, repair), settings and security, plan and disk, backups, the custom domain, the catalog card, the application audience and the costs. Team membership, server deletion, access links and key rebinding stay with the owner. Team membership is edited by the owner or a portal administrator on the Collaboration tab.
Access comes from membership, not from the key, so removing somebody from the team closes
their access immediately. The feature is gated by the server-collaboration flag.
A team member's deploy differs from the owner's in three ways. Environment variables are
preserved: the env field of their request is not applied (the response carries a warning),
and the variables already set on the application survive the deploy — including galaxy
applications, where every deploy used to wipe them. The Bitrix24 catalog card and the release
announcement stay with the owner: displayName, description and the changelog
announcement sent by a Developer are ignored with a warning, while the version note itself is
still stored in the source depot.
There is also an overwrite guard: the baseVersionId field in the body of
POST /v1/infra/servers/:id/deploy ("this is the source version I
based my work on", e.g. v12). If a newer version has appeared in the depot, the deploy is
refused with SOURCE_VERSION_STALE, carrying the current version number and a download link —
instead of silently overwriting.
The declared version is checked for existence: a number ahead of the depot is refused with
BASE_VERSION_NOT_FOUND and the current version number — otherwise any non-existent number
would switch the guard off.
On a server whose owner has assembled a development team the field is required as soon as the
depot holds any version: a deploy without it answers BASE_VERSION_REQUIRED and names the
current version. A blind deploy over a colleague's changes is therefore refused even when
nobody knew about the guard. On a server without a team the field stays optional and a deploy
without it behaves exactly as before. So that the version number has somewhere to come from,
the version-download response now carries a versionId field next to the link — see
Source code storage.
A 409 EXEC_BUSY refusal now names whoever is holding the server: error.hint.holder carries
the employee name, the kind of operation and when it started, and error.hint.reason begins with
that name. A background platform operation has no human behind it, so holder.name comes back
empty there. The field is additive — the existing fields of the refusal are unchanged.
A team member now sees the server in the API instead of working with it blind.
GET /v1/infra/servers also returns the servers whose development team
the caller belongs to: those rows carry an access block with via: "collaborator", the
role, the allowed actions and the endpoints open to them, while the key's own rows carry
access.via: "owner". GET /v1/infra/servers/:id answers a member with 200 and a reduced
card (no IP, no SSH access, no managing key; costs and the sleep threshold follow the role)
instead of the former 404. GET /v1/me lists the memberships in infra.collaboratorServers.
Waking the machine, POST /v1/infra/servers/:id/wake, is open to
the Developer role as well — the matrix promised it before, but the call answered 404.
Searching for an employee to configure the audience,
GET /v1/infra/servers/:id/b24-users, is open to the Admin role —
it used to answer a refusal even though that same role already configures the audience itself.
Additionally: a team admin can now perform, via their own personal API key, every server
operation their role allows — lifecycle control (stop, start, reboot, sleep, manual repair,
wake schedules, metrics), settings (SSH access, port, portal event subscriptions, switching the
BLACKHOLE/OPEN mode via PATCH /v1/infra/servers/:id/mode), the
catalog card (PATCH /v1/infra/servers/:id — name and description),
and access audience (access policy, user/department access list). Previously these operations returned 403/404 via a
member's V1 key regardless of role — they only worked from the Vibecode dashboard. V1 membership
lists can now return a page: GET /v1/infra/servers accepts optional page and limit
and returns a true total alongside. Without the parameters the response is unchanged and
complete — the contract stays as published. In /v1/me the infra.collaboratorServers
block gained a total field.
Deleting source versions stays with the owner: DELETE /v1/infra/servers/:id/sources/:versionId
and the bulk POST /v1/infra/servers/:id/sources/cleanup answer a team member of any role with
403 NOT_AUTHORIZED. Listing, downloading, depositing and tagging remain open to them.
A member's deploy that must preserve the environment refuses to run when the environment cannot
be read: instead of silently deploying with an empty environment it answers
502 GALAXY_PRESERVED_ENV_UNREADABLE with a retryable marker. The application keeps running
its previous version.
A role refusal is now distinct from "no such server". A management operation not covered by
the member's role answers 403 SERVER_ROLE_FORBIDDEN; error.hint carries their role, the
role required, the denied action and the endpoints that are open to them. For an unrelated
key the existence of the server stays hidden — it still gets a 404.
An app whose owner has been deleted survives only while its team still has an admin — someone who can take it over. A team of developers alone does not hold the app: nobody can manage it, so it is cleaned up like any other ownerless app.
The appUrl field in GET /v1/infra/servers, GET /v1/infra/servers/:id
and GET /v1/me (the infra.collaboratorServers block) no longer promises a team member a
link they are not allowed to open. Previously the field came back non-empty as soon as the
server had a subdomain, regardless of whether the application's audience actually let that
particular person open it — following such a link answered with a refusal. Now appUrl is
empty whenever the audience (accessPolicy) does not open the application to every portal
employee and the member has no personal access grant. There is one exception: a member whose
access comes through a department grant does not see the link yet even though they could open
the application — for now the server's owner or team admin has to hand it to them directly.
FIX-0823-3: platform public URLs no longer point away from the stand
Before
OAuth metadata (GET /.well-known/oauth-authorization-server) built issuer and the endpoint URLs from the APP_URL variable, which the deployment does not set for the backend, with a http://localhost:3000 fallback. With that variable unset the document advertised an address no client can reach.
The welcome page at GET /v1/me had a different cause with the same outcome: it built every URL from the instance domain at once — both the images and the "Documentation" / "Home" buttons. On a stand those buttons led to the production dashboard.
After
OAuth metadata resolves through a shared helper that refuses a loopback value in production and falls back to the instance public domain.
The welcome page now keeps two addresses apart: images still load from the instance domain (otherwise they would not open), while everything clickable leads to the dashboard of the stand the page was opened from.
2026-08-22
NEW-0822-1: promo code on an occupied seat: the preview returns branches and redemption executes them
Before
POST /v1/cowork/coupon/preview answered only what the code grants, and on an occupied seat
redemption refused with "the seat is already paid" — with nothing the person could do about it.
After
The preview returns a decision field — what this code does to the seat and what may be offered:
apply, extend (same tier, the paid window moves), choose (branches to pick from, where
"apply now" carries losesDays — the whole days of paid term that burn) or refuse with a refusal
code. The chosen branch goes to POST /v1/cowork/coupon/redeem in the new optional field action
(apply | extend | force | resume-and-apply); omitting it keeps the previous behaviour.
The action is re-checked at redemption time: if the seat changed meanwhile, the answer is
409 COUPON_ACTION_NOT_AVAILABLE and the code is NOT spent. The correct reaction is to re-read the
preview and offer the branches it returns now. An unknown action value answers 400 INVALID_ACTION
— such a body previously answered 400 INVALID_CODE, pointing at the code field, which was not the problem.
BC-0822-2: the create and rotate responses now match the documented key shape
Old format supported until: not provided
Before
On some accounts POST /v1/keys and POST /v1/keys/:id/rotate returned the whole key
row with only the secret fields removed. Alongside the documented shape, the response
carried internal platform fields that appear neither in the key-shape reference nor in
GET /v1/keys and GET /v1/keys/:id: preMigrationScopes, ownerActive, deletedAt,
isOAuthApp, appId, purpose, scopesAuthoritative, linkedServerId,
userAgentAutoModel, webhookScopesRepairedAt, tokenExpiresAt.
After
Both methods return exactly the documented key shape — the same field set as
GET /v1/keys/:id, plus the one-time rawKey. The internal fields listed above are
gone from the response; none of them was documented or part of the contract.
If your integration read any of them, switch the source: key state is status, access
mode is accessMode, and the issuing channel is issuedVia. Documented fields, expiry
and error codes are unchanged.
BC-0822-3: key rotation now requires the same Bitrix24 plan as creation
Old format supported until: not provided
Before
On an account whose Bitrix24 plan does not grant access to the platform,
POST /v1/keys/:id/rotate issued a new key, while POST /v1/keys on the same account
answered with a plan-required refusal. Rotation stayed a way around that requirement.
After
Both methods answer the same way. On an account whose plan does not grant access,
rotation returns 403 with the same code as creation, INT_TARIFF_REQUIRED.
Only accounts without a qualifying plan are affected. Self-hosted accounts, accounts on a qualifying plan and already-issued keys are unchanged: nothing is revoked or stopped, and rotation resumes as soon as the account is on a qualifying plan.
NEW-0822-4: a key now reports the channel it was issued through, and a refusal reports its exact cause
Before
POST /v1/keys and POST /v1/keys/:id/rotate did not report which channel issued the
key on the account. On accounts where only the platform module can create an inbound
webhook, both methods answered with an issuance error even though issuance on the
account itself worked.
After
The key shape carries an optional issuedVia field — the issuing channel; it arrives
in the responses of POST /v1/keys, POST /v1/keys/:id/rotate, GET /v1/keys,
GET /v1/keys/:id and PATCH /v1/keys/:id. A refusal body gained an optional
error.reason field with the exact cause, while error.code on existing refusals is
unchanged. Accounts where the platform module issues keys are now served by both
methods.
The create and rotate responses are now identical regardless of the issuing channel —
some channels used to return a narrower field set. The message field stays
human-readable and may change: branch on error.code, not on its text.
FIX-0822-5: a rotation refused over scopes now reports the same code on every account
Before
POST /v1/keys/:id/rotate on a key left with only application-context scopes —
placement, entity, userfieldtype — answered differently depending on the channel the
account issues keys through. Where the platform module issues the key,
400 PERSONAL_KEY_WEBHOOK_SCOPES_INVALID arrived, while other accounts got a generic
502 BITRIX_UNAVAILABLE after a failed issuance attempt.
After
The scope set is checked before the account is contacted, so the refusal is the same
everywhere — 400 PERSONAL_KEY_WEBHOOK_SCOPES_INVALID, the code creation and update
already return. Nothing changes on the client side: the refusal is more precise and
arrives earlier. To let rotation through, add a data scope to the previous key via
PATCH /v1/keys/:id.
NEW-0822-6: platform requests to Bitrix24 now carry a signed origin proof
Before
The portal refused the call on plan grounds or for a missing Market subscription, and a platform request looked to it exactly like any other integration.
After
Every Bitrix24 request carries an X-Vibecode-Origin header — a short-lived signed token. The token names the origin: cowork for a paid Cowork/Code seat key, app for every other user key, vibecode for the platform's own service calls. A portal with the Vibecode connector module installed admits Cowork/Code requests on that proof; other values grant nothing. Portals without the module ignore the header; existing integrations are unaffected.
FIX-0822-7: Reliable source downloads for dedicated VM deploys
Before: a dedicated virtual machine fetched source.url through its own network path. One orchestrator retry did not tolerate intermittent egress or NAT failures, and callers received raw agent networking text.
After: for servers enrolled in the staged rollout, the platform downloads the archive and streams it to the VM through the tunnel, with up to four attempts and backoff inside a shared five-minute budget. The compatible VM-download fallback and public /upload receive bounded retries, a safe outbound policy, and stable messages under the same rollout; before enrollment, the existing contract is preserved. DEPLOY_FAILED is unchanged; in the new mode a failed download step adds causeCode, retryable, and attempts. See the deploy documentation.
Impact: request payloads do not change. Automation can branch on the new fields instead of parsing message text; archives up to 500 MB are streamed and do not consume an inline-memory slot. For public /upload, the long phase in the new URL mode completes under an HTTP 200 keepalive response, so determine success or failure from JSON success and error.code; pre-dispatch checks and pre-enrollment behavior retain their previous HTTP statuses.
BC-0822-8: rotating and bringing back platform-issued keys is closed
Old format supported until: not provided
Before
POST /v1/keys/:id/rotate rotated any key of its owner, and
PATCH /v1/keys/:id moved any key back to ACTIVE and pushed its expiry
back — including the keys the platform issues on its own separate endpoints: a Cowork/Code desktop
key, an agent key and a project deploy key. In the GET /v1/keys listing such a key is barely
distinguishable from a personal one, so a "rotate or switch back on all my keys" script walked over
it along with the rest: rotation answered 201 with a fresh rawKey, switching back on answered
200 — and the key value never changed there, so the previous one started working again.
After
Both operations on such a key answer 403: rotation with the code SYSTEM_KEY_ROTATE_FORBIDDEN,
moving back to ACTIVE and extending the expiry with the code SYSTEM_KEY_REACTIVATE_FORBIDDEN.
The response text says where a key of that class is issued again. The key is left as it was in both
cases. Both operations bypassed the checks that guard the issuance of each such key: for keys
carrying vibe:cowork — Cowork/Code access, the account "third-party clients" policy and the
subscription state; for a project deploy key — three platform switches, the Cowork/Code key
requirement and an active subscription. Rotation also handed out a value with no expiry at all: on a
project deploy key that removed the seven-day lifetime, the only thing that limits a leaked key.
Revoking a key and shortening its expiry are still allowed: those stop a key rather than hand one
out. The former behaviour is gone at once, with no transition period.
What integrators should do
Exclude the keys the platform issues from bulk rotation and bulk switch-on: a Cowork/Code desktop
key, an agent key and a project deploy key. The first two are recognisable in the GET /v1/keys
response by the vibe:cowork scope; a project deploy key has no separate marker in the listing, so
treat a 403 with these codes as the final answer for that key and do not retry. A desktop key is
issued again by connecting the Cowork/Code desktop app, an agent key — by re-issuing the agent key
in the Vibecode dashboard, a project deploy key — by calling POST /v1/cowork/deploy-key again.
Other keys rotate and switch back on as before.
NEW-0822-9: POST /v1/cowork/deploy-key is closed to external-agent keys
Before
The endpoint checked the vibe:cowork scope only.
After
On POST /v1/cowork/deploy-key a key issued for a third-party agent gets
403 COWORK_HARNESS_KEY_FORBIDDEN. Such a key lives inside
someone else's application, while minting a project deploy key revokes the previous one and moves the
owner's servers and applications onto the new key. Desktop and agent keys are unaffected.
NEW-0822-10: Reactivating a subscription key checks the issuance gates
Before
A subscription-billed key went back to active through an ordinary status field change, and its
expiry was cleared with null. Only the permanent block was checked, so a revocation by the
Bitrix24 account administrator was undone in a single request — and the secret itself never changed.
After
PATCH /v1/keys/:id answers 403 with the matching gate code when the
request moves a subscription key to ACTIVE, or pushes back or clears expiresAt, while Cowork
access, the Bitrix24 "third-party clients" policy or the subscription state is closed. Shortening
the expiry, revoking and editing the other fields work as before. Ordinary keys are unaffected.
NEW-0822-11: Rotating a subscription key checks the issuance gates
Before
Rotation carried the key's scopes and purpose over verbatim, asking nothing.
After
POST /v1/keys/:id/rotate answers 403 with the matching gate code when
rotating a subscription-billed key while Cowork access, the portal "third-party clients" policy or
the subscription state is closed. Ordinary keys rotate as before.
NEW-0822-12: POST /v1/keys rejects subscription billing
Before
The billing field did not exist in the request body; the unknown key was silently dropped by the schema.
After
On POST /v1/keys the value billing: "subscription" is rejected with
BILLING_MODE_NOT_SUPPORTED. Subscription-billed keys are
issued from the Vibecode dashboard only — that is where the access, Bitrix24 account policy and
subscription-state checks live, and the public route has none of them. billing: "wallet" and requests without the field
behave exactly as before.
FIX-0822-13: The inactive-subscription refusal now points at the subscription page
Before
The 402 refusal carrying code cowork_subscription_inactive advised activating the
subscription by calling POST /api/cowork/subscription. That route works only inside a
signed-in web session and is unavailable to a client authenticating with an API key. A
third-party agent client shows the message field verbatim, so the advice looked actionable
while there was nothing to act on.
After
The same refusal on POST /v1/chat/completions names the state and the place where it changes: the subscription is resumed in the Cowork/Code section of your Vibecode account, and the key stays the same. The code, the status and the other body fields are unchanged.
2026-08-21
FIX-0821-1: iblockTypeId=structure is accepted on /v1/lists
Before
iblockTypeId accepted only lists, lists_socnet, and bitrix_processes. The value structure (the absence-calendar infoblock type) returned 400 INVALID_IBLOCK_TYPE, even though GET /v1/lists/:iblockId/type returned it.
After
structure is in the allowed set on every /v1/lists route. The Bitrix24 response for that type (data or 422/403) is no longer replaced by our 400. Other unknown types still return 400 INVALID_IBLOCK_TYPE. The default is unchanged: lists.
Impact on integrations
Requests to structure infoblocks, including the stock absence calendar, now reach Bitrix24. Existing integrations that use other types require no changes.
FIX-0821-2: currencyId and explicit selection of non-returned fields
Before
POST /v1/products and PATCH /v1/products/:id accepted the currency only under the currency name. An explicit selection of a field marked notReturned: true was accepted without a warning even though the response contained no value.
After
Product write methods accept currencyId as an additional name for currency. If both names are present, currency wins, and responses still carry only currency. The additional name is marked writeOnly and notReturned in /fields, /v1/guide, and OpenAPI. Explicitly selecting any non-returned canonical name now adds an UNKNOWN_SELECT_FIELD warning: products.currencyId, tasks.realStatus, product-sections.sort, bank-details.entityTypeId, telephony-lines.serverName, bizproc-templates.templateData, and catalog-products.iblockSection. The native Bitrix24 name select=CURRENCY_ID continues to select the readable products.currency field. Each name is covered by the field references for products, tasks, product sections, bank details, telephony lines, business process templates, and catalog products. Currency filtering remains unsupported and returns 400 UNSUPPORTED_FILTER.
Impact on integrators
Read requests still return 200 and the other selected fields, but now carry a warning. Remove non-returned names from select: use currency for the product currency and status for the task's actual status.
FIX-0821-3: product description filter is no longer rejected
Before
GET /v1/products with filter[description] and POST /v1/products/search with the same filter returned 400 UNSUPPORTED_FILTER, even though the description was stored when the product was created.
After
Exact match and $in on description are accepted the same way as on name. Operators, including $contains, still return 400. The price and currency filters are unchanged.
FIX-0821-4: the refusal on creating an employee without a department now names the field to pass
Before
POST /v1/users without departmentId on an account with the extranet module installed answered 422 with the text no_extranet_field. No field of that name exists in the request body or in the GET /v1/users/fields output, and nothing in the response pointed at the department — the refusal gave no way to tell what to correct. The employee was not created.
After
The response now carries a hint: it names the field in both spellings — UF_DEPARTMENT for a direct call and departmentId for the POST /v1/users wrapper (both are accepted) — points at GET /v1/departments as the source of values, gives the root department as a working example, and mentions POST /v1/users/invite, which supplies the department itself. For an external user the alternative is named — EXTRANET together with SONET_GROUP_ID instead of a department. The departmentId description in the field reference now states the requirement too, so the condition is visible before the request is sent.
What integrators should do
No action is required: the status code and the envelope shape are unchanged, only the hint was added. The departmentId field is deliberately not marked mandatory on the Vibecode API side — an account without the extranet module accepts a create with no department, and a hard requirement would break those requests.
NEW-0821-5: Open Channels dialog metadata via the API
A new endpoint POST /v1/openlines/dialogs/lookup — a wrapper over the Bitrix24 method imopenlines.dialog.get. It returns the card of a single Open Channels dialog (name, type, line, message count, dates) by one of the identifiers: chatId, dialogId or sessionId. It does not return the message history. Requires the imopenlines scope.
The response carries a derived lineId field — the line identifier parsed from the dialog binding; it is the entry point to GET /v1/openline-configs/:id. Lookup by sessionId resolves a dialog straight from a session identifier taken from the id field of POST /v1/openlines/sessions/search.
BC-0821-6: a structured value in a scalar field is refused instead of silently lost
Old format supported until: not provided
Before
An entity write accepted an object or an array in a field declared scalar and answered with success. POST /v1/deals carrying {"title": {"a": 1}} returned 201 while the deal card showed the string Array — that is how Bitrix24 casts an array to a string. The value could not be recovered and the client saw no error.
After
Such a request is refused before the Bitrix24 call — 400 with code INVALID_PARAMS and the field name. The rule covers fields declared string, number, boolean, date and datetime, on entity writes under /v1/<entity>: create and update, POST /v1/batch, per-entity batch write and import.
What did NOT change: a number or a boolean in a string field is still accepted (Bitrix24 stores 123456 and 1, so nothing is lost), null is still accepted, and fields declared object, array or multi-value accepted structures before and still do.
Impact on integrators
Review any code that builds a write body from an external source: where an object or an array reached a scalar field, the request used to succeed while losing the value and will now return 400. Send a scalar value to such a field.
The boundaries are stated explicitly. A name absent from the entity schema (user fields UF_*, propertyNNN, a typo) is not checked — it has no declared type. The refusal shape differs by surface, and so does its reach. The global batch refuses only its own sub-call and puts the failure in data.errors["<id>"] with separate code and message fields. The per-entity batch write and import refuse the whole request: a 400 with code BATCH_ITEM_VALIDATION or IMPORT_ITEM_VALIDATION, with the element index and INVALID_PARAMS inside message. Such a response carries no per-item results, and nothing reaches Bitrix24. A few bespoke write routes are not covered yet — addresses, task comments, document templates, Open Channels config and product rows; there the previous behaviour still applies, and they are closed separately.
FIX-0821-7: PATCH /v1/infra/servers/:id/access-policy now preserves the access list on policy change
Before
Switching PATCH /v1/infra/servers/:id/access-policy away from NAMED_USERS or DEPARTMENT to any other policy permanently deleted the access list (users and departments added via POST /access) — even though the docs promised the records stay in the database and simply stop applying until the policy becomes a named one again.
After
The list is preserved when leaving NAMED_USERS/DEPARTMENT — behavior matches the documentation again.
Impact on integrators
No action required — behavior now matches the already-published documentation.
BC-0821-8: a busy shared galaxy exec channel now answers 409 instead of 502
Old format supported until: not provided
Before
POST /v1/infra/servers/:id/exec for an application in a galaxy (kind: "GALAXY_APP") answered 502 with the EXEC_BUSY code when the host's shared exec channel was busy. The response carried neither a Retry-After header nor the retryable / retryAfter fields, so a machine client read the refusal as a gateway failure and did not retry. The same refusal on POST /v1/infra/servers/:id/deploy already arrived as 409 with a retry signal.
The error.hint in 409 EXEC_BUSY responses on galaxy routes offered to unstick the channel via POST /v1/infra/servers/:id/unstick. That call is not available to an application owner or a host owner by contract — it answers 409 GALAXY_UNSTICK_UNSUPPORTED, because the exec channel is shared by every application on the host.
After
A busy shared host exec channel arrives as 409 with the EXEC_BUSY code, a Retry-After header and the retryable: true / retryAfter (seconds) fields — the same contract as every other "busy" refusal. Other execution failures for an application in a galaxy still arrive with status 502.
Two different states now share the 409 status, and they are machine-distinguishable by the presence of error.hint.autoExpiresInSeconds: the application's own lock carries it (the remaining lock TTL), a busy shared host channel does not. recoveryAction cannot tell them apart: on a galaxy application and on the galaxy host itself that field names no concrete call at all — only "wait" and "retry" — because releasing a lock there can abort a running operation and, on a shared host, the commands of neighbouring applications with it. The machine-actionable advice is therefore the same in both states — wait and retry at the Retry-After interval.
Important: this is said about the galaxy states, not about the /exec route as a whole. On a dedicated virtual machine (kind: "STANDALONE") the recoveryAction of the same 409 still names DELETE /v1/infra/servers/:id/lock, unconditionally — and against a running deploy that call removes the per-server serialization the deploy relies on. So recoveryAction must never be executed without reading error.hint.recovery first, in any state. The lock-release address stays in error.hint.recovery, with the server identifier substituted, the owning key required and the caveat that it is only for when you have confirmed nothing is running.
Reading the log answers by the same contract now. GET /v1/infra/servers/:id/logs for an application in a galaxy goes through that same shared host exec channel, and with the channel busy it used to answer 200 with an empty logs list — claiming the application had no output. The busy state now arrives as 409 with Retry-After, and other read failures as 502 carrying the agent's code.
What integrators should do
A client that branched on the HTTP status and treated 502 on /exec as a terminal failure must stop doing so: the busy state now arrives as 409 and should be retried at the Retry-After interval. A client that read error.code needs no change. If your flow called POST /v1/infra/servers/:id/unstick because the platform suggested it, that call never worked on galaxy servers — replace it with a retry, and contact support if the refusal persists. If you branch between the two 409 states of /exec in code, key on the presence of error.hint.autoExpiresInSeconds rather than on the text of recoveryAction.
BC-0821-9: the per-entity batch now explains what it did with select
Old format supported until: not provided
Before
POST /v1/{entity}/batch with the list action applied select to the response but said
nothing about what it dropped. A name the entity does not have simply vanished: the record came
back narrowed and no explanation existed anywhere — no error, no warning. The single list and the
global batch both report such a name as a warning; this door was the only one where
field selection was completely silent.
After
The sub-call carries its own meta.warnings with the UNKNOWN_SELECT_FIELD code and the field
name — the same shape the single list uses. The warning belongs to ITS sub-call: neighbouring
items get no meta of their own, and the key never arrives as an empty array — nothing to say
means no key.
Entities where an unknown name answers with an error (today, calendar events) now refuse such a
sub-call here too — UNKNOWN_SELECT_FIELD before the Bitrix24 call. The refusal is per-call, as
in the global batch: only that item fails, the other forty-nine still run. Before, the hard
selection guard did not fire on this door at all.
The value * still means "return every field": no selection is applied and an unknown name next
to it refuses nothing. A warning still arrives: a select of *,titel returns every field AND
says the second name means nothing — exactly as on the single list and in the global batch.
Impact on integrators
The additive half breaks nothing: meta is a new optional key next to data and total. The
breaking half is the hard selection: a sub-call that used to answer with a narrowed record and
code 200 now answers with an error in its slot on calendar events. There is deliberately no
support window: the previous behaviour was itself the defect — the requested name vanished
silently, with nothing to tell that apart from "the field is not in the record". Review handlers
that treated the absence of an error as proof that every select name was recognised.
NEW-0821-10: Open Channels session transcript via the API
A new endpoint POST /v1/openlines/sessions/history — a wrapper over the Bitrix24 method imopenlines.session.history.get. It returns the message history of a chat's latest Open Channels session by the chat identifier: messages, participants and file metadata in one response. Requires the imopenlines scope.
Input is by the chat identifier only (chatId, the chat2043 form is accepted too). By it the latest session of the chat is taken. The method has no pagination — the transcript comes in full. For page-by-page reading of messages use GET /v1/chats/:dialogId/messages.
The endpoint is enabled gradually by the Vibecode platform: while it is off, the call answers 403 OPENLINES_HISTORY_DISABLED — a sign the capability is not active yet, not an integration error.
NEW-0821-11: galaxy app multipart deploy stores the archive in source storage
The multipart deploy of a galaxy app no longer buffers the whole archive in platform memory: where
multipart depositing is enabled, the archive is stored as a version in the
source storage and reaches the host through a signed link. Where the host
is allowed to fetch the archive itself, a recognised tar.gz is downloaded by the host — verifying
the size and checksum recorded for that version — and a failed fetch then arrives as a separate
UPLOAD_DOWNLOAD_FAILED / UPLOAD_EXTRACT_FAILED / UPLOAD_NO_SPACE code in the buildLog tail;
a zip and an unrecognised format keep the previous path, through the agent. The size limit on an
archive sent in the request body is unchanged — the body still travels through the platform. The
version stays in the depot even when the deploy fails: it is listed among the versions and can be
redeployed by source.versionId.
2026-08-20
NEW-0820-1: the Cowork subscription snapshot is described in the API specification
Before
GET /v1/cowork/me worked and was listed in the /v1/guide directory, but the V1 specification did not carry it. You could not generate a client from the spec or verify the response shape — reading the directory by eye was the only option.
After
The operation is described: tier and subscription state, the three quota windows as integer percentages with their reset time, and the next charge date. It also states that the successful body is the object itself, with no success wrapper, while errors arrive in the usual { success: false, error: { code, message } } envelope. The offPeak and relief blocks are described as absent when the feature is disabled: check for the presence of the key rather than comparing the value with null.
FIX-0820-2: `include` examples use relation names
Before
OpenAPI and MCP showed plural related-entity names for the include parameter, causing requests to fail with INVALID_INCLUDE.
After
Examples use the actual relation names contact,company.
FIX-0820-3: MCP now forwards timeline log action parameters correctly
Before
The manage_timeline_log tool accepted only string id values, omitted the request body for pin and unpin, and omitted query parameters for get_note and delete_note.
After
The tool accepts string or numeric id values and forwards the required parameters for these actions to /v1/timeline-logs/*. The delete description now states that personal keys cannot delete these entries: deletion is available only to the same OAuth application that created the entry.
NEW-0820-4: calendar sections now expose schema and search routes
Before
calendar-sections had no GET /v1/calendar-sections/fields or POST /v1/calendar-sections/search, so agents could not discover fields and valid calendar types in advance or use the standard search surface.
After
GET /v1/calendar-sections/fields returns the static schema with required fields and the user, group, company_calendar, and location types. POST /v1/calendar-sections/search accepts owner context through filter.type and filter.ownerId; additional filters are explicitly rejected because the Bitrix24 method does not support them.
FIX-0820-5: personal keys report only executable scopes
Before
GET /v1/me could report the placement, entity, and userfieldtype scopes for a personal key even though these features require an OAuth application context. Key creation and update also accepted userfieldtype as a regular scope.
After
GET /v1/me, personal-key creation, and personal-key update exclude placement, entity, and userfieldtype. The userfieldconfig scope remains available. OAuth application keys are unchanged.
Impact on integrations
No action is required. Use an OAuth application key for placements and custom user field types.
FIX-0820-6: the tasks permission now works on every task operation, no matter how the key was issued
Before
A key holding the tasks permission was refused on some task operations — which ones depended on how the portal had issued the key's webhook. Bitrix24 accepts two spellings of the same tasks permission (task and tasks), and they open different groups of operations: older task operations require the first spelling, newer ones the second. A key kept the spelling its owner had picked, and on some issuing paths only that one spelling reached the portal. Inside POST /v1/batch such a refusal arrived within a 200 response, on the sub-command rather than the whole request.
After
The tasks permission is registered on the portal in both spellings regardless of the issuing path, so all task operations are available. Keys issued earlier reach the same state when their webhook is reconnected. The key's own permission list and the GET /v1/me response are unchanged — no client action is required.
FIX-0820-7: bot chat creation returns the identifier the other actions accept
Before
After creating a chat, the manage_bot_chat tool put a numeric identifier in the most visible places — data.chat.id and data.recentConfig.chatId. Reusing that number in the next action — GET /v1/bots/:botId/chats/:dialogId, leaving the chat, transferring ownership — returned a BITRIX_ERROR "chat does not exist": for Bitrix24 a bare number in dialogId means the personal dialog with the user of that id, not the group chat with that id. The usable identifier, such as chat471, sat deeper in data.chat.dialogId, and the tool description did not name that format.
After
The chat creation response starts with a chatId field holding a value such as chat471, and that is the value the other actions of the tool accept. The Bitrix24 fields data.chat.id and data.recentConfig.chatId are unchanged and stay numeric. The description of the chatId parameter now names both formats: chatN for a group chat, a bare number for the personal dialog with that user.
Impact on integrations
MCP callers need to do nothing. The POST /v1/bots/:botId/chats response is unchanged for REST: there you still have to take chat.dialogId rather than the numeric chat.id. GET /v1/bots/:botId/chats/:dialogId and the other actions still accept both chatN and a number as a personal dialog identifier.
FIX-0820-8: AI provider refusal: a readable message instead of relayed prose, and one error shape for streaming and non-streaming
Before
When the AI provider refused the platform's request, POST /v1/chat/completions,
POST /v1/embeddings and POST /v1/audio/transcriptions answered 502 ai_provider_unavailable
and put the provider's own wording into error.message verbatim. The provider could report a
failure of its own infrastructure as an authorization error — so the response spoke about
authorization where neither the caller's API key nor the caller's network was involved, and the
caller went auditing both.
Streaming (stream: true) returned the same failure in a different shape than a plain request:
code arrived in upper case instead of lower case, there was no type field at all, and
retryAfter and retryable were reserved for a stalled stream and for overload. A client
branching on error.type or on retryable could not tell a temporary refusal from a final one,
and had nothing to wait on.
After
The wording now depends on who owns the credential the provider refused. A key the caller
connected themselves (BYOK) — the provider's message passes through as before: it is addressed to
the key owner and tells them what to do. An account-level credential — the response names the
object and the person who can update it, and states plainly that retrying will not help. A
platform credential — the response says the caller's API key and network are not the cause. The
502 status, the ai_provider_unavailable code and the providerStatusCode field are unchanged
in every case.
The streaming error took the same shape as the non-streaming one: code in lower case, type
present, and every frame carrying a retryAfter pause now carries retryable: true as well. Both
fields appeared for a temporarily unavailable provider, for rate limiting and for the cooldown
after a run of failing calls — previously only a stalled stream had them.
The retry hints now agree with the wording of the response: a refusal a retry cannot fix carries
neither the pause nor the flag. That covers a refusal of the request body itself
(ai_provider_rejected) and a provider-refused account credential or the caller's own key, where
the response states outright that the credential has to be updated. A temporarily unavailable
provider stays retryable.
FIX-0820-9: an attachment whose extension does not match its content is no longer rejected
Before
POST /v1/feedback/attachments compared the type declared by the client with the file's own signature and answered 400 MIME_MISMATCH when they differed. The client derives that type from the extension, so a JPEG saved as image.png arrived as image/png and was rejected — even though both formats are allowed and the file was intact.
After
Processing is driven by the file's actual format. A mismatch between two allowed formats (PNG, JPEG, WebP, GIF) is accepted, the file is re-encoded from its content, and its mime in the response is the result of that re-encode, as before. MIME_MISMATCH is left for the single case where the file's signature is not recognized at all.
FIX-0820-10: MCP now preserves one chat identifier format
Before
The manage_chat.add_users action passed a value such as chat457 to a numeric backend route without normalization, so Bitrix24 reported an empty chat ID. A chat created through the tool could not be left through the same MCP tool, and find looked like text search.
After
add_users and the new leave action accept the standard chatN dialogId value and pass a numeric ID to the route. add_users preserves its legacy positive numeric chat ID for compatibility, while the new irreversible leave action requires an unambiguous chatN; the caller must convert a numeric ID returned by chat creation to chatN before calling leave. A missing or invalid ID is rejected before any network call. The tool warns that an owner must transfer chat ownership first using the numeric ID without the chat prefix. The find description now names the required entityType and entityId parameters for a CRM-linked chat lookup.
FIX-0820-11: addresses now apply select — records narrow, and an unknown name is no longer lost
Before
The select parameter is documented for every entity, but on
addresses it did nothing at all. List, search and get-by-composite-key
answered with the full record no matter how many names the caller listed, and an unknown name
disappeared without a trace — no error, no warning. This was the only entity where field selection
was completely silent.
After
All three address doors apply select the way every other entity does: only the listed fields stay
in the records, canonical names and native Bitrix24 names are both accepted (CITY projects city),
and * still means "return every field".
An unknown name behaves differently from door to door. Get-by-composite-key picks the fields on the
Vibecode side, so a name absent from the schema arrives as an UNKNOWN_SELECT_FIELD warning in
meta.warnings while the record itself comes back — that door gains a meta block only when there
is something to warn about. List and search pass the listed names on to Bitrix24, so the account
decides the outcome there: one that has no such field rejects the whole call — the answer is
422 BITRIX_ERROR, the name is quoted in the message, and no data arrives.
The composite address key — typeId, entityTypeId, entityId — always comes back, even when the
select does not list it. On most entities a single id field plays that role: it is what tells one
record from its neighbour and what the update and delete addresses are built from. On addresses all
three fields carry that role together.
Separately: the name id in select follows the same split. On get-by-composite-key it is no longer
treated as a typo — addresses have no id of their own, so select=id,city used to return a warning
about the id field, and that request now reads as "tell me which record this is" and returns the
composite key. On list and search the name id goes to Bitrix24 and is subject to that same
account check.
Field selection stopped stripping the route key on smart processes and telephony lines
On most entities the operation address is built from id, and select=id,… worked as expected. But
there are two where the route key is named differently, and field selection threw it away: on
smart processes it is entityTypeId, on telephony lines
(/v1/telephony-lines) it is number. Both fields now stay in the response even when the select
does not list them: GET /v1/telephony-lines?select=name returns {name, number} instead of {name}.
On smart processes this removes a trap: the record also carries an internal id field that takes no
part in operation addresses — field selection used to keep exactly that one, and an address built from
it led to a different record.
On every other entity the rule is unchanged and worth remembering: if you list fields in select,
list id too — the Bitrix24 methods that honour the selection (deals, contacts, companies, leads,
quotes, smart process items) return exactly what was asked for, and an unrequested id will not be
in the response.
Impact on integrators
Nothing to change if you never passed select to addresses — the response is the same as before.
A call that passed select and relied on getting the full record back will now receive only the
requested fields: that is the documented behaviour of the parameter, brought in line with every other
entity. Check field names against
GET /v1/addresses/fields.
BC-0820-12: a caller-supplied bot token is validated for length and alphabet
Old format supported until: not provided
Before
PATCH /v1/bots/:botId accepted any fields.botToken value — say support-bot-2026. The call answered 200 and the portal took the new token. There were no length or shape checks: Bitrix24 applies its 40-char cap when a bot is registered and when it is switched to webhook mode, but not on a plain update.
After
A caller-supplied token is validated before the portal call: 32 to 40 chars from the [A-Za-z0-9_-] alphabet. Any value present in the request that does not fit that bound — too short, empty, carrying stray characters, or not a string at all — is rejected with 400 and code BOT_TOKEN_INVALID; the portal is not called and the bot token stays as it was. The bound is the same one used at bot registration, and its lower edge matches the length of the token the platform issues itself.
The reason is that this very token authenticates the events delivered to the bot at POST /api/bot/webhook: a short or guessable token would let anyone forge a bot event without any authentication.
What integrators should do
If you supply the token yourself, use a random value of 32 chars or more (32 hex chars, for instance) from the [A-Za-z0-9_-] alphabet. If you do not supply one, there is nothing to do: the platform issues the token and it passes the bound. There is deliberately no support window for the old behaviour — a weak token already left the bot inoperable, because the platform never stored such a value on its side.
FIX-0820-13: bot events delivered by webhook without a portal address are no longer lost
Before
The POST /api/bot/webhook receiver identified a bot by the pair "portal + bot number": the bot number is a per-portal sequence rather than a global identifier, so the portal had to be resolved from auth.domain or auth.member_id in the request body. For a bot registered through an incoming webhook neither field is reliable: the portal address does not arrive in every envelope, and member_id only resolves for portals whose id the platform already knows. An event without a portal address was rejected with 403 AUTH_FAILED, and imbot webhooks are never re-sent: the user's message was lost for good, and from the platform side it looked as if nobody had written to the bot at all.
After
The bot is identified by the top-level auth.application_token — for a webhook-registered bot that value points at one specific bot on its own, so the portal is no longer needed for it. The former domain-based path is kept and behaves as before: it serves registrations whose token arrives in a different shape. Token verification is not weakened — the comparison stays constant-time, and an event carrying another bot's number in the body is still rejected.
The receiver's rejections also got their own codes — BOT_WEBHOOK_AUTH_FAILED, BOT_WEBHOOK_ID_MISMATCH, BOT_WEBHOOK_INVALID_BOT_ID, BOT_WEBHOOK_BOT_DISABLED — so lost events show up in the rejection statistics instead of only in the logs.
One more change in PATCH /v1/bots/:botId
A token supplied by the caller in fields.botToken is now stored on the platform side as well. Previously it only reached Bitrix24 while the platform kept the old value, leaving the bot silently inoperable in both directions: outgoing calls got 401 and inbound events were rejected.
Impact on integrations
No action required. The receiver's HTTP status codes are unchanged (403 / 400 / 410), and the error field in the body stays as it was, with a code field added next to it. Bots whose events used to be rejected start receiving them without re-registration and without a token change.
FIX-0820-14: the portal receives Bitrix24 scopes only, and a platform-only key no longer gains a webhook on rotate
Before
When a webhook was issued, the portal received not just Bitrix24 scopes but Vibe platform scopes as well — vibe:infra, vibe:ai, vibe:search, vibe:storage. Bitrix24 does not know such scopes and silently ignored them, so the key's real permissions were unaffected, but the set on the wire differed per issuing path: the dashboard sent one thing, POST /v1/keys another, the self-hosted channel a third.
The same divergence had a visible effect on secret rotation. A key whose scope set holds platform scopes only gets no webhook on the portal — there is nothing to register. Yet rotating such a key sent the portal a set of platform scopes alone and got a webhook back that the key did not have before: b24Ready flipped from false to true even though the key still could not call Bitrix24.
After
The portal receives Bitrix24's own scopes only, identically on every issuing channel. When no Bitrix24 scope remains, no webhook is requested at all: the key stays platform-only and b24Ready stays false on create and on rotate alike.
Impact on integrations
The response shape, the error codes and the key's stored scope set are unchanged: vibe:* still appear in scopes and still open the platform sections /v1/ai, /v1/search, /v1/storage, /v1/infra. No action is required. The only visible difference is for anyone who rotated a key holding no Bitrix24 scope and expected b24Ready: true — such a key now honestly answers false, exactly as it does on creation.
FIX-0820-15: on the international segment the tariff refusal names a Vibe+ plan
Before
An account on a free Bitrix24 plan received the INT_TARIFF_REQUIRED refusal whose
userMessage named a paid Bitrix24 plan as the access condition. The pricing page,
meanwhile, states that full access to the Vibecode platform is unlocked by a plan of the
Vibe+ line — the customer read two different conditions inside one product.
After
On the international segment the userMessage of this code names a Vibe+ plan, the same
condition the pricing page sells. Every surface of the code is covered: the refusal body on
infrastructure creation and wake, the capabilities.servers.create slot in GET /v1/me,
the gateway interstitial and the key-issuance toast.
The machine field details.requiredTariffs is unchanged: it still lists the tariffs that
clear the refusal. Build the purchase advice from that field and use the human-readable
string to explain the reason. The refusal code, its HTTP status and the envelope shape are
unchanged.
Kazakhstan and Uzbekistan accounts, self-hosted accounts and the Russian segment keep the previous copy — the Vibe+ line is not sold there.
NEW-0820-16: assigned promo code — the code works only for its recipient
Before
A promo code was redeemed by any portal employee who happened to have it. A workshop batch was handed out by name, but the platform did not know the recipient: a forwarded code worked for whoever entered it first.
Now
A code can be issued to a specific email. Such a code is checked against the
Vibecode account email: on a mismatch POST /v1/cowork/coupon/redeem answers
409 with COUPON_NOT_ASSIGNED_TO_YOU, and the code itself stays unspent and
still available to its recipient. The POST /v1/cowork/coupon/preview check returns
the same code in the reason field.
Codes without a recipient behave as before — any portal employee can redeem them.
NEW-0820-17: blocking a leaked key now reports whether its access links were revoked
POST /v1/platform/keys/revoke-leaked now revokes the access links the key issued along with the key itself, and the response carries a new tokensRevoked field. The key is always blocked; tokensRevoked: false means the key itself is already dead while its links are still alive — the revocation did not go through because of a transient database failure. Repeat the same call: it is idempotent and finishes the job.
FIX-0820-18: revoking a key now closes the access links it issued
Before
PATCH /v1/keys/:id with status REVOKED disabled the key itself but left the access tokens it had issued untouched: share links and bearer tokens kept working until their own expiry, which reaches ten years. An owner revoked a key and believed access was closed, while the links stayed alive.
After
Revoking a key now revokes the access tokens it issued and drops them at the gateway. This matches key deletion, where it always worked that way, and behaves identically on both surfaces — through the API and through the dashboard. Links issued by keys revoked BEFORE this release are closed too: a one-off data fix retires them, not just the new behaviour. Other key changes (name, rate limit, access mode) still leave tokens alone.
2026-08-19
FIX-0819-1: bot message sending accepts top-level dialogId
Before
When sending a bot message through MCP, top-level dialogId was not included in the POST /v1/bots/:botId/messages request, while a request without a recipient returned a platform error.
After
Top-level dialogId is forwarded as the canonical recipient, while the previous body.dialogId remains a compatible fallback. A request without a non-empty dialogId string returns MISSING_PARAMS before sending the message.
FIX-0819-2: the `X-Tariff-Checked-At` header is sent only after a successful plan lookup
Before
The timestamp was written after ANY plan lookup attempt against Bitrix24,
including a failed one: a portal whose lookup timed out or was rejected for a
key without the required scope still received a fresh X-Tariff-Checked-At.
Headers gave no way to tell "the plan was read an hour ago" from "an hour ago we
tried and failed".
After
The header is sent only when the last lookup actually read the plan. A failed
attempt now sends no header at all, so its absence means "there is no reliable
lookup", not "this portal was never checked". The X-Tariff-Is-Commercial
header is unchanged and arrives as before.
FIX-0819-3: clarified personal-key embedding and handled OAuth without state
Before
GET /v1/me incorrectly claimed that a personal key could not publish an embedded application at all. When Bitrix24 returned an OAuth code without state, /v1/bitrix-handler stayed on the intermediate page.
After
portalEmbedding describes the working POST /v1/apps → OAuth authorization → POST /v1/apps/:id/publish flow and separately warns that a personal key cannot call placements/bind directly and does not itself provide transparent auth. An OAuth callback without state now redirects to a controlled error page without forwarding the code.
FIX-0819-4: money paths now survive a race for the shared portal balance
Before
Under a rare race of concurrent writes to a portal's balance (a Postgres
Serializable-transaction conflict, 40001), one of the competing money paths
could fail outright and never complete: the payment provider's webhook credit,
Bitrix24 events (bonus grant and revoke, package revoke), the real-time web-search
charge, Cowork subscription renewal and tier change, and the daily crons (including
vibe-package expiry) — every one of them wrote the balance with no retry and failed
on the very first concurrent attempt.
After
Each of these paths now survives a serialization conflict: the platform automatically retries the specific money operation until it succeeds (a finite number of attempts, with jitter between them), so a rare overlap of two concurrent operations on the same balance no longer loses money or returns a failure to the caller.
Impact on integrators
No action required. The payment provider's webhook and Bitrix24 events are processed as normal even under contention with another operation on the same balance — this change requires no additional manual retries on the integrator's side.
BC-0819-5: the freshnessWindowMinutes field is no longer returned
Old format supported until: not provided
Before
GET /v1/me returned a freshnessWindowMinutes field set to 10 inside the capabilities.apps.sourceStorage block, and the 409 SNAPSHOT_REQUIRED refusal of POST /v1/apps/:id/publish carried the same field inside hint. The field named the window, in minutes, within which a saved source snapshot counted as usable for publication.
After
The field is present neither in the capability block nor in the refusal hint. It is returned only when publication checks snapshot age, and right now it does not — a snapshot of any age can be published. No explicit null takes its place: the field is simply absent. The rest of the capabilities.apps.sourceStorage block and the other hint fields are unchanged.
What integrators should do
Read freshnessWindowMinutes as an optional field: if your code requires it, coerces it to a number without checking, or drives a re-save timer from it, drop that dependency. Treat the missing field as "snapshot age is not checked". The field returns only if publication starts checking age again, and its value will be meaningful again at that point. The full hint schema — Source storage.
BC-0819-6: publication refuses when the files of the saved source version are gone
Old format supported until: not provided
Before
POST /v1/apps/:id/publish picked a saved source version by its record, without checking whether its files were still there. A version whose files had already been purged from storage or marked for deletion therefore qualified for publication: the request answered 200, the app moved to PUBLISHED, and downloading the published version afterwards answered 410 SOURCE_VERSION_BYTES_PURGED. Sources that did not exist ended up published.
After
Publication does not pick versions without files at all. If the app or the server has no other saved version, the answer is 409 SNAPSHOT_REQUIRED with hint.reason = app_snapshot_missing or server_snapshot_missing: no files means no snapshot. If such a version is passed as an explicit sourceVersionId, the answer is the same 409, and hint.lastSnapshot arrives as null. This change is unrelated to the snapshot-age check and applies at all times.
What integrators should do
Handle a 409 SNAPSHOT_REQUIRED from publication in the case where it previously could not occur — an app whose source files have been purged from storage or marked for deletion. There is one recovery path: save the archive again through POST /v1/apps/:id/sources or POST /v1/infra/servers/:id/sources and retry publication. Retention and file deletion are described in Source storage.
FIX-0819-7: app publication no longer refuses because the saved sources are old
Before
POST /v1/apps/:id/publish answered 409 SNAPSHOT_REQUIRED when the saved source snapshot was older than ten minutes, even if the code itself had not changed. The check measured the time since the last save rather than whether the sources matched, so the usual order — deploy, then authorize the app on the Bitrix24 account, then publish — ran into a refusal: the authorization step is done by a person and easily takes longer than the window. Getting past it meant saving the very same archive again through POST /v1/apps/:id/sources or POST /v1/infra/servers/:id/sources. The refusal hint carried hint.reason set to app_snapshot_stale or server_snapshot_stale.
After
The age of the saved sources no longer limits publication: a snapshot of any age is accepted. A 409 SNAPSHOT_REQUIRED refusal is left only when there is no usable snapshot at all, and then hint.reason holds app_snapshot_missing or server_snapshot_missing. The values app_snapshot_stale and server_snapshot_stale no longer appear in the response — they come back only if publication starts checking snapshot age again.
Impact on integrators
Nothing to change: publication has simply lost one of its reasons to refuse. Re-saving an unchanged archive before publishing is now redundant and can be dropped from the flow. If your code branches on hint.reason, the branches for app_snapshot_stale and server_snapshot_stale stop firing, yet stay valid: the set of values has not changed.
FIX-0819-8: Publishing an app no longer marks the source version as successfully deployed
Before
After a successful publish the platform wrote deploy status success onto the selected source version — regardless of how the deploy of that version ended, or whether there had been one at all. In the version list (GET /v1/infra/servers/:id/sources), in the manifest and in the sources registry, a version from a failed deploy looked successful after publishing, and a hand-saved version received a deploy status it never had.
After
Publishing writes only its own marker — a linkedDeployId of the form publish:<timestamp>. The deployStatus field keeps whatever the deploy made it: success, failed, or empty for versions saved by hand.
Impact on integrators
No action required. If you read deployStatus as "this version is published", that was never its meaning; publication is carried by the published entry in tags and by the publish: prefix in linkedDeployId.
BC-0819-9: promo code on a closed seat: the `COUPON_SEAT_CANCELLED` refusal is gone
Old format supported until: not provided
A promo code now works on a closed seat — that is how a departed customer comes back. Redemption turns on the granted tier from the moment it is applied, the seat becomes active again, and its closing mark is cleared.
As a result the COUPON_SEAT_CANCELLED refusal code disappeared: nothing produces it any more. If your code matches refusal reasons against a list, drop it — that branch is now unreachable.
One refusal on a closed seat remains, under a different code. A seat closed early while its paid term had not expired answers COUPON_SEAT_ALREADY_PAID_LONGER: redemption would overwrite the paid term with the gifted one, and the purchased months would vanish from the row a refund is computed from.
Affected endpoints: POST /v1/cowork/coupon/preview, POST /v1/cowork/coupon/redeem
FIX-0819-10: a promo code no longer eats the paid term of a downgraded seat
Before
When a platform admin downgraded a seat off a prepaid plan, the row kept its paid term (an
end date in the future and the price of the purchased month) while no charge was scheduled
any more. Redeeming a promo code on such a seat went through: POST /v1/cowork/coupon/redeem
answered with success, and the granted gift rewrote the end of the paid term to the length of
the gift. The purchased months disappeared from the row the refund is computed from.
After
Such a redemption is refused with COUPON_SEAT_PAID_TERM_ACTIVE, new in the refusal set of
POST /v1/cowork/coupon/redeem. The paid term on the seat is left alone, the promo code stays
with the person and is redeemed once the term ends. The other refusals of the set and their
conditions are unchanged.
BC-0819-11: an unbind the platform could not confirm no longer looks like success
Old format supported until: not provided
Before
POST /v1/apps/{id}/unpublish answered 200 and cleared placements whether or not the platform had removed the bindings on the Bitrix24 account. POST /v1/apps/{id}/publish and PATCH /v1/apps/{id} with a changed placement set answered 200 even when the placements being dropped could not be removed. POST /v1/placements/unbind removed the placement from the application list without waiting for the account to confirm it.
After
Unpublish still answers 200 and moves the application to UNPUBLISHED, but placements now carries the codes it could not remove and warnings explains each one. Publish and PATCH answer 502 with code PLACEMENT_UNBIND_FAILED and the code list in error.placements when removal is unconfirmed; the application is not published and the catalog metadata is not saved. A single-placement unbind answers 502 with code BITRIX_UNAVAILABLE and keeps the placement in the application list.
Separately: POST /v1/apps/{id}/publish and PATCH /v1/apps/{id} started answering 503 with code NETWORK_DEVKEY_REQUIRED when the account is switched to developer-key transport and the application author holds no such key. Neither endpoint used to return that code.
What integrators should do
The capability is rolling out gradually and is enabled per account: before it is enabled on your account all four endpoints behave as before, an unconfirmed removal still looks like success, and the codes PLACEMENT_UNBIND_FAILED and NETWORK_DEVKEY_REQUIRED do not occur at all. The code BITRIX_UNAVAILABLE on unbinding a single placement existed before the rollout too — only its condition changes: once enabled, an unconfirmed removal answers with the same code. Once enabled, read placements in the unpublish response: an empty list means everything was removed. On 502 with code PLACEMENT_UNBIND_FAILED, retry — the listed placements are still on the account. If your flow treated 200 as proof of removal, switch the check to placements being empty. On 503 with code NETWORK_DEVKEY_REQUIRED a retry will not help: ask the application author to reconnect the account.
Affected endpoints: POST /v1/apps/{id}/publish, POST /v1/apps/{id}/unpublish, PATCH /v1/apps/{id}, POST /v1/placements/unbind
NEW-0819-12: a self-hosted portal can start the Marketplace demo
Before
POST /v1/portals/{id}/activate-market-trial and POST /v1/cowork/activate-market-trial
always refused a self-hosted portal: 409 TRIAL_ACTIVATION_UNAVAILABLE, with
/v1/cowork/state reporting not_cloud as the reason. The Marketplace demo was
available to cloud portals only.
After
A self-hosted portal starts the demo through the same endpoints. Eligibility is decided
by Bitrix24 from the portal licence, so the refusal reason is now more precise:
demo_used when the demo cannot be granted, and not_supported while eligibility has
not been read yet. Response shapes and error codes are unchanged.
The capability is rolled out gradually and is off by default.
BC-0819-13: request body parsing for POST /v1/cowork/deploy-key
Old format supported until: not provided
Before
A request with the Content-Type: application/json header and an empty body answered 400. A request with the Content-Type: text/plain header and a non-empty body was accepted.
After
An empty body is accepted with any header: the endpoint does not read the body. A non-empty body of an unknown type answers 415.
What to do
Drop Content-Type: text/plain from the call or send no body at all. The former behaviour is removed at deploy time, there is no support window.
NEW-0819-14: revoking a Cowork/Code device key with the key itself
Before
A device key could only be revoked from the Vibecode dashboard: both sign-out endpoints take session auth, while an application only holds a key. There was nothing for the app to call.
After
DELETE /v1/cowork/key is available. It revokes the presented key — no identifier is passed, so another key cannot be revoked. It requires the vibe:cowork scope and a Cowork/Code desktop-class key, otherwise 403 COWORK_DESKTOP_KEY_REQUIRED; without the scope, 403 INSUFFICIENT_SCOPE. Calling it again with the same secret answers 401 KEY_INACTIVE. The rate is 5 requests per minute per key, over the limit 429 RATE_LIMITED.
The endpoint works on a zero balance and past the daily quota as well: an emergency exit is never locked.
2026-08-18
BC-0818-1: application and source-management contract clarified
Old format supported until: not provided
Before
OpenAPI did not describe the exact success responses, metadata limits, and some error codes for the application source-storage operations. One application's OAuth key could mutate another application and read or mutate the same author's server sources when the server belonged to a different key. The note limit counted UTF-16 code units, so some valid Unicode strings were rejected. An intermediary cache could retain a temporary download URL.
After
The eight-operation contract now describes the actual response schemas, limits, error codes, and access matrix. An OAuth application key can now directly manage only its own application and access sources of a server owned by that same key. When publishing its own application, an explicit sourceServerId may select the same author's server under a personal key, but not a server owned by another OAuth application. The author's personal keys and account-administrator keys retain access. V1 application-operation audit records store the key owner's Vibe UUID rather than the numeric Bitrix24 user ID. The note limit is counted in Unicode code points. Application and server temporary-link responses are marked Cache-Control: private, no-store, and the URL, including its storage path, is explicitly identified as a short-lived bearer credential.
No transition window is provided because an OAuth application key's former access to another key's resources was an access-control defect.
FIX-0818-2: `?refresh=tariff` on a self-hosted portal re-checks its access state at the source
Before
GET /v1/me?refresh=tariff on a self-hosted portal re-checked the plan but returned the portal's access state from the previous snapshot. A change made right before the call stayed invisible until a background check picked it up.
After
The request re-checks that state at the source as well, and the response is built from fresh data. A change made right before the call is visible immediately.
NEW-0818-3: workgroup roster with member roles
A new endpoint GET /v1/workgroups/:groupId/users returns the members of a workgroup together with their role in it. Previously the /v1/workgroups facade exposed only the owner and the member count, so an application that had to tell a head, a moderator and an ordinary member apart could not do so.
Every row carries userId and role. The role field holds the Bitrix24 letter verbatim: A owner, E moderator, K member. An unrecognised letter is passed through rather than mapped onto a known one or dropped, so branch on the values you know and treat anything else as no permission.
The operation takes no pagination parameters and returns the whole roster, so meta.total always equals the number of rows in data. An empty roster is an ordinary answer, and the owner is not necessarily among the rows — read the owner identifier from GET /v1/workgroups/:id. A 404 means the workgroup does not exist or is not visible to the Bitrix24 identity the key acts as, because Bitrix24 refuses both cases identically.
The endpoint returns membership data, not an authorization decision: what it shows depends on the identity behind the key, and portal-administrator status is a separate fact served by GET /v1/users/me. Nested resources are not addressable through POST /v1/batch, so the operation is called on its own path only. Requires the sonet_group scope.
FIX-0818-4: deals: seven fields from GET /v1/deals/fields are now accepted in a filter
Before
GET /v1/deals/fields listed fields the filter would not accept. A
POST /v1/deals/search carrying filter[leadId], filter[quoteId],
filter[taxValue], filter[originId], filter[originatorId],
filter[additionalInfo] or filter[lastActivityBy] was rejected with
400 UNKNOWN_FILTER_FIELD before Bitrix24 was called at all, even
though Bitrix24 does accept those fields in a filter. A report selecting deals by
their lead could not be built.
After
All seven fields are declared in the deals schema and are accepted in filter —
on GET /v1/deals, on POST /v1/deals/search and on POST /v1/deals/aggregate.
They were already accepted in select, but with an UNKNOWN_SELECT_FIELD
warning; that warning is gone. Sorting by them already worked before this change
and is unaffected. Field names in responses are unchanged. The set of groupBy
axes is untouched.
Two notes on values. leadId, quoteId, originId, originatorId and
additionalInfo are now marked nullable in GET /v1/deals/fields and in
OpenAPI: they return null on a deal that was not created from a lead, from a
quote or by import. They were absent from the specification before, so a client
generated from it must be ready for null. And on the three string fields
(originId, originatorId, additionalInfo) an empty string from Bitrix24 is
normalised to null, exactly as it already is on every other string field of the
API. Across a 300-deal sample no empty string occurred at all — Bitrix24 returns
null on these fields — so no change on real data is expected; but if your code
compares such a field with "", compare it against an empty value instead.
Six fields — utmSource, utmMedium, utmCampaign, utmContent, utmTerm
and contacts — stay rejected in filter and sort, because Bitrix24 does not
support them there. The rejection is deliberate: accepting them would produce a
silently wrong selection. The reason is now visible up front, in the
description of each field in GET /v1/deals/fields. To filter by contact, use
contactId (the primary contact) or contactIds (any linked contact).
On leads, quotes and smart-process items, the five UTM fields remain available
in responses but no longer pass the local filter or sort guard: Bitrix24 does
not accept them in crm.item.list. Instead of a Bitrix24 422, the request now
receives 400 UNKNOWN_FILTER_FIELD or 400 UNKNOWN_SORT_FIELD before Bitrix24
is called. Other operations on these fields are unchanged.
FIX-0818-5: empty workday method results are returned as null
Before
When Bitrix24 returned an empty successful result, the data field could contain a service envelope with result, total, and next fields instead of the result value.
After
POST /v1/workday/open, POST /v1/workday/close, POST /v1/workday/pause, GET /v1/workday/status, GET /v1/workday/settings, and GET /v1/workday/schedule return data: null when the successful result is empty.
Impact on integrations
Clients no longer need to extract an empty value from the Bitrix24 service response envelope.
NEW-0818-6: the field reference for orders and order statuses now returns names and explanations
Before
GET /v1/orders/fields and GET /v1/order-statuses/fields answered with nothing but a type and a read-only flag per field. There was no human-readable name and no explanation for any of the 54 fields, so a client had only the field name to go on. Generating a form or a typed model from such a schema was not possible: a name like recountFlag or empStatusId explains nothing on its own.
After
Every field of both entities now carries a label (short name) and a description (explanation) in the language of the segment. The explanation states what the type cannot: why price is accepted on create only (Bitrix24 recalculates the amount from the basket items), that requisiteLink arrives as an empty array when the link is unset, that clients, payments, basketItems and propertyValues are returned by the order card only, and that Bitrix24 requires the type field on every status update. The response grew; the set of fields and their types did not change, so existing requests keep working.
NEW-0818-7: machine issuance of promo codes: three V1 methods and two new integration-key scopes
Vibecode now exposes three V1 methods for an integration that hands out promo codes on its own: POST /v1/platform/coupons/issue issues a batch of codes inside an existing campaign, GET /v1/platform/coupons/campaigns lists the campaigns available for issuance, and GET /v1/platform/coupons/campaigns/{slug} reads one campaign by its code. Authorization is a platform integration key in the Authorization: Bearer header, with the coupons:issue and coupons:read scopes respectively; a platform administrator grants them when the key is issued.
The key issues codes but neither creates campaigns nor moves their ceiling: both stay with a platform administrator, otherwise the campaign ceiling would stop being a ceiling. A campaign is addressed by the same code (slug) that prefixes every promo code it issued, so the integrator and support share one identifier. The remainingToIssue field answers how many codes may still be issued; null means the campaign has no ceiling.
The Idempotency-Key header is required. Codes are returned once and are not stored on the platform side — only their hashes are — so there is nothing to replay: a request with an already used key gets 409 IDEMPOTENCY_KEY_ALREADY_USED with a reference to the issued batch, not a second set of codes. Issuing into a draft campaign is refused with 409 COUPON_CAMPAIGN_IN_DRAFT: codes handed out before the campaign is activated would be refused at redemption and cannot be reissued. The remaining refusals: 409 COUPON_CAMPAIGN_NOT_ISSUABLE — the campaign is finished or archived, 409 COUPON_CAMPAIGN_CAP_REACHED — the batch does not fit under the ceiling, 403 INSUFFICIENT_SCOPE — the key lacks the required scope, 404 CAMPAIGN_NOT_FOUND — no campaign carries this code.
Redeeming a promo code through a machine method is not part of this release: a code is redeemed by a person in the dashboard, under their own session and on their own account.
NEW-0818-8: promo codes for Cowork/Code tiers and the refusal codes of redemption
Vibecode now has promo codes. A partner gets a code from the organiser and redeems it in the Cowork/Code section of the dashboard: the granted tier switches on for their seat for a term the platform pays for, with no debit from the account balance. Campaigns, issuing a batch of codes and revoking unissued codes are run by the platform administrator; redemption is a user action in the dashboard and has no public API method.
A refused redemption answers with a code in the code field, and most causes collapse into a single COUPON_INVALID on purpose: "not found", "revoked", "already redeemed", "expired", "the campaign is over" and the campaign limits are indistinguishable from one another, so a refusal never confirms that a live code exists. Only the causes a person can act on stay distinguishable: COUPON_TOO_MANY_ATTEMPTS — too many attempts in a row; COUPON_PORTAL_ACCESS_GATED — the account has no Cowork/Code access yet; COUPON_SEAT_PAUSED, COUPON_SEAT_CANCELLED and COUPON_SEAT_CANCELLATION_SCHEDULED — the state of the seat prevents the grant; COUPON_TIER_DOWNGRADE_BLOCKED, COUPON_SEAT_ALREADY_ON_TIER, COUPON_SEAT_ALREADY_PAID_LONGER and COUPON_SEAT_IS_PAID — the current tier is already no lower than the granted one, or is paid further ahead. CONCURRENT_REDEMPTION stands apart: several codes of one campaign were redeemed at the same moment, and the attempt only needs repeating.
A promo code never downgrades the current tier and is never applied on top of an already paid seat — in both cases it stays with its holder and is redeemed later. A redeemed promo code cannot be revoked: revocation applies only to a code nobody has used yet.
NEW-0818-9: promo code in the Cowork/Code app: check a code and redeem it with a desktop key
The Cowork/Code app now accepts a promo code itself instead of sending the person to the dashboard. Two methods have appeared: POST /v1/cowork/coupon/preview shows what a code grants (tier, term, campaign name) without changing anything, and POST /v1/cowork/coupon/redeem redeems it and turns on the granted tier for a term the Vibecode platform pays for.
Both methods require a Cowork/Code desktop key with the vibe:cowork scope. An agent key carrying the same scope gets 403 COWORK_DESKTOP_KEY_REQUIRED: redemption is irreversible and one-shot, and there is no human behind such a key to make the decision.
The check answers 200 and reports valid: false with reason COUPON_INVALID when the code does not work. One reason covers every "code does not work" case, so the method never hints to someone guessing codes how a non-existent code differs from a revoked one. Valid here means "the code is live and the campaign is open": the seat's own conditions (already paid, already on this tier) are verified at redemption and may refuse after a successful check.
Redemption returns the granted tier, the term and an accessGranted field. false means the tier was granted but the portal administrator has not opened access to Cowork/Code yet — show that separately, otherwise the person sees success and runs into a closed door.
Affected endpoints: POST /v1/cowork/coupon/preview, POST /v1/cowork/coupon/redeem
NEW-0818-10: applications catalog in the API: list and card
The Vibecode API now exposes an applications family: GET /v1/applications returns the list and GET /v1/applications/:id a single card. Rows are scoped to the key OWNER rather than to the calling key, so applications created in the Vibecode dashboard are included too — their servers belong to other keys of the same person and never appear in GET /v1/infra/servers.
The scope parameter accepts mine, shared (applications of other people that you can access — both those shared with you personally and those open to the whole Bitrix24 account) and feed (the default), alongside the page and limit paging parameters. The card is also available to someone the application was shared with, not only to its owner; the viewer's relationship to the application arrives in the viewerState field.
The list response carries truncated next to total. In the feed scope the ordering is computed by the platform, so the selection has an upper bound: when truncated is true, total is that bound rather than the full number of applications, and there are no pages beyond it. Deriving a page count as total / limit is only valid while truncated is false; when it is true, reach for the mine and shared scopes, which have no such bound.
Every card carries two extra blocks. sources reports whether saved source versions exist and returns the latest one in the very form the version download accepts, so no upfront call for the version list is needed. activeOperation reports an operation in flight: its kind, step and start time; the value unknown means the operation did start but its outcome is not known. Both blocks are filled in only for whoever manages the application: a viewer the application was merely shared with receives an empty sources (hasVersions: false, both fields null) and activeOperation: null. The response shape does not change, so beware of the wrong conclusion: an empty sources on someone else's application means "this data is not disclosed to you", not "there are no versions".
One caveat about activeOperation: null means "no operation with a stored record", not "nothing is happening to this application". The field covers deployment, repair, server plan changes and moving a container between galaxies — other actions are not journalled by the platform and never surface here.
Where to open an application is no longer yours to work out: the card returns a ready openUrl and openTarget pair. openTarget currently has a single value — app, meaning the application's own address opens; both fields are null when there is nothing to open. The value set is closed and may grow, so treat an unfamiliar value as "nothing to open here" rather than as an error.
Important: for an application embedded into Bitrix24 both fields arrive null — the platform does not yet know the address such an application opens at inside the account. Its own address is deliberately NOT substituted into openUrl: that address leads to the gateway sign-in page rather than into the application, so following it would look successful without being so.
The isEmbedded field tells those two states apart — "embedded, opens inside Bitrix24" versus "not published yet". Both arrive with an empty open pair, yet the text a user should see differs. Do NOT infer embedding from the presence of a server: an embedded application with no server of its own is a normal state (the embedding is done, the code has not been deployed yet), and the flag does not depend on the server at all. The field is disclosed both to the owner and to someone the application was shared with.
The server summary gained a reachable flag — true when the server both runs and answers over the network. It is separate from status because those are different facts: a server can be up while the network tunnel to it is not, and by status alone such an application looks healthy. A client cannot check this from outside, so the platform computes the flag.
The flag already implies the RUNNING state: it never arrives true for another server state, so there is no need to conjoin it with status. For an application inside a galaxy (server.kind: "GALAXY_APP") the second half of the flag is taken from the galaxy HOST, not from the container itself — the host holds the connectivity, the container has no tunnel of its own by design. Hence a consequence worth knowing up front: a freshly created container that has not reached RUNNING yet (it only does so after its first source upload) arrives with reachable: false even on a fully healthy host. That means "the container is not up yet", not "the host is unreachable" — the flag alone cannot tell the two apart, status can.
The server summary gained a lastDeployedAt field — when the application was last deployed successfully. It is the only signal in this section that an application is actually lived in: updatedAt only moves when the card is edited, and sources.latestSavedAt means "code saved" and reaches only whoever manages the application. The stamp is written by the deployment itself at the moment it records success, so a failed deployment never moves it. Important: the field carries no history: for servers created before it existed it arrives null until their next deployment — we did not reconstruct the history, because the available source covers only one of the three deployment paths and a date would appear for some server kinds while missing for others.
Two more things worth knowing up front. The application id from this section and the id from GET /v1/apps are different values of different entities: an application card here, an application registration on the Bitrix24 account there. And the same thing by meaning arrives under different names: name here, title there. Do not carry one over into the other — the fields are not synchronized.
The list ordering is now stated explicitly so a page walk is reproducible. In the feed scope: pinned by you → your own → other people's, and within a group by updatedAt newest first, ties broken by id. In mine and shared: by createdAt newest first, ties broken by id. That secondary key is not a formality — applications created in a batch carry identical timestamps, and without it two pages of one walk could overlap or skip an application. Important: updatedAt only moves when the card itself is edited — a deployment does not touch it, so "freshness" in the feed means "when the card was last changed", not "when the application was last deployed".
2026-08-17
FIX-0817-1: repeated PostgreSQL runtime deploy no longer fails on an existing database
Before
POST /v1/infra/servers/:id/deploy with a PostgreSQL runtime tried to create the app database again on a repeated run, so the runtime setup step could fail.
After
The node20-pg, node20-pg-redis, node20-rag, python311-pg, and python311-rag runtimes create the app database only when it is absent. A genuine creation failure still stops the deploy.
Impact on integrators
A repeated deploy with the same PostgreSQL runtime no longer requires deleting the database manually or changing the runtime.
FIX-0817-2: calls statistics reference shows working filters and sorting
Before
The machine-readable hint for GET /v1/calls/statistics suggested non-existent date-range fields and did not explain that the sort field and direction are separate parameters.
After
The hint and OpenAPI show filter[>CALL_START_DATE] / filter[<CALL_START_DATE] comparisons, the sort + order pair, and the actual single-page limit of up to 50 records. The adjacent call-management operations now also use the mounted :callId paths and the actual registration, finish, transcription, auto-call, and callback request bodies.
Impact on integrators
Use canonical Bitrix24 comparison operators in filter keys and pass the sort direction in the separate order parameter.
FIX-0817-3: the Cowork/Code deploy-key mint now recovers a fleet stranded by earlier mints
Before
POST /v1/cowork/deploy-key moved links onto the fresh key only from the key that was active at mint time. Anything left on keys revoked earlier was never picked up: those servers stayed out of GET /v1/infra/servers, answered 404 on a single read and 403 WRONG_KEY on publish. When the owner had no live deploy key at all, the mint moved nothing, so requesting another key did not repair such a loss either. The only remaining path was manual: rebinding the server to a live key in the dashboard.
After
A mint now drains links from every previously revoked project key of the same owner and portal, not just the currently live one — up to five keys per call, newest first. Server ownership, the application card, live access tokens and the read scope of deploy operations move onto the fresh key inside the same transaction as the mint itself, so the server list and publishing work immediately after the response. The "no live key" case is no longer special: links are moved on a first mint too.
Influence on integrators
No client change is required. The previous entry's caveat — that a stranded fleet comes back only through a manual rebind in the dashboard — no longer applies; requesting the key again is enough. If links piled up on more than five keys, later mints drain the rest; the key lives for seven days, so this needs no separate action.
NEW-0817-4: quota relief is visible in the Cowork/Code subscription responses
GET /v1/cowork/me and GET /v1/cowork/state now return a relief block — the moments Vibecode platform support last reset the usage counters and last granted a temporary limit increase. Both stamps come to the hour and stay visible for 7 days, after which the field is empty. Along with the block, the subscription summary now returns boostPct and boostExpiresAt — the size and the deadline of an active increase, which were carried by the full state only before.
An empty relief.boostGrantedAt does not mean there is no increase: the stamp is there for a one-off notification and lives for 7 days, while an increase is granted for up to 30 days. Whether an increase applies is told by boostPct, and by nothing else.
The capability is switched on account by account. Until it is on, none of the keys listed above are present in the response body at all, so test for the presence of the key rather than for its value. Existing calls keep working unchanged.
NEW-0817-5: notification type dictionary
GET /v1/notifications/schema is now available — a directory of the Bitrix24 account modules and the notification types they send. The operation takes no parameters.
The response carries a modules array; every module has an identifier, a name and a list of types. The module identifier matches the notifyModule field of a notification in the feed and the type identifier matches notifyEvent, so the directory answers the "show a human a readable name instead of a technical code" task and fits notification-settings screens.
Bitrix24 hands the directory over as a map whose key repeats the module identifier inside the entry, and the Vibecode API unwraps it into an array. The element order comes from the account and is not a sort contract. The contents depend on the account and change as Bitrix24 is updated, so cache the response but re-read it when you meet an unfamiliar module or type.
NEW-0817-6: Bitrix24 account calendar settings are available through the API
A new endpoint GET /v1/calendar/settings returns the Bitrix24 calendar settings: work day start and end, weekly days off, holidays, working Saturdays and the first day of the week. These are the values the Bitrix24 interface uses to mark non-working days. The request takes no parameters and needs a key with the calendar scope.
The fixed response fields arrive in camelCase — workTimeStart, weekHolidays, yearHolidays. Section addresses for non-standard calendar types arrive as one field per type, named exactly as the account returns it, so the response field set is not fixed.
When the key owner is not an employee of the account, Bitrix24 returns a reduced set of two fields carrying default values instead of the account settings. Such a response is indistinguishable by value from a configured account, so it carries meta.warnings with the code calendar_settings_partial.
The endpoint has a limit of its own — 120 requests per minute per Bitrix24 account, on top of the shared request limit to Bitrix24. The settings change rarely, so the response is meant to be cached on the client side rather than polled.
Calendar events and sections remain available as entities — /v1/calendar-events and /v1/calendar-sections.
2026-08-16
NEW-0816-1: the app icon accepts raster files, and a Bitrix24 portal can push one itself
The icon upload POST /v1/infra/servers/:id/icon now accepts raster formats — PNG, JPG, GIF, WEBP, up to 5 MB and up to 4 megapixels (2000×2000, for example) — in addition to SVG. The format is detected from the file contents, not from its name or its Content-Type header. Whatever you upload, the serve URL still returns a 256×256 PNG: the platform scales the image into a square, keeping its proportions and padding the rest with transparency. Existing SVG uploads keep working unchanged: the SVG limits — 256 KB and the same 4096×4096 ceiling as before — are untouched.
New refusal codes: ICON_UNSUPPORTED_FORMAT for a format that is unrecognized or unsupported, ICON_TOO_LARGE for a file above the size limit, ICON_TOO_MANY_PIXELS for an image with more pixels than the platform will unpack, ICON_RASTERIZE_FAILED for a file that cannot be read.
In your Vibecode account the icon picker narrowed the other way: the server card and the application card take PNG, JPG, GIF and WEBP, and now refuse SVG — that format stays in the API alone.
The other half of the change is on the portal side: an app owner can now replace the icon straight from the Bitrix24 catalog card, and the portal pushes it to the Vibecode platform together with the title and the description. That icon lands on the same catalog card as one uploaded through the API.
NEW-0816-2: two fields describing a temporary limit increase in the Cowork/Code state
The GET /v1/cowork/state response now carries two fields describing a temporary limit increase: boostPct — the increase in percent (100 means limits are doubled), and boostExpiresAt — when it ends. With no increase active they read 0 and null.
The pctUsed shares of all three windows already account for the increase, so there is nothing to apply on the client side — the fields exist so you can show the size and the deadline to the user.
NEW-0816-3: new 402 company_budget_exhausted rejection on calls that spend credits
Before
The only money-related rejection on /v1/search/* and on AI calls was insufficient funds on the account — INSUFFICIENT_BALANCE. An administrator had no way to cap the spend of an individual
employee or of the whole account.
After
An account administrator can set a monthly budget for metered spend — for the whole account and for
an individual employee. Once the budget is exhausted, web search and research calls, as well as AI calls — POST /v1/chat/completions, POST /v1/embeddings and POST /v1/audio/transcriptions — that spend credits receive a 402 with
the code company_budget_exhausted.
The rejection body carries two extra fields. scope tells whose budget is exhausted: USER is the
caller's own budget, PORTAL is the budget of the whole account. canRequest tells whether an
increase can be requested from the product: true for a personal budget, false for the account
budget, which only an administrator raises.
The rejection is returned only on calls that actually spend credits: a search made with the customer's own key costs nothing and keeps working when the budget is exhausted, and so do AI calls made with the customer's own key, calls served inside the plan quota, and calls covered by a subscription. A call already in flight is completed: the limit applies from the next request.
Budgets are enabled separately and are off by default, so accounts without them see no change.
FIX-0816-4: the Cowork deploy-key handout no longer loses servers
Before
POST /v1/cowork/deploy-key revoked the previous key but left everything that referenced it behind: server ownership, the application card and live access tokens. Versioned infrastructure reads are scoped to the calling key, so after every re-issue the earlier servers dropped out of GET /v1/infra/servers, answered 404 on a direct read and 403 WRONG_KEY on deploy. The only way back was the dashboard.
After
The handout moves those links onto the fresh key, exactly as key rotation does. The server list and deploy work under the new key straight away, containers on a shared galaxy host included.
The read scope of started operations moves with them, so GET /v1/infra/operations/{operationId} keeps returning the outcome of a run begun under the previous key. A client whose transport dropped — and which therefore asked for a new key — used to get 404 on its own operation, indistinguishable from "no such operation".
Impact on integrators
Nothing to change: from this fix onward a re-issue no longer loses the links.
Important: the fix works forward and does not recover what is already lost. The move starts from the key that is active at handout time, while orphaned links sit on keys revoked earlier, so they are outside its reach. If your servers disappeared before this rollout, restore them by hand: in the Vibecode dashboard, open the server card and bind it to the key you use now.
The key lives for seven days and is not re-issued on a schedule. If it goes untouched for longer, the servers stay with the expired key and become visible again after the next handout, which moves the links off it.
2026-08-15
BC-0815-1: combo runtimes with a database no longer slip into Galaxy silently
Old format supported until: not provided
Before
Deploying an app into Galaxy with a runtime that promises a database (node20-pg, node20-mysql, node20-redis, node20-mysql-redis, node20-pg-redis, node20-rag, python311-pg, python311-mysql, python311-redis, python311-rag, php83-mysql) succeeded. The database suffix was stripped and the app got the language image only: no database engine, no connection variables (DATABASE_URL, PG*). The failure showed up on the first database call.
After
Such a deploy is refused up front: 400 with code GALAXY_RUNTIME_DB_UNSUPPORTED, a details.suggestedRuntime field (the language replacement — node20 instead of node20-pg, for example) and a recovery hint. The catalog GET /v1/infra/runtimes now returns supportedPlacements on every runtime: ["standalone"] — dedicated virtual machine only, ["standalone","galaxy"] — Galaxy as well. The name and packages fields are unchanged: they describe the dedicated-machine install, where a combo runtime really does install the database.
What integrators should do
Filter the catalog by supportedPlacements containing galaxy before a Galaxy deploy. If a Galaxy app needs a database, take a language runtime and pass the connection string of an external database through env. If the database must live on the same machine, create a dedicated virtual machine (placement: "dedicated", create without source) and deploy the original runtime to it in a second step. Apps already running are unaffected: the refusal only happens on a new deploy.
NEW-0815-2: the bound placements list now states whether the account was checked
The GET /v1/placements response gained three optional fields. The portalSync field states how the reconciliation of the list against the Bitrix24 account ended: ok — the account returned every bound code, drift — the account did not return some of the codes, unknown — no reconciliation happened. The reason for a value other than ok is returned in portalSyncReason — no_oauth_session, empty_vibe_list, b24_unreachable, unpublished or missing_on_portal. On a divergence the codes that are listed as bound in Vibecode and were not returned by the account are named in missingOnPortal, and the same codes get a line in warnings.
An application withdrawn from the catalog is not treated as a divergence: the unpublished value states that the platform itself removed the placements from the account and kept the codes for republishing. The reconciliation runs when a session token is passed together with the application key and the application has bound placements. Without those conditions portalSync is returned as unknown — the response behaviour did not change, it is simply stated explicitly now.
Previously the outcome had to be inferred from the presence and the length of the handlers array, which did not separate "the account answered and the code is not in the answer" from "the account response could not be obtained": both produced an empty array. The handlers field is now absent in the second case, and the reason is visible in portalSyncReason. Existing calls keep working, the new fields are additive.
FIX-0815-3: AI quota is resolved by plan in every Western zone
Before
A Bitrix24 account plan is recorded as "account zone plus edition" — for example
jp_pro100. The platform stripped the zone prefix for only ten of the Western zones,
so an account in the cn, id, it, vn, jp, ms, th, hi, co or ae zone
matched no plan setting at all. Its monthly AI quota was then sized by the fallback
rule instead of by its own plan.
After
The prefix is stripped in all twenty-one Western zones, and the quota is sized by the setting of the plan the account actually holds. No client-side change is required; for affected accounts the monthly quota size changes from the next billing period — up or down, depending on how their plan is configured.
NEW-0815-4: an app can be added to the Bitrix24 catalog with a dedicated call
A new call is available — POST /v1/infra/servers/:id/b24-catalog/publish. It creates the app card in the Vibecode apps catalog on the Bitrix24 account. Previously the card appeared only after a successful deployment, so an app brought up any other way was missing from the catalog, and granting access to people did not surface it.
GET /v1/infra/servers and GET /v1/infra/servers/:id now return a b24CatalogSync block with status, itemId, attempts, pendingOp and eligible. A non-null itemId is the "the app is in the catalog" signal. The eligible field answers a different question — whether a card can exist at all: an agent runtime, a galaxy host, a server without a subdomain and a server with no app deployed to it never get one.
Existing calls keep working unchanged: editing the access policy or the access list still does not create a card, it updates an existing one.
FIX-0815-5: the Cowork monthly limit now follows the tier grid without waiting for a renewal
Before
/v1/cowork/state and /v1/cowork/me measured a paid seat's monthly window
against the volume frozen when the seat was bought. When a platform
administrator raised a tier's monthly limit, the new value reached the seat only
at its next renewal: month.pctUsed did not move, and a seat sitting at
exhausted stayed blocked until the paid period ended even though the tier
limit had already grown. The weekly and five-hour windows updated immediately,
so the three windows of one seat answered by different rules.
After
A paid seat's monthly window is measured against the greater of two values: the
live tier limit and the volume sold for the current period. A raise applies at
once — month.pctUsed drops with no request made, and month.exhausted returns
to false when the new limit exceeds what was spent. A cut does not touch the
paid period: until it ends the seat is measured against the volume sold, and the
new value takes effect from the next period. A free seat is still measured
strictly against the live limit.
The response still carries no absolute numbers — only the shares, the
exhausted flag and the resetAt moment change. A client caching
month.pctUsed should re-read the state before deciding to block, rather than
assuming the share only ever grows.
2026-08-14
NEW-0814-1: thirty-one more live operations are now in the machine schema
Thirty-one V1 operations that were already live and documented are now included in GET /v1/openapi.json: smart-process custom fields, requisite links, CRM card configuration, configurable activities, mail, task time tracking, server icon and unstick operations, bot transfer and human-resources node search, plus app blueprints. The methods and their responses did not change — only the machine-readable descriptions were added for clients and agents that build integrations from the schema.
BC-0814-2: port pinning refuses instead of confirming falsely
Old format supported until: not provided
Before
On a pinned server PATCH /v1/infra/servers/:id/port wrote the requested port into the agent settings without checking whether any process on the machine was listening on it. The public address could stop answering entirely after such a change, and the only way back was the non-obvious port: 0. A verified: true reply meant no more than that the agent had come back online: an agent that came up with automatic port detection, that is without the pin in force, produced exactly the same confirmation. Two calls in a row could leave the agent on the port of the other request, and both answered with success.
After
A port nothing listens on is refused with 409 PORT_NOT_APPLIED before the settings are rewritten; the message lists the ports the agent sees listening. Entry NEW-0812-3 stated that a pinned server never receives this code — it does, in exactly this case and without the agentError field.
The confirmation is stricter: verified: true now means that the agent settings carry the requested port, that the agent really did restart, and that it came up with the pin in force rather than with automatic detection. The reply additionally carries data.pinned — whether the machine is still pinned after the call.
While a port change is still in progress, a second call for the same server receives 409 SERVER_BUSY. The endpoint itself now has a rate limit of 10 requests per minute.
What integrators should do
Start the process on the target port BEFORE changing the port: the order "pin the port first, launch the application on it afterwards" now answers with a refusal rather than a success. Do not send a port change in parallel with a deployment or with another port change of the same server — wait for the previous call to answer. Stay within 10 requests per minute.
FIX-0814-3: the dimensions parameter for the bitrix/embeddings model
Before
The dimensions parameter was declared in the POST /v1/embeddings schema, but any request carrying it for the bitrix/embeddings model received 400 ai_provider_rejected — whatever the value, including the dimensionality the model already returns.
After
For bitrix/embeddings the parameter works: an integer from 32 to 4096 is accepted, and the response carries a vector of that dimensionality re-normalised to unit length. A value outside the range is rejected with 400 invalid_request and a param field. Requests without the parameter are unchanged: the full dimensionality is 4096.
A smaller dimensionality neither reduces input-token usage nor speeds up processing — the saving is on the integrator's side, in index size and search speed. Vectors of different dimensionality must not be mixed in one similarity index. Details — Create embeddings.
FIX-0814-4: leading service markers are no longer included in model content
Before
After a tool call, the final answer from POST /v1/chat/completions sometimes started with service markers before the text. A response_format request whose answer was only those markers returned 200 and a non-empty content.
After
Leading service markers are removed from content. If no text remains, content is null — the same as a textless answer. For a response_format request this is the already documented empty-content case without tool_calls: HTTP 422 and code structured_output_truncated. If text remains after the markers are removed, the response stays 200 with the cleaned content.
Impact on integrators
In the common case no client change is required: the answer is the same minus the prefix, and stripping the prefix on your side stays safe. The change does affect you if you relied on a 200 for a response_format request whose answer carried no text: that response now comes back with 422 and structured_output_truncated, as described on the method page. The markers are removed in streaming mode as well. The usage field is not recalculated.
FIX-0814-5: the blocking server wake returns WAKE_TIMEOUT far less often on a cold start
Before
The blocking wake — POST /v1/infra/servers/:id/wake with ?wait=true, and the automatic wake of a sleeping server on POST /v1/infra/servers/:id/deploy — returned 503 WAKE_TIMEOUT in two cases that had nothing to do with how long it waited. When the cloud lost the start command, the platform never re-issued it and eventually reported that the machine had not come up. And when the agent connected its tunnel before the platform refreshed the server status, readiness was still not recognised, so a machine that was already up was put back to sleep.
After
While waiting for readiness, the platform now re-checks the machine state with the cloud and re-issues the start command if it never landed. A connected tunnel counts as proof of readiness on its own: the server moves to running and the response returns without waiting for the next status refresh. Error codes, the response shape and the wait window (~6.5 minutes, then 503 WAKE_TIMEOUT and the server returns to sleeping) are unchanged, so clients need to change nothing. A machine that genuinely did not come up still reports WAKE_TIMEOUT honestly.
FIX-0814-6: a fractional or malformed path id no longer returns a DIFFERENT record
Before
GET /v1/tasks/1.5 answered 200 and returned the record with id 1: the non-integer value was forwarded to Bitrix24 as-is, Bitrix24 truncated it, and the client got a DIFFERENT real record instead of an error. Update and delete behaved the same way — a write silently landed on the wrong record. This affected entities with an integer id; the wrong-record substitution was observed live on twelve of them (tasks, workgroups, users, departments, statuses, storages, sites, pages, folders, timelines, Open Channels configs, requisite presets), while for the rest Bitrix24 rejected the request itself. Some entities (deals, for example) already answered 400, so the behaviour differed inside one API.
After
A non-integer id is rejected with 400 INVALID_PARAMS before the Bitrix24 call — consistently on read, update and delete, in single requests and in batches (/v1/batch, /v1/<entity>/batch). The rule applies to every entity with a numeric id. Integer values work as before. Entities whose id is not a number (order statuses N/P/F, currencies, business-process codes) and chats, which accept the chat1 form, keep their previous behaviour.
NEW-0814-7: reading the notification feed and the unread counter
GET /v1/notifications is now available — it reads a user's notification feed together with the unread counter. Until now the Vibecode API could only send notifications, mark them read and delete them, so an inbox application had to keep a second, separate Bitrix24 integration just to read them.
The response carries the notification list, the cards of their authors, the total counter, the unread counter and a hasMore flag. Page size comes from limit (1 to 50, 50 by default); a value outside the range is brought to the nearest bound, and the size actually used is always visible in meta.appliedLimit. Paging walks the Bitrix24 cursor: lastId and lastType are sent together. To read the unread counter without pulling a page, call with limit=1.
The feed belongs to the token owner — the operation has no parameter selecting whose inbox to read, so a personal key returns its own owner's feed, while a given employee's feed requires an OAuth application key with an Authorization: Bearer header. The feed is not filtered by application: it also carries other applications' notifications and the Bitrix24 account's own system ones, so pick yours by notifyTag or notifyModule.
NEW-0814-8: Workday history in the V1 API
A new endpoint is available — GET /v1/workday/records returns one employee's workday history over a period. It reports the start and end of the day, seconds worked, break length and the approval flag, so lateness and overtime reports can be built through the Vibecode API without a separate integration with the account's time tracking.
userId is mandatory: the Bitrix24 account refuses the call without it. The period is set by the optional from and to in ISO-8601 with an explicit timezone offset or Z; with no period given, the last 7 days are returned. A bare date is rejected — a workday boundary depends on the timezone, and silently widening it to UTC would reclassify the very lateness the endpoint exists to report.
A single request returns at most 50 records; page deeper with offset or page. meta.hasMore signals a continuation. The meta.total field is present only when the page came back shorter than the requested limit: the size of the selection is known exactly in that case, whereas on a full page it is not, and no invented number is put there.
The scope is unchanged — timeman. Rights to read another employee's records are decided by the Bitrix24 account: they belong to an administrator or the employee's direct manager.
FIX-0814-9: the INT_VIBE_PLUS_REQUIRED refusal now points at the plan page inside the account
Before
For the INT_VIBE_PLUS_REQUIRED code, details.upgradeUrl and alternatives[0].url carried the generic Bitrix24 pricing page. A Vibe+ plan is enabled inside the account itself, so that page did not show what to actually do.
After
Both fields now carry an address on the customer account that opens the explanation for the required plan: https://<account domain>/online/?feature_promoter=limit_why_pay_tariff_vibe. The servers.create slot of GET /v1/me returns the same address — it used to disagree with the refusal body.
When the account domain cannot be recognised, both fields still carry the generic pricing page: no broken address is ever returned.
Impact on integrators
Nothing to change. The response shape is unchanged and both fields remain address strings. A client that sent the user to details.upgradeUrl now lands them on the plan they need instead of a generic price list. The refusal code, the field set and the response status are unchanged.
BC-0814-10: audio transcription accepts the file only in the file field, never truncates it silently, and returns a recognition refusal as 400
Old format supported until: not provided
Before
POST /v1/audio/transcriptions took the first multipart file part regardless of its field name and did not check the filename extension: an unknown extension was labelled audio/mpeg and forwarded for recognition. A file over the 25 MB limit was not rejected but silently cut at the limit: the answer was 200 with a transcript of only the beginning of the recording, and it was billed — nothing in the response indicated the cut. Any non-2xx from the recognition service came back as 502 ai_provider_unavailable, including a rejection of the request body.
After
The file part must be named file, otherwise 400 no_file — whatever the size of the file sent. That check runs before the recognition call. The filename extension is NOT checked: the recognition service detects the container from the content, so rare voice-recorder formats, a name without an extension and a part with no name are accepted exactly as before. A file the recognition service could not read comes back as 400 ai_provider_rejected. A file over 25 MB is rejected outright — 413 request_too_large, nothing charged; a truncated transcript no longer happens. A recognition refusal with HTTP 400 or 422 is returned as 400 ai_provider_rejected with a providerStatusCode field; rate limiting on its side is returned as 429 rate_limit_exceeded with a Retry-After header; unavailability and authentication errors stay 502 ai_provider_unavailable.
Integrator action
Name the file part file — a previous name such as audio no longer works. Split recordings above 25 MB before sending: such a request is now rejected rather than partly transcribed. If your code branched on the status, note that a body rejection now arrives as 400 rather than 502, and retrying such a request cannot help. There is no need to change extensions: the list includes the formats that used to be accepted silently. The old behaviour is not coming back.
NEW-0814-11: catalog product image metadata
Added GET /v1/catalog-products/:productId/images for a product-scoped snapshot of native images and GET /v1/catalog-products/:productId/images/:imageId to retrieve one image. The snapshot includes the detail picture, preview picture, and MORE_PHOTO gallery, but not files stored in other custom properties. Both methods require the catalog scope, return an untrusted detailUrl, and never expose the signed downloadUrl; server-side fetching requires the platform SSRF policy. Documentation.
NEW-0814-12: audio transcription can now draw on the Cowork/Code subscription quota
Calling POST /v1/audio/transcriptions with a key carrying the vibe:cowork scope used to consume nothing: transcription was metered against the Bitrix24 account AI quota, which subscription keys skip. A platform administrator can now price the transcription model per minute of audio, and such a call draws on the subscription quota, exactly like chat does.
Until a price is set the behaviour is unchanged: the call is free and no subscription limit applies to it.
Once a price is set and the quota window is exhausted, the endpoint answers 402 with code cowork_quota_exhausted — the same code chat already returns — plus a Retry-After header holding the seconds until the window resets. The body carries window (5h / week / month), resetAt and nextTier.
Response formats that carry no duration (text, srt, vtt) are billed at the per-call price, because the audio length is not reported for them.
FIX-0814-13: a wake schedule no longer shortens the auto-sleep you set
Before
When a server carried both an auto-sleep timeout (sleepAfterMinutes — 30, 60 or 240 minutes) and an enabled wake-schedule window, the idle threshold was silently replaced with the platform's short 15-minute one. The server fell asleep after 15 minutes instead of the value it was given, while GET /v1/infra/servers/:id and the server card kept reporting the chosen value — the divergence was not visible anywhere.
After
The value you set applies as set: a schedule only decides when the server wakes up. The short threshold stays exactly where it was introduced for — a server with no auto-sleep at all (sleepAfterMinutes: null), so that it still sleeps between windows. No integration change is needed; servers holding both an auto-sleep timeout and a schedule now stay up until their own threshold.
NEW-0814-14: a write with a lossy-charset value now reports it
Before
When a title or description arrived with its non-ASCII characters already replaced by question marks, the platform stored the value silently. The catalog card then showed ????????? ????????, and the only way to notice was to look at it.
After
The value is still stored, and the response now also carries a warnings array naming the fields that arrived with no non-ASCII character left, suggesting the text be re-sent as UTF-8. This applies to POST /v1/infra/servers and PATCH /v1/infra/servers/{id} (displayName, description), POST /v1/infra/servers/{id}/deploy (displayName, description), POST /v1/apps and PATCH /v1/apps/{id} (title), POST /v1/apps/{id}/publish (catalogTitle, catalogDescription). The warning is emitted only for a field the call actually applied: re-sending the same value, or a field the deploy dropped because it was already set, stays silent. The field is optional and absent when there is nothing to report, so existing clients keep working unchanged.
2026-08-13
FIX-0813-1: a failed native-module build now states its own cause instead of "Python not found"
Before
A Node app with a dependency shipping compiled code (better-sqlite3, for one) deployed only
sometimes. Such dependencies install in two steps: first a prebuilt file is downloaded, and if that
fails the module is compiled from source on the spot. When the download broke on the external
network, the second step began and failed — the app image carries no build tools. The deploy
response and the app card then showed the last line of that second step,
Could not find any Python installation to use, which pointed away from the real cause: the
customer went looking for a bug in their own code and in their Python version, while neither was
broken. The failure category came out generic — INSTALL_FAILED, with a hint about a missing
compiler.
After
This failure has its own category, NATIVE_PREBUILD_UNAVAILABLE, delivered in the category field
of the 502 GALAXY_APP_BUILD_FAILED response and in the app card's buildHint. The hint says what
is true: the prebuilt artefact could not be downloaded, the external source did not respond, this
is temporary, retry the deploy in a few minutes. The category is assigned only when both pieces of
evidence appear in the build log — the download broke on the network AND there was nothing to build
from source with; failures a retry cannot cure (no prebuilt artefacts are published for this
platform, the host ran out of disk) stay out of it and keep their previous text. The client retries
the deploy — the platform does not re-send the build on its own.
FIX-0813-2: API response shapes are described in the machine-readable schema
Before
The machine-readable schema did not show the actual response shapes for POST /v1/duplicates/find and GET /v1/lists/{iblockId}/elements. This made it harder to handle duplicate-search results and Bitrix24-native list-element fields.
After
The schema describes an entity-type keyed object or an empty array for duplicate search, and a list-element array in its envelope with dynamic property keys and a meta.total counter, without changing the actual API response.
Impact for integrators
Integrations can use the machine-readable schema to select the response shape. For list-element requests, account for the limit parameter being ignored.
NEW-0813-3: the real state of a sleeping galaxy app in the server GET and in the logs answer
GET /v1/infra/servers/:id now returns a reachability block for a galaxy app, answering the question "can the app respond right now". The status field is unchanged — it is the state of the app record, and during a wake it lags behind the machine: the record stays sleeping while the platform brings the galaxy up, which takes minutes on a cold machine. Beside it, reachability carries the summary effectiveStatus, the carrying galaxy state hostStatus and hostTunnel, the live-read container and forwarder, the check outcome probe and its time probedAt. The check runs only against a galaxy that is up and never wakes a sleeping one, so for a sleeping galaxy container and forwarder arrive as unknown. For every other server type the block is null.
The GET /v1/infra/servers/:id/logs answer for an app on a sleeping galaxy gained a recovery field and names the path that works: the wake call, the logsPreserved marker (the wake starts the same container, so lines written before the sleep stay in the log), the cold-galaxy boot budget in seconds, what to check readiness with, and whether a recurring wake window is available for this app. The hint field is still a string and now names the wake call as well.
The states and the order of actions — Galaxy app sleep and wake.
FIX-0813-4: file download declares its required scope in openapi.json
Before
The GET /v1/files/{fileId}/download operation carried no x-required-scope field in the machine-readable spec, although the runtime answers 403 SCOPE_DENIED without a scope. A client or agent deriving key scopes from the spec read it as "no scope needed" and was refused on the first call. The same operation also appeared in every openapi.json?scope= slice, including slices of other modules.
After
The operation declares x-required-scope: disk, its primary scope. The handler also accepts crm: a key holding only that scope downloads a file attached to a CRM file-type user field. The second scope is declared in a new optional x-alternative-scopes field next to the primary one. In slices the operation is kept for openapi.json?scope=disk and ?scope=crm, and is gone from the rest.
Integrator impact
A client reading only x-required-scope will request disk and keep working as before. A client holding a crm-only key can now see from the spec and from the operation page that the call is available to it — previously that was stated only by the refusal message and the key self-description in /v1/me.
FIX-0813-5: A readable build error instead of raw builder output and external addresses
Before
When a build failed because the base-image registry was unreachable, provisionError and the 502 body carried the builder output verbatim: the external registry name, the request path and a public IP address. Other build failures could also carry external links and public IP addresses in their text.
After
An unreachable image registry is now described by a fixed sentence: the cause plus the action — re-send the same deploy in a few minutes, the slot and its data are untouched. The failure category (provisionErrorCategory) and the retryable flag are unchanged. For every other build failure the cause is still passed through as-is, but external addresses in it are replaced with markers — full links, a scheme-less image-registry name where the line itself is about the image, an address named with no path at all in a network-failure line (getaddrinfo ENOTFOUND …, "could not resolve host"), and public IP addresses. First-party platform addresses, local and internal ones stay visible, and so do the image name with its tag, file names and package names: the marker goes where the text itself calls the token an address — so a package named socket.io stays readable even though its shape is identical to a registry's. One exception: a four-part numeric run (11.0.16.1) is indistinguishable from an IP address, so a version of that shape becomes a marker too.
Impact on integrators
Nothing to change: error codes, categories and the retryable flag are the same. A client that parsed provisionError by substring now gets a stable sentence for this failure class instead of changing builder output, and the full build log is still available in the buildLog field of the same response.
NEW-0813-6: the agent bundle manifest now reports the runtime verification mode
Two optional fields were added to the GET /v1/agent-bundles/:kind/manifest.json response.
runtime_integrity is either "on" or "off" and tells the agent whether to verify the files of its environment against the checksums recorded at install time. The agent verifies only on "on"; an absent field means verification is disabled.
runtime_integrity_budget is an integer that the platform currently always sends as one. The agent reads it as permission: above zero means recovery is allowed, zero forbids it. The agent caps the attempt rate on its own at one per hour, so values above one do not change behaviour. Recovery is governed by a platform setting rather than by this field; the field exists so that an agent receiving no value performs no recovery at all.
Existing requests are unaffected: the fields are optional, and the remaining manifest fields and the archive checksum are unchanged. Clients reading the manifest need to do nothing.
NEW-0813-7: the agent bundle manifest gained a targeted runtime repair operation
The operations object of GET /v1/agent-bundles/:kind/manifest.json now carries a repair_runtime key.
The operation reinstalls the agent runtime in place when the files of its environment have diverged from the hashes recorded at install time. Previously such a divergence could only be cleared by recreating the whole application.
Its available_to field holds the single value admin: the operation is rare and manually triggered, so a call made on behalf of scheduled jobs is rejected with OPERATION_FORBIDDEN. Success requires more than a zero exit code — the agent must also confirm that the environment now agrees; otherwise the operation answers status: "failed" with the reason in the error field.
Existing requests are unaffected: the other operations, the manifest fields and the archive checksum are unchanged. Clients reading the manifest need to do nothing.
FIX-0813-8: The reauth refusal no longer advises OAuth to keys that have none
Before
POST /v1/bots/{botId}/reauth answered 410 REAUTH_REQUIRED with one and the same advice on any dead credential — re-run authorization through POST /v1/oauth/authorize, or recreate the personal key. A key that reaches Bitrix24 through an inbound webhook has no way to follow it: there is no authorization flow and no refresh token behind such a key. Its owner read the instruction and hit a dead end.
After
The refusal text now depends on how the key authorizes, and error.details carries a new credentialKind field — oauth, webhook or unknown. For a webhook-backed key the refusal names the real cause (the webhook is dead, most often deleted on the Bitrix24 side) and the real remedy — re-minting the webhook, which preserves the key id and string so linked bots keep working. For an OAuth-backed key the text is unchanged.
Impact on integrators
Nothing to change: the response code and its status are the same. A client that parsed the refusal text as a string will see new wording for webhook-backed keys — branching on error.details.credentialKind is the sturdier option.
FIX-0813-9: deleting a galaxy app on a host that cannot be woken now answers a terminal 409
Before
DELETE /v1/infra/servers/{id} for an app on a galaxy host that cannot be woken (frozen
balance or a wake block) answered 502 GALAXY_HOST_UNREACHABLE with an error.hint object
and advice to retry later. That advice never worked: the wake was refused, not failed, so
clients kept retrying until their own timeout.
After
The refusal is terminal and arrives as 409 GALAXY_HOST_WAKE_BLOCKED. The body carries
error.reason with the cause: BILLING_FROZEN, ACCESS_EXPIRED, STOPPED or UNKNOWN.
The error.hint object is gone on this path — it described recovering an unreachable host,
and there is nothing to recover here; for genuine unreachability the 502 with hint stays
unchanged. Retrying is pointless — the cause has to be cleared.
The 409 GALAXY_HAS_APPS text for deleting the host itself was corrected too: deleting a
galaxy together with its apps is available in the dashboard, and the public API has no such
operation.
NEW-0813-10: the Marketplace trial can be started from a Cowork/Code desktop key
A new endpoint POST /v1/cowork/activate-market-trial starts the one-time Marketplace trial for the Bitrix24 account the calling Cowork/Code desktop key is bound to. Until now the programmatic path existed only for personal keys and application keys — the desktop key was rejected by POST /v1/portals/{id}/activate-market-trial, and that refusal stays in place.
There is no path parameter: the account comes from the key, so a client never needs an internal identifier. The body is required — {"acknowledgedOneTimeConsumption": true}, the literal true and nothing else. With it the client confirms that the user was shown that a one-time, non-revocable trial is being started, and was told when it ends. The confirmation is recorded in the account's audit trail.
A successful answer is {"success": true, "data": {"status": "activated", "trialEndsAt": "…"}}; instead of activated you may receive already_active (the trial or the paid access is already in force) or pending — the activation went through and Bitrix24 has not confirmed it yet, and in that case the request must not be repeated. The length is set by Bitrix24, so show the user the date from trialEndsAt rather than a number of days of your own.
Refusals: 400 DISCLOSURE_REQUIRED (no confirmation), 403 INSUFFICIENT_SCOPE (the key has no vibe:cowork right), 403 COWORK_DESKTOP_KEY_REQUIRED (a key of another class — an agent seat key carrying the same right, for example), 403 WRITE_BLOCKED_READONLY_KEY, 409 ALREADY_ACTIVATED, 409 TRIAL_ACTIVATION_UNAVAILABLE, 503 TRIAL_ACTIVATION_RETRY. The ceiling is 3 requests per hour per account.
Before showing the activation step, read activation.marketTrial.available on GET /v1/cowork/state. It is a forecast: false is final, true means offering is fine but does not promise success, so keep handling a refusal at activation time. In regions on the tariff access model this trial does not exist as a product: the pre-check answers region_not_supported and the call itself refuses with 409 TRIAL_ACTIVATION_UNAVAILABLE.
FIX-0813-11: transient errors no longer close the trial for good
Before
Every account carried a counter of failed trial activations, and it advanced on ANY error — including the ones Bitrix24 never ruled on: the request did not reach it, it answered with an internal error, or our own authorization failed. Once the counter reached its ceiling the account got 409 TRIAL_ACTIVATION_UNAVAILABLE on every further request, with nothing to bring it back. Meanwhile 503 TRIAL_ACTIVATION_RETRY invited a retry — so the advice led straight into the trap where a run of network failures cost the account its one-time trial.
After
Only Bitrix24 verdicts about the account itself spend the counter. A transport failure, an internal error on the Bitrix24 side and a problem with our authorization no longer cost an attempt — retrying after 503 TRIAL_ACTIVATION_RETRY is now as safe as the response says.
Impact on integrators
Nothing to change. Retry after a 503 exactly as before, except that it genuinely no longer moves the account closer to a refusal. 409 TRIAL_ACTIVATION_UNAVAILABLE stays final and keeps its meaning: a Bitrix24 verdict about the account itself.
FIX-0813-12: activating the trial again allows three attempts per hour
Before
POST /v1/portals/{id}/activate-market-trial promised three requests per hour per account in its description but delivered one: the x-ratelimit-limit header came back as 1, and a second request within the hour got 429 RATE_LIMITED with a retry-after of about an hour. For a user whose activation failed on a transient error, a "Try again" button stayed useless for the rest of the hour.
After
An account gets the three attempts per hour the description promises. The x-ratelimit-limit value matches what is documented, and 429 arrives on the fourth request.
Impact on integrators
Nothing to change. If you hard-coded an hour-long pause after the first 429, you can go back to honouring the retry-after header.
NEW-0813-13: new access refusal code INT_VIBE_PLUS_REQUIRED
The code reference gains INT_VIBE_PLUS_REQUIRED (HTTP 402). On the international surface it arrives where INT_TARIFF_REQUIRED arrives — on infrastructure creation and wake and on key issuance — and means the Bitrix24 account needs a Vibe+ plan. The addition is additive: existing codes and the response shape are unchanged, so treat an unknown code as a denial and branch on error.code rather than on the message text. Code breakdown — in Errors.
NEW-0813-14: Cowork/Code state now reports the paid term
The GET /v1/cowork/state response gained two fields inside the subscription object. termMonths is how many months the seat is paid for at once (1, 3, 6 or 12; 1 for a monthly seat). paidThroughAt is the date the seat is paid through, in ISO 8601.
This is not the same as currentPeriodEnd. The billing period is the quota window and it rolls every 30 days regardless of the term bought. On a seat paid for a year currentPeriodEnd falls a month from now while paidThroughAt falls eleven months from now.
The description of subscription.pendingTier is clarified along with it: a tier downgrade takes effect on paidThroughAt, not on currentPeriodEnd. Before terms existed the two dates always coincided, so the earlier wording was accurate; the coincidence now holds only for monthly seats. The same date is how long access lasts once a subscription is cancelled: cancelling stops the automatic renewal, and the term already paid for is served out in full.
Existing calls keep working unchanged — both fields are additions, nothing was renamed or removed.
FIX-0813-15: a server rename now shows up in the dashboard
Before
PATCH /v1/infra/servers/:id changes a server's displayName and description — the documentation for that endpoint calls them the two texts a person sees in the dashboard and on the application card in the Bitrix24 catalog, and there is no other endpoint for them. But the dashboard's application list and detail rendered neither: they rendered a copy of the server's name and description taken when the application was created. After a rename GET /v1/infra/servers/:id and GET /v1/me/sources returned the new value while the dashboard kept the old one, with no way to correct it.
After
An application with a linked server shows the server's displayName in the dashboard. The description comes from the application card when one was set there, otherwise from the server, and a description edited through this endpoint now reaches the application even when it had its own description: the last edit wins.
The name is now one value behind two doors: renaming the application in the dashboard changes the server's displayName, and renaming the server updates the card. The Bitrix24 catalog card title is re-published automatically after either edit. A name typed in the dashboard when the application is created now also becomes the new server's displayName, not just the card's.
No client action is required. Applications created before this fix follow the server too: the name shows up in the dashboard at once, the description from the first edit through this endpoint onwards, and until then the dashboard keeps the previous text.
2026-08-12
NEW-0812-1: nextPollAfterMs — the platform can ask you to poll events less often
GET /v1/bots/{botId}/events now returns an optional nextPollAfterMs field — how many milliseconds to wait before the next request. The field only reaches a bot whose events are delivered by webhook (eventMode is webhook): the Event.get queue of such a bot is empty by Bitrix24 design, so frequent polling brings nothing back. A bot in fetch mode never receives the field — keep polling at your current rate.
An absent field means "poll as before", so it never arrives empty or zero. The field is optional: a client that ignores it keeps working unchanged. When the response still has an unread remainder (hasMore is true), drain the queue without waiting out the requested pause — the pause applies to the next empty poll.
FIX-0812-2: `touSavedPct` is now a share of the monthly allowance, not of consumption
Before
GET /v1/cowork/state computed touSavedPct as a share of the undiscounted bill — saved / (saved + used). Such a ratio requires the numerator and the denominator to cover the same window, and the savings counter started mid-period. The field therefore returned null on purpose until the end of the first period — in practice for almost every subscription.
After
The denominator is the subscription monthly allowance, the same one the month quota bar is a share of. The value is meaningful from day one and is no longer suppressed: null arrives only when nothing has been saved yet in the current period. The value is capped at 100.
The two shares now read side by side: "N% of the monthly allowance used" and "off-peak hours gave back M%". The field type and range are unchanged; no client action is required.
NEW-0812-3: The app port is pinned to the server and survives a wake
Before
GET /v1/infra/servers/:id returned only localPort, which defaults to 3000 on every server — the response gave no way to tell whether the agent routes strictly to that port or elects one itself. A port set through PATCH /v1/infra/servers/:id/port lived only in the agent's memory: after the server woke up or the tunnel was repaired the election ran again, and the public address could start serving a neighbouring process.
After
A successful deploy that passed its healthcheck writes the port into the agent settings on the machine and marks the server as pinned. The GET /v1/infra/servers/:id response carries portPinned: when true, the agent proxies strictly to localPort and never re-elects the port, neither after a wake nor after a repair; when false, the port is still auto-detected from the listening sockets.
On a pinned server PATCH /v1/infra/servers/:id/port applies the port by rewriting the agent settings and restarting the agent instead of switching it on the fly. What this means for a client: 409 PORT_NOT_APPLIED never comes back on such a server; the tunnel drops for a few seconds; verified: true means the settings carry the requested port and the agent is back online, while verified: false means the port is written but the agent's return could not be confirmed (not a failure — the port takes effect once it comes up). port: 0 removes the pin. Two failure codes were added: 502 AGENT_CONFIG_WRITE_FAILED — the machine refused the settings write, 502 GATEWAY_ERROR — the command never reached the machine.
The data.steps[] of POST /v1/infra/servers/:id/deploy gained a port_pin step — the outcome of writing the port into the agent settings. It does not affect deploy success: a warning there means the application is deployed and running but the port was not pinned.
BC-0812-4: context in feedback submissions must be a JSON object
Old format supported until: not provided
Before
POST /v1/feedback accepted context values of any JSON type, including strings, arrays, numbers, booleans, and null.
After
context remains optional. When supplied, its top level must be a JSON object, including {}. A string, array, number, boolean, or null receives 400 VALIDATION_ERROR; the ticket is not created.
What integrators should do
Send an object: {"context":{"endpoint":"/v1/feedback"}}. Senders that used a string, array, number, boolean, or null must switch to an object: the old format stops being accepted with this release.
NEW-0812-5: app publication accepts sources from a named server
POST /v1/apps/:id/publish accepts an optional sourceServerId field (or the X-Source-Server header) — the identifier of the server whose sources are published. This closes the case where a deploy auto-save answered autoSaved: true yet the next publication still returned 409 SNAPSHOT_REQUIRED: when the server does not belong to the published app's OAuth key (for example, the deploy ran under a personal vibe_api_ key), the snapshot is kept with the server rather than the app, and the publish check could not see it. Naming the same server you called deploy with is now enough; saving the sources again through POST /v1/apps/:id/sources is unnecessary.
The platform never picks the server — you pass the identifier. Rights over the server are checked separately from rights over the app: the server-owner key, a personal key of the same user, or a Bitrix24 account administrator all qualify. No such server, a server from another Bitrix24 account, or a deleted one — 404 SERVER_NOT_FOUND; no rights — 403 NOT_AUTHORIZED; an X-Source-Server header that is not in UUID form or is sent twice — 400 VALIDATION_ERROR. The body field takes precedence over the header: when sourceServerId arrives in the body, the header is neither read nor validated. Together with sourceServerId, the sourceVersionId field means a version of that server: a server has its own version numbering.
The published tag and the publication stamp are applied only to the app's own version. Publication does not mark a server's version: on it, those same fields hold the deploy history returned by GET /v1/infra/servers/:id/sources, and the tag means indefinite retention plus a delete block. There is a consequence worth knowing: a published server version has no indefinite retention — it lives by the ordinary cleanup rules and can be removed while the app stays published. That is why warnings carries a line with a ready-made PATCH /v1/infra/servers/:id/sources/vN you can use to tag the version yourself; when the version is already tagged, there is no such line. If source storage is disabled for the Bitrix24 account, the sourceServerId you passed is not used at all — publication proceeds and warnings carries a line saying the selector went unused.
Without sourceServerId the behaviour is unchanged — the check looks for a fresh snapshot of the app. The 409 SNAPSHOT_REQUIRED refusal gained a hint.reason field with four values (app_snapshot_missing, app_snapshot_stale, server_snapshot_missing, server_snapshot_stale), and on the path without a named server a hint.serverKeyedSources block that explains how to publish sources from a server. The message text of that refusal was rewritten: it used to open with the word "Deploy" and offered POST /v1/apps/:id/sources as the only way out.
Details — Publish an app and Source storage.
NEW-0812-6: deal product rows: the response now signals truncation
Before
GET /v1/{entity}/{id}/products (deals, leads, invoices, quotes, smart-process items) returned only
the first page of product rows — Bitrix24 pages them at 50 — and gave no indication of it. The response
carried success and data with no meta field at all, so an item with 125 rows and an item with 50
rows produced identical-looking responses, and an app lost rows with no way to notice. The limit and
offset parameters had no effect on this route.
After
The response now carries a meta block: meta.total is how many product rows the item has in total
(as reported by Bitrix24), and meta.hasMore tells whether rows exist beyond the returned page. The
success and data fields are unchanged, so a client reading only those keeps working as before.
Alongside that, limit and offset now work on
GET /v1/deals/:id/products and its counterparts for leads,
invoices, quotes and smart-process items. A limit of up to 5000 returns that many rows in one call,
and higher values are clamped to 5000. offset counts rows rather than pages: offset=7 starts at the
eighth row. meta.hasMore is correct for any requested window, not just the first one. A call without
limit and offset behaves exactly as before. A limit=0 is not a page size: the default applies and
meta.warnings carries an entry with code LIMIT_ZERO_IGNORED, the same as on the list routes. The
behavior is documented in the knownIssues list of GET /v1/guide.
NEW-0812-7: the machine schema now describes the live app, AI and telephony methods
Eighteen operations that were live and described on the documentation pages were missing from the machine schema at GET /v1/openapi.json. A client checking the schema did not find the method and concluded it did not exist. The schema now covers them: the applications family (GET /v1/apps, create, read, update, delete, publish, unpublish and OAuth re-link), AI spend and quota (GET /v1/ai/usage, GET /v1/ai/quota), the off-peak schedule (GET /v1/off-peak), AI follow-ups for finished calls (POST /v1/calls/followups/list, GET /v1/calls/followups/:callId) and the line list (GET /v1/voximplant-lines).
The methods themselves are neither new nor changed — same addresses, same parameters, same response shapes. Only the description is new, so there is nothing to change in an integration.
Four OpenAI-compatible AI methods answer at two addresses at once: /v1/models and /v1/ai/models, and likewise for chat completions, embeddings and audio transcriptions. Only the short addresses were in the schema. Both are described now, and the /v1/ai/ form is explicitly marked deprecated: its responses carry Deprecation: true and X-Deprecated-Use naming the canonical address. Calls on it keep working, but move to the short address. The exception is GET /v1/ai/usage and GET /v1/ai/quota — they have no short form, they are canonical themselves and carry no deprecation headers.
The schema still leaves out the hint addresses that exist only to answer with a wrong-path error listing the real ones, and the single-model detail route: its identifier contains a slash, which an OpenAPI path parameter cannot express.
FIX-0812-8: the free-tier plan list is returned as the public identifier
Before
On the international surface, allowedPlans in the 402 PLAN_NOT_ALLOWED_ON_TRIAL body and in
capabilities.servers.create.limits (GET /v1/me) carried an internal plan identifier — not the
one the same plan is given in the GET /v1/infra/providers/:providerId/plans catalog. The same identifier was
interpolated in quotes into the human-readable note, so an AI assistant relayed it to the user
verbatim, and the neighbouring requestedPlan could echo a plan chosen by the platform itself
(agent creation) rather than by the client.
After
Both fields and the note text now carry the same neutral identifier the plan catalog returns, so
the gate response and the catalog finally agree. Identifiers from the previous catalog are still
accepted as server-creation input, so a client sending back a value it received earlier keeps
working unchanged. In the dashboard, the card of a freshly created server no longer shows an
identifier in place of the plan name.
FIX-0812-9: Requisite custom-field filtering is corrected
Before
GET /v1/requisites with filter[ufCrm_1698325419] forwarded the custom-field name unchanged. Bitrix24 ignored that key, so the successful response did not narrow the list.
After
Both spellings of one field — filter[UF_CRM_1698325419] and filter[ufCrm_1698325419] — are accepted and filter identically. A spelling Bitrix24 never produces (filter[ufCrm_taxId], for one) is still not applied. No UNKNOWN_FILTER_FIELD is introduced for custom fields.
FIX-0812-10: an unconfirmed Marketplace trial activation now answers pending instead of activated
Before
POST /v1/portals/{id}/activate-market-trial returned two success statuses: activated and already_active. When Bitrix24 switched the trial on but sent no confirmation for it, the call still answered activated with a trialEndsAt field — reporting a completed activation that nobody had confirmed.
After
The unconfirmed outcome has a status of its own — pending, with no trialEndsAt. It means the trial on the Bitrix24 side is already switched on and cannot be switched on again, but the confirmation has not reached us yet. The activated and already_active statuses are now returned for confirmed outcomes only.
Impact on integrators
Review your data.status === 'activated' branch: some successful activations will no longer land in it. Treat pending as a completed call — re-read the account state in a minute (Key self-description) and do not repeat the request, it will run into the rate limit.
FIX-0812-11: a dedicated-machine deploy keeps the build's internal links and checks the application root
Before
The platform removed every symbolic link from the extracted tree before starting the app. A prebuilt server-rendering bundle links its own files relatively inside its own directory, so the app lost part of its dependencies and answered HTTP 500 at the root address. The health check polled only the path given in healthPath, so a light endpoint such as /api/health kept answering 200 and POST /v1/infra/servers/{id}/deploy reported success. Inside Bitrix24 the app still did not open: the error arrived with the app's own X-Frame-Options header, so instead of an error page the user saw the browser's connection-failed message.
After
Links inside the deploy directory are kept. Only unsafe ones are removed: absolute links, links whose target does not exist, and links whose real path leads outside the deploy directory.
In addition, when healthPath is something other than /, the deploy probes the application root as well once that path has passed — the address Bitrix24 opens. A 5xx at the root stops the deploy with an error on the healthcheck step. A 4xx does not stop the deploy and arrives as a separate app_root step with status warning and the reason in stdout. With the default healthPath (/) nothing changes: no extra request is made and no app_root step is returned.
Judging the links costs more than the old blanket removal, so on a very large tree the pass may not fit its time budget. Such a step used to report success anyway — the normalize_windows_paths and cleanup_metadata steps now arrive with status warning and an explanation in stdout instead, and the deploy continues.
Impact on integrators
No call has to change. An app that answers at the root behaves as before. A server with no page at the root — an API-only backend, for instance — still deploys successfully: its 404 arrives as a warning, and to silence that warning point the application at the path it really answers on via PATCH /v1/apps/{id}. A deploy whose root answers with a 5xx now ends with an error instead of a false success — the previous green result was wrong in that case.
BC-0812-12: task time numeric fields are returned as numbers, not strings
Old format supported until: not provided
Before
GET /v1/task-time and GET /v1/tasks/:taskId/time returned id, taskId, userId, seconds, and minutes as JSON strings, while the field reference declared them numeric — the description and the response disagreed.
After
Those five fields are returned as JSON numbers. The source field stays a string. The GET /v1/task-time/fields schema is aligned in the same change.
Integrator action
Update strict schemas on your side: where a string was expected, a number now arrives. The old format is not returned any more, so a comparison such as seconds === "900" stops matching and must compare against a number.
FIX-0812-13: a filter with a reserved field name is rejected instead of returning the whole collection
Before
A filter with a reserved field name — filter[__proto__], filter[constructor], filter[prototype] — was accepted and silently returned the FULL, unfiltered collection (and on /{entity}/aggregate, a count over the whole collection). A client that built the filter field name from user input believed the result was filtered.
After
Such a filter is rejected with 400 UNKNOWN_FILTER_FIELD (like any other unknown field), on every entity and on /{entity}/aggregate.
FIX-0812-14: a key allowed to deploy can also patch the application on that server
Before
A key bound to a server through its application deployed code successfully: POST /v1/infra/servers/:id/deploy answered with success. But exec, upload and log reads with the same key answered 403 WRONG_KEY, and there was no programmatic way to obtain that right — the managing key is changed from the dashboard only. A small runtime fix on a server that accepts deploys was unreachable. The refusal on exec carried no hint object, so it offered no recovery guidance either.
After
Rights on a server split by what the call touches. Operations on the application's content — deploy, exec, upload and log reads — accept either the server's managing key or a key whose application is bound to that server. The machine itself (start, stop, wake, reboot, sleep, delete, repair, mode, access policy, port, SSH), access tokens and the icon upload still require the managing key.
A 403 WRONG_KEY refusal now carries a hint object on all four routes and names both recovery steps: rebind the server in the dashboard and switch the key your client sends — the rebind does not change the secret you are already sending. The response code and its meaning are unchanged: what was refused before is still refused.
Integrator impact
No changes required. Calls that worked keep working; calls that answered 403 WRONG_KEY because of an application link now go through. Details — server access recovery.
FIX-0812-15: room in a shared galaxy is decided by the machine's own measurement, not by a fixed seat count
Before
A request to place a new application into a shared galaxy was refused with GALAXY_FULL as soon
as the machine held a fixed number of residents. That number was derived from the memory its plan
advertises rather than from what is actually in use, so the refusal also arrived on a nearly idle
machine, and another galaxy was raised for the request.
After
The answer is built from two measured conditions of the machine itself: free disk space and a
memory forecast. The forecast takes the occupancy the machine reported at its last measurement and
adds the cost of the applications that were not running at that moment, against its measured
memory. While both hold, the galaxy keeps accepting residents, and GALAXY_FULL means there is
genuinely no room.
A machine with no measurement yet, or a stale one, answers as before — by the seat count.
Rollout
The new rule ships disabled and is enabled per portal. While it is off, answers do not change: the previous seat count still decides. The measurements are introduced by this same update and fill in gradually, by sweeping awake machines, so there is nothing for the rule to apply to any earlier.
NEW-0812-16: deploy operation id and after-the-fact outcome reconciliation
Deploying an app to a server now issues an operation id BEFORE the work starts, not at the end. It arrives as the X-Vibe-Operation-Id response header in both modes, in streaming mode additionally as a first operation event (a browser EventSource does not expose headers), and is mirrored as an operationId field in the envelope — on success and on failure alike.
That id reads the outcome through a separate request: GET /v1/infra/operations/:operationId. It answers with a status of running, succeeded, failed or unknown (the process died between the start and the outcome write), the step, the start and finish times, and the error code. The handle is read-only and has no side effects.
Why it exists: the outcome used to live only inside the deploy response itself, so a dropped connection left no way to learn how it ended — the only option was to deploy again without knowing whether the first attempt had worked. The id is now issued before the long work begins, so an interrupted call can be reconciled.
An id is issued only for a deploy to a standalone virtual machine (kind: "STANDALONE"). A galaxy application (kind: "GALAXY_APP") gets none — not in the header, not in the frame, not in the body — and reconciling its outcome afterwards is not supported yet.
Outcomes are kept for 7 days and each is addressable on its own, including earlier attempts on the same server. An elapsed id answers differently from "not found" — 410 with code OPERATION_OUTCOME_EXPIRED, meaning "the operation existed, its outcome is no longer stored". There is no operation list yet.
A missing header does NOT mean the deploy never started: if the platform could not open the record, the deploy proceeds without an id. Do not read a missing header as a failure and launch a second deploy on top of the first.
NEW-0812-17: A failed deploy reports its outcome as data, not prose
A dedicated-VM deploy could end like this: the app is deployed and serving, the health check answers
with an error, and the platform has already moved the app from restricted privileges back to full
ones. Telling that apart from "the deploy never happened" was only possible by reading English prose
inside error.message — while the consumer here is a machine that reads fields.
The healthcheck step in data.steps[] now carries three facts about the probe: httpCode — the
status of the last attempt (the field is absent when no attempt got one; zero or null are never
sent instead), healthPath — the path that was actually probed, and portOwner — service,
foreign or unknown. These are facts about the attempt, not a verdict: a step with the error
status may legitimately carry httpCode: 200, because the outcome is decided by success and the
step status.
The hardening step now also arrives on a failed deploy — previously it appeared only on a
successful one. It carries the hardeningRollback field: completed — the revert finished and the
app runs with administrator privileges; incomplete — the revert started and did not finish, so the
app may still be running with restricted privileges. The same value is mirrored in the error
envelope as error.hardeningRollback. The field describes the revert of the unit and of the deploy
directory ownership, and nothing else; its absence means "no revert was attempted", not "restricted
privileges were never applied" — the latter is answered by the service_user step.
All fields are optional and additive: existing calls keep working unchanged.
2026-08-11
NEW-0811-1: server plans now name their price unit
Every plan in the GET /v1/infra/providers/{providerId}/plans response carries a new currency field set to "Vibes" — the unit priceMonthly and sleepPriceMonthly are counted in. Prices used to arrive as bare numbers and the unit had to be read out of the documentation prose; it is now the same machine-readable marker the search cost object in GET /v1/me uses.
The field is additive: existing calls keep working unchanged, and neither the numbers nor their meaning moved. The catalog price may still differ from what a given Bitrix24 account is actually billed.
NEW-0811-2: the application data directory is declared in the deploy body
An application on a dedicated virtual machine starts under an unprivileged account, and the platform handed that account only the extraction directory. A state directory outside it — the very /opt/data our documentation recommends for data that must survive a rollout — is created by the administrator-run install and preStart steps, so it stayed with the administrator and the application's first write there failed with a permission error. The only way around it was handing out permissions by hand in preStart on every deploy.
Before
The application could only write to its own extraction directory. There was no declarative way to name a state directory.
After
Two optional fields were added to the body of POST /v1/infra/servers/{id}/deploy. dataDirs takes up to eight directories outside the extraction directory; the platform creates each one and hands it to the application account on every deploy, and hands it back on a rollback. A path must be absolute, already normalized and inside /opt, /srv or /var/lib; the bare roots are refused with the new INVALID_DATA_DIRS code before the deploy takes the server. dataDirsRecursive additionally hands over the contents of the declared directories — needed only for a pre-seeded tree, and not accepted for /opt/data, because our own database restore recipe keeps a password file there.
What is handed over is the directory itself, not its contents: files the administrator put there earlier do not change owner. Whoever owns a directory can delete and replace the files inside it, so keep scripts you run as administrator and any credentials in a directory you did not declare.
Existing calls behave exactly as before: without these fields no directory is created and no ownership changes.
NEW-0811-3: issuing a key with exactly the selected platform rights
The POST /v1/keys body accepts an optional exactScopes field. With exactScopes: true the key stores exactly the rights listed in scopes: the four platform ones (vibe:infra, vibe:ai, vibe:search, vibe:storage) are not appended at issue, and vibe:ai / vibe:search are not added to the request's rights on the fly. As a result GET /v1/me returns exactly the stored set, and a key without vibe:infra answers 403 INFRA_SCOPE_REQUIRED to POST /v1/infra/servers.
The default is unchanged: without the field the four platform rights are still added to the requested ones, so scripts already written keep working as before.
Keys issued in the dashboard are now exact as well — clearing a platform-right checkbox means the key does not hold that right. Previously the Vibecode set was appended unconditionally, and the only way to narrow the rights was to edit the key after it had been issued.
NEW-0811-4: off-peak hours are visible in the Cowork/Code subscription and in the exhausted-quota refusal
During certain hours of the week the quota is consumed more slowly — the same call takes a smaller share of the limit. This used to be visible only in the off-peak schedule, and now the same information arrives in the Cowork/Code subscription responses and in the refusal on an exhausted quota, so an app can offer to move a bulk job into a cheap hour instead of assembling the schedule itself.
| Response | What was added |
|---|---|
| GET /v1/off-peak | currentWindowEndsInHours — in how many hours consumption stops being this favourable. null when it never gets more expensive within a week ahead |
| GET /v1/cowork/me | The offPeak block — whether a discount applies right now, the consumption multiplier and when the next cheap hour arrives. Without the hour grid |
| GET /v1/cowork/state | The same offPeak block together with the week-long hour grid, plus the touSavedPct field — the share of this billing period's consumption the off-peak hours took off |
The 402 cowork_quota_exhausted refusal on POST /v1/chat/completions |
offPeakHint — in how many hours the block is lifted (inHours) and which consumption multiplier will be in effect at that moment (multiplier). It arrives only when that moment falls into a discounted hour |
All of these keys are optional and arrive when the capability is enabled for the Bitrix24 account. It is switched on account by account, and until it is on, the keys are absent from the response body entirely — they never arrive as null. Test for the presence of the key, not for its value.
Off-peak hours are not in effect on this platform yet: the schedule reports full price, currentWindowEndsInHours arrives as null, and neither the subscription block nor the refusal hint arrives at all. The fields are part of the contract, so a client written against them keeps working once the hours are switched on here.
The multiplier is a consumption coefficient, not the discount size. A value of 0.5 means the call takes half the quota share it would take without a discount.
The block fields, response examples and specifics — Off-peak hours in the Cowork/Code subscription.
FIX-0811-5: the next off-peak hour is counted from the hour boundary, not from the minute of the request
Before
The nextWindow.inHours field of GET /v1/off-peak was counted from the moment of the request. The schedule is hourly, so an answer of "in 2 hours" received at 10:55 pointed at 12:55 — the middle of the cheap hour that had started at 12:00. A client adding that number to the time of its request landed inside the cheap hour with most of it already gone. The same counting behaved the same way in the off-peak card of the dashboard (GET /api/ai/tou).
After
The count runs from the boundary of the current hour in the schedule's timezone. The same "in 2 hours", received at any minute between 10:00 and 11:00, points at 12:00 — the start of the cheap hour. The field is now what its documentation describes — the nearest hour that is cheaper than the current one. The divergence from the previous reading is at most one hour. The same fix applies in the off-peak card of the dashboard.
Impact on integrators
Nothing to change, the response shape and the field type are the same. A scheduler that adds inHours to the time of its request now starts at the beginning of the cheap hour instead of its middle, and the start moment can shift by at most one hour. Off-peak hours are not in effect on this platform yet, so nextWindow arrives as null here until they are switched on.
BC-0811-6: a server whose operating system does not boot answers with a dedicated code
Old format supported until: not provided
Before
A server whose guest operating system does not start after an interrupted update looked like an
ordinary "temporarily offline" one. Repair was launched again and again and each time returned a
message about the SSH key, which has nothing to do with the failure, while
POST /v1/infra/servers/:id/start answered 200 and brought up a machine that would not boot
anyway. There was no way to tell a hopeless machine from a temporarily unreachable one from the API
responses.
After
Such a machine carries the machine-readable marker provisionErrorCode = "GUEST_NOT_BOOTING" in
GET /v1/infra/servers/:id and in the server list. POST /v1/infra/servers/:id/repair
and POST /v1/infra/servers/:id/start answer 422 with the code GUEST_NOT_BOOTING instead of
doing knowingly useless work, availableActions keeps only delete, and GET /v1/me
advises recreation instead of repair in its infra.unhealthyServers block. Billing for such a
machine is closed at the moment of the verdict.
What integrators should do
Check provisionErrorCode before start and repair: the value GUEST_NOT_BOOTING is terminal,
retrying will not help, the server has to be recreated. A client that calls start on a schedule
will get 422 on such a machine instead of the former 200 — handle that code as "recreate", not
as a temporary error. The old behaviour is not kept: the 200 meant starting a machine that does
not boot anyway, and kept billing for it.
NEW-0811-7: the "app is not responding" reply now has a separate code for a server with no deploy
Before
When the tunnel to the server was open but the app did not respond, a machine caller always got the
same 503 with the code BH_APP_STARTING and a Retry-After header. A server that had never been
deployed to was indistinguishable from one where the app had crashed or was listening on the wrong
port, and retrying on a timer looked reasonable where there was nothing to wait for.
After
If the platform has no deploy on record for that server, the reply carries the code
BH_APP_NOT_DEPLOYED and no Retry-After header — retrying on a timer is pointless, the fix is a
deploy or a check of the address. The previous BH_APP_STARTING with Retry-After stays for the
case where a deploy did happen: there the app really can come up on its own. The status is 503 in
both cases, so handling that does not branch on the code keeps working as before.
FIX-0811-8: document template creation declares its required scope in openapi.json
Before
The POST /v1/doc-templates operation carried no x-required-scope field in the machine-readable spec, although the runtime answers 403 SCOPE_DENIED without the documentgenerator scope. A client or agent deriving key scopes from the spec read it as "no scope needed" and was refused on the first call. The same operation also appeared in every openapi.json?scope= slice, including slices of other modules.
After
The operation declares x-required-scope: documentgenerator, like the rest of the module. In slices it is now kept only for openapi.json?scope=documentgenerator and in the full spec.
Impact on integrators
No action required: the endpoint itself behaves exactly as before. Clients that derive their scope set from the spec will now request documentgenerator up front and stop hitting the 403.
FIX-0811-9: Requisite user fields in responses
Before
crm.requisite.get did not return requisite user fields in V1 responses.
After
After a successful crm.requisite.get, the service fetches requisite user fields with a narrow side-read and keeps the successful response if that extra call is unavailable.
NEW-0811-10: granted_scopes — the issued key's actual scopes in the exchange response
POST /v1/connect/token now returns a new granted_scopes field — the scope set the issued API key actually carries. The scopes field is unchanged: it still holds what the app requested at authorization. The two sets usually match, but they diverge for apps whose scopes the platform assigns itself — with the Cowork desktop device sign-in, for instance, the app requests nothing, so scopes arrives empty while granted_scopes lists the key's full set. The field is optional and may be absent if the platform could not read the issued key's rights. It never arrives empty, so a missing field means the set is unknown rather than that there are no rights. Existing calls keep working.
FIX-0811-11: OpenAPI: required scopes for bespoke operations
Before The machine-readable OpenAPI contract omitted x-required-scope for bespoke operations even though their handlers already checked the scope before calling Bitrix24.
After The contract publishes the handler-enforced scope for CRM, bots, calls, chats, lists, note, notifications, posts, scrum, tasks, timelines, userfields, warehouses, workday, and workflows operations. The download proxy with two accepted scopes remains unannotated because one scalar annotation would be inaccurate.
FIX-0811-12: the /v1/guide reference returns a docs link for the telephony-lines entity
Before
In the GET /v1/guide response the telephony-lines entity arrived without a docs field. The page describing lines existed, but the reference response gave no way to find it.
After
The telephony-lines entity carries docs pointing at the telephony section /docs/telephony/lines. No other response field changed and no client action is required.
FIX-0811-13: a write with Content-Type: text/plain is now rejected instead of creating an empty record
Before
A create or update request (POST/PATCH) with Content-Type: text/plain and a non-JSON body was accepted: the body was silently dropped and the write still ran — POST created an empty entity with 201, PATCH silently changed nothing. Every other non-JSON type already returned 415.
After
Such a request returns 415 Unsupported Media Type, like other non-JSON types; no empty record is created. Send the body as application/json.
BC-0811-14: deploy no longer reports success when the port stayed with the previous process
Old format supported until: not provided
Before
A deploy on POST /v1/infra/servers/{id}/deploy could answer success: true with healthcheck: ok while the application port was still held by a process that had been there before the deploy started. The health check got its 200 from that process rather than from the new application, so the public address kept serving the previous version. The stop_existing step did warn that the port owner was unrelated to the new application, yet the deploy went on and finished as a success.
After
When the health check gets a 200 while the port is held by a process that was already on it before this deploy began, the healthcheck step fails. The error text names the port owner. The response becomes success: false with status: "error" on that step, and in streaming mode the stream stops at the same step.
Deploys where the port is published by a process this very deploy started still finish successfully — including one brought up by the install or preStart step, that is, before the service itself starts. The case where the port owner cannot be determined is unchanged as well: such a deploy still counts as a success.
What integrators should do
Check whether the application port is held by a process that outlives deploys: a reverse proxy, a process manager, or a container or systemd unit brought up once from the install or preStart step and not recreated on later deploys. That setup used to answer success: true and will now answer success: false: the platform cannot confirm that the new version is the one answering at that address. Give the application port to the application and move your own process to a different port. Everyone else has nothing to change: the response shape is the same, and a flow that used to receive success: true while the new version never came up now receives success: false with the reason.
NEW-0811-15: Cowork/Code tier change cost preview
GET /v1/cowork/subscription/preview?tier=<FREE|PRO|MAX|ULTRA> returns the amount that will be debited right now if the caller switches to the given tier: the full price of the operation, the credit for the unused part of the paid month, and the resulting net. Scope vibe:cowork, the response is never cached, and the endpoint is limited to 30 requests per minute per account and user.
Build the confirmation screen on this response rather than on the tier price in tiers[].feeVibes from GET /v1/cowork/state: that price is a sticker, and the debited amount already differs from it in four cases. Choosing the tier the seat already holds debits nothing; a downgrade queued for the end of the paid period debits nothing now; asking again about such a queued downgrade also debits nothing; and an upgrade credited for the unused month debits the price minus the credit. The pair netVibes: "0" and scheduled: true answers "will this charge me now" outright, so those rules need not be reimplemented in the client.
Vibe credits arrive as a decimal string in major units, matching the wallet balance and account movements. Alongside them the response carries currency (an ISO 4217 top-up currency code or null) and topUpAvailable; both describe topping up the wallet, not the price of the tier — the response carries no money price for a tier and no Vibe-to-money rate.
FIX-0811-16: app install failure message now names the portal's answer
Before
When Bitrix24 refused a developer-key app install and the cause matched no known
code, the response carried DEVKEY_MINT_FAILED and the generic text Failed to install app via developer key. It gave no way to tell an access refusal from an
unavailable REST module or a transport failure.
After
The same text now carries what we observed: Failed to install app via developer key (Bitrix24 answered HTTP 403 BITRIX_REST_V3_EXCEPTION_ACCESSDENIEDEXCEPTION).
The error code (error.code), the HTTP status and the userMessage field are
unchanged, so code-based handling keeps working as is. Refusals with a recognised
cause — subscription, plan, stale key — keep their previous text. The same
wording reaches the dashboard: the warning about an auth key that was not issued now
names the portal's answer.
The answer is appended only when there was one: on a transport failure, where the portal never replied, the text stays as before. The refusal code is appended in machine form and never longer than 64 characters.
FIX-0811-17: read-only keys no longer reject read operations as writes
Before
A read-only key could receive 403 WRITE_BLOCKED_READONLY_KEY for requests that read data through the Bitrix24 methods timeman.status, timeman.settings, calendar.event.getbyid, bizproc.workflow.instances, lists.get.iblock.type.id, lists.element.get.file.url, crm.type.getByEntityTypeId, and crm.activity.call.getTranscript.
After
A read-only key allows these read operations. Unknown methods are still treated as writes and blocked.
BC-0811-18: marking messages read now requires a key with write access
Old format supported until: not provided
Before
A key in read-only mode could mark notifications and messages as read: POST /v1/notifications/read, POST /v1/chats/:dialogId/read and POST /v1/bots/:botId/chats/:dialogId/read went through and changed portal state — the unread counter, notification statuses. They passed because write access is decided from the name of the Bitrix24 method being called, and these names end in the word "read", so they were taken for reads.
After
Under a read-only key all three operations answer 403 WRITE_BLOCKED_READONLY_KEY. The code is now listed in the error reference of each of the three pages. A key with write access works as before.
What integrators should do
If your scenario marks things read, issue or switch a key to read-write mode in the keys section of the dashboard. Reading notifications and messages with a read-only key is unchanged. Chat event subscription (POST /v1/chats/events/subscribe and POST /v1/chats/events/unsubscribe) is still available to a read-only key — a deliberate exception, without which a read-mode agent could not follow events.
FIX-0811-19: five more read operations stopped being rejected under a read-only key
Before
Entry FIX-0811-17 lifted the rejection for eight read methods, but five operations of the same class remained: GET /v1/tasks/:taskId/chat/messages, GET /v1/humanresources/nodes/:id/children, GET /v1/humanresources/employees/:id/subordinates, GET /v1/mail/messages/:id/thread and GET /v1/mail/mailboxes/:id/senders. A key in read-only mode answered them with 403 WRITE_BLOCKED_READONLY_KEY: write access is decided from the name of the Bitrix24 method being called, and these operations end in a generic word, so an unrecognised name was treated as a write. No one had reported any of them — the mismatch surfaced from a sweep of every method against its route.
After
All five answer a read-only key like any other read. The 403 WRITE_BLOCKED_READONLY_KEY rejection stays on write operations in the same sections: sending an e-mail, editing the org structure, posting to a task chat.
Impact on integrators
Nothing to change: requests that used to be rejected now go through. If you issued a read-write key just for these operations, switch it back to read-only.
NEW-0811-20: Cowork/Code state tells the account how to open up work with Bitrix24
The GET /v1/cowork/state response now carries an activation block. It names the access model of the account region (model: subscription or tariff), links to the Bitrix24 plan terms (tariffInfoUrl — only when model is tariff and the account is not self-hosted) and answers up front whether the Marketplace trial is worth offering: marketTrial.available and marketTrial.unavailableReason (trial_already_activated, subscription_active, demo_used, region_not_supported, not_cloud, portal_state, not_supported), plus the journal of our own attempts — status, endsAt, activatedAt.
International accounts run on the tariff model, so they get model: tariff with the plan terms link, and the trial is reported as unavailable with region_not_supported. The flag is computed before any attempt and false is final; true means offering is fine but does not promise success, so keep handling a refusal at activation time. The list of reasons may grow — read an unknown value as "do not offer the trial". The activation block itself may be absent from the response: that is how a platform that does not know about it yet answers, and it is a normal state — test for the block, then read the value of the field inside it.
FIX-0811-21: Cowork/Code state now has a polling ceiling
Before
GET /v1/cowork/state accepted requests at any rate, even though the documentation recommends polling it once every 15–30 seconds.
After
There is a ceiling now, shared by the account and the key owner, so every device of one person draws on the same budget. Above it the endpoint answers 429 RATE_LIMITED.
Impact on integrators
A client that keeps the recommended interval will not notice the ceiling — it spends about two requests per minute, several times below the limit. If your polling is faster, space it out or handle the 429.
FIX-0811-22: the upgrade link in the capabilities response no longer leads nowhere
Before
In GET /v1/me, when server creation was refused, the capabilities.servers.create.alternatives[].url field (and the same address inside the userMessage text) pointed at the licence page inside the account itself. That address differs between Bitrix24 editions and versions and answered 404 on some accounts — the platform moved off it in every other response back in April, and only this one was left behind.
After
A stable address arrives instead: the account checkout page where access is opened by a subscription, and the shared Bitrix24 plan terms page where access is opened by a commercial plan. The "subscribe" wording is no longer shown to regions that have no subscription as a product — they get the general wording plus the plan terms link.
Impact on integrators
No action required: the field is still there and may still be absent. Do not persist the value and do not parse its host — it is a platform address, not an address inside your account.
FIX-0811-23: the ai_congested retry pause now scales with the configured base and is capped
Before
When the platform throttled the flow of AI requests, POST /v1/chat/completions,
POST /v1/embeddings and POST /v1/audio/transcriptions
answered 429 with the ai_congested code, a Retry-After header and a retryAfter body field. The pause
barely varied — consecutive refusals came back with near-identical values — and it had no upper bound.
After
The pause varies more widely and may come back longer than it used to; it is now capped — never more than an hour. How much longer depends on how the platform is throttling at that moment, so the only correct behaviour is the one it always was: wait exactly as long as the response says.
The Retry-After header and the retryAfter body field still carry the same number.
Impact on integrators
No action needed: the response shape, the ai_congested code and the set of fields are unchanged. If your
client assumed the pause fits within the base plus two seconds, drop that assumption and wait for as long as
Retry-After says. The same applies to calls through the deprecated /v1/ai/* addresses, which reach the
same handlers.
NEW-0811-24: an interrupted galaxy deploy now names its cause and flags repeats
The 502 GALAXY_DEPLOY_INTERRUPTED response now carries two new optional fields. Existing clients keep working unchanged: the error code, retryable and error.hint are all still there.
error.subcause names what actually happened — until now every interrupt looked the same and the only advice was "retry":
http_window_exhausted— the request window ran out before the platform checked even once. The build may well have finished on the host, and the next attempt confirms it in seconds.exec_channel_busy— the host command channel stayed busy until the wait budget ran out.no_this_deploy_container— the host answered, but no healthy container of this very deploy was found.tail_unreached— the connection dropped and no clean answer arrived before the deadline.source_fetch_interrupted— the source archive fetch was interrupted before the build started, and nothing of the app was touched.
error.repeated turns true once the same app has been interrupted several times inside a short window. error.hint then stops advising a plain retry and asks you to check whether the app starts at all — and, if the app is known-good, to contact support with the server id.
The error text also stopped claiming the host is reachable in the case where the platform never asked it.
Affected endpoints: POST /v1/infra/servers/:id/deploy
FIX-0811-25: galaxy app creation now reports that the provider, plan and region you sent were not applied
A galaxy app is a container on a shared host: it has no machine of its own, so it inherits the provider, plan and region from that host. This was always the case, but the response said nothing about it — the values you sent were replaced by the host's without any signal, and a typo in a plan identifier looked like an accepted request.
Before
POST /v1/infra/servers on a portal that places apps in galaxies answered 201 and returned the host's values in data.provider, data.plan and data.region. Nothing in the response distinguished "the platform used mine" from "the platform used something else".
After
The same request still answers 201 and still inherits the host's values — the behaviour is unchanged. The response changed: a warnings[] entry now sits next to data whenever a value you sent differs from the one in force. It names the diverging fields, shows both values, warns that re-sending will change nothing, and points at placement set to dedicated — the way to get a machine whose characteristics you choose. Fields that matched are not mentioned, so a correct call stays free of warnings. A value that is over-long or not shaped like a catalog identifier is described in words rather than echoed back.
BC-0811-26: uploading a file to a galaxy app is refused instead of writing to the shared server
Old format supported until: 01.02.2027
Before
POST /v1/infra/servers/:id/upload accepted the id of a galaxy app (kind: GALAXY_APP) and wrote the file — not into the app container, but onto the filesystem of the shared server that hosts the account's other apps. The response was 200, so the miss looked like success.
After
The same call answers 400 GALAXY_APP_USE_GALAXY_ROUTE — the code exec, logs and deploy already return for galaxy apps. The message names the cause: the write went to the shared server, not into the container.
What integrators should do
Ship files into a galaxy app by rebuilding its image from sources — source in the body of POST /v1/infra/servers/:id/deploy. Uploading to a standalone server (kind: STANDALONE) is unchanged.
FIX-0811-27: an app from the Bitrix24 catalog now opens for an employee with no Vibecode account
Before
An app opened from the Bitrix24 catalog started only for employees who already had a personal Vibecode account. Everyone else got a "sign in to VibeCode and connect this account" screen — even when the app had been granted to the whole Bitrix24 account. So an administrator, who had an account, saw a working app while regular employees did not.
After
A Vibecode account is no longer a condition of access. An app with the whole-account policy (as well as any-authenticated and public) opens for every employee: membership is vouched for by the signed form, and the platform additionally asks Bitrix24 that this is an active employee, not a fired one and not an external guest. Identity-bearing policies (owner only, named users, departments) still do not open through this path — they decide on a specific person, so the sign-in screen stays there.
What to keep in mind when building an app: for an employee with no account the X-Vibe-User-Role header always arrives as MEMBER, even when they are an administrator, and X-Vibe-User-Name arrives as Unknown. Do not gate irreversible decisions on those headers; check rights by calling Bitrix24.
BC-0811-28: AI requests now carry a service deadline: a 429 refusal instead of a hang
Old format supported until: not provided
Before
A call to POST /v1/chat/completions, POST /v1/embeddings or their /v1/ai/* aliases had no declared upper bound. A regular response ran into an internal wait limit and arrived as a dropped connection with no body, and a streamed response had no overall time limit at all: once the cluster stopped sending chunks, the connection hung until the client's own timeout. There was nothing to tell "still working" from "will never answer".
After
A request now has a service deadline. When it is exceeded you get 429 with the body {"error":{"code":"ai_deadline_exceeded","type":"rate_limit_exceeded","retryAfter":<seconds>}} plus the Retry-After and X-AI-Deadline-Ms headers (the request's actual budget in milliseconds). On a streamed response, where the status has already been sent, the same body arrives as a stream event followed by data: [DONE].
A client may ask for a shorter budget with the X-AI-Deadline-Ms request header, in milliseconds. It only shortens the deadline: nothing longer than the platform value is granted, and where the deadline is switched off the header does not switch it on. A non-numeric or non-positive value counts as absent and never rejects the call.
The deadline also covers the retry on a fallback model: it is measured from the moment the request arrived, not from the start of the attempt.
What integrators should do
Treat 429 with code ai_deadline_exceeded as an invitation to retry: wait the number of seconds in Retry-After and send the request again. For streamed calls, add handling for an event carrying an error field — the stream did not emit one for this reason before. If your client has its own wait limit, pass it in X-AI-Deadline-Ms: then the refusal comes from us with an explanation instead of timing out on your side.
FIX-0811-29: the server region catalog on the international version no longer returns zones from unavailable regions
Before
GET /v1/infra/providers/{providerId}/regions on the international version of the platform returned region zones that are not available for provisioning in that segment — they were listed alongside the available ones.
After
The list is filtered by segment: on the international version the response keeps only the zones available for provisioning in that segment. No client change is required — the response is simply correct now. On the primary version of the platform the list is unchanged.
2026-08-10
NEW-0810-1: issuing a share link is recorded in the account access journal
The POST /v1/infra/servers/:id/access-tokens response is unchanged — what changes is what happens on the account side.
Before
Issuing a mode=share-url link left no trace in the account oversight layer: an administrator could not see who opened an application with a link, or when.
After
After a link is issued, the platform records the exposure change in the account journal, and — when the link does not require a Bitrix24 sign-in (identityBound=false) and the application was not already open to outsiders — sends the account administrators a chat-bot message linking to the list of open applications.
Impact on integrators
The request format, the response and the error codes are unchanged — no client change is needed. Note that every open link you issue is now visible to the account administrators.
BC-0810-2: a filter error in POST /v1/{entity}/batch is now a per-sub-call error, not a whole-request one
Old format supported until: 04.02.2027
Before
One bad filter key in any sub-call of POST /v1/{entity}/batch cancelled the
whole request: 400, the code in error.code, and the results of every other sub-call
discarded. The global POST /v1/batch behaved differently — it placed the error
next to the sub-call that caused it and ran the rest.
After
Both surfaces behave the same way. The response is 200, the refusal code sits in
data[i].error.code for the sub-call that caused it, and the remaining sub-calls run and
return their data. The set of refusals and their codes is unchanged — only the blast radius
is: UNKNOWN_FILTER_FIELD, INVALID_FILTER_OPERATOR, INVALID_FILTER_FIELD,
UNSUPPORTED_FILTER.
The message used to be prefixed with Call at index N: — the sub-call's position is now
visible from its place in the data array, so the prefix is gone.
Integrator migration
If your code reads a filter refusal as HTTP 400 with error.code, add handling for 200
with data[i].error.code — otherwise a sub-call whose filter was refused will read as
successful. Check each data element for an error, exactly as you already do for the
global POST /v1/batch.
BC-0810-3: batch reads no longer bypass an operation the entity turns off
Old format supported until: 04.02.2027
Before
An entity can turn a single operation off — usually because the generic handler is wrong for
it: the real list lives on its own route with a different envelope, or the Bitrix24 method
ignores the filter and returns the whole table. The single routes honoured that, and so did
batch writes, but batch reads did not. So POST /v1/{entity}/batch and
POST /v1/batch with action list, get, search or fields answered
200 where the same operation answers 404 on its own route — serving exactly the result the entity
turned the operation off to avoid.
Separately, the legacy GET /v1/{entity}/aggregate never asked whether the entity has an
aggregate at all. For an entity whose every numeric field is an identifier,
POST /v1/{entity}/aggregate answers 404 while this address answered 200.
After
Both batch surfaces answer a disabled operation with 400 ACTION_NOT_SUPPORTED before any
Bitrix24 call: on the per-entity batch that is the whole request, on the global one it is a
per-sub-call error and the remaining sub-calls still run. The legacy
GET /v1/{entity}/aggregate is registered by the same predicate as its POST sibling, so an
entity without an aggregate now answers 404 on both.
Integrator impact
Only the entity-and-operation pairs whose answer was already wrong are affected. Check all
four reads — list, get, search, fields: six entities disable search while keeping a
working list, so a batch that used to go through whole now answers ACTION_NOT_SUPPORTED
on that sub-call. The full
set of actions is in operations.batch of the GET /v1/guide response and in the OpenAPI
description; data.batch of GET /v1/{entity}/fields lists WRITE actions only and cannot
answer the question about reads. An entity with every
operation off keeps its route, but every action answers ACTION_NOT_SUPPORTED — the refusal
names the action, which a 404 on the path cannot.
BC-0810-4: a `filter` that is not an object is refused instead of being lost
Old format supported until: 04.02.2027
As in the sibling entry about an unknown field name, "the old format" here means a wrong answer, not a working one.
Before
filter is an object of conditions, but nothing stopped a client sending a string, a number,
a boolean or an array instead. A string and an array went to Bitrix24 as they were, a number
and a boolean became an empty filter — and in every case the request answered 200 with the
WHOLE collection:
POST /v1/tasks/search { "filter": [{ "responsibleId": 1 }] } -> 200, every task on the account
After
Such a request is refused before the Bitrix24 call — 400 with the code
INVALID_FILTER_SHAPE; the message says what arrived instead of an object and shows the
correct form.
This applies on every entity and every surface where filter arrives in a body: search,
aggregate and both batches.
Integrator migration
Send filter as an object. The query string is a separate story: conditions are written in
the bracket form there (?filter[responsibleId]=1), and a filter JSON-encoded as one string
is no longer dropped as of this release — it is parsed and applied, where it used to be lost
silently and return the whole collection.
BC-0810-5: an unknown filter field name is refused on fifteen more entities
Old format supported until: 04.02.2027
Until now such a filter answered
200with the whole collection — so "the old format" here means a wrong answer, not a working one.
Before
A filter on a name the entity does not have was sent to Bitrix24. Bitrix24 does not refuse
such a key — it drops it silently and answers 200 with the WHOLE collection. A typo in a
field name therefore looked like a successful request with an implausibly large result:
GET /v1/tasks?filter[responsable]=1 → 200, every task on the account
That was the behaviour of tasks, users, workgroups, requisites, bank details, requisite presets, addresses, sites, pages, timeline comments, workflow templates, Open Channels configs and universal-list elements.
After
Such a request is refused before the Bitrix24 call — 400 with the code
UNKNOWN_FILTER_FIELD and the available names listed in the message — that list IS the
precise answer to "what can I filter by".
Only the NAME is checked: operators, ranges, $in/$nin and AND logic behave as before.
Besides declared fields it accepts custom fields (UF_*, ufCrm*), and the id key on the
entities that declare one.
Separately, for workflow activities and
workflow robots the Bitrix24 method accepts no filter in
any form, so any filter key there is refused with the code UNSUPPORTED_FILTER.
Full description — Filtering and search.
Integrator migration
Check the field names in your filters against the list in the error message. A request that used
to "work" but returned more records than it should will now answer 400 naming exactly which
field was not found — which is the mistake it always contained. Other filters are unaffected.
FIX-0810-6: a host out of free disk space is a distinct retryable deploy failure, not a broken app
Before
When the shared host of a galaxy app ran out of free disk space, the build failed and POST /v1/infra/servers/:id/deploy answered 502 GALAXY_APP_BUILD_FAILED — the same code as an error in the app's own source. The app was marked broken even though it never stopped working: the failure happened during the build, before the container was replaced, so the previous version went on answering requests. Telling a full disk apart from a genuine build error was only possible from the log tail in buildLog, and re-sending the same deploy produced the same result.
After
The same case returns 502 GALAXY_LOW_DISK with retryable: true and a structured error.hint: the cause, what to do, and a warning not to delete the slot. The app is not marked broken — the slot, its container and its data volume are intact, and the version already running keeps serving traffic. Re-send the deploy once space has been freed on the host: disk is not reclaimed on its own, so an immediate retry hits the same refusal. The new code is listed in the machine-readable deploy contract returned by GET /v1/me and GET /v1/openapi.json.
Impact on integrators
Nothing to change: successful deploys are unaffected. Keep your branch on GALAXY_APP_BUILD_FAILED — it still arrives for genuine build errors, while a full disk now has its own code that makes clear the problem is not in your source. An automatic retry happens only where the platform rebuilds an app that already exists: there attempts continue without client involvement until the wait budget runs out. When an app is created together with its source in one step, the refusal arrives right away and is not retried — the decision to retry is yours.
FIX-0810-7: A catalog launch now warns that the app is not authorized
Before
An app opened from the Bitrix24 app catalog by a user who had not granted it access yet started as usual, but the gateway did not set the X-Vibe-Authorization header. API calls answered 401, and the app displayed the text our own documentation prescribed for a 401 — "re-open the app from the Bitrix24 menu". That advice led nowhere: the user had just done exactly that, and a catalog launch does not grant access.
After
The platform now intercepts such a launch and shows a screen with an "Authorize the app" button; the second route is to open the app once through a placement in the Bitrix24 menu. The same screen carries an unobtrusive "open without authorizing" link to the very same launch address, so an app that does not need the session opens exactly as before. The screen appears only where the button has somewhere to lead: a cloud account, an app key, and a registered OAuth application. Every other launch behaves as before.
Impact on integrators
No code changes are required, and no launch becomes unavailable. Only your own 401 message is worth revising: the status has two causes — an expired session and access that was never granted — so "re-open from the menu" as the single wording misleads the user. The recommendation is updated in App runtime.
NEW-0810-8: CRM document list now present in the machine-readable OpenAPI schema
The GET /v1/crm-documents endpoint is now described in the OpenAPI schema served at /v1/openapi.json. The call itself already worked, but clients and AI agents that build integrations from the machine-readable description treated it as non-existent. The description states the required entityTypeId parameter, the optional entityId, select, order and start, the required crm access, the response shape with the document array and the meta block holding total, start and next, plus the refusal codes MISSING_PARAMS, INVALID_ENTITY_ID, INVALID_START, TOKEN_MISSING and SCOPE_DENIED. The endpoint takes no page-size parameter and the description declares none: for the next page pass the meta.next value as start. The behaviour of the endpoint itself is unchanged.
FIX-0810-9: a direct-link visit now carries the visitor's name and access token
Before
An app opened by its direct address (rather than from its tile inside Bitrix24) received the visitor's name from their Vibecode account instead of their employee card: someone who belongs to several Bitrix24 accounts saw the name they hold in a different one. The X-Vibe-Authorization header did not arrive on this path at all, so the app could not call Bitrix24 on the visitor's behalf.
After
X-Vibe-User-Name and X-Vibe-User-Name-Encoded carry the name from the employee card of the Bitrix24 account that owns the app — the same as on the tile path. The X-Vibe-Authorization access token is resolved by the employee id, so it arrives here too.
Impact on integrators
The header format is unchanged, no client work is needed. When the employee card cannot be read, the name stays as before, taken from the Vibecode account: the header is never delivered empty.
FIX-0810-10: invoice user fields
Before
Invoice user fields could not be read or created through either path.
GET /v1/userfields/invoices answered UNKNOWN_ENTITY, and
GET /v1/items/31/userfields answered with a Bitrix24 access
error, because the invoice was addressed the same way as an ordinary smart process.
After
Both paths work and return the same result: six operations (list, type catalog, read, create,
update, delete) over invoice user fields. The /v1/userfields/invoices path is there for callers
who prefer the entity name over the numeric type id.
Impact on integrators
No action required. This covers invoices in their current form; legacy invoices remain unavailable through the API.
BC-0810-11: The product-row field reference now describes what the responses actually contain
Old format supported until: not provided
Before
GET /v1/deals/{id}/products/fields returned the Bitrix24 field set verbatim, and it disagreed with the product-row responses in three places at once. The discount amount was called discountSum in the reference but discount in the data and on write. The external code and the account-currency price arrived in every row yet were missing from the reference. Owner, owner type and warehouse were the other way round: present in the reference, stripped from the responses.
Worse, a client that generated its writer from this reference sent discountSum — the wrapper did not recognise the name, answered 201 Created and discarded the discount silently. Any other unknown field behaved the same way: success reported, data not written.
After
The reference is derived from the same tables that build the responses, so the two can no longer drift apart. The discount is called discount, matching the data, the write contract and the documentation. The previous name discountSum did not go away: it remains a deprecated alias, still returned by the reference and still accepted on write, so code written against the old field list keeps working — except that the discount now actually lands instead of being dropped silently. Product rows themselves carry only discount. priceAccount and xmlId were added — Bitrix24 returns them in rows but does not describe them in its own field set. ownerId, ownerType and storeId now arrive in the list and single row responses; storeId is null when inventory management is off.
isReadOnly and isRequired describe this API's contract rather than the Bitrix24 one: ownerId, ownerType, customized and measureName are marked read-only because the wrapper cannot write them, and ownerId and ownerType are no longer required — they are taken from the request path. id gained a description: it is read-only as an attribute, and inside PUT /products items it is accepted. Correction of 2026-08-18: a live check showed that echoing id back does not preserve row identity — a row whose fields are unchanged keeps its id even with no id in the body, and a modified row comes back with a new id. To edit a row and keep its identifier, use PATCH /v1/deals/:id/products/:rowId.
A write carrying an unknown field name no longer reports success: POST, PUT and PATCH return 400 INVALID_PARAMS and list the writable fields. Read-only fields are still accepted and ignored, so an object read back via GET can be sent as is.
A body id is accepted only inside PUT /products items. On create and update it is dropped: the row is identified by the request path there, and a body carrying the id of an existing row is exactly what you get by sending back an object read via GET.
On write, taxIncluded accepts a boolean again: true and false reach Bitrix24 as Y and N. The boolean used to be forwarded as is, leaving the row's tax inclusion effectively unset — so a row read via GET and sent straight back lost the flag. If you worked around this by sending "Y" and "N" as strings, nothing changes: that form is still accepted.
When Bitrix24 returns incomplete metadata the reference still comes back complete, but the response now carries meta.warnings[].code = "fields_partial" — previously such an answer was indistinguishable from a healthy one. The warning text distinguishes two cases: no metadata arrived at all, or only some fields were left undescribed (it then names them).
What integrators should do
What breaks is a write carrying a foreign key in the body. Check that creating or updating a product row sends nothing beyond the writable fields — your own bookkeeping markers, leftovers from an internal model, Bitrix24 upper-case names (PRICE_ACCOUNT, XML_ID). Such a request used to answer 201 Created and discard the value silently; it now answers 400 INVALID_PARAMS and lists what is accepted. There is deliberately no support window for the old behaviour: it was the defect this change exists to fix, and keeping it alongside means keeping silent data loss. The full list of writable fields comes back in the refusal and in the field reference.
Nothing else needs changing. discountSum is still returned and still accepted — move to discount at your own pace, that is the name product rows carry, while the alias lives only in this reference. If you branch on isReadOnly or isRequired, re-check the new values for ownerId, ownerType, customized and measureName. If you replace rows wholesale, note that the identifiers of modified rows change: re-read the set via GET after the write.
FIX-0810-12: server member lookup no longer comes back empty because of a recent key without Bitrix24 access
Before
When a server's managing key granted no Bitrix24 access, GET /v1/infra/servers/{id}/b24-users
and the member lookup in the interface fell back to the server owner's personal keys, but checked
only one — the most recently created eligible key. If that key had neither a webhook nor an
installed application token, the response came back empty (data: [] with a hint) even though the
owner had another active key with the required rights. Keys the platform issues itself for tasks
that never call Bitrix24 are always the most recent ones, so the lookup could stay broken
indefinitely.
After
All eligible personal keys of the owner are checked, newest to oldest, and the first one that really has Bitrix24 access is used. Keys without access are skipped, and Bitrix24 still receives a single request. Key requirements are unchanged: as before, only an active, non-expired personal key of the server owner in the same account qualifies, and it must not be bound to an application.
The hint text is corrected as well: it used to name only an unauthorized app and a revoked key, so
an active key looked revoked. A third reason is now named — no key grants Bitrix24 access — together
with the action to take, issuing a personal key with the required scope. The response shape is
unchanged.
FIX-0810-13: placement bind names the install-rights refusal instead of a generic gateway error
Before
Bitrix24 refused the embedding install and the account's plan state could not be confirmed at that moment — POST /v1/placements/bind answered 502 BITRIX_UNAVAILABLE and put the Bitrix24 code into details. There was no named code in the answer, and no hint about what to do next. The refusal reached accounts whose plan already allowed the install.
After
When the check cannot be performed and the account is already on record as entitled in Vibecode, the same refusal arrives as 403 B24_EMBEDDING_INSTALL_DENIED with details.remedy set to install-rights. No upgrade link is attached: the entitlement is there, what is missing is the right to install local applications. With no live entitlement from either source the answer stays 502 BITRIX_UNAVAILABLE.
Impact on integrators
Nothing to change. Your branch on 502 BITRIX_UNAVAILABLE keeps working for the other refusals, while this case leaves it for 403. Retrying it is pointless — the developer key has to belong to a user allowed to install local applications and holding access to the application.
BC-0810-14: commands, deploys and file uploads addressed by a galaxy host id are refused
Old format supported until: 07.08.2026
Before
A galaxy is a single machine that hosts the applications of several keys of one Bitrix24 account, each in its own container. The server list returns both the applications (kind=GALAXY_APP) and the machine carrying them (kind=GALAXY), and calling POST /v1/infra/servers/:id/exec, POST /v1/infra/servers/:id/deploy or POST /v1/infra/servers/:id/upload with the id of that machine ran on the machine itself, that is outside the caller's own container.
After
The same call against a GALAXY machine is refused: commands return 403 GALAXY_HOST_EXEC_FORBIDDEN, deploys and file uploads return 400 GALAXY_HOST_NOT_A_DEPLOY_TARGET. The refusal names the replacement: address the application by its own id (kind=GALAXY_APP). Nothing changes for STANDALONE machines or for galaxy applications.
What integrators should do
Take your application's row from the server list (kind=GALAXY_APP) and use its id. If the flow relied on reaching the machine to learn how much disk space is left, that figure is now available as data rather than as the output of a command.
There is no support window for the previous behaviour: the refusal applies from the moment this ships. The reason is that the previous behaviour opened access to a machine carrying other keys' containers, and access like that cannot be kept alive for six months for the sake of compatibility.
NEW-0810-15: galaxy host disk usage is exposed in the server list
A galaxy host row (kind=GALAXY) in GET /v1/infra/servers and in the single-server response now carries four fields: diskTotalMb and diskFreeMb — size and free space in mebibytes, diskState — the verdict (ok, warning, critical, or unknown when nothing has been measured yet), diskProbedAt — the measurement time in ISO-8601.
The platform keeps both lines well clear of the edge: warning means space is running out, critical means less is left than the platform considers safe; both light up before a deploy actually runs out of room. The measurement refreshes on its own while the machine is awake; a sleeping machine shows the last known value together with its timestamp.
For STANDALONE machines and for galaxy applications (kind=GALAXY_APP) all four fields are null: the former are never probed, the latter have no disk of their own.
NEW-0810-16: agent model bitrix/bitrixgpt-5.6-agent
GET /v1/models now lists a new agent model, bitrix/bitrixgpt-5.6-agent, with a 1,048,576-token context. It supports streaming, tool calls (tools) and schema-constrained output — response_format with type: "json_schema". The model public id is part of the ai.structuredOutputs.models list returned by GET /v1/me.
The model is additive and replaces nothing: existing calls are unaffected. It is enrolled in the quota programme, so it is also reachable with a Bitrix24 partner token.
Affected endpoints: GET /v1/models, POST /v1/chat/completions, GET /v1/me
NEW-0810-17: bitrix/bitrixgpt-5.5-agent is deprecated
Responses from POST /v1/chat/completions for bitrix/bitrixgpt-5.5-agent now carry Deprecation: true, X-Model-Replacement: bitrix/bitrixgpt-5.6-agent and a Link header pointing at the successor (rel="successor-version").
The model keeps working without restrictions and stays in the GET /v1/models listing. No shutdown date is set — the Sunset header is not sent, and no integration changes are required. The successor for new integrations is bitrix/bitrixgpt-5.6-agent.
FIX-0810-18: a transient database transaction failure no longer returns 500 with an internal engine code
Before
When a database transaction closed or expired before the operation finished, the request answered 500 and put the engine's internal code in the body — {"error":{"code":"P2028"}}. That code appears on no documentation page, the response carried no Retry-After header, and nothing in it said the request was worth repeating. It showed up most often on DELETE /v1/apps/{id}: the application stayed in place and a retry looked pointless.
After
The same class of failure returns 503 with code DB_TRANSIENT, an error.retryAfter field, and a Retry-After header — the same retry posture as POOL_EXHAUSTED. The change applies neither partially nor fully on such a failure, so a straight retry after a few seconds is safe. On the AI routes (/v1/ai/*, /v1/chat/*, /v1/audio/*, /v1/models) the code arrives lowercased — db_transient — in the OpenAI-compatible envelope. The engine's internal code no longer appears in the response body.
Impact on integrators
Nothing to change. If your handler treated 500 as a terminal refusal, this case now arrives as 503 with a stated delay and falls into your retry branch instead. No special handling of the DB_TRANSIENT code is required: honouring Retry-After on any 503 is enough.
FIX-0810-19: preserveEnv no longer loses .env when a deploy fails after the directory is cleaned
Before
With cleanDeploy: true together with preserveEnv: true the existing .env was read before the
clean but written back only at the env step — after the runtime and the dependencies were
installed. If the deploy aborted earlier, POST /v1/infra/servers/:id/deploy
answered DEPLOY_FAILED and the directory was left with no .env at all. The application
settings had to be uploaded again by hand.
After
The saved copy is put back on disk on any abort after the clean — on dependency install, on the
runtime, on the archive download, on the clean itself, and when the connection to the server
drops. The restore is best-effort and adds no separate step to the response. The precedence rule
is unchanged: an env passed in the same request still wins, and an explicit empty env: {}
means "clear it" and does not bring the saved copy back. A successful deploy behaves exactly as
before, including the time-zone injection for wake schedules. The flag still applies to a
dedicated virtual machine only (kind: "STANDALONE").
Impact on integrators
Nothing to change on your side. The DEPLOY_FAILED response shape and the set of steps in
data.steps are unchanged. If you worked around this defect by re-uploading .env by hand after
every failed deploy, that workaround is no longer needed.
2026-08-09
BC-0809-1: meta.total is no longer returned by default in lists
Old format supported until: 09.02.2027
Before
GET /v1/{entity} and POST /v1/{entity}/search sent meta.total — the number of records matching the filter — unless the request declined the count explicitly. Declining was possible with the withTotal=false parameter or the totalDefault setting on the key, but the platform default meant "count", so an integration that knew nothing about counting always received the number.
After
The platform default changed to "do not count": the count is now ordered explicitly. There are three ways to ask for it, and they override one another in this order: the withTotal=true request parameter (on POST /v1/{entity}/search it is the body field "withTotal": true), the totalDefault setting on the API key, and the platform default.
If the number was not asked for, the presence of meta.total follows the shape of the call:
| Call | meta.total |
|---|---|
limit at most 50, offset 0, page shorter than requested |
arrives, exact number — 0 included |
limit at most 50, full page or offset above zero |
absent |
limit above 50 |
arrives |
A short page proves the count by itself, so the exact number arrives for free and no count is ordered. On a call with limit above 50 the platform needs the count to plan the walk, so the number arrives as it used to — passing withTotal=false there buys nothing: the parameter removes the number, not the cost.
An explicit withTotal=false removes the key on any of these calls: behind that parameter the "no such field" promise stays unconditional. The totalDefault setting on the key and the platform default do not forbid the exact number a short page proves, so two identical requests from two different keys can come back in different shapes.
All of this applies to calls where the count can be skipped. Where it cannot, withTotal=false is ignored and meta.total arrives as before. Check for the field in the response at hand rather than deriving it from the key settings.
The new default also applies to list sub-calls inside POST /v1/batch: there the number arrives in data.totals and meta under the call id, and it is absent by the same rules.
The rest of the response is unchanged: data holds the same records in the same order, and meta.hasMore is still there and still tells you whether more pages remain.
What integrators should do
If your code reads meta.total, pick one of two options.
Once, for the whole integration — turn on the totalDefault setting on the API key: the keys page in the dashboard, or PATCH /v1/keys/:id with the body {"totalDefault": true} (a vibe_live_ management key). No code change needed.
Per call — add withTotal=true to the calls that genuinely need the number: GET /v1/deals?withTotal=true, or "withTotal": true in the search body.
What applies to your key right now is shown by the totalDefault block in GET /v1/me: key is the key setting, platform is the platform default, and effective is what you get when the request sends no withTotal.
Separately: if meta.total was your paging loop bound, switch to meta.hasMore — that is more reliable either way. And if you needed the number itself, ask for it directly: POST /v1/{entity}/aggregate with the count function returns the count in one call. Do not walk the collection page by page just to get a counter — that is dozens of calls instead of one.
2026-08-07
NEW-0807-1: CRM record import: the original author and dates, with no automation run
An existing database can now be moved into the CRM with one request per entity: POST /v1/leads/import and the same route on deals, contacts, companies, quotes, invoices and smart-process items. Up to a hundred records at a time, an items array in the body, and the same record fields the ordinary create takes.
Import differs from create in three ways, and all three are properties of the Bitrix24 operation itself rather than parameters of ours. It checks a separate "import" permission that the account administrator grants explicitly. It does not run robots, triggers or business processes configured to fire on record creation. And it accepts the service fields the ordinary create silently ignores: who created the record (createdBy), who changed it (updatedBy), who moved it to its stage (movedBy) and the matching dates. Only an account administrator may set them — for any other user Bitrix24 refuses that record, while the rest of the batch is still created. The available set differs per object; the exact list comes from GET /v1/leads/fields, where those fields are marked importable.
The creation date has a window set by Bitrix24: no later than the current moment, and no earlier than that of the newest existing record of this object. So a full history transfer works into an empty CRM, or into one whose records are all older than what you are moving; Bitrix24 will not let you backdate records into a populated CRM. If you do not need the history, omit the dates — the author is set without them.
The response comes back with status 200 even when some records failed: import is not transactional, so the outcome is read per record in results[], with the totals in summary. Always check summary.failed — the status says "the request was processed", not "everything was created". A repeated import creates duplicates; if repeats are possible, write your previous system's identifier into originatorId and originId.
The route is rate-limited per account so a bulk transfer cannot block the account's other integrations. Import in a single stream: order is guaranteed within one request but not between parallel ones, and Bitrix24 requires non-decreasing creation dates.
Details, error codes and examples are on the CRM record import page.
FIX-0807-2: a source version deploys by its number on its own server
Before
A version saved through POST /v1/infra/servers/:id/sources could not be deployed on that same server: POST /v1/infra/servers/:id/deploy with body { "source": { "versionId": "v1" } } answered 400 SOURCE_VERSION_REQUIRES_APP whenever the server's owning key was not bound to an application — that is, on a personal vibe_api_* key. The only way around it was manual: download the archive by link and deploy it as { "source": { "url": … } }.
After
When the server belongs to the same key that makes the call, the version is looked up in that server's context, so deploying by versionId works, personal keys included. Not found on the server and the owning key is bound to an application — the lookup falls back to the application's context, as before. Found nowhere — 404 SOURCE_VERSION_NOT_FOUND; the message now names the server and the save endpoint instead of an application.
The SOURCE_VERSION_REQUIRES_APP code is not removed but narrowed: it now arrives only where the server belongs to a key other than the calling one — a management key, or access through an application card. If you branch on this code, keep that branch.
Integrator impact
Nothing to change. A call that used to be rejected now goes through, and the sha256 field in the response is preserved.
One rare exception: when a server and its application both hold a version under the SAME number — which happens for a version saved before the server was rebound to another key — the server's version now wins over the application's. The sha256 field in the response tells you which one was deployed.
FIX-0807-3: reading and updating a nonexistent activity answers 404, not 422
Before
GET /v1/activities/{id} and PATCH /v1/activities/{id} with an id that does not
exist on the account answered 422 with code BITRIX_ERROR and a message like
Bitrix24 API error: 400. The cause is upstream: on these two methods Bitrix24
returns a refusal whose error code and description are both empty, so there was
nothing to recognise "no such record" by. Meanwhile DELETE /v1/activities/{id}
on the same id already answered 404, because there the account does send an
error message. One and the same missing id produced two different answers
depending on the verb, and both documentation pages —
get and
update — promised 404.
After
Both methods answer 404 with code ENTITY_NOT_FOUND and the message
Activity is not found. — the same one DELETE returns. The rule is bound to
these two methods and fires only when the account sent neither a code nor a text:
a refusal carrying any code or message (including a validation error on update)
is unchanged. The error list in the documentation did not change — the response
did, and now it matches.
FIX-0807-4: a body-less request reaches its handler instead of failing at parse time
Some HTTP clients (axios, PowerShell Invoke-RestMethod, a few fetch wrappers) attach
Content-Type: application/x-www-form-urlencoded to every POST, PATCH and DELETE — even when
they send no body at all. Others send no Content-Type whatsoever. Neither form reached the
handler before this fix.
Before
POST /v1/deals with no body and no Content-Type header
answered 500 INTERNAL_ERROR — the handler failed on the empty body before it could report
that there was nothing to create. The same call with an empty body and an
application/x-www-form-urlencoded header answered 415 Unsupported Media Type before the
API key was even checked. POST /v1/chats/events/subscribe —
which needs no body at all — answered 415 with the form header and 400 with an empty body
under application/json.
After
An empty body is accepted whatever the header says: POST /v1/deals with no body answers
400 EMPTY_CREATE_BODY, the same reply POST /v1/deals with a {} body already gave, and
POST /v1/chats/events/subscribe with no body succeeds.
A Content-Type header on an empty body no longer gets in the way anywhere across entities
(/v1/deals, /v1/contacts, /v1/tasks and the other generated routes, batch and aggregate
included), chats (/v1/chats/*), custom fields (/v1/userfields/*,
/v1/items/:entityTypeId/userfields), the knowledge base (/v1/note/*) and keys
(/v1/portals, /v1/keys). The separate case of no Content-Type header at all is a
different failure, and it is closed on entities: a request with neither body nor header now
gets the ordinary field-validation reply instead of a 500.
Impact on integrators
Nothing to change: a request that worked keeps working. A non-empty body under an unknown
Content-Type is still rejected with 415 — the same status as before; 413 now arrives
only when the body really is over the size limit. A malformed JSON body under
application/json now answers 400 INVALID_JSON_BODY on all of the routes listed above
(chats previously returned Fastify's own parser code).
BC-0807-5: commenting on your own ticket no longer looks like a platform reply
Old format supported until: 07.08.2026
Before
The ticket author was the specific key that created it. A comment sent with another key of the same owner (or a ticket filed from the dashboard and followed up with a key) took the platform branch: it was recorded as authorType: PLATFORM, moved the ticket to AWAITING_USER, and overwrote Feedback.resolution with its own body. The key that created the ticket could not follow up on a resolved one at all — POST /v1/feedback/:id/comments answered 409 FEEDBACK_CLOSED. The only workaround was changing the status through PATCH /v1/feedback/:id.
On top of that, Feedback.resolution acted as a mirror of the team's last reply: any comment with the vibe:feedback scope overwrote the resolution text, and there was no way to get the previous value back. The comment operation had no rate limit at all.
After
Authorship is the key owner. A ticket created by another of your personal keys, or filed from the dashboard, is yours: the comment is recorded as authorType: USER, the resolution field is not overwritten, and a supplied status is ignored. One condition: such a key needs the vibe:feedback scope — without the scope, only the key that created the ticket counts as yours. The rule does not extend to application keys and management keys — there the key owner and the person writing are different people.
An author comment on a RESOLVED ticket brings it back into the queue (NEEDS_REVIEW) and clears resolvedAt / resolvedBy; the resolution text is kept. ARCHIVED and WITHDRAWN stay closed and still answer 409 FEEDBACK_CLOSED.
The resolution field is filled only by a comment that closes the ticket (target status RESOLVED or ARCHIVED). With any other status the field is left alone, and the comment text still reaches the author by email and is visible in the thread.
The comment operation gained a rate limit — 20 comments per minute, matching the same operation in the dashboard. The counter is shared per KEY OWNER: several of your own keys share one budget. Going over returns 429 RATE_LIMITED with a Retry-After header.
What integrators should do
Five places need edits, and they are worth locating in your code before you update.
- The
409 FEEDBACK_CLOSEDhandler. A resolved ticket now answers201and returns to the queue. If that code was your "the ticket is closed, stop writing" signal, move the check to the status in the response: onlyARCHIVEDandWITHDRAWNstay closed. - Branching on
authorType. For a second key of the same owner the value changed fromPLATFORMtoUSER. Code that rendersPLATFORMas "a support reply" will now render it as a user message — which is correct, but any logic hanging off that branch needs a second look. - Reading
resolution. It no longer works as "the team's last reply": it holds the verdict of the last closure, and on a ticket that was never closed the field is empty. For the team's latest answer, read the comment thread — the last entry withauthorType: PLATFORM. - Closing your own ticket with a comment. If you closed your own ticket through
POST /commentswith astatusfield, that route no longer works: for the authorstatusis ignored silently, the response is201, and the status stays as it was. To withdraw a ticket, use PATCH /v1/feedback/:id withstatus: WITHDRAWN. - Handling
429on comments. The operation gained a rate limit it never had. If your code posts comments in a batch or a loop, add handling for429 RATE_LIMITEDand back off by theRetry-Afterheader. The budget is per key owner, so minting a second key does not widen it.
There is no parallel support for the previous behaviour: the old way of filling resolution was the very defect this change fixes — there is nothing to keep running.
Affected endpoints: POST /v1/feedback/:id/comments, GET /v1/feedback/:id, GET /v1/feedback
BC-0807-6: an AI agent's server no longer accepts an application deploy
Old format supported until: 07.09.2026
Before
POST /v1/infra/servers/{id}/deploy accepted an archive for a server owned by
an AI agent. The deploy went through, the agent's own code was overwritten by
the application, and the agent went silent. It still reported as running, and
neither the response nor the UI showed a trace. For the same reason an agent's
server could be reused for an application when creating a new server under the
same name, or when re-binding an application.
After
Such a deploy is refused with 403 and the code AGENT_SLOT_DEPLOY_FORBIDDEN;
the message names the working alternative — the Retry button on the agent card,
or a separate server for the application. The reuse paths no longer pick an
agent's server: a fresh one is created instead. Managed bots are unaffected —
deploying their own code through the same call is their supported path.
Minting a maintenance key for an agent whose server is gone now answers 409
with the code AGENT_SERVER_GONE instead of handing out a key with nowhere to
go. Reading the key state still answers 200, with a new reason field set to
SERVER_GONE.
NEW-0807-7: `GET /v1/cowork/state` reports a scheduled downgrade
Before
Moving to a lower tier applied immediately and wiped the paid month, so there was nothing to report: the tier in the response changed at that same moment.
After
A downgrade is now queued for the end of the paid period, and the subscription
object gained a pendingTier field — the tier code the seat will move to at the next
charge, or null. The date is the existing currentPeriodEnd field.
The field is additive: clients that do not read it keep working. A cancelled
subscription always returns null — a cancellation outranks a plan, and a seat that
is closing must not be promised a tier.
FIX-0807-8: the galaxy host now fetches the source archive itself
Previously a dedicated agent action delivered the archive to the galaxy host under a hard ninety-second ceiling. The host now downloads and unpacks it on its own, verifying size and checksum against the saved version in place. The ceiling is gone, and failures are named honestly: an expired link, a dropped connection, no free space and a corrupt archive no longer collapse into one "unpack failed" message.
Error codes and response fields are unchanged; UPLOAD_NO_SPACE is added for an out-of-disk host. An unrecognised archive format and links supplied in the request body keep the previous path. Affects POST /v1/infra/servers/:id/deploy.
Separately, extractTo is now validated on the platform side, not only on the machine. The set of accepted paths is unchanged on both POST /v1/infra/servers/:id/deploy and POST /v1/infra/servers/:id/upload, which had no check of its own — the same values are rejected as before, but immediately and with a clear INVALID_EXTRACT_TO code.
FIX-0807-9: a deploy no longer fails while stopping a slow application
Before
A repeat POST /v1/infra/servers/:id/deploy over a running application that does not exit
immediately on SIGTERM failed at the stop_existing step after ~45 seconds:
DEPLOY_TIMEOUT, Deploy step timed out: GATEWAY_TIMEOUT: no response within 45s. The new
version was never rolled out. The platform allowed the stop 15 seconds while the operating
system on the server allows up to 90, so an application needing 45–90 seconds to shut down
failed the deploy every time. The hint pointed at repairing the tunnel — a false lead.
After
The stop_existing step now waits as long as the stop is allowed to take on the server (the
step budget is 105 seconds) and never aborts the deploy: when the stop could not be
confirmed — no result arrived, or the server answered that it failed to stop — the step returns
warning stating honestly that the outcome is unknown, and the deploy continues.
The second case was previously invisible: the step reported ok, so a failed stop left no
trace anywhere. The neighbouring clean step had the same hole: it too could report ok having
deleted nothing when the server never ran the command, and the deploy then failed two steps
later with a message that named no cause. Such a case now stops the deploy at once and says
why. When the port is also still held, the warning keeps both facts. The response stays
success: true; the step status is visible in data.steps[].
FIX-0807-10: renaming a requisite preset field is checked before the write
Before
PATCH /v1/requisite-presets/:presetId/fields/:id carrying a fieldName that is not
among the available ones answered 200 and {"updated": true}, and the nonexistent
name really landed on the preset row. The Bitrix24 update method, unlike the add
method, does not validate the name and accepts any string — so the row kept a name
with no field behind it and stopped showing data.
After
When fieldName in the request body differs from the name the row already carries,
the name is checked against GET /v1/requisite-presets/:presetId/fields/available
before the write. A name outside that list returns 400 with code
INVALID_FIELD_NAME and the update is not performed; no field of the row changes. A
name another row of the same preset already holds is absent from the available list
and is rejected too. Renaming to an available name works as before, and the name's
letter case is normalised to the spelling Bitrix24 returned.
Impact on integrators
Three answers changed. A garbage name returns 400 instead of 200: that 200 used
to store a name with no field behind it, which also lost the other fields of the same
request — silently. A fieldName sent as something other than a string also returns
400 INVALID_FIELD_NAME: such a value used to reach Bitrix24 and settle into the row
as the word Array. A request carrying fieldName for a row that does not exist
answers 404 before the write instead of relaying the Bitrix24 answer.
A request without fieldName behaves as before. A request carrying the name the row
already has still succeeds, but costs one more Bitrix24 call: to learn that the name
is unchanged, the platform reads the row first.
NEW-0807-11: field schema for task time tracking
GET /v1/task-time/fields has been added — the machine-readable field set of a time entry. Each of the ten fields carries a type, a read-only flag, a label and a description. Until now the field set was only described in prose, and a client could not fetch it with a call.
The schema is the same for every task, so the path is flat. The nested GET /v1/tasks/:taskId/time/fields still returns 400 WRONG_PATH, but now names the correct path in the error text. The request needs the task scope and makes no Bitrix24 call.
The fields that look numeric — id, taskId, userId, seconds, minutes, source — are declared as strings, because strings are what the responses actually carry. The userId field is marked createOnly: it is accepted on creation and refused on update.
Affected endpoints: GET /v1/task-time, GET /v1/task-time/fields.
FIX-0807-12: labels and descriptions for the daysBeforeClose, fm and FILES fields in the /fields response
Before
Three fields that Bitrix24 returns live came back with no explanation, and two of them also carried an awkward label. GET /v1/smart-processes/fields returned daysBeforeClose with a sentence-long label instead of a short name. GET /v1/leads/fields returned fm labelled "FM". GET /v1/timelines/fields returned FILES with a label but no description, so the attachment format had to be looked up on the comment-creation page.
After
All three fields now carry a description. For daysBeforeClose the label is shortened to a short name and the former long text moved into the description. For fm the label is replaced with a human-readable one, and the description points at the flat phone and email fields, which expose the same data in a form that is easier to read and write. For FILES the label stays exactly as Bitrix24 sent it — it depends on the account language — and the description states the attachment format for both writing and reading.
Impact on integrators
Nothing to change: the field type and the read-only flag (readonly) still come from Bitrix24 and did not change. If your code shows the label of these fields to a user, the text will differ — it is read from the response rather than stored on your side.
FIX-0807-13: the feedback list now honours the bracket filter form
Before
GET /v1/feedback?filter[status]=RESOLVED answered 200 with the whole accessible list: the bracket form was parsed but never read, so both the records and total came back unfiltered. Same for filter[category]. Only the flat form worked — ?status=RESOLVED.
After
Both forms behave the same. GET /v1/feedback applies filter[status] and filter[category] with the same validation as the flat params: case-insensitive, and an unknown value returns 400 INVALID_FILTER_VALUE instead of silently returning everything. When both forms are sent, the flat one wins — requests that worked before keep their exact answer. A value that is not a single value (filter[status][]=NEW) is also rejected with 400 INVALID_FILTER_VALUE.
FIX-0807-14: updating an order no longer loses the amount, the mark, and its reason silently
Before
PATCH /v1/orders/:id accepted price, marked, and reasonMarked and answered 200, but Bitrix24 does not save these fields on update. For marked and reasonMarked the value simply disappeared. For price it was worse: the amount is recalculated from the basket items, so a request with a manual amount did not change it, and with an empty basket the stored amount became 0 — meaning an update sent with any other field zeroed the order's price. Nothing in the response said so.
After
All three fields are rejected on update with 400 READONLY_FIELD before Bitrix24 is called — on all three write surfaces: the single PATCH, POST /v1/orders/batch with action: "update", and POST /v1/batch with action: "update". Creation is unchanged: POST /v1/orders and both batch creations still accept these fields and pass their values through. In the GET /v1/orders/fields response such a field is flagged readonlyOnUpdate: true, to tell it apart from readonly (not allowed on creation either) and from createOnly (the value is immutable after creation — which is not true of an order amount, Bitrix24 recalculates it).
Impact on integrators
There is no parallel support window for the previous behaviour: the previous behaviour was that the value was silently lost. If your update request sent these fields, remove them from the body, otherwise it will start answering 400. The most common case is a client that reads the whole order and sends the object back: drop price, marked, and reasonMarked from such a body. To change the order amount, edit the basket items.
NEW-0807-15: deploy now reports a displayName or description it did not apply
Deploy seeds displayName and description, it does not rename them: displayName is written only while it still equals the server's technical identifier, description only while it is empty. A value that conflicted with an already-set field used to be dropped silently — the response was a plain 200 with no sign that the field had not been written.
Such a response now carries an extra warnings[] entry: it names the dropped fields, confirms the deploy itself succeeded, and warns that re-sending will not change them. It also includes a ready-to-paste body for PATCH /v1/infra/servers/{id} with the current name already filled in — that endpoint requires displayName, so the sample prevents accidentally overwriting the name while editing only the description. The entry is appended last, and the write behaviour is unchanged.
The rename operation is now declared in the machine-readable API description (GET /v1/openapi.json) too — the schema previously claimed no rename existed in this API.
Affected endpoints: POST /v1/infra/servers/{id}/deploy, PATCH /v1/infra/servers/{id}
FIX-0807-16: reopening a ticket no longer erases the resolution text
Before
PATCH /v1/feedback/:id carrying only a status — for example {"status":"REVIEWING"} — cleared the resolution field when it returned a ticket from RESOLVED, WITHDRAWN, or ARCHIVED, even though the field was absent from the request body. The response was 200 and said nothing about the loss. If the team's answer had not been duplicated in a comment, there was no way to recover it.
After
Returning to an active status clears resolvedAt and resolvedBy only. The resolution field is left unchanged when you do not pass it: a ticket returning from ARCHIVED keeps its archive reason too. To replace the text, pass resolution in the same request; to clear the field, pass "resolution": null. The comment path (POST /v1/feedback/:id/comments) already left resolution alone — both surfaces now agree.
BC-0807-17: an activities aggregate needs a narrowing filter
Old format supported until: 07.02.2027
Before
POST /v1/activities/aggregate accepted a request with no filter. On a small account it answered in a second; on a large one it never answered: the very first call to Bitrix24 (counting every activity in the account) did not fit into the time allowed for one call, and the client got 503 BITRIX_TIMEOUT with a Retry-After header and a hint saying reads are safe to repeat. Repeating produced the same result, because the cause was not transient. The documentation, meanwhile, offered {} as "the fastest query".
meta.truncated meant exactly one thing: "more than 5000 records matched the filter". If some pages of records never reached us, the response came back with truncated: false — that is, the numeric aggregations and the groups were computed over part of the records and the response did not say so.
After
An activities aggregate requires one narrowing out of three: the ownerTypeId + ownerId pair, or responsibleId, or a date bound on createdAt / updatedAt / deadline. The platform switches the requirement on per account. While it is off, behaviour is unchanged; once it is on, a request with no narrowing gets 400 MISSING_REQUIRED_FILTER — message lists the accepted narrowings and a ready-to-paste example body, and Bitrix24 is not called at all.
Independently of that switch, a request without a narrowing that Bitrix24 failed to answer in time now returns 422 AGGREGATION_LIMIT_EXCEEDED instead of 503: the refusal is terminal, there is no Retry-After header, and the text says what to do instead of repeating. A request with a narrowing still gets 503 plus Retry-After on a timeout — there, repeating is honest advice, because we do not know why it was slow.
Both answers also come from the deprecated GET /v1/activities/aggregate — the rule cannot be side-stepped by calling it.
meta.truncated now means "some records never reached us" in both the old case (a selection wider than 5000) and the new one (fewer records processed than matched the filter). In the second case meta.recordsShortfall arrives alongside it — how many records are missing. count and meta.totalRecords stay complete: only data.groups and the numeric aggregations are partial. Important: this half of the change applies to the aggregate of ANY entity, not only activities: previously such a response came back with truncated: false, i.e. the incompleteness was reported nowhere.
The list of accepted narrowings, and whether the requirement is on for the account right now, arrive in data.aggregateFilterRequirement of the GET /v1/activities/fields response: the anchors field carries the narrowings, the enforcement field is either enforced or advisory. The static narrowings, without the account state, are also in GET /v1/guide.
What integrators should do
Add one of the narrowings to any activities aggregate — that is enough both before and after the requirement is switched on. If your code has a 503 branch for this endpoint today, add a 422 branch and do not repeat the request from it. If your code reads meta.truncated, note that it now also rises when records are lost, and look at meta.recordsShortfall.
FIX-0807-18: a file in a CRM field no longer hits the 1 MB wall, and the refusal code is documented
Before
The value of a "File" custom field travels in the request body as base64, and the body was capped at 1 MB on every method. The practical ceiling for one file was around 750 KB: PATCH /v1/deals/{id} with anything larger answered 413 with the FST_ERR_CTP_BODY_TOO_LARGE code, which appears on no documentation page. The same ceiling hit bot and chat file uploads.
After
The body is capped at 40 MiB on record create and update (POST /v1/{entity}, PATCH /v1/{entity}/{id}, including POST /v1/items/{entityTypeId}), and on POST /v1/bots/{botId}/files and POST /v1/chats/{chatId}/files — just under 30 MiB of the original file after base64. Every other method keeps the previous 1 MB ceiling: search (POST /v1/{entity}/search), batch calls, service methods.
The 413 refusal code is now PAYLOAD_TOO_LARGE on every /v1/ method except the AI routes (/v1/ai/*, /v1/chat/*, /v1/audio/*, /v1/models), which keep their OpenAI-compatible error envelope. It is the same code the edge layer already returns at its own threshold. The change also covers the streaming source uploads for apps (POST /v1/apps/{id}/sources) and servers (POST /v1/infra/servers/{id}/sources), and the 413 on a wrong Content-Type for app publishing, placement binding and document template creation. The former internal code no longer appears in responses.
The order of checks changed in favour of security: on record writes and on bot and chat file uploads the key is verified before the body is read. A request with no key or a wrong key now gets 401 where it could previously get 400 about unparsed JSON or 413 about size.
A new 429 refusal appeared, with the LARGE_BODY_BACKEND_BUSY code and a Retry-After: 5 header: the number of bodies over 1 MB processed at the same time is bounded. It protects server memory — the raised ceiling is not self-limiting, and every call waiting in the queue holds its own body. An ordinary client will never see it; a multi-threaded bulk upload will, and the right reaction is the one for any 429: wait and retry.
Mind the clock: the call to Bitrix24 is capped at 15 seconds with no retry, so a file right at the edge may fail with BITRIX_TIMEOUT on a slow account. Leave headroom, or move large files to a "File (Drive)" field via POST /v1/files/upload.
2026-08-06
NEW-0806-1: 402 for an exhausted Cowork/Code quota carries a Retry-After header
The 402 response with code cowork_quota_exhausted on POST /v1/chat/completions now carries a Retry-After header — the number of seconds until the exhausted quota window (5h, week or month) resets. Previously the reset moment was visible only in the resetAt field of the response body; the header is understood by plain HTTP clients without parsing the body. The 402 response with code insufficient_balance does not carry the header — an empty balance has no reset time.
FIX-0806-2: Web search: the response status distinguishes a provider key rejection from a provider failure
Before
Every search provider error on POST /v1/search arrived as 502 UPSTREAM_ERROR — a provider rejecting the key (401/403), provider throttling (429), and a genuine failure looked the same. Clients retried requests that could never succeed.
After
For a BYOK key, provider 401 and 403 responses keep their status — the provider rejected your key, replace it. A provider 429 keeps its status for any key and carries the Retry-After header. The error code stays UPSTREAM_ERROR in every case, and the body additionally carries the upstream_status field with the provider's original status. Every other provider error, including a rejected platform-engine key, still arrives as 502. Additionally, the message field of the 400 INVALID_REQUEST response is now length-capped — the received value is no longer echoed in full.
Impact on integrators
A handler that retried on any 5xx keeps working. If you branched on 502 as "any provider error", add branches for 401/403 (replace the BYOK key) and 429 (retry per Retry-After); the reliable "this is a provider error, not an authorization error" signal is the upstream_status field in the body.
FIX-0806-3: the operation registry in GET /v1/guide now lists what actually works
Before
An entity's operation list in GET /v1/guide disagreed with the set of working endpoints in both directions.
It stayed silent about working operations. No entity declared fields, although
GET /v1/{entity}/fields answers for 46 of 49 entities. Bookings were missing
list and search, although GET /v1/bookings and POST /v1/bookings/search are
served by dedicated handlers — a robot read the entity as write-only and had no way
to obtain an identifier. Open-channel configs were missing list, search,
create, update and delete, leaving only getById, aggregate and batch in
the registry. The same mechanism hid create for document templates, list and
create for addresses, search for org-structure nodes and delete for a user,
while the address search was described with the generic windowed-search contract
its handler does not implement.
And it promised what an entity does not have: the batch example named the create
action for eight entities, where that action answers 400 ACTION_NOT_SUPPORTED.
After
An operation is declared exactly when its route is really registered. Added:
fields for the entities that serve that route, list and search for bookings
(with the mandatory dateFrom and dateTo parameters stated in the description),
the full list / search / create / update / delete set for open-channel
configs, create for document templates, list / search / create for
addresses, search for org-structure nodes and delete for a user. The
descriptions of these operations list the parameters their own handler reads rather
than the generic search contract: windowed search (autoWindow, windowCount)
does not exist for them.
The batch example names an action the entity accepts, and the key that action
reads: ids for delete, items for other writes, calls for reads. An entity
with no available write operation gets a read example.
The note on such an entity no longer states the read set as one fixed sentence
("accepted: list, get, fields"): it names the actions that really answer with data
for THIS entity, and separately what the envelope does with the rest. There are three
reasons an action stays blind: on an entity with no field-schema method fields answers an
empty object (and the note points at GET /v1/{entity}/fields when that route exists); on
an entity with no addressed read method get answers the collection's first record rather
than the one asked for; on a REST 3.0 entity the batch sub-call does not reach the method at
all and comes back as a per-call error inside the 200; and on the open-channel config list
the sub-call reaches Bitrix24 without the envelope imopenlines.config.list.get requires,
answering 200 with the whole collection while silently dropping the filter.
The same statement is corrected in the two other places a client sees it: the
400 ACTION_NOT_SUPPORTED body no longer ends in "Supported batch actions: list, get,
fields" (it now carries the same computed set — previously a client that read the honest
registry and then tripped the refusal got the misleading list back), and the OpenAPI
description of the open-channel config batch read now says which of the accepted actions
answer with data.
The same descriptor's select description is corrected too: for the open-channel config
list the comma-separated form (?select=id,name) IS read — only the indexed form
(?select[0]=id) is not.
An entity that does not have an operation still does not get one: fields did not
appear for task comments, for calendar sections or for mail mailboxes. What should not be declared stays
undeclared too: routes that exist only to refuse a call and point at the correct
path, and aggregate on an entity where only counting works — the search
description still names that path.
Impact on integrators
The change is additive: existing operations fields are neither renamed nor
removed. A client that built its list of available calls from this registry now
sees operations it previously had to guess or look up in the documentation. A
client that copied the batch example verbatim will stop receiving
400 ACTION_NOT_SUPPORTED on read-only entities.
FIX-0806-4: the task service fields are accepted on writes — exactly as Bitrix24 itself accepts them
A task carries seven service fields: the creator (createdBy), who changed it (changedBy), who closed it (closedBy), who changed its status (statusChangedBy), and the creation, change and closing dates (createdDate, changedDate, closedDate). Bitrix24 accepts and stores all of them — both when a task is created and when it is updated. Vibecode refused six of the seven, which made it stricter than the platform for no gain.
Before
POST /v1/tasks and PATCH /v1/tasks/:id answered 400 READONLY_FIELD for changedBy, closedBy, statusChangedBy, createdDate, changedDate, closedDate and never reached Bitrix24. The upper-case spellings were refused the same way — CHANGED_BY and the rest. The refusal also came from a POST /v1/batch sub-call. The seventh field, createdBy, worked on creation and was refused on update.
After
All seven are accepted on both operations and on all three write surfaces — the single route, the entity batch request and the global batch request. Both spellings, createdBy and CREATED_BY, are accepted. The value is applied within the permissions of the calling user: when Bitrix24 refuses to edit the task, the refusal arrives as it is — a 422 carrying its own text, with no substitution by an error of ours and no false success.
A change of the creator is written to the task change log, and the real calling user stays visible there. For the other six fields no log entry exists. One more subtlety — for the three dates, a value without a timezone gets the offset from the X-Vibe-Timezone header when sent as createdDate, while as CREATED_DATE it goes through unchanged. Both subtleties are covered on the PATCH /v1/tasks/:id page.
What did NOT change: id is still refused — Bitrix24 assigns the identifier itself and ignores a submitted value, so an explicit refusal is more honest than a silent loss. dateStart, activityDate and realStatus stay closed as well, but for a different reason: their behavior on write was not verified, and we will not declare a field open without verifying it.
Pass an existing employee only — in all four fields that carry a user id. Bitrix24 does not check the value for existence and will store any number, and a task whose creator does not exist stops being manageable through the API: a further update and a deletion are both refused, even for an administrator key and even directly in Bitrix24, bypassing us. The warning is on the task-update page.
Impact on integrators
Nothing to change: requests that used to be refused now go through. If your code treated 400 READONLY_FIELD as protection for authorship and history, it never played that role — Bitrix24 itself accepts the same values through its own interface, bypassing our layer. Only the Bitrix24 permission model can restrict overwriting the service fields — that is a separate change on the platform side. For leads and deals the author field stays closed, which has not changed: Bitrix24 silently ignores the value there, so an explicit refusal remains the honest answer.
FIX-0806-5: a catalog application card opens its subpath instead of the server root
Before
An application card in the Bitrix24 catalog always opened the root of its Black Hole server. An application serving its interface from a subdirectory could not be opened from the catalog at all: the click answered HTTP 200 and rendered whatever lives at the root of the same server. The application address (appUrl) had no effect on this, and editing it through PATCH /v1/apps/:id never reached the card.
After
The card now opens the full address of the linked application — subpath, query and fragment included — whenever that address points at the same Black Hole subdomain as the card's server. Everything else keeps the previous server root: a different subdomain, a custom domain, a different scheme, an empty or unparseable address.
Editing appUrl through PATCH /v1/apps/:id now queues the card for an update, so the new address reaches Bitrix24 on its own. Already-published cards are reconciled platform-side — no integrator action required.
Integrator impact
Nothing to change. An application serving its interface from the root behaves exactly as before. An application in a subdirectory no longer needs a manual workaround — it is enough for appUrl to carry the subpath.
FIX-0806-6: speech recognition now reports a temporary provider pause
Before
When the cluster was temporarily unavailable, POST /v1/audio/transcriptions could respond with 502 ai_provider_unavailable without telling the client how long to wait before retrying.
After
In this state, the endpoint responds with 429 ai_provider_cooldown and a Retry-After header in seconds. The request is not executed and consumes neither quota nor money. Wait for the stated interval and retry the same request.
FIX-0806-7: speech recognition now really waits the stated 15 minutes
Before
For a long recording, POST /v1/audio/transcriptions could answer 503 ai_provider_timeout after about 5 minutes, even though the endpoint documents a wait of up to 15 minutes. The error text described a network-layer timeout.
After
The endpoint waits the full stated period — up to 15 minutes — and answers 503 ai_provider_timeout with a Retry-After header only once it expires. The limit on recording length is unchanged: for files longer than ~30 minutes, keep splitting the recording into parts.
FIX-0806-8: galaxy app exec now targets the container by its real name
Before
POST /v1/infra/servers/{id}/exec on a galaxy app always addressed the container by its subdomain. An app restored from a clone runs under a different name, so the command targeted a container that does not exist — the call failed, and in the worst case it could reach a leftover container from an earlier deployment. The rest of the galaxy lifecycle (deploy, migrate, stop) already accounted for the rename; only exec did not.
After
The container name is resolved by one shared rule for every operation: the on-host name when the app was renamed, otherwise subdomain. The resolved name is validated before it is interpolated into the command; an unusable name returns 409 GALAXY_APP_NOT_READY with invalid on-host name instead of running anything. Apps that were never restored from a clone are unaffected.
BC-0806-9: V1: server status in JSON is always lowercase
Old format supported until: 06.09.2026
Before
GET /v1/infra/servers and GET /v1/infra/servers/:id returned a lowercase status (running, sleeping), while POST /v1/infra/servers/:id/wake, POST /v1/infra/servers/:id/refresh, the currentState.status field on 422 responses of POST /v1/infra/servers/:id/start, POST /v1/infra/servers/:id/stop, POST /v1/infra/servers/:id/reboot, and infra.unhealthyServers[].status in GET /v1/me exposed the stored enum value in UPPERCASE (RUNNING, SLEEPING, PROVISIONING). A client that learned status === 'running' from the docs and GET broke on the wake and refresh responses.
After
Every listed field of the public V1 JSON carries a lowercase server status: provisioning, running, stopped, sleeping, error, deleted. The refresh data field is still a string, not an object: compare data === 'running', not data.status. The blackholeStatus field is unchanged — it stays UPPERCASE (CONNECTED, DISCONNECTED, NONE).
Impact on integrators
Replace equality checks against 'RUNNING' / 'SLEEPING' / 'PROVISIONING' and the other uppercase values with lowercase ones, or compare case-insensitively. Read the status from the structured fields (data, currentState.status) rather than from message / userMessage prose, where an uppercase status may still appear.
NEW-0806-10: deploying a galaxy app from a link or a saved version
Previously a galaxy app accepted an inline archive only: source.url and source.versionId were rejected with 400 GALAXY_DEPLOY_CONTENT_ONLY. POST /v1/infra/servers/:id/deploy now accepts both forms where the platform has enabled link deploys for your Bitrix24 account; where it has not, GALAXY_DEPLOY_CONTENT_ONLY is returned as before, and the inline source.content keeps working in all cases.
A link is downloaded by the host itself, so the archive never travels through the request body: the inline size limit (413 GALAXY_UPLOAD_TOO_LARGE) does not apply to this path, and it does not consume a concurrent large-request slot (429 DEPLOY_BACKEND_BUSY). source.versionId deploys a version already held in source storage: the platform mints the signed link itself and links that version to this deploy, so the history shows exactly what went to production.
On a galaxy app the link points at source storage only — the signed link of a saved version qualifies. Any other address is refused with 400 GALAXY_SOURCE_URL_NOT_ALLOWED, so a large archive is first saved as a version and then deployed by source.versionId. A separate virtual machine has no such restriction.
Creating a server with a source (POST /v1/infra/servers) accepts source.url on the same terms. source.versionId is not accepted there: at create time there is no server yet whose storage would scope the version lookup — deploy a saved version as a second step, via /deploy.
The dashboard routes keep accepting the inline archive only.
FIX-0806-11: the smart-process items field reference no longer shows the contacts field
Before
GET /v1/items/:entityTypeId/fields listed a contacts field of type crm_contact. No value for it ever arrived, either in the list or in the item card, and it could not be written: Bitrix24 accepted an empty array only and rejected any non-empty value with an error from its own internal data layer. The field reached the reference through the passthrough of the Bitrix24 schema; the platform never declared it.
After
The field is gone from the reference. Linked contacts are read and written through contactId and contactIds, which are unchanged. Filtering and sorting by contacts are still refused with UNKNOWN_FILTER_FIELD and UNKNOWN_SORT_FIELD.
Impact on integrators
No action needed: the field never had a value, so a client reading it always got nothing. If you generated a data model from the reference, drop contacts from it and rely on contactIds.
FIX-0806-12: the pages field reference now says which fields can be empty
Before
The GET /v1/pages/fields response gave no way to tell a field that always has a value from a field that arrives as null. Two descriptions also promised something other than what arrives: datePublic was described as "arrives as an empty object", and dateCreate, dateModify and datePublic as dates in a fixed template. A client that wrote its parsing against those descriptions tripped over an empty value, and a date filter in the wrong format returned an empty list with code 200.
After
Nine fields Bitrix24 does not always fill are marked with a nullable flag: description, xmlId, tplId, tplCode, folderId, searchContent, initiatorAppCode, rule, datePublic. The set was measured over the whole page collection of a live account rather than derived from the Bitrix24 method reference.
datePublic is described honestly: the wrapper returns null, and that is the usual value even for a published page, so read active or public to tell whether a page is published. The descriptions of dateCreate, dateModify and datePublic no longer promise a fixed template: the value is a string in the account locale format, identical in the list and in the card. The filter needs the same format: a value in another locale's format, or in ISO, is not recognized by Bitrix24 and returns an empty list with code 200.
The same fields are marked in the generated OpenAPI schema, where the type is now written as ["string", "null"], so a client validating the response against the schema no longer fails on an empty value. The write contract is untouched.
Impact on integrators
No action needed: the field set, the types and the values are unchanged — only the nullable flag was added and the descriptions were made accurate. If you were telling whether a page is published by the presence of datePublic, switch to active or public.
NEW-0806-13: the key-limit refusal now states the numbers
On a KEY_LIMIT_REACHED (409) refusal, POST /v1/apps now puts the quota state
into error.details: limit — how many keys per person the Bitrix24 account
administrator allows, used — how many are taken right now.
Before
{
"success": false,
"error": { "code": "KEY_LIMIT_REACHED", "message": "Maximum number of API keys reached" }
}
After
{
"success": false,
"error": {
"code": "KEY_LIMIT_REACHED",
"message": "Maximum number of API keys reached",
"details": { "limit": 10, "used": 10 }
}
}
The field is additive — clients reading only code see no change. used also
counts keys the platform issued itself (apps, agents, bots), so it can exceed the
length of the GET /v1/keys list.
NEW-0806-14: the site and employee field maps now carry labels, descriptions and value lists
GET /v1/sites/fields now returns a label and a description on all 22 fields — previously only the type field had them, and the other 21 arrived with nothing but a type and a read-only flag. The descriptions say what the type cannot: that active is not settable through the API, that code is stored in a slash-wrapped form, that landingIdIndex/landingId404/landingId503 are settable on update only, and that dateCreate and dateModify arrive as a string in the Bitrix24 account locale format rather than ISO 8601.
GET /v1/users/fields gained an enum of allowed values on gender (personalGender: M, F) and on account type (userType: employee, extranet, email), each value with its own label. Labels and descriptions also appeared on ten work-details fields that had no label in Bitrix24 at all, where the field name used to arrive in place of one: WORK_FAX, WORK_PAGER, WORK_STREET, WORK_MAILBOX, WORK_STATE, WORK_ZIP, WORK_COUNTRY, WORK_PROFILE, WORK_LOGO, WORK_NOTES. When the Bitrix24 account labels such a field itself, its own label is kept unchanged.
Gender also gained a nullable: true flag, and in the OpenAPI schema the property type is now declared as ["string", "null"]. An unfilled gender comes back empty (null) — 48 of 50 employees on the measured Bitrix24 account answered that way — while the schema without the flag promised a string and nothing but a string, so a client validating the response against our own published schema failed on almost every record. The flag describes the read only: in the request-body schema the field type is still a plain string.
The value lists also arrive on the two other machine-readable surfaces — GET /v1/guide (the entity's fieldsDetailed block) and the OpenAPI schema (x-enumValues on the property). Labels and descriptions of declared schema fields are carried by OpenAPI alone (title and description on the property), so a schema-generated client picks them up with no extra calls; the guide does not carry labels, by design. Labels and descriptions of the ten work-details fields arrive in the field map itself only.
The change is additive: responses gained new keys while the field set and the values stay the same, so existing integrations keep working untouched.
FIX-0806-15: product sections: the sort field is marked read-only and not-returned
Before
GET /v1/product-sections/fields advertised sort as writable, POST /v1/product-sections and PATCH /v1/product-sections/:id accepted it without an error, and Bitrix24 did not save the value. Meanwhile no read response — the card, the list, search, the create echo — carried the field, even when it was requested explicitly in select. The client got a success and went on believing the order had been set.
After
The field is marked read-only and notReturned: true. Sending sort in a create or update body is refused with 400 READONLY_FIELD before the Bitrix24 call; the field stays visible in the field map together with a description of the reason, so it can be read on the spot. Ordering by it works as before: ?sort=sort&order=asc and order=desc give a different order. Filtering by sort is still refused with 400 UNSUPPORTED_FILTER.
Impact on integrators
Remove sort from product-section create and update bodies — otherwise the whole request now gets 400 READONLY_FIELD instead of the former success. If your code read sort out of a response, it was never there: the value came back undefined. Change the order of sections in the Bitrix24 interface, and read the order by sorting the list on that field. There is no parallel support for the previous behaviour: the previous behaviour was that the value was silently lost, so there is nothing to keep.
FIX-0806-16: the active field of a site is read-only now — activation goes through publishing
Before
GET /v1/sites/fields described active as an ordinary writable field, and a request carrying it went through: POST /v1/sites and PATCH /v1/sites/:id answered with a success. The value was dropped. Bitrix24 accepts ACTIVE neither in landing.site.add nor in landing.site.update — their contract does not declare the field, and a new site is always created inactive. A live round-trip on both verbs confirmed the loss: a create with active: true and an update to active: true both answered with a success while the flag stayed off.
After
active is marked readonly. Passing it in a create or update body is refused with 400 READONLY_FIELD before the Bitrix24 call. The field stays in the list/get response and in the /fields directory — reading it is unchanged, filtering and grouping by it included.
A site is activated by publishing it in the Bitrix24 account interface.
Impact on integrators
If your code passed active in a site create or update body, drop it. The value was never stored anyway, but now the whole request is refused, so the rest of the body is not applied either — title, code, domain, description. There is no parallel support for the previous behaviour: the previous behaviour was that the value was silently lost, so there is nothing to keep.
FIX-0806-17: base image registry unavailable during a galaxy app build is now transient, not fatal
Before
When the public image registry was unreachable at build time, POST /v1/infra/servers/:id/deploy
returned 502 with a raw Docker message and the app was marked broken — the retry had to be issued
by hand and the response carried no hint.
After
The response carries the GALAXY_BASE_IMAGE_UNAVAILABLE code, retryable: true and a hint
telling you to re-send the same request in 2-3 minutes. The slot is not marked broken, so the retry
lands on it. For agents the platform retries on its own. One-shot create-with-source
(POST /v1/infra/servers with source) no longer holds the HTTP response, so there the app is
still marked broken — but the error text names the cause and asks you to re-deploy.
FIX-0806-18: issuance refusals now name a service problem instead of an access one
Before
When Bitrix24 refused key issuance or app installation, the answer was chosen from a stale local snapshot of the account's marketplace state. An account whose entitlement was in force could still receive an access-paywall answer with a purchase call to action — while nothing was actually missing on the account side.
After
While the account's marketplace entitlement is in force, an issuance refusal is returned as 502 CONNECTOR_REST_UNAVAILABLE with a "try again / contact support" message and no purchase call to action. The response carries error.details.reason (the original refusal reason) and error.details.retryable: true. When the entitlement really is missing, the access answer is unchanged. Affects POST /v1/apps and the matching in-product routes for key issuance and app creation. Additionally, a "REST unavailable" refusal is now retried once automatically, which clears the race right after an entitlement is granted.
FIX-0806-19: the platform now passes the application its port in the PORT variable
Before
The platform agreed with itself about the application port on three levels — public traffic forwarding, the image EXPOSE, and the healthcheck — but never told the application. An application written to the common cloud-platform convention (listen(process.env.PORT)) read an empty value, bound a random free port, and nothing listened on the expected one: the gateway served "application not found" even though POST /v1/infra/servers/:id/deploy reported success.
After
The platform passes the port number in the PORT environment variable — always equal to the request's port field (3000 by default). On a dedicated virtual machine this is a separate platform file .vibe-platform.env that the systemd unit loads after your .env; your .env is neither read nor rewritten. In a galaxy application PORT arrives in the container environment at start. The response gained a platform_env step.
The PORT key is now reserved by the platform: if you pass your own env.PORT that differs from the port field, the platform overrides it and says so with a line in the response warnings[]. Passing a matching value is fine — there will be no warning.
Impact on integrators
In the normal case there is nothing to change: an application listening on process.env.PORT now works without an explicit env.PORT, and already running applications receive PORT on their next deploy. One exception is worth checking: if you kept something other than your own listen port in env.PORT (a database port, an upstream service port), rename that variable — PORT now belongs to the platform and your value no longer reaches the application. The deploy response carries a warnings[] entry when that happens. If you read the .env file directly instead of the process environment, read process.env.PORT — the platform value lives in a separate file. With your own systemd unit (systemd: false) the platform file is written but YOU load it — until you do, your env.PORT keeps winning, and the response warning says exactly that; POST /v1/infra/servers/:id/deploy describes how to load it.
BC-0806-20: quota consumption is reported as percentages only
Old format supported until: 06.02.2027
Before
GET /v1/ai/quota returned absolute consumption counters in data.byModel[] — tokensIn, tokensOut and audioSeconds — alongside the pctOfLimit share.
{ "modelId": "bitrix/bitrixgpt-5.5", "calls": 1240, "tokensIn": 800000, "tokensOut": 350000, "audioSeconds": 0, "pctOfLimit": 1.2 }
After
The three fields are gone. Quota consumption — like the limit itself — is exposed only as a relative value: pctOfLimit (the share of the monthly limit consumed by the model) and calls (the call count).
{ "modelId": "bitrix/bitrixgpt-5.5", "calls": 1240, "pctOfLimit": 1.2 }
What integrators should do
There is one authoritative figure for the account — data.pctUsed: it is computed from the charge ledger and accounts for the time-of-use discount. The byModel[].pctOfLimit breakdown shows WHERE the quota went and is computed by repricing the call journal at today's prices, so there is no need to add the per-model shares up and compare the sum with pctUsed — the two will differ. If you need token counts for your own accounting, take them from GET /v1/ai/usage or capture them at call time — the POST /v1/chat/completions response still carries the usage block with prompt_tokens and completion_tokens.
FIX-0806-21: an exhausted Cowork/Code quota is no longer served by a substitute model
Before
When the Cowork/Code quota ran out, a request from a desktop key was served by the reserve model with the request's tools and system prompt passed through unchanged. The model answered fluently and could report work it had not done. The response carried HTTP 200 and X-Cowork-Fallback: true.
After
One behaviour for every key: tools, tool_choice and response_format are stripped, and the model states that the limit is reached and when it resets. HTTP 200 when a reserve model is configured, otherwise 402 cowork_quota_exhausted as before. The X-Cowork-Fallback: true header and the COWORK_QUOTA_FALLBACK warning remain, but now mean "the limit was announced", not "the request was served by another model".
Impact on integrators
Do not expect tool_calls or a structured response on an exhausted quota: response_format is stripped, so you get prose, not JSON. Detect the state via the X-Cowork-Fallback header, the COWORK_QUOTA_FALLBACK warning, or the 402.
Separately, the monthly reset date on the free plan is fixed. resetAt.month (GET /v1/cowork/me), windows.month.resetAt and subscription.currentPeriodEnd (GET /v1/cowork/state) used to return the date stored on the subscription row, and on a free seat that date stopped moving once the period ended — so it arrived in the past and the countdown read "less than a minute" forever. All three now return the period the seat is actually in: the monthly counter resets on the first request after the period ends. In the 402 cowork_quota_exhausted body, the month window's resetAt is no longer 1970-01-01.
Impact on integrators
If you cached a free seat's currentPeriodEnd as a fixed date, re-read it: on an overdue seat it moves forward.
FIX-0806-22: server repair now reports why it failed and no longer leaves the agent stopped
Before
On failure GET /v1/infra/servers/:id/repair-status returned an error with no cause —
SSH install failed (exit 255); serial fallback: Serial console install failed. That text
could not distinguish a closed port from an unreachable machine or from a download that
never completed. The agent install also stopped the running service BEFORE downloading the
replacement: if the download failed (no outbound connectivity, unreachable download host),
the agent stayed stopped and the next repair attempt repeated the same sequence.
After
error now carries the cause: the SSH message for the regular path (Connection refused,
Connection timed out and so on), and a short excerpt of the console output for the
emergency-console path (for example curl: (6) Could not resolve host: …). The excerpt is
stripped of secrets and length-bounded. The install downloads the new agent first and only
then stops the service, and brings the agent back up if any later step fails.
FIX-0806-23: key issuance now checks platform access
Before
POST /v1/keys and POST /v1/apps issued a new key to any authenticated account, including accounts whose access to the platform was closed. The key then worked — issuance never consulted the access check.
After
Access is verified before the key is issued. An account without access is refused with INT_TARIFF_REQUIRED, the same code it already receives on other surfaces.
Keys already issued keep working. Key rotation, automatic recovery and ownership transfer are unaffected: an account whose access lapsed must still be able to wind its own affairs down.
Impact on integrators
Handle the refusal on issuance the same way it is handled on the other surfaces: restore access and retry. Keys issued earlier need no changes.
NEW-0806-24: a clear refusal when a personal key is left with only placement or entity
A personal key is backed by a Bitrix24 incoming webhook, and that surface does not store
the placement and entity scopes — they require an application context. Such a request
used to fail opaquely: key creation returned 502 DEVKEY_MINT_FAILED advising the caller
to contact the account administrator, and a scope edit returned 502 DEVKEY_SCOPE_SYNC_FAILED.
Both cases now answer 400 PERSONAL_KEY_WEBHOOK_SCOPES_INVALID with a message that says
what to do: add at least one regular scope (for example crm or user_brief), or create
an OAuth app if you need placements.
The refusal fires only when dropping those two scopes leaves nothing Bitrix24 can bind to
the webhook. A mixed set (placement + crm) still succeeds.
Affected endpoints: POST /v1/keys, PATCH /v1/keys/{id}, POST /v1/keys/{id}/rotate
Related response change: for a personal key, the scopes field of a create or update
response no longer echoes placement and entity — the webhook never carried them, and
the response used to promise a scope the key does not have. App keys and system keys are
unaffected.
FIX-0806-25: a Galaxy Python build no longer fails on a version that exists
Before
When a runtime: python311* deploy could not reach the package index, No matching distribution found for <package> was all you got — for a version that exists and installs fine. The platform showed nothing next to that text, so it read as your mistake.
After
The default package index on this platform is the canonical PyPI, and that has not changed. Your own --index-url in requirements.txt or in the install command still wins over ours.
If the index still does not respond, error.category is now INSTALL_REGISTRY_UNAVAILABLE (was GENERIC), and error.buildHint — plus the field of the same name on GET /v1/infra/servers/:id — carries a readable reason instead of nothing. The value is additive: existing codes are unchanged. The failure is terminal — the platform does not retry it for you.
2026-08-05
FIX-0805-1: key rotation no longer strands the bot registered with it
Before
A bot registered via POST /v1/bots remembers the key it was created with. After POST /v1/keys/:id/rotate that binding stayed on the old key: the new key got 403 BOT_ACCESS_DENIED on every call for that bot, and once the old key expired after its grace period the bot went silent entirely — incoming events kept queueing, but nothing could fetch them (GET /v1/bots/:id/events). The only recovery was POST /v1/bots/:botId/transfer.
After
Rotation moves the bot to the new key along with the application: calls for the bot and event polling with the new key keep working with no manual step.
What is still not ideal
Bots owned by an AI agent or a managed bot are not repointed by this change — they have their own key-recovery path, and moving them from here would desynchronize their own fields. Their behavior is unchanged.
Impact on integrators
No action needed. A manual POST /v1/bots/:botId/transfer after rotation is no longer required — it remains only for handing a bot to a different key.
FIX-0805-2: a container on a shared host is no longer lost after key rotation
Before
The repoint done by POST /v1/keys/:id/rotate skipped servers on a shared host (kind=GALAXY_APP): such a container stayed on the old key, and once the grace period ended, deploy, exec, file upload, and log calls made with the new key stopped finding it.
After
A container on a shared host switches to the new key together with the other servers. Exactly one narrow exception remains: the container is never moved onto an application authorization key (vibe_app_) — that binding is irreversible and breaks deployment.
What is still not ideal
The platform does not replace the key stored in the container's own environment variables: their values are set when the container starts. Update the variable and deploy the application again — the key's grace period gives you time.
Impact on integrators
No action needed. Calls made with the new key against a container on a shared host now keep working after the grace period ends.
FIX-0805-3: key rotation no longer disconnects the server and app bound to it
Before
After POST /v1/keys/:id/rotate the server and app created with that key kept referencing the old key internally. Once the old key expired after its grace period, deploy, exec, upload, and log calls made against that server with the NEW key stopped finding the server.
After
After rotation the server, the app, and its live access tokens (api-bearer, minted via POST /v1/infra/servers/:id/access-tokens) switch to the new key along with it — deploy/exec/upload/logs calls made with the new key keep finding the server, and refreshing such a token (POST .../access-tokens/:tokenId/refresh) no longer fails on a key mismatch.
What is still not ideal
The old key loses access to the server and app IMMEDIATELY, at the moment of rotation — not once its grace period ends. The key itself stays formally active for those hours (KEY_GRACE_PERIOD_HOURS), but the server and app have already moved to the new key, so calls made with the old key against that server stop finding it right away.
Impact on integrators
No action needed. A client that treated the post-rotation disappearance as a persistent failure now sees continuous access — except for the old key itself, which loses visibility of the server sooner than its formal expiry.
BC-0805-4: the task field reference now matches the task response, and numbers are numbers
Old format supported until: 04.02.2027
Before
GET /v1/tasks/fields described 92 fields, 66 of them under names such as
MARK, NOT_VIEWED, STAGE_ID, CHAT_ID — keys that GET /v1/tasks and
GET /v1/tasks/:id never return. Most of the keys the response does carry were
missing from the reference. Values disagreed with the declared type as well:
id, status, priority, groupId, chatId, responsibleId, createdBy,
changedBy, closedBy, statusChangedBy, timeEstimate and
timeSpentInLogs were declared numbers but arrived as strings ("289", "2").
Yes/no flags arrived as the strings "Y" and "N", and "N" is truthy in every
language. Empty tags, group, accomplicesData and auditorsData arrived as
an empty array while the reference declared an object. Fields that really do come
back empty were not marked as nullable. chatId also changed type between
surfaces: a string in the list, a number in the card.
A client generated from that reference did not work.
After
The reference and the response name the same fields. 69 fields are declared, and
the raw upper-case names are gone from the reference (only the account's custom
fields and CHECKLIST remain — the latter is served by the task checklist
endpoints). Fields declared as numbers arrive as numbers, yes/no flags arrive as
true and false, and empty tags, group, accomplicesData and
auditorsData arrive as an empty object. 27 fields are marked nullable.
chatId is a number on both surfaces.
What integrators should do. Check your code for: comparisons against strings (status === "2",
id === "289"), flag checks that test for a non-empty string, and code that
treats empty tags / group / accomplicesData / auditorsData as an array.
subStatus (list only) and action, checklist, checkListTree,
checkListCanAdd (card only) are still returned and are deliberately not in the
reference — task lists and task cards differ in key set on the Bitrix24 side.
realStatus is filter- and sort-only and never appears in the response — the reference now says so machine-readably, via the notReturned marker.
FIX-0805-5: the model-unavailable pause now lengthens until the cluster recovers
Before
The pause behind a 429 ai_provider_cooldown refusal always lasted about a minute. When it expired the platform let the full load back into the model cluster, and if the cluster had not recovered the cycle repeated: a minute of waiting, a burst of retries, more failures. Retry-After always carried the same value, so a client that hard-coded one minute behaved exactly like one that read the header.
After
The first pause is still about a minute, but if the cluster is still failing when it expires, the next pause doubles — up to a four-minute ceiling. As soon as a call succeeds the count resets and the next pause starts from a minute again. The Retry-After header (and the retryAfter field in the terminal streaming frame) carries the current remainder, so take the wait from the response rather than from a constant in your own code.
NEW-0805-6: deploy warns when the verified app path disagrees with the address Bitrix24 opens
Before
A deploy with a healthPath other than / verified the app on a subpath and answered 200, saying nothing about the address Bitrix24 opens the app at. When the app answered only on that subpath while appUrl stayed the bare server address, the placement iframe opened the root: a green deploy, an app that does not open inside Bitrix24, and not a word about it in the response.
After
POST /v1/infra/servers/{id}/deploy now adds an entry to the optional warnings array for that combination (plain JSON response and the SSE done event, exactly like the existing displayName/description and changelog nudges). The entry names both halves of the contradiction — the verified path and the framed address — and both ways out: serve the bundle at /, or move the subpath into the application's appUrl via PATCH /v1/apps/{id}. A subpath in appUrl is supported, not rejected.
The entry is absent when healthPath is unset or /, when appUrl already carries a path, when the app address is outside the platform domain, and when no application is linked to the server yet. The response shape is unchanged: warnings was optional before and stays optional.
FIX-0805-7: Placement binding names the reason it was refused
Binding a placement with an application key no longer answers with an unnamed 502 BITRIX_UNAVAILABLE when Bitrix24 rejects the registration.
Before
POST /v1/placements/bind returned the same answer for every Bitrix24-side refusal — 502 BITRIX_UNAVAILABLE with "Failed to register placement on Bitrix24 via dev key". There was no way to tell "the application's grant lacks the required scope" from "a required placement option is missing": the account returns both as one opaque code. For the chat widgets IM_SIDEBAR, IM_NAVIGATION, IM_TEXTAREA a call without options.iconName landed in that same unnamed refusal.
After
The reason is named:
403 PLACEMENT_APP_GRANT_MISSING— the placement is not available to the Bitrix24 application.detailscarries the requiredrequiredScope, the placements the application CAN bind (availablePlacements,availablePlacementsTotal) and the way to widen the grant inremediation.400 PLACEMENT_OPTIONS_REQUIRED— a required placement option is missing and the platform could not fill it in (missingindetails).400 PLACEMENT_NOT_REST_BINDABLE— the code cannot be bound over the API at all.502 BITRIX_UNAVAILABLEremains for everything else and now carriesplacementInAppListindetails, plusdiagnostics("placement_list_skipped"or"placement_list_empty") when the diagnosis was unavailable.
A chat widget icon is no longer mandatory: when options.iconName is absent the platform fills one in and reports it in the success response as optionsDefaulted. Your own value always wins over the filled-in one.
The reference GET /v1/placements/available returns three new fields per code — requiredScope, requiresIconName, restBindable — and now lists ten codes that were bindable but missing from it, including the task card tabs and panels. The placements.bindPrerequisite block in key data states the scope requirement up front.
Impact on integrators
No call changes are needed. If you branch on placement-bind refusal codes, add the three new ones; if you relied on options.iconName being mandatory, it is now optional and behaviour with a supplied value is unchanged.
FIX-0805-8: the tunnel survives an app restart, and port auto-detection no longer loses the target
Before
An agent in port auto-detect mode dropped its detected target after a single failed observation. An app restart (1-3 s), or a first response slower than 1.5 s, made the tunnel serve the stub page for about 5 more seconds after the app was answering again. Separately, the target was re-elected on every successful scan, so a sidecar appearing on a lower port took the tunnel away from a perfectly healthy application — HTTP 200 with the wrong content and no error anywhere.
data.warning on PATCH /v1/infra/servers/:id/port and the tunnel_routing step of
POST /v1/infra/servers/:id/deploy promised that auto-detection converges "within ~30s".
After
The agent now separates two signals. While the app's port is present among the listening ones, the target is held; it is released only after several consecutive observations that the port is gone (~15 s), or — if the port listens but never answers — after about two minutes. A responding target is no longer re-elected, except when the current target is 80/443 and a real application port answered.
The data.warning and tunnel_routing texts were rewritten honestly. Auto-detection
picks up the new port within about a minute if the previous port was released; if a
live process still answers on the previous port, the scanner deliberately keeps it and
will not switch on its own — set the port explicitly with PATCH /v1/infra/servers/:id/port
or stop that process. POST /v1/infra/servers/:id/repair is not the tool for this case:
it reinstalls the agent with auto-detection, so the election simply runs again — with the
same outcome while the old process keeps answering.
The change reaches a server together with the agent update to 1.3.7.
FIX-0805-9: the MISSING_FIELDS hint for requisite links names the field names instead of sending you to another endpoint
Before
The 400 MISSING_FIELDS refusal of POST /v1/requisite-links stated that raw UPPER_SNAKE names are accepted alongside camelCase, and suggested taking them from GET /v1/requisite-links/fields. They are not there: the /fields response returns names in camelCase. Whoever read the refusal went looking for the list where the list is in the other notation, and came back with nothing.
After
The message lists all six names inline: ENTITY_TYPE_ID, ENTITY_ID, REQUISITE_ID, BANK_DETAIL_ID, MC_REQUISITE_ID, MC_BANK_DETAIL_ID. Both notations are still accepted on write; the refusal code and its condition are unchanged.
FIX-0805-10: the API schema now covers the session exchange and embed slots, and states its own coverage honestly
Before
GET /v1/openapi.json was described as complete, and GET /v1/guide recommended slicing it by scope so it fits an AI agent's context window. Some live methods were nevertheless absent from it, so an agent that followed that advice concluded the method did not exist. The concrete case was the embed-context-to-session exchange: the method worked and was covered in the documentation, but the schema listed only authorization start, callback, code exchange and revocation under /v1/oauth/ — so we received a request to add something that had shipped long before.
After
The schema now covers the embed-context-to-session exchange, the authorization-result poll for environments that cannot receive a redirect, and all four embed-slot methods: list registered, reference of available codes, register and remove. The mutating ones declare the access scope their handler actually enforces.
More importantly, the schema no longer promises completeness it does not have. Entity paths are generated from the live registry and are complete, while the hand-written sections are still being backfilled — so both the schema description and GET /v1/guide now say it plainly: a missing path does not mean a missing method, and they explain how to settle it in one call (a genuinely absent path answers ROUTE_NOT_FOUND, a live one answers a validation error). Both also name the sections that will never appear there: inbound handlers the platform receives rather than exposes, wrong-path hints, and sections available only to a management key.
FIX-0805-11: a client on a smart process item is written, and a disabled client block answers with a refusal instead of a false success
Before
The contactIds field on smart process items (PATCH /v1/items/:entityTypeId/:id) and on quotes (PATCH /v1/quotes/:id) was marked read-only, so a write was rejected with 400 READONLY_FIELD. The stated reason was that the contact binding is not managed through crm.item.update; a check against a real account did not confirm it — the method does change the binding set.
The second half of the same story: when a smart process has the client block disabled, Bitrix24 accepts contactId, contactIds and companyId, answers with success, and does not store the value. The platform passed that success through as is — the caller received a 200 for a write that never happened, and the only way to learn about it was to read the item back.
After
contactIds is writable on smart process items and on quotes. Send the full list: the binding set is replaced rather than merged, and the first contact in the list becomes the primary one. The contacts field (expanded objects rather than identifiers) stays read-only.
Writing a client to a smart process whose client block is disabled is now rejected before any Bitrix24 call — 400 with code CLIENT_BLOCK_DISABLED. The message names the offending field and GET /v1/smart-processes/:entityTypeId, whose isClientEnabled field shows the state of the block. The rule covers all three client fields — contactId, contactIds, companyId — because the block gates them identically.
The rule holds on every write surface, batch included: both POST /v1/items/:entityTypeId/batch and POST /v1/batch. On the per-entity batch the refusal applies to the whole batch and names the offending item index; on the global batch it arrives per sub-call and leaves the other sub-calls alone.
Empty values are not covered by the rule: 0, '', [] and null mean "no client", not a client write. That matters for the read-modify-write pattern: on an item with the block disabled the client fields read back exactly like that and are echoed in every update. If the type metadata cannot be fetched, the write proceeds — a failure to read settings does not block an update.
Impact on integrators
A request that wrote a client to a smart process with the block disabled previously received 200 and will now receive 400 CLIENT_BLOCK_DISABLED. That is the fix: nothing was stored before either, but now it is visible immediately. Either enable the client block on the smart process type, or stop sending client fields. Requests against types with the block enabled are unaffected.
FIX-0805-12: a JSON-object filter is applied, and an OR attempt gets its own error code
Before
The filter parameter on list requests had two spellings, and the second silently did nothing. The bracket form (?filter[id]=3) was applied. The JSON-object form (?filter={"id":3}) — the one the documentation examples show — was not recognised: the parameter was discarded, the request answered 200 and returned the entire collection. Nothing in the response distinguished a working filter from a discarded one.
A separate problem was expressing OR. The query-string parser supports two levels of bracket nesting, so ?filter[$or][0][id]=1 never reached the filter at all and was read as a field name. On deals that produced UNKNOWN_FILTER_FIELD naming the "field" filter[$or][0][id] — an answer that sent the caller off to check field names instead of saying that OR cannot be expressed in one filter. Shorter spellings meanwhile answered the correct INVALID_FILTER_OPERATOR, so one mistake got two different answers.
After
Both filter spellings are equal: bracket notation and a JSON object. A value that is neither (a string that does not parse as JSON, or parses to a number, an array or null, or a parameter that arrived as an array — the ?filter[]= form) is rejected with 400 and code INVALID_FILTER — the refusal happens before any Bitrix24 call. An empty ?filter= still means "no filter". A repeated ?filter=a&filter=b does not arrive as an array: the parser keeps the last value, which is then rejected for not being JSON.
The same INVALID_FILTER code rejects a request that mixes both forms — ?filter={"id":3}&filter[amount]=5. The query-string parser writes them into the same place, so the second form replaces the first and half of the conditions are lost while the response looks correctly filtered. The lost half cannot be recovered, so the request is rejected.
The logic keys $or, $and, $not and logic are now rejected with 400 INVALID_FILTER_OPERATOR at any nesting depth and on any entity, with one shared message: it names $in for same-field OR, a batch request for cross-field OR, and reminds that AND is the default. A field whose name merely starts with such a key (logicGroup, for instance) is not caught by the rule.
Impact on integrators
A request that sent filter in an unrecognised shape previously received 200 and the whole collection, and will now receive 400 INVALID_FILTER. That is the fix: the old response looked successful while the data came back unfiltered. The same applies to a request that mixes both forms: half of the conditions used to be applied, and now the request is rejected — put the whole filter in one form. Working requests — entirely bracket or entirely JSON — are unaffected.
FIX-0805-13: a galaxy build failure is no longer reported as npm help text
Before
An app shipped without a dependency lock file goes through the platform's automatic dependency install: it tries npm ci first and, when that refuses, installs the usual way. The step itself succeeds, but the npm ci refusal stays in the build log, and its last line is advisory text along the lines of "Run npm help ci for more info".
If the build then failed for an entirely different reason — in the interface bundler, say, or in the type checker — the short provisionError field showed that advisory line. It reads like dependency-install guidance, so the real cause never surfaced: the developer rebuilt again and again, chasing a step that had actually passed.
After
npm's boilerplate and advisory lines ("Run npm help … for more info", "command failed", "command sh -c …") can no longer become the error headline — they are dropped the same way log-file pointers already were.
The platform also learned to recognise bundler and type-checker failures: a failed file transform, an "expected one thing, found another" syntax line, a type-checker diagnostic, a failed import resolution. When no specific line exists, the build tool's own failure message is used — it at least names what broke. The full log remains available in buildLog.
FIX-0805-14: employee directory resolves through the server owner's personal key
Before
GET /v1/infra/servers/:id/b24-users returned an empty list with a hint for a server bound to an application authorization key until the application was authorized on the account — even when the server owner had a working personal key.
After
When neither the server key nor the linked application resolves account access, the directory is read through the server owner's active personal key. The response shape is unchanged; the hint is returned only when no source works.
FIX-0805-15: on a self-hosted account a module refusal ends the issuance again
Before
The change published on 4 August made a module refusal non-final: installing an
app (POST /v1/apps) on a self-hosted account was retried through the developer
key instead of returning 403.
After
That change is withdrawn. The refusal is final again: the request answers 403
with code INT_TARIFF_REQUIRED, and the second route is not attempted. This is
the same behaviour that applied before 4 August. Cloud accounts were affected
neither by that change nor by its withdrawal.
Impact on integrators
If you relied on the note published on 4 August, the retry through the developer
key no longer happens, and the response follows what the account is entitled to.
Accounts that see this 403 need their Bitrix24 plan to cover the feature.
NEW-0805-16: region is now optional when creating a server
Before
Creating a standalone server required the full provider + plan + region triple. A request without a region was rejected with 400 INVALID_REQUEST stating that all three fields are required. The same applied to galaxy creation.
After
region may be omitted — the platform resolves the provider default itself (its preferred region first, otherwise the first one in the catalog). provider and plan stay required. An explicitly passed region is still honoured exactly as before. If the provider exposes no regions at all, the response is 400 INVALID_REGION naming that provider.
This covers POST /v1/infra/servers and galaxy creation.
NEW-0805-17: fields of six directories now come with a name and a description
The field directory is the /fields response a client or an AI agent reads to understand what a field actually holds. For six entities it did not answer that question: a field was described by its type and a read-only flag, and what it contained had to be looked up in the documentation.
Now a name (label) and a description (description) are present for every declared field: GET /v1/payments/fields — 44 fields, GET /v1/basket-items/fields — 27, GET /v1/pages/fields — 27, GET /v1/catalog-sections/fields — 10, GET /v1/items/:entityTypeId/fields — 34, and in GET /v1/statuses/fields the service field extra gained a label — the only one of the eleven that lacked it.
The descriptions name the things that are easy to get wrong. For payments: Bitrix24 marks datePayBefore deprecated, accepts companyId without using it, psStatus is a Y/N flag rather than status text, and priceCod and externalPayment belong to the self-hosted edition. For pages it is now stated outright which fields arrive as the string "Y"/"N" (deleted, public, sys, sitemap, folder) — unlike the boolean active. For basket items the measurement-unit codes are named, along with the fact that properties and reservations come back only from the single-item endpoint and are absent from the list.
Along the way, fields the API already returned in its data but never described in the directory are now declared: for smart-process items — entityTypeId and the UTM block (utmSource, utmMedium, utmCampaign, utmContent, utmTerm); for companies — eleven fields: the phone numbers and e-mail addresses split by type (phoneWork, phoneMobile, phoneMailing, emailWork, emailHome, emailMailing), the Open Channels contact imol, the actual and legal addresses, entityTypeId, and the service search string searchContent — whose description says outright that its composition can change without notice and should not be relied on.
The keys are added to the field description and the existing type and readonly of previously described fields are unchanged — the labels themselves require no action. This release also carries FIX entries where writes did tighten: for companies, for smart-process items and for the active field of site pages, writing a value Bitrix24 never stored is now refused instead of falsely succeeding. If your code sends those fields in a body, read those entries — they say what to drop.
FIX-0805-18: an empty value of the declared company fields now arrives as null, and a write to them is no longer silently ignored
Eleven company fields and six smart-process item fields used to arrive in the data while the /fields directory did not describe them. As long as a field is undeclared, the platform passes its value through as is and does not validate a write — hence two consequences a client can observe.
Before
For the company fields emailWork, emailHome, emailMailing, phoneWork, phoneMobile, phoneMailing, imol, address, addressLegal and searchContent an unfilled value arrived as the empty string "". Writing any of them — like entityTypeId, like the UTM tags of smart-process items — was accepted with a success and silently changed nothing: Bitrix24 does not store those fields from a body. That held for create, for update and for both batch surfaces: POST /v1/companies carrying address returned 201, the company was created, and the address was lost.
After
An unfilled value of those fields arrives as null, the same as for every other string field of the platform, so an if (value) check behaves uniformly. Writing any of them in a body is refused with 400 READONLY_FIELD — on create, on update and inside a batch sub-call: there is no more silent success without a result. A filter and a sort over those company fields now work as well — filter[phoneWork], for instance — where the request used to be refused as a reference to an unknown field. That follows from describing the field rather than being a feature of its own: a filter over the service string searchContent is now accepted too, but its composition can change without notice, so do not build on it. The values are still written the same way: phone numbers and e-mail addresses through the phone and email multifields, addresses in the requisites of the company, and the entity type through the request path.
Impact on integrators
If your code reads those company fields and expects a string (taking its length or calling trim, for example), add a null check. If your code passed any of those fields in a create or update body, drop it: the value was never stored anyway, and now the whole request is refused, so the rest of the body is not applied either. There is no parallel support for the previous behaviour: the previous behaviour was that the value was silently lost, so there is nothing to keep. Writes to every other field and the list/get response shape of the previously described fields are unchanged.
FIX-0805-19: the active field of a site page is read-only now — publishing goes through its own call
Before
GET /v1/pages/:id/fields described active as an ordinary writable field, and a request carrying it went through: POST /v1/pages and PATCH /v1/pages/:id answered with a success. The value was dropped. Bitrix24 accepts ACTIVE neither in landing.landing.add nor in landing.landing.update — their contract does not declare the field, and a new page is always created inactive. The client got a "done" and an unpublished page, and found the discrepancy when looking at the site.
After
active is marked readonly. Passing it in a create or update body is refused with 400 READONLY_FIELD. The field stays in the list/get response and in the /fields directory — reading it is unchanged.
Publishing and unpublishing run through their own calls, which do work: POST /v1/pages/:id/publication and POST /v1/pages/:id/unpublish.
Impact on integrators
If your code passed active in a page create or update body, drop it and call publication separately. The value was never stored anyway, but now the whole request is refused, so the rest of the body is not applied either — title, code, description. There is no parallel support for the previous behaviour: the previous behaviour was that the value was silently lost, so there is nothing to keep.
NEW-0805-20: creating a server with inline code joined the shared queue for heavy requests
A server-creation request may carry a code archive in the body itself — the source.content field. Such a request is expensive in memory, and until now it was the only heavy one running unqueued: POST /:id/deploy and POST /:id/upload already bounded their concurrency, creation did not.
Before
Concurrent server creations carrying an inline archive were unbounded. The response was always on the merits — either success or a field-validation error.
After
Such a creation now shares the counter with code uploads. Once the cap is taken, the call returns 429 with the DEPLOY_BACKEND_BUSY code and a Retry-After: 30 header. Requests without source (plain server creation) and requests carrying a link instead of an archive are unaffected.
To stay out of the queue entirely, create the server without source and upload the code in a separate request with a link — {source: {url: ...}}.
FIX-0805-21: repair restores the tunnel even when inbound SSH is unavailable
Before
POST /v1/infra/servers/:id/repair reported the serial_console step as successful, then failed
ssh_install with SSH install failed (exit 255) after roughly 10 seconds, leaving the server
DISCONNECTED — the documented recovery to CONNECTED did not happen, and deploy / exec on
that server stayed blocked. Separately, agent installation over the serial console never worked on
a server without a public IP.
After
The serial_console step no longer confirms success when opening the firewall failed. Agent
installation over the serial console is fixed and now works both for a server without a public IP
and as a fallback: when the inbound SSH attempt fails, repair installs the agent out-of-band over
the serial console (the agent only needs an outbound connection). Step names in
GET /repair-status are unchanged; if both paths fail, the
error field carries both reasons joined by ; serial fallback:. The fallback adds up to two
minutes to an already-failing call.
FIX-0805-22: placement binding: when Bitrix24 names the reason, so do we
Before
When Bitrix24 refused a placement binding with "application not found" or "access denied", POST /v1/placements/bind answered 502 BITRIX_UNAVAILABLE. The stated reason was visible only in the diagnostic fields, and the status code could not tell "the application is not on the account" from "no rights to install it".
After
Two refusals now carry their own code:
404 B24_EMBEDDING_APP_NOT_FOUND— Bitrix24 does not know the application id: the local application was deleted or reinstalled. Remedy: create the local application again and callPOST /v1/apps/:id/relink-oauthwith the newbitrixClientIdandbitrixClientSecret.403 B24_EMBEDDING_INSTALL_DENIED— an access denial that survived a confirmed-active subscription check: the user whose developer key makes the call may not install local applications and/or has no access to the application itself.
The second code is emitted only where the subscription state could be confirmed as active. Where it could not, the denial stays 502: an unknown cause is never dressed up as a specific one.
A third refusal — the developer key lacking the required scope — used to be reported on a self-hosted account as an administrator-rights requirement, even though granting an admin role changes nothing: a key's scope is fixed when it is issued. It now arrives as 403 BOX_WEBHOOK_NOT_DEVELOPER_KEY — the same code the dashboard sections already return — and before the subscription check.
Integrator impact
A client that branched on 502 for these causes now receives 4xx — switch the error handling to the codes. The new codes are listed in placements.bindPrerequisite.errorCodes on GET /v1/me, limited to the ones the account can actually receive.
Affected endpoints: POST /v1/placements/bind, GET /v1/me
FIX-0805-23: a personal key with no Bitrix24 webhook now says what it is missing
Before
A personal key (vibe_api_*) with no Bitrix24 webhook answered every entity call with
401 TOKEN_MISSING and the text "API key has no OAuth tokens configured. Key may need
re-authorization." Such a key has no OAuth at all — it reaches the Bitrix24 account through
a webhook — so the re-authorization advice pointed the wrong way. Meanwhile GET /v1/me
answered 200 and looked healthy, and the key list did not tell a working key from a dead one.
After
The personal-key text names the missing webhook and points at /v1/me for the reason. The
response code is unchanged (TOKEN_MISSING); an optional error.details now carries a
machine-readable reason — INT_TARIFF_REQUIRED when the Bitrix24 account has no paid plan,
VIBE_SCOPES_ONLY when the key requests no Bitrix24 scope at all, WEBHOOK_NOT_CONFIGURED
otherwise — plus paywallCode and upgradeUrl where an upgrade resolves it. details is
returned on /v1/{entity} and POST /v1/batch.
GET /v1/me for a personal key carries a b24Credentials block — ready, and when
ready: false also reason, paywallCode, upgradeUrl and a hint when the access state is
worth re-reading. The key list and single-key read (GET /v1/keys, GET /v1/keys/{id}) return
a b24Ready flag: true — the key carries credentials for account calls, false — it does
not, null — not applicable to this key (an authorization key or a management key). No secrets
were added to any response.
FIX-0805-24: the app subdomain answers machines with JSON instead of a page, and survives a brief tunnel drop
Before
While a server was waking up or its tunnel was reconnecting, every request to the app subdomain got the HTML wake page with status 503. A browser polled it and eventually reached the app, but a webhook or an integration got markup instead of a response: the body, method and path were dropped, and the response could not tell the caller whether the action had been applied. A Bitrix24 event that arrived in that window was lost entirely.
After
A caller that is not a browser (it carries Authorization, X-Api-Key, Accept: application/json, X-Requested-With, Sec-Fetch-Dest: empty, or it is a POST/PUT/PATCH/DELETE) gets the ordinary error envelope with a code and a Retry-After header: BH_SERVER_WAKING (503), BH_TUNNEL_CONNECTING (503), BH_TUNNEL_DISCONNECTED (502), BH_APP_STARTING (503), BH_SERVER_ERROR (500), BH_SERVER_NOT_FOUND (404), BH_WAKE_BLOCKED (402). For the first four the Retry-After header and the error.retryAfter field agree.
On top of that, a brief tunnel drop on an already-running server is now absorbed silently: the request is held for up to 15 seconds, and if the tunnel returns in time it is delivered to the app and the caller gets the real response. A cold start does not fit that window — there the caller still has to repeat the request.
The browser wake, startup and error pages are unchanged, including their polling. The page poll (?_bh_poll=) is untouched.
FIX-0805-25: the feedback quota is counted per key, not shared across all callers
Before
POST /v1/feedback answered 429 RATE_LIMITED even when your key had sent fewer than five reports a minute: the counter was shared by every caller at once, so someone else's traffic drained your quota. It showed up as a rare unexplained refusal on the very first call.
After
The counter is kept per authorization key: someone else's traffic no longer spends your quota.
Impact on integrators
No action required. Refusals caused by another caller's traffic go away on this method. Keep your threshold as it is: handling 429 RATE_LIMITED and retrying on the Retry-After header is still the only reliable way to learn your own limit.
FIX-0805-26: Open Channels configuration paging: the window is no longer shifted twice
Before
GET /v1/openline-configs and POST /v1/openline-configs/search applied limit and offset twice: Bitrix24 applied them first, then the wrapper cut the window out of the already-prepared page a second time. The client received an empty or shifted result with no error: limit=3&offset=3 came back empty, and with offset=2 the first record was the fourth rather than the third. The hasMore field was computed over that same trimmed page, so a full page always reported false and a page walk stopped after the first request.
Separately, a fractional limit below 1 (for example limit=0.5) floored to zero, and the underlying method reads a zero limit as "no limit". The response came back empty with hasMore: true, so a walk driven by that flag never finished.
After
Bitrix24 cuts the window and the wrapper no longer moves it. The request asks for one record beyond the requested limit, and hasMore is derived from whether that record arrived; at the limit=200 ceiling the flag is derived from the page coming back full, so the last full page may be followed by one empty response. A fractional limit below 1 falls back to the default of 50, the same way limit=0 and non-numeric values already did.
Impact on integrators
No code changes are required. A page walk over offset and hasMore now returns the complete result — records were previously lost silently. The meaning of total is unchanged: it is still the number of records in the current window rather than in the whole result, and a paging loop is bounded by hasMore.
BC-0805-27: aggregate over a large pipeline: per-stage counts without reading deals, refusal instead of truncation
Old format supported until: 05.02.2027
Both changes ship disabled and are switched on per account by a platform administrator.
Before
POST /v1/{entity}/aggregate that needs rows to answer (numeric operations and/or
groupBy) still fetched the first 5000 records when total > 5000 and flagged the answer
meta.truncated: true. On a large pipeline that fetch did not finish — the client waited
twenty seconds and got a dropped connection instead of an answer.
After
With the refusal mode on, such a request answers 422 AGGREGATION_LIMIT_EXCEEDED
immediately and fetches no rows at all. The error text says what to do: narrow the filter,
ask for the count only, or (for deals) ask for the count grouped by stage, which is
answered without reading rows.
With stage grouping on, POST /v1/deals/aggregate with groupBy: ["stageId"] or
["stageSemanticId"] and a scalar categoryId in the filter answers on a pipeline of any
size: each stage count comes from a separate cheap count on the account side. The
response carries meta.recordsProcessed: 0, meta.truncated: false,
meta.aggregatePath: "fanout" and meta.stageCountDelta — the difference between the
overall total and the sum of the stage counts (0 when the split is complete). Numeric
operations per stage stay available while the combined group size fits in 5000.
Unchanged: a count-only request without grouping (aggregate: [{"function": "count", "field": "*"}]) answers as before — one count, at any volume; selections up to 5000
records behave exactly as they did.
2026-08-04
BC-0804-1: V1 /wake and /start wake galaxy apps via the host
Old format supported until: 22.08.2026
Before
POST /v1/infra/servers/:id/wake and /start for kind=GALAXY_APP returned 422 VM_MISSING (no cloud VM) and suggested redeploy even when the container was only idle-sleeping. Scheduled jobs could not recover via API after the first sleep.
After
For a placed galaxy app both verbs use host-mediated wake (same as the dashboard): wake the shared host if needed, then start the container. Success is HTTP 200. Galaxy ?wait=true does not wait until RUNNING with WAKE_TIMEOUT — after a cold wake the app may still be SLEEPING; poll GET. Host preventWake blocks both /wake and /start (403 SERVER_WAKE_BLOCKED or the 402 paywall code INT_TARIFF_REQUIRED — not freeze markers like TRIAL_EXPIRED). A slot without host/galaxyId → 404 GALAXY_HOST_NOT_FOUND (not 422 VM_MISSING). STANDALONE VM_MISSING / /start override behaviour is unchanged.
BC-0804-2: the declared archive type is now checked against the first bytes
Old format supported until: 03.08.2026
Before
Saving a source version — POST /v1/infra/servers/:id/sources and POST /v1/apps/:id/sources — trusted the Content-Type header. A zip archive sent with Content-Type: application/gzip was accepted, stored as gzip and named with a .tar.gz extension; deploying that version to a server then failed during extraction with an opaque archive-read error. Auto-saving a version after a deploy always recorded the format as gzip, whatever the request body actually contained.
After
The first bytes of the archive are read on intake. A direct contradiction between the declared type and the content — application/gzip declared while the bytes are zip, or the other way round — is rejected with 415 UNSUPPORTED_ARCHIVE_FORMAT; the response body carries error.hint.declared and error.hint.detected. The types application/x-tar and application/octet-stream never trigger this refusal: their signature is not readable within the first eight bytes, so no contradiction can be established. Unrecognised content is accepted as before.
Auto-save after a deploy never refuses: the client declares no archive type there, so the recognised bytes are simply recorded honestly — a zip is stored as a zip and goes to the right extractor on the next deploy.
What integrators should do
Send the Content-Type that matches the archive (application/gzip for tar.gz, application/zip for zip), or application/octet-stream when the type is unknown. Clients already sending the correct header need no change.
NEW-0804-3: field names and descriptions for product sections, requisite presets and bank details
What is new
GET /v1/product-sections/fields, GET /v1/requisite-presets/fields and GET /v1/bank-details/fields used to return only type and readonly — not a single field had a human-readable name, so the reference could not tell you what, say, rqAccNum means. Names are now declared for every field: 7 for product sections, 11 for requisite presets, 34 for bank details. For requisite presets and bank details a description arrives alongside the name.
The response gains label and description keys next to the existing type and readonly — no previously returned field changed.
The dynamic Bitrix24 reference could not supply these names: it only fills in fields absent from the static schema, so for a declared field its own labels were discarded. The aggregate operation on bank details stays disabled.
NEW-0804-4: `limit=0` is no longer ignored silently
What is new
limit=0 is not a page size: the parameter is dropped and the default page size applies. That used to happen without any signal — the response came back 200 with a full page of records, as if the parameter had been honoured. Such a response now carries a warning in meta.warnings:
{
"meta": {
"warnings": [
{
"code": "LIMIT_ZERO_IGNORED",
"field": "limit",
"message": "limit=0 is not a page size and was ignored. Valid range: 1..5000; pass an explicit limit (e.g. 5000) to read the whole collection."
}
]
}
}
It applies to GET /v1/{entity} and POST /v1/{entity}/search for every entity. The applied value itself has not changed — existing calls keep returning the same number of records as before; to read a whole collection pass an explicit limit (5000 maximum).
FIX-0804-5: bank details: `entityTypeId` marked as a field that is never returned
Before
GET /v1/bank-details/fields described entityTypeId as an ordinary numeric field — readable and writable. The fact that Bitrix24 accepts the value on create but never returns it on read was stated only in the field description text. A client that generates its model from the machine-readable reference rather than from the prose put the field in its read type and found nothing where a number was expected.
After
The field now carries notReturned: true — in GET /v1/bank-details/fields, in GET /v1/guide and in the OpenAPI schema (there as an x-notReturned annotation plus a sentence in the description). This release introduced the same flag for serverName on telephony lines.
The field stays writable: POST /v1/bank-details still accepts entityTypeId (always 8 — the owner requisite). The flag speaks only about reading.
Impact on integrators
Nothing to change — the flag is additive. If you generate types from the reference, entityTypeId can be dropped from the read model and kept in the create model.
FIX-0804-6: discover names the real path identifier
Before
GET /v1/guide and the OpenAPI schema advertised the item path as /:id for every entity. For four that was untrue: smart-processes is addressed by the public entityTypeId (1030 and up), telephony-lines by the line number, and bizproc-robots / bizproc-activities by their code — those two have no id of their own at all. A client that read {id} sent the record's own id field and got 404 SMART_PROCESS_NOT_FOUND naming that same value as an entityTypeId — while the published documentation already said :entityTypeId and :code.
After
GET /v1/guide shows /v1/smart-processes/:entityTypeId, /v1/telephony-lines/:number, /v1/bizproc-robots/:code and /v1/bizproc-activities/:code; every other entity keeps /:id. In OpenAPI the parameter name stays id: a path parameter's name has to match the placeholder in the path template, and the address itself did not change — instead the parameter description now names the real identifier. Where an entity declares no id field of its own (the telephony lines and both bizproc registries) the description says exactly that, rather than sending the caller to compare against a field that does not exist. The description of the id field on smart-processes was sharpened too.
Impact on integrators
Nothing to change: routes and response codes are unchanged, only descriptions are. If you were putting the record's internal id into the smart-processes path, the 404 now has an explanation — use entityTypeId; for business-process robots and activities, use code.
FIX-0804-7: telephony lines: `name` declared nullable, `serverName` marked as never returned
Before
GET /v1/telephony-lines/fields declared name as a plain string, although a line created without a name comes back as null — a model generated from the reference broke on the first such value. The serverName field looked like an ordinary readable field even though Bitrix24 neither stores nor ever returns it.
After
name now carries nullable: true — in GET /v1/telephony-lines/fields, in GET /v1/guide and in the OpenAPI schema (there as the type: ["string","null"] form). serverName now carries notReturned: true in the same places, plus an x-notReturned annotation and a sentence in its OpenAPI description. The field deliberately stays in the reference: a write to it is still rejected with 400 READONLY_FIELD, and a client must be able to look the field up and read why.
Impact on integrators
Nothing to change — both flags are additive. If you generate types from the reference, name becomes string | null and serverName can be dropped from the read model.
FIX-0804-8: workgroups: `limit > 50` and row-exact offset now work
Before
GET /v1/workgroups?limit=500 returned the first 50 records no matter how many groups were available, and meta.hasMore did not help read the rest. The cause: this entity's list method is not a .list one but sonet_group.get, and the "this is a list" flag was not declared for it, so neither limit nor auto-pagination ever reached Bitrix24. The offset was floored to a page boundary at the same time: offset=30 returned records starting from the first one, not the thirty-first.
After
limit reaches Bitrix24, and for limit > 50 the platform reads as many pages as needed — the way it has long worked for users, departments and storages. The offset is row-exact: offset=30 starts the window at the 31st record. The same behaviour applies to POST /v1/workgroups/search and to a list sub-call of POST /v1/batch.
Impact on integrators
If you paged workgroups by hand and compensated for the floored offset on your side (for example by dropping the leading records of a page), remove that compensation — otherwise records will be skipped twice. For deep paging a cursor is more reliable: filter {">id": lastId} with sorting by id.
FIX-0804-9: deleting an application now completes its removal from the account
Before
DELETE /v1/apps/:id removed the application from the Bitrix24 account in a single attempt. If the account was unreachable, the rights had been revoked, or the application author had no developer key, the attempt was silently lost: the application was deleted on our side but stayed installed on the account, with its menu entry still in place. Widgets were unbound only for applications published in the catalog, and only on cloud accounts.
After
The outcome of the attempt is stored, and an unfinished removal is retried in the background with a growing delay until the account confirms it. Widgets are unbound for any application, whether or not it was published in the catalog.
Impact on integrators
The endpoint response is unchanged — still 204 right after the deletion on our side.
What changed is the result: the application entry on the account now disappears in the
cases where it used to stay forever.
NEW-0804-10: Gateway timeout on exec now carries a recovery hint
A GATEWAY_TIMEOUT failure of POST /v1/infra/servers/:id/exec now carries a hint object with reason, recovery and recoveryAction fields — the same way EXEC_TIMEOUT and an agent-side EXEC_BUSY already do. The hint states the essential part: no exit status came back, so the outcome of the command is unknown and it may still be running on the server. Re-running it blindly can start a second copy alongside the first, so establish the real state first — read the logs or issue a short read-only command. For work that legitimately outlives the time limit the hint points at a detached background job.
The code and message fields are unchanged — the hint is additive and existing calls keep working. It arrives in both response modes: in the JSON envelope and as an SSE error event.
FIX-0804-11: model-unavailable refusal is now 429 with a wait hint, not 502
Before
When access to the models was temporarily closed after a run of failing calls, POST /v1/chat/completions and POST /v1/embeddings answered 502 with code AI_PROVIDER_UNAVAILABLE, and error.message carried an internal service string instead of an explanation. There was no Retry-After header, so a client had nothing to wait on — the typical library reaction to a 5xx is an immediate retry, which prolonged the outage.
After
The same refusal arrives as 429, error.type: "rate_limit_exceeded", error.code: "ai_provider_cooldown" (the streaming frame uses the same name in upper case, as the rest of this code family does), with a Retry-After header in seconds; in a streaming response the same value arrives as a retryAfter field inside the terminal error frame. The value is the remainder of the wait window, never below one second. error.message no longer contains internal strings or addresses. The refusal is temporary: wait out Retry-After and repeat the same request.
FIX-0804-12: a multipart archive deploy now keeps the source version even when the deploy fails
Before
POST /v1/infra/servers/:id/deploy with a multipart/form-data body saved the archive to source storage only after a successful deploy. When the deploy failed, no version was created at all — nothing to inspect, and a retry meant sending the same bytes again.
After
The archive is placed into source storage before the deploy starts, and the deploy proceeds from a link to that version. The version stays in history either way: deployStatus: "success" on success, "failed" on failure. The data.source block is unchanged — autoSaved, savedVersionId, sha256 and newVersion are filled in as before, and re-sending identical bytes still deduplicates instead of minting a new version.
A new refusal code SOURCE_DEPOT_UNAVAILABLE (502) was added: source storage did not accept the archive, the deploy never started, and the same request can be retried as is. Previously such a failure surfaced as VALIDATION_ERROR (400), i.e. it looked like a bad request.
Impact on integrators
No action required. The version list (GET /v1/infra/servers/:id/sources) of clients that deploy via multipart may now contain versions from failed deploys — those carry deployStatus: "failed".
NEW-0804-13: address fields come with human-readable labels and descriptions
Previously GET /v1/addresses/fields returned the Bitrix24 field name in title for six of the fourteen fields — TYPE_ID, ENTITY_TYPE_ID, ENTITY_ID, COUNTRY_CODE, ANCHOR_TYPE_ID, ANCHOR_ID. Such a label cannot be shown to a user, and the meaning of the codes had to be looked up in the documentation.
Now those six fields carry a label in the account language under title, and the fields that have something to add to the label gained a description key with the purpose of the field and the meaning of its codes: address types (all twelve, 1 through 12 — which of them are available depends on the account country zone) and owner types (1 — lead, 3 — contact, 4 — company, 8 — requisite). The countryCode description no longer promises a format: Bitrix24's own REST documentation marks that field as unused and kept for backward compatibility. Code 1 additionally carries the label the English Bitrix24 interface uses for it — Street address: Bitrix24 itself names that type differently in its Russian and English versions, and without the note the dictionary would not match the label the user sees in the interface.
Labels that Bitrix24 supplies itself are unchanged. Alongside title, the same label now also arrives under label — the key every other entity uses — so labels can be read one way on any entity. The description and label keys are added to the field description and the previous keys stay in place, so no action is needed.
The address type codes were also corrected in the documentation for creating, reading, updating and deleting an address: the values listed there were wrong, and code 13 does not exist at all — the set of types ends at 12, and which of them are available depends on the account country zone.
NEW-0804-14: product fields come with labels, descriptions and a description-format dictionary
GET /v1/products/fields described all twenty-one product fields with nothing but a type and a read-only flag — no label, no description. Such a response gave no way to tell that measure is a measurement unit and vatId is a VAT rate.
Now every one of the twenty-one fields carries a label in the account language, and the fields that have something to add to the label also carry a description: where to get the list of allowed values (GET /v1/currencies, GET /v1/product-sections, GET /v1/catalogs, GET /v1/users), how sorting works and what sets the description format. The descriptionType field gained an enum dictionary with the values text and html.
The keys are added to the field description and the existing type and readonly are unchanged, so no action is needed. Catalog properties PROPERTY_<N> still arrive with the label from the account settings.
NEW-0804-15: all 45 quote fields come with a label and a description
GET /v1/quotes/fields returned a label for thirty of the forty-five fields. The remaining fifteen were exactly the base ones — id, title, dealId, contactId, companyId, amount, currency, assignedById, createdBy, comments, isManualOpportunity, beginDate, closeDate, createdTime, updatedTime — described by nothing but a type and a read-only flag. Not a single field carried a description.
Now every one of the forty-five fields has a label, and each gained a description: what the field is for, where to get the list of allowed values (GET /v1/currencies, GET /v1/users, GET /v1/deals and others), write-time behaviour. The naming mismatches that are easy to get wrong are stated explicitly: the amount is named opportunity in Bitrix24, the currency is currencyId, and the start and close dates are begindate and closedate, all lowercase.
The stageId field deliberately has no value dictionary: the set of stages is configured in the account, so its description points at the GET /v1/statuses?filter[entityId]=QUOTE_STATUS directory — a static dictionary would go stale.
The keys are added to the field description and the existing type and readonly are unchanged, so no action is needed.
NEW-0804-16: catalog product fields come with labels and descriptions
GET /v1/catalog-products/fields described all forty-two catalog product fields with nothing but a type and service flags — no label, no description. Such a response gave no way to tell how purchasingCurrency differs from the price currency, or quantityTrace from canBuyZero.
Now every one of the forty-two fields carries a label in the account language, and thirty-eight of them also carry a description. The descriptions state what previously had to be found out by trial: iblockId is set on create only and does not allow moving a product between catalogs; iblockSection is accepted on write only, while on read the primary section arrives as the scalar iblockSectionId; available and bundle are computed by Bitrix24; recurSchemeLength, recurSchemeType and trialPriceId work only in on-premise Bitrix24 for content sales. Where a value comes from a directory, the endpoint is named — GET /v1/catalogs, GET /v1/catalog-sections, GET /v1/currencies, GET /v1/users.
For previewTextType and detailTextType the value set arrives as a machine-readable enum dictionary (text and html) — the same shape the product description format uses, instead of prose inside the description.
The keys are added to the field description and the existing type, readonly, createOnly and nullable are unchanged, so no action is needed.
FIX-0804-17: requisite preset field schema declares inShortList as boolean
Before
GET /v1/requisite-presets/:presetId/fields/schema described the inShortList field with the char type, while reading rows of the same preset returns true/false and writing accepts true/false. The schema contradicted the data it describes, and a client relying on the declared type prepared to parse a single-character string.
After
In the same response inShortList.type arrives as boolean. The other keys of the field description (isRequired, isReadOnly, title and the rest) are unchanged, and so are the types of the other fields.
Impact on integrators
No action needed: the data was already boolean. A check comparing inShortList.type against the string char will stop matching — compare against boolean instead.
NEW-0804-18: downloading call recordings and timeline attachments
Two endpoints were added for CRM files that Disk file download could not reach.
GET /v1/activities/:activityId/files/:fileId/download returns an activity file, including a call recording. Doing this through the API was previously impossible: an activity file is not a Disk object, its identifier lives in a separate space that overlaps Disk identifiers, and the link in the activity response arrives with an empty authorization parameter — requesting it returns the sign-in page with code 200. The endpoint adds the authorization itself, verifies that the file really belongs to the named activity, and returns a byte stream.
GET /v1/timelines/:commentId/files/:fileRef/download returns a timeline comment attachment. fileRef accepts either identifier: the attachment ID the account interface shows, and the Disk object ID — the object key in the files field of the comment response. The first computes access through the comment itself, so it reaches attachments that Disk file download refuses to serve; the second goes through personal Disk permissions. The comment itself is read first, then the Disk object ID, and the attachment ID only when the comment does not list that reference; you do not have to state which one you pass.
Both endpoints require the crm scope and never return the download address: it contains an authorization code, so only the content leaves. A file whose membership in the named activity or comment is not confirmed gets 404 — including an attachment that hangs on a different kind of record, a task with the same number for instance — without that check the endpoint would allow enumerating the account's files, because Bitrix24 itself answers such a request with a page under code 200 rather than a refusal.
The address check extends to redirects: the endpoint walks them itself, checking every hop, bounds the chain in length and in time, and does not follow a redirect to an internal address — such an answer becomes 502. The same applies to Disk file download, which uses the same wrapper.
A 404 on the attachment download means the reference itself is wrong. A temporary cause — a Bitrix24 request limit, an unavailable account, the repeated-error guard tripping — comes back as itself: 429 or 502/503 with a Retry-After header. The practical difference: retrying a 404 is pointless, whereas a 429/5xx should be retried with a delay. The Retry-After value is computed for the Bitrix24 call that actually hit the limit.
The contract of Disk file download is unchanged: same parameters, same responses. Through the shared wrapper it inherited only the address and redirect check described above.
FIX-0804-19: a bot message without text is refused with a clear error instead of a false success
Before
POST /v1/bots/:botId/messages with the text under an unrecognized key — for example {"dialogId": "…", "text": "hi"} — reached Bitrix24 with no content, and Bitrix24 answered 422 with the code EMPTY_MESSAGE and the text "Message can't be empty". That response gave no way to tell the field name was the problem: the text had been passed.
Update behaved worse. PATCH /v1/bots/:botId/messages/:messageId answered 200 {"result": true} in the same situation while the message text stayed unchanged — a false success after which an integrator considered the edit applied.
After
Both requests check for content before calling Bitrix24 and answer 400 with the code MESSAGE_REQUIRED when there is none. The error text lists the unrecognized body keys and states that the message text belongs in the message field. Neither an empty attach array nor an empty message string counts as content; an attach block without text does, and so does a number (0 is the text "0").
The fields wrapper is how a body is passed through in Bitrix24's own shape, and "a wrapper was supplied" is now understood the same way at every processing step. A value that is not a wrapper (false, 0, an empty array) is not treated as one: {"dialogId": "…", "fields": false} gets the same 400 with the code MESSAGE_REQUIRED, and {"message": "hi", "fields": []} sends the text instead of losing it.
This is the same code and the same wording as the sibling POST /v1/chats/:dialogId/messages: one contract for the same mistake across two related endpoints.
Impact on integrators
A request with the text in the message field works as before. A request that used to get 422 EMPTY_MESSAGE now gets 400 MESSAGE_REQUIRED telling it what to fix. The text field does not become a synonym for message: on read the message content really is called text, but silently accepting both names would split the contract with the chats endpoint.
2026-08-03
FIX-0803-1: reopening an archived ticket now clears the resolution stamp too
Before
Reopening a ticket from ARCHIVED into an active status (NEW/REVIEWING/AWAITING_USER/NEEDS_REVIEW) — via PATCH /v1/feedback/:id or POST /v1/feedback/:id/comments — did not reset resolvedAt/resolvedBy when the ticket had been resolved and then archived. On reads (GET /v1/feedback/:id) such a ticket looked both active and resolved. The clear only fired for RESOLVED/WITHDRAWN sources.
After
ARCHIVED joins RESOLVED/WITHDRAWN: reopening out of any closed status into an active one clears resolvedAt/resolvedBy. On the PATCH path resolution is cleared too (including the archive reason) — an active ticket carries no resolution; an explicit resolution in the same request still wins. On the comment path resolution equals the comment body. Moving into ARCHIVED still preserves the stamp (archiving keeps resolution history).
FIX-0803-2: missing attachment bytes now answer 404 instead of a truncated response
GET /v1/feedback/{id}/attachments/{attId}/file and .../thumb now confirm the bytes exist before any header is sent. When the attachment record is present but its bytes are not in storage (after manual cleanup or a cascading delete), the answer is a plain 404 NOT_FOUND.
Before
The response opened as 200 and then broke off mid-body: the client received a truncated image or an empty stream under an already-sent success status, indistinguishable from a slow network.
After
404 { "success": false, "error": { "code": "NOT_FOUND", "message": "Not found" } } — the same code these routes already return for someone else's or a deleted attachment. Clients that already handle 404 here need no changes.
The change accompanies moving attachment files into object storage: serving an attachment no longer depends on which machine accepted the upload. Response shapes and route paths are unchanged.
FIX-0803-3: server creation now says plainly when it returned the application's existing server
When the calling key belongs to an application whose server slot is already filled, POST /v1/infra/servers returns that server instead of creating a new one. That was already the behaviour, but the response gave you almost nothing to notice it by: the only signal was an undocumented reused field, and the name in the response belonged to the existing server rather than the one you asked for.
Such a response now carries the full disclosure: data.reusedReason with the value APPLICATION_ALREADY_HAS_SERVER, data.requestedName echoing the name you sent (always, even when it equals the existing name), and a warnings array next to data with at least one entry naming the existing server and stating that deploying replaces the code currently running on it. The reused and deploying fields are now declared in the schema and in the documentation.
The second silent loss is disclosed the same way: a reuse does not apply the displayName and description you sent — the server keeps its own name and description. The response now says so through data.metaIgnored and a dedicated warning; rename the server deliberately with PATCH /v1/infra/servers/:id.
When the request carried a source that cannot be built onto the reused server (the galaxy application already has a live container, or the server is a dedicated virtual machine), the archive is discarded — and the response now says so through data.sourceIgnored and a dedicated warning. It used to be discarded silently.
On a reuse response data.next now arrives only when there is nothing to overwrite. It used to arrive on every two-step reuse, including one that returned a server already running someone's code — so the machine-readable "next step: deploy" contradicted the hint in the same body. When the server may be running code the field is deliberately absent: confirm the server is the right one first. A missing next is not an error.
Two existing fields changed meaning, though no client code has to change: on a reuse response data.hint was rewritten wholesale — instead of "deploy here" it now opens with REUSED — no new server was created and explains the risk; and the schema description of data.next was corrected, having previously named an empty galaxy slot as the only reason the field appears when it also arrives on a reuse response. No existing field or code changed and no client action is required. Do read reused before calling POST /v1/infra/servers/:id/deploy: a deploy replaces whatever is already running on that server.
NEW-0803-4: dictionary of catalog list-property values
A new entity catalog-product-property-enums exposes all possible options of a trade-catalog list property: GET /v1/catalog-product-property-enums, GET /v1/catalog-product-property-enums/:id, POST /v1/catalog-product-property-enums/search, and GET /v1/catalog-product-property-enums/fields. The entity is read-only: write operations and aggregation are not registered and answer 404, and data.batch comes back as an empty array.
Previously a product's list-property value was available only as an option identifier: the /v1/products family returns an object with a numeric value in PROPERTY_<N>, and GET /v1/products/fields describes the property by name alone — there was nothing to expand the identifier into text with. The option list is now requested directly: GET /v1/catalog-product-property-enums?filter[propertyId]=166&limit=1000 returns identifier-and-text pairs from which a String(id) → value map is built.
The filter[propertyId] filter is required: the dictionary is read one property at a time, and a request without it is rejected with 400 MISSING_REQUIRED_FILTER before Bitrix24 is called — on the list and search endpoints. The check looks at the presence of the key and does not extend to batch sub-calls. Only properties with propertyType: "L" have an enumeration — for a property of any other type the response is an empty list, not an error. Pagination is ordinary: limit + offset, with the end of the selection signalled by meta.hasMore. The ceiling is 5000 records per call. The catalog scope is required.
Existing pages were clarified in the same change. GET /v1/catalog-products/:id now shows a captured response carrying a list property and explains the value / valueEnum / valueId triple, plus the shape a listType: "C" property returns — a bare "Y"/"N" scalar, that is the checkbox state rather than an option id. GET /v1/products/:id explains that an empty property value means either an unfilled property or one served by the catalog family alone, and how to tell the two apart with a single cross-call. GET /v1/products/fields states outright that a PROPERTY_<N> descriptor carries the name only, and where to go for the values. The error reference now lists the real set of entities that require a filter.
FIX-0803-5: Support tickets work while the account is balance-frozen
Before
On a balance-frozen account every /v1/feedback route returned 402 ACCOUNT_FROZEN — you could not report a problem from within the product exactly when it mattered most.
After
The whole conversation stays reachable by key: POST /v1/feedback (create), GET /v1/feedback (list), GET /v1/feedback/{id} (open a ticket) and POST /v1/feedback/{id}/comments (reply to the team). All four only read and write your own data. Attachment uploads (POST /v1/feedback/attachments), attachment downloads and PATCH /v1/feedback/{id} remain behind the freeze gate, as does every other /v1 endpoint.
FIX-0803-6: include now returns the full related record, not just link metadata
Before
With ?include=<relation> the nested object carried only the link metadata — without the related record's id or fields.
After
include returns the full related record (id + fields), as the contract describes — a separate follow-up request for the related entity is no longer needed.
FIX-0803-7: rolling-out Open Channels dashboard methods return METHOD_NOT_YET_AVAILABLE
Before
On a portal where the update had not yet arrived, the Open Channels dashboard methods returned a raw 422 BITRIX_ERROR — indistinguishable from a real integration error.
After
That response is recognised and returned as 422 METHOD_NOT_YET_AVAILABLE with the release version — a clear signal that the method is not yet available on this portal, not an integration failure. The answer stays the same under regular polling: such calls no longer count toward error-loop protection, so a clear 422 is not replaced by 429 ERROR_LOOP_DETECTED (for methods that are not rolling out, the protection works as before).
FIX-0803-8: source-storage writes return precise error codes instead of a generic 500
Before
Client-side source-storage write failures were masked behind a generic 500 SOURCE_STORAGE_ERROR, giving no actionable signal.
After
The cause is now distinguishable: insufficient balance returns 402 BILLING_INSUFFICIENT; a transient storage access-key issuance failure returns 503 STORAGE_STS_UNAVAILABLE (safe to retry). The source-storage error table lists both.
FIX-0803-9: a value list in a statuses filter is rejected with a clean 400, not a 500
Before
GET /v1/statuses with a value list in the filter ({field: {$in: [...]}} or an array) was forwarded to Bitrix24, and the dictionary method answered differently per field: on id and name an internal error that reached the client as 502 BITRIX_UNAVAILABLE, on entityId, statusId, semantics and sort a "value must be a string" error, and on categoryId a success response carrying another pipeline's records.
After
A value list is rejected before the Bitrix24 call with 400 UNSUPPORTED_FILTER on every filter field — the dictionary method supports it on none of them. A single exact value is accepted ({field: value}); request several values in separate calls or via POST /v1/batch.
FIX-0803-10: /v1/tasks/:taskId/time accepts a key with the task scope
Before
The task time-tracking endpoint returned 403 INSUFFICIENT_SCOPE to a key holding the task scope — only tasks worked, even though the two are aliases of one permission.
After
task and tasks are treated as aliases (as on every other task endpoint) — a key holding either one is accepted.
NEW-0803-11: the one-shot galaxy app create now accepts `healthPath`
The body of POST /v1/infra/servers with a source field now accepts the optional healthPath — the path used to check the app's readiness inside its container. Validation matches POST /v1/infra/servers/{id}/deploy: a string of up to 500 characters starting with /. The default is /.
Previously healthPath was declared only in the deploy body, so the one-shot call the platform itself recommends in GET /v1/me answered 400 UNKNOWN_PARAM — while that same description called healthPath honored on the galaxy path. The field is now accepted exactly where the recommendation promises it; on a standalone create it is ignored.
Impact on integrators
Nothing to change — the field is optional. If you previously had to split the call into two steps just for healthPath, one call is now enough.
FIX-0803-12: `/start` and `/wake` on a galaxy app now explain why they do not apply
Before
POST /v1/infra/servers/{id}/start and POST /v1/infra/servers/{id}/wake checked the status before the server kind, so a galaxy app outside the allowed statuses (say RUNNING, ERROR or STOPPED) got the standalone-server text: "Server is RUNNING; /start requires one of SLEEPING, ERROR, PROVISIONING". Technically true and useless: those statuses would not have helped either, and nothing said that a container app has no cloud VM at all.
After
The kind check now comes first, mirroring /reboot. For a galaxy app in such a status message names the reason (this is a galaxy app, it has no cloud VM) and userMessage names the operations that do work: POST /v1/infra/servers/{id}/deploy (valid in any of these statuses) and POST /v1/infra/servers/{id}/reboot (only for a running or errored app). The error code is unchanged — still SERVER_WRONG_STATE (422) with currentState and availableActions.
Impact on integrators
Nothing to change: the HTTP status and code are the same, only the text changed. The response for statuses INSIDE the allowed list is byte-identical — in particular /start and /wake on a sleeping galaxy app still answer the documented VM_MISSING.
FIX-0803-13: a galaxy app failure now names its actual cause in `provisionError`
Before
When an app built, started and then died, provisionError carried only the generic verdict: "the app did not stay running, check the logs" or, when the out-of-memory flag fired, "likely OOM at the galaxy memory limit". The real cause — say TypeError: webidl.util.markAsUncloneable is not a function from an incompatible runtime version — sat in buildLog, while the server list surfaces provisionError only. So the short text could not tell memory apart from code, and the "move to a standalone server" advice pointed the wrong way.
After
The same wording now gains a line from the container logs: … Actual cause from the container logs: <line>. The line is picked from the tail the platform captures on failure: first a typed exception or error code (TypeError: …, EADDRINUSE, FATAL ERROR: … heap out of memory), then the known build causes, then the last error-ish line. The tail goes through the same scrubbing as buildLog — internal host paths are replaced with <build-context>. When the tail holds nothing useful the text stays exactly as before. The memory wording is preserved and gains the cause: the oom flag sometimes fires where memory was not involved, and then the quoted line is the only truth the reader gets.
Impact on integrators
Nothing to change. The previous substrings are preserved, so a client matching on them keeps working; only the appended tail is new. The full log is still available in buildLog (GET /v1/infra/servers/{id}).
FIX-0803-14: `/reboot` on a sleeping galaxy app now kicks the host repair and says so
Before
A sleeping galaxy app whose host had lost its tunnel had no way back. The documented way to wake an app is a deploy, and a deploy against an unreachable host fails. The host tunnel repair was already kicked from the app reboot, but the kick sat BEHIND the status check that rejects a sleeping app — so it was never reached.
After
Before the same 422 SERVER_WRONG_STATE refusal the platform kicks a background host tunnel repair and, when a repair actually started, adds an optional hint object with reason, recovery (which call to retry) and retryAfterSeconds (a floor for the wait, not a promise). When no repair started — the kill switch is off, the tunnel is in fact alive, a repair is already running, or the machine is blocked from waking — hint is absent: claiming a repair that did not start would be a lie the client acts on. The same behaviour was added to the dashboard reboot.
Impact on integrators
Nothing to change: the code and HTTP status are the same and hint is additive. A client that reads hint only needs to retry POST /v1/infra/servers/{id}/deploy after the named delay.
FIX-0803-15: a zip source archive no longer fails at extraction on a galaxy app
Before
The platform detects the archive format from its leading bytes and advertises .zip as supported, but on the galaxy path (POST /v1/infra/servers with source, and POST /v1/infra/servers/{id}/deploy for a galaxy app) the archive was handed over for extraction without preparing the host. When the extractor was missing there, the deploy failed with text like exec: "unzip": executable file not found in $PATH — which said neither what to do, nor that the same archive as .tar.gz would have worked.
After
Before uploading a zip archive the platform installs the extractor on the host (the same step the standalone server path already ran). The step is idempotent: with the extractor already present it does nothing and costs no time on later deploys. If the install fails, the archive is not uploaded at all and the deploy ends with an honest reason plus the suggestion to re-send the same source as .tar.gz; the text is available in buildLog and in provisionError.
Impact on integrators
Nothing to change. Deploys with .tar.gz take the previous path unchanged.
NEW-0803-16: Source versions accept up to 500 MB, and a deploy from our own link now links to the version on any key
Before
An archive could be stored as a version only up to 200 MB, while the same archive was allowed inline in a deploy body up to 500 MB. A large project had exactly one way to ship — entirely inside the request body.
Separately: a {"source": {"url": "…"}} deploy using a link obtained from GET /v1/infra/servers/:id/sources/:versionId/download was not linked to the version when the server belongs to a personal vibe_api_* key. The response carried data.source.autoSaved: false with skippedReason: "external-url-or-toggles-off", and the version kept linkedDeployId and deployStatus empty.
After
The source-version cap is 500 MB on both intake endpoints: POST /v1/infra/servers/:id/sources and POST /v1/apps/:id/sources. The body is still read as a stream, so archive size does not affect intake speed. A Content-Length above the cap is rejected with 413 before the body is read. The value is published as capabilities.apps.sourceStorage.limits.maxBlobBytes in GET /v1/me — now 524288000.
A deploy from our own link is linked to the version regardless of the owner key type: the response carries data.source.autoSaved: true and savedVersionId, and the version gets linkedDeployId and deployStatus filled in.
Affected endpoints: POST /v1/infra/servers/:id/sources, POST /v1/apps/:id/sources, POST /v1/infra/servers/:id/deploy, GET /v1/me
FIX-0803-17: revoking access kills every key of the connection, not just the latest
Before
When a user went through consent again for the same application, the platform issued a new key but the previous one kept working. Revoking access killed only the key from the latest authorization — earlier keys still reached the API, while the user believed access was closed.
After
Revoking access kills every key the user issued to the application for that Bitrix24 account, including keys from earlier authorizations. A key that used to survive revocation now answers 401 KEY_INACTIVE. Keys issued by other employees of the same account are not affected. On the partner side the usual 401 KEY_INACTIVE handling is enough — prompt the user to authorize again.
2026-08-02
FIX-0802-1: a batch sub-call with its own start=-1 no longer returns a fabricated total
A POST /v1/batch sub-call that carries its own params.start: -1 no longer receives a fabricated record count. -1 is Bitrix24's own instruction to skip counting the collection, so the portal's answer holds no count; the envelope used to take whatever was at hand as the size — the result_total echo from methods that answer 0 next to a full page in this mode, or simply the length of the first page. Both branches are affected: a sub-call with limit up to 50, and a sub-call with limit above 50, which is read by its own auto-pagination.
Before
{"entity":"activities","action":"list","params":{"start":-1}} → meta.<id>.total: 0 and data.totals.<id>: 0 next to 50 records, meta.<id>.hasMore: false.
{"entity":"deals","action":"list","params":{"limit":5000,"start":-1}} → 50 records, meta.<id>.total: 50, meta.<id>.hasMore: false — a request for 5000 records answered with a confident "there are only 50".
After
Such a sub-call carries no total key in meta.<id> and none in data.totals — no count was ordered, and there is nothing to build one from. hasMore is decided by page fullness: a page filled up to the requested limit → true, a short page → false. Fullness is measured against the ceiling that actually went to Bitrix24, so {"limit":10,"start":-1} answered with 10 records is a full page and now reports hasMore: true instead of false. The walk builds no continuation plan out of an invented size and returns a contiguous prefix.
The "no count ordered" signal is read off a normalised value: start is coerced to an integer the way Bitrix24 does it (truncation toward zero, a string read by its numeric prefix), so -1, "-1", -1.5, "-1abc" and "-1 x" are one and the same. A positive offset (start: 100) is an ordinary counted cursor and still returns its total.
A value that is not a number at all ("abc", an empty string, null, an object, an array, true) is no longer sent to Bitrix24 — it reads as if it had not been supplied. The page does not change ("start from zero" and "no start" are the same page), but such a sub-call falls under the platform's ordinary choice and may come back without a total.
Separately: a sub-call that failed no longer publishes data.totals.<id> next to its error.
Impact on integrators
A client that sent start: -1 and read total was given a knowingly wrong number: zero for the methods that echo result_total: 0, the page length for the rest. A read loop driven by meta.hasMore stopped on the first page. Check for the presence of the key (meta.<id>.total !== undefined) and drive continuation from meta.<id>.hasMore. If you need an exact count, do not send start: -1: that value is the client's own instruction to skip counting, so there is nothing to return for such a sub-call and withTotal: true will not bring it back.
BC-0802-2: self-hosted account access is no longer granted unconditionally
Old format supported until: 30.01.2027
Before
A self-hosted account was always treated as commercial: the flag was set when the account was connected, not by anything the platform had checked. Nothing about the account's own state could change its access.
After
A self-hosted account is now evaluated against the same access rules as any other account, instead of being granted access unconditionally. Where those rules are not met, POST /v1/infra/servers answers 402 with a denial code in the body instead of creating a server, and capabilities.servers.create in GET /v1/me comes back unavailable with the reason. The same applies when creating agents and bots that provision a server.
A paid self-hosted licence does not by itself grant access: the licence covers the installation, access is decided separately. An installation that is paid for but does not meet the access rules is denied.
While the account's state cannot be read, access is not restricted: a missing signal is not treated as a failed check.
The denial is switched on by a separate decision, not by the release: until then the responses are unchanged. Two fields change earlier — on the release itself:
wasEverCommercialin GET /v1/me stops being one-way for self-hosted accounts: where no qualifying history and no payments were found, the value changes once fromtruetofalse— the previous value was set at connection time, not by observation.placements.bindPrerequisitein the same response starts describing the model that actually applies to the account (a differenterrorCodesset and a differentnote) — an account with no resolved region used to be described as international.
What integrators should do
Check capabilities.servers.create in GET /v1/me before creating infrastructure, and handle 402 on creation — the denial code and message come in the response body. If the account's state has changed and the denial persists, force a refresh: GET /v1/me?refresh=tariff. Do not rely on wasEverCommercial being monotonic for self-hosted accounts.
2026-08-01
FIX-0801-1: AI endpoint response bodies no longer carry platform-internal fields
Before
Responses of POST /v1/chat/completions on Bitrix24 models returned a platform infrastructure identifier in system_fingerprint. Alongside the declared fields, other undocumented internal fields were returned as well — both in the response envelope itself and inside choices and choices[].message. Streaming responses and POST /v1/embeddings were not observed to carry those fields, but they had no check in place either.
After
system_fingerprint now carries the neutral value vibecode. When the upstream sends no fingerprint, the field is absent from the response, as before. Internal fields have been removed from the response envelope and from the choices objects; inside choices[].message the provider_specific_fields container has been removed. In an in-stream error event the error object keeps its message, type, param, code, retryAfter and retryable fields; internal fields next to them have been removed.
The declared contract is unchanged: id, object, created, model, choices, usage for chat and object, data, model, usage for embeddings arrive exactly as before — including provider extensions inside usage, reasoning fields inside choices[].message, and tool calls. The in-stream error event is still delivered and still carries error. No client changes are required.
The change affects the response body of Bitrix24 models only. In an in-stream error event error.message is now normalised to the public model name, and if the upstream put something other than a string in message the field is not returned at all. The text of ordinary platform errors (4xx, 5xx) is unchanged.
FIX-0801-2: galaxy app deploy no longer reports an interruption after an update that actually succeeded
Before
If the connection to the host dropped mid-way through POST /v1/infra/servers/:id/deploy, the platform re-checked the app state once and, when that check went unanswered, returned 502 GALAXY_DEPLOY_INTERRUPTED. In the common case the update was still in progress and completed successfully — seconds later the app answered health, served the new version and applied the new environment variables. Telling such a spurious failure apart from a real one required a manual check, and repeating the deploy redeployed the very same version.
After
After a drop the platform re-checks the app state for a bounded period instead of once, and answers success when the new version came up, exactly as if no drop had happened. 502 GALAXY_DEPLOY_INTERRUPTED is now returned only when no confirmation appears within that period. The wait is sized against the window the platform must answer within, so worst-case request duration does not grow.
The body of that error additionally carries error.retryable: true, so a client can tell it apart from a permanent failure without parsing prose. The existing fields (error.code, error.message, error.hint) are unchanged.
BC-0801-3: publication checks application authorization before snapshot freshness
Old format supported until: 30.01.2027
Before
POST /v1/apps/:id/publish checked source-snapshot freshness first and application authorization only afterwards. For a caller without authorization the response depended on an unrelated condition: a stale snapshot gave 409 SNAPSHOT_REQUIRED, a fresh one gave 400 NO_USER_TOKEN. The alternation read as "the token check passes sometimes", although authorization was absent in both cases.
After
Authorization presence is checked before the freshness gate. An application without authorization gets 400 NO_USER_TOKEN immediately, whatever the snapshot state. The sequence is now monotone: you clear authorization first, and only the snapshot requirement remains.
The change affects the cases that previously answered with a different code: no authorization AND the snapshot is stale or missing — previously 409 SNAPSHOT_REQUIRED, now 400 NO_USER_TOKEN; no authorization AND the resolved catalog title exceeds the limit — previously 400 TITLE_TOO_LONG_FOR_CATALOG, now 400 NO_USER_TOKEN (same status, different code). If authorization exists but the token could not be renewed, the response is still 400 and still arrives after the freshness gate. Publication on a self-hosted account through a developer key needs no application authorization and is not affected by this check.
What integrators should do
If your handler reacted only to 409 SNAPSHOT_REQUIRED and re-saved sources in a loop, add a branch for 400 NO_USER_TOKEN — there you need to authorize the application, not save the sources again. The error.hint field in that response describes the action.
NEW-0801-4: hints in publication responses: hint on NO_USER_TOKEN and presentedAt on SNAPSHOT_REQUIRED
The 400 NO_USER_TOKEN response of POST /v1/apps/:id/publish now carries an error.hint object with requiredAction (what exactly to do to authorize the application), docsUrl and oauthDocsUrl. The object shape matches the error.hint of 409 SNAPSHOT_REQUIRED on the same endpoint.
The 409 SNAPSHOT_REQUIRED response gained error.hint.lastSnapshot.presentedAt — when the version was last presented by a save. That is what ageMinutes is counted from, so the pair of fields shows why the version is considered stale. The neighbouring timestamp field still means the version creation time.
Both fields are additive: existing calls work unchanged.
FIX-0801-5: re-saving the same sources unblocks publication
Before
The freshness check before POST /v1/apps/:id/publish counted its window from the version creation time. Re-saving the same bytes returned HTTP 201 with deduplicated: true but created no new version and did not move that creation time, so publish kept answering 409 SNAPSHOT_REQUIRED. A caller whose sources had not changed entered a publish → 409 → POST /v1/apps/:id/sources → publish → 409 loop that never converged: the only way to refresh the snapshot was to change the archive contents.
After
A save marks the version as presented again, and the freshness window is counted from that mark. A deduplicated save unblocks publication on a par with a real one. The version creation time (data.timestamp) does not change, so the stored file name and the version's place in the retention policy stay the same.
Impact on integrators
Nothing to change. The recipe from the 409 hint — save the sources and retry — now works even when the sources have not changed.
BC-0801-6: a broken exec no longer answers with success and exitCode -1
Old format supported until: 01.02.2027
Before
When the POST /v1/infra/servers/:id/exec stream ended without sending an exit status, the response arrived as success: true with exitCode: -1. The command's outcome on the server is unknown in that case, so such a response was not a success. In streaming mode (?stream=true) the stream simply closed in silence.
After
On a standalone virtual machine (kind: "STANDALONE") that response now arrives as success: false with the code EXEC_NO_EXIT and a hint object pointing at a server-state check. data carries the output collected up to the break (stdout, stderr); the exitCode, duration and truncated fields are absent — their values are unknown, and filling them with zeroes would assert something the platform does not know. In streaming mode an error event with the same code arrives. On a galaxy app this case still arrives as before.
What integrators should do
Handle EXEC_NO_EXIT alongside the other error codes. If your code read data.exitCode without checking success, it will now get undefined instead of -1 — branch on success. The command can be repeated if it is idempotent; if it is not, inspect the server state first via GET /v1/infra/servers/:id/logs.
FIX-0801-7: the JSON response body of /exec and /deploy now starts with an opening brace
Before
In JSON mode (without ?stream=true), POST /v1/infra/servers/:id/exec and POST /v1/infra/servers/:id/deploy hold the connection by sending spaces every 15 seconds. Those spaces went before the JSON document, so for a command longer than 15 seconds the response body started with spaces. Clients that validate the format strictly refused to parse it — while the command itself had completed successfully on the server.
After
The keepalive spaces now go inside the already-opened JSON object, so the body starts with { from its very first byte. The set of response fields is unchanged.
Impact on integrators
Clients that parsed the response with an ordinary JSON parser will notice nothing — both body shapes are valid. Clients that stripped leading spaces by hand no longer need to.
FIX-0801-8: datetimes without a timezone no longer drift when the caller's zone differs from the webhook owner's
Before
A datetime value without an explicit offset — e.g. a deadline of 2026-07-15T13:00:00 — reached Bitrix24 verbatim and was read in the timezone of the portal's webhook owner, not the caller's. An integration in Berlin that wrote 13:00 had 10:00 UTC stored instead of 11:00 UTC: one hour off in summer, two in winter. The value looked plausible, so corrupted payment dates, deadlines and meetings went unnoticed.
After
A client can declare its own timezone with the X-Vibe-Timezone header (an IANA name such as Europe/Berlin; a browser reads its own from Intl.DateTimeFormat().resolvedOptions().timeZone). Datetimes without an offset are stamped with that zone's offset as it was in force on the value's own date — daylight-saving transitions are honoured per value. This applies to fields that genuinely keep a time of day: Bitrix24 stores several fields it calls datetime as plain dates, where an offset would move the stored day, so those are left alone. Values that already carry Z or an offset are never rewritten. Without the header (or with an unrecognised zone) behaviour is unchanged — existing integrations are unaffected.
The header affects writes only. Bitrix24 discards a timezone suffix inside a filter, so the platform strips it and filter values are always read in the portal account's zone. With the header set, writing "2026-07-15T13:00:00" and then filtering on that same literal will not match: send the instant you want compared, or use a range wide enough to cover the offset.
FIX-0801-9: bot deletion is idempotent: "bot already gone" now succeeds instead of returning 502
Before
DELETE /v1/bots/:botId answered 502 BOT_DELETE_PARTIAL and kept the Vibecode database record on ANY Bitrix24 error — including when Bitrix24 reported that it no longer had such a bot. The desired end state was already reached, yet the call counted as failed and the record stayed in GET /v1/bots forever: a plain delete could never remove it, only ?force=true did.
After
When Bitrix24 answers that the bot does not exist, the deletion counts as successful: the record is removed and the 200 response carries a new data.alreadyAbsentOnB24: true field. In addition, when Bitrix24 rejects the unregister for another reason, Vibecode checks once whether the bot is still on the account: if it is already gone the call succeeds, if the removal could not be confirmed the previous 502 BOT_DELETE_PARTIAL with a preserved record is returned. With ?force=true, a "bot does not exist" answer now also carries alreadyAbsentOnB24: true instead of forced: true — nothing is orphaned on the Bitrix24 side in that case. The 502 body gained an error.incidentCode field — a six-character code to quote to support.
Impact on integrators
No changes required. The "deleted a bot, got an error, the bot stayed in the list" scenario no longer happens; ?force=true remains only for accounts that are unreachable for good. If your code branches on data.forced, note that the "bot already gone on Bitrix24" case now returns data.alreadyAbsentOnB24 instead.
FIX-0801-10: Bitrix24 error messages no longer carry HTML markup
Before
Validation text received from Bitrix24 was forwarded into error.message with a trailing <br> tag attached. A client rendering the message as text — which is what a JSON contract implies — showed the literal tag to the user after every error, in their own language. The same applied to error.validation[].message.
After
The markup is removed at the response boundary: <br> in any spelling becomes a newline. A newline rather than a space, because Bitrix24 uses that tag to join errors for different fields, and the boundary between them is worth keeping. Localisation is unchanged: the text stays in the language of the account and is not translated. Angle brackets inside user data (an email address quoted in the error text, or a comparison sign) are left alone — what is removed is the line-break tag, not markup in general. The same applies to POST /v1/batch responses, where an error arrives per sub-call. If you were stripping <br> on your side, that handling can go.
Two technical bounds were also introduced: a message longer than 8192 characters is truncated, and at most 100 entries of error.validation are returned. Both exist because the text and the number of fields come from the account and are otherwise unlimited; neither triggers on real responses.
Coverage: error.message and error.validation[] in every V1 error envelope, POST /v1/batch and POST /v1/{entity}/batch responses, meta.pageErrorSample during auto-pagination, and POST /v1/bots. In POST /v1/chats/messages/bulk the per-sub-call error fields are now normalised to the common {code, message} shape — previously the account's error object was forwarded as-is, with its own error/error_description keys.
Separately: count phrases in French and Portuguese e-mails now follow CLDR, where zero takes the singular form. And the English hint about the Universal Lists module no longer carries a Russian module name on international accounts.
NEW-0801-11: CALL_CARD placement — panel inside the call card
Before
GET /v1/placements/available omitted CALL_CARD, and POST /v1/placements/bind
answered VALIDATION_ERROR for that code, even though Bitrix24 supports it.
After
CALL_CARD is listed and accepted for binding. The application needs the
telephony scope: Bitrix24 offers this placement only to applications holding it,
otherwise the bind is refused with "Placement not found". The same scope is now
required for the neighbouring TELEPHONY_ANALYTICS_MENU — it used to be listed
without the scope, so binding it failed silently on the Bitrix24 side.
FIX-0801-12: four silent refusals: activities, warehouses, files, user fields
Before
Four requests answered with 200 and returned something other than what was asked for.
Activities: the providerParams and settings fields are declared as object, but an empty value arrived as an empty array. The field type depended on the content, so a client generated from the schema broke while deserializing exactly those records where the value was empty.
Warehouses: GET /v1/warehouses and GET /v1/warehouses/:id/stock returned the first page for any offset that was not a multiple of 50 — offset=1, offset=2, and offset=3 all returned the same records, while hasMore reported that more existed. Paging by offset looped on page one.
Files and folders: a filter on a field Bitrix24 cannot filter by (createdBy, size, updatedBy) was silently dropped, and the whole folder came back instead of the selected records. A non-existent field name behaved the same way.
User fields: DELETE /v1/userfields/:entity/:id and DELETE /v1/items/:entityTypeId/userfields/:id with a Content-Type: application/json header and an empty body answered with the FST_ERR_CTP_EMPTY_JSON_BODY error — the request never reached the handler. Without the header the same request worked.
After
Activities: an empty providerParams or settings arrives as an empty object, so the declared type is always correct. Non-empty values are unchanged.
Warehouses: the offset is row-exact — offset=1&limit=3 returns the second, third, and fourth records. When the requested window is not covered by a single Bitrix24 page, data arrives empty and meta.warnings carries the OFFSET_BEYOND_FETCHED_PAGE code.
Files and folders: a filter on an unsupported field is refused with 400 UNSUPPORTED_FILTER listing the fields you can filter by: id, name, code, storageId, type, folderId (parentId for folders), deletedType, createdAt, updatedAt, deletedAt. Tree navigation is untouched: the parent folder stays on the allowed list, so both forms — the ?folderId= parameter and the ?filter[folderId]= filter — work as before.
User fields: deletion is accepted both with and without the Content-Type: application/json header. Malformed JSON is still refused, with the INVALID_JSON_BODY code.
Impact on integrators
No action required. Three caveats if your code relied on the previous behaviour: an Array.isArray check no longer distinguishes an empty activity value from a populated one (count the keys instead); paging warehouses by offset now genuinely moves row by row rather than page by page; and a files or folders filter on a field outside the list above now returns an error instead of the whole folder.
NEW-0801-13: task service fields now carry names, descriptions and a dictionary of accepted values
GET /v1/tasks/fields now returns a human-readable label and description for twenty Bitrix24 service fields that used to carry the field name itself instead of a name: NOT_VIEWED, DURATION_TYPE, GUID, CHAT_ID, CHECKLIST, FAVORITE, IS_MUTED, IS_PINNED, IS_PINNED_IN_GROUP, ALLOW_CHANGE_DEADLINE, ALLOW_TIME_TRACKING, NEW_COMMENTS_COUNT, SERVICE_COMMENTS_COUNT, FORUM_ID, FORUM_TOPIC_ID, EXCHANGE_ID, EXCHANGE_MODIFIED, OUTLOOK_VERSION, SITE_ID, XML_ID.
Alongside that, the field schema gained two keys Bitrix24 has been sending all along while we dropped them: values — the dictionary of accepted values as an array of [{ "value": "Y", "label": "Yes" }], and default — the value the Bitrix24 account substitutes when the field is not passed. They arrive for every field where the account returns them, not only for the ones listed above — the Y/N dictionary is now visible on MULTITASK, TASK_CONTROL, SUBORDINATE, ADD_IN_REPORT, REPLICATE as well. Captions inside values are produced by the account itself and follow its localisation, so a field with bare codes may have no captions — DURATION_TYPE arrives as codes only: secs, mins, hours, days, weeks, monts, years (the monts typo comes from Bitrix24, that is the spelling the account accepts).
values is a separate key; it does not replace items: items still returns the raw enumeration directory on fields of type enumeration as [{ "ID": "1", "VALUE": "First" }]. The guarantee is per key — each always has its own shape, so read the one you need by name. On today's Bitrix24 accounts a field carries only one of the two, but both being present is not forbidden.
Existing calls keep working unchanged: the new keys are additive, and neither the set of fields nor their types changed. Five fields — FAVORITE, IS_MUTED, IS_PINNED, NEW_COMMENTS_COUNT and NOT_VIEWED — describe how the user the API key acts as relates to the task rather than a property of the task itself: a different key on the same Bitrix24 account will see different values.
2026-07-31
BC-0731-1: source uploads require Content-Length
Old format supported until: 30.01.2027
The source-upload endpoints — POST /v1/apps/{id}/sources and POST /v1/infra/servers/{id}/sources — now accept a body only with the Content-Length header. A request without it (chunked transfer, Transfer-Encoding: chunked) gets 411 with code MISSING_CONTENT_LENGTH.
The reason: the archive body is no longer assembled in memory in full — it is forwarded to storage as it arrives, and that requires the length to be known up front.
The vast majority of clients already send the header: curl --data-binary, fetch with a buffer body and any HTTP client posting a whole file all set it. Only clients that deliberately stream a body of unknown length are affected.
Re-saving an identical archive also costs a little more now: the content match is determined after the body has been received, so the response comes slightly later and the volume counts toward storage operations. The outcome is unchanged — deduplicated: true and the same version.
What integrators should do
Send the archive as a whole (--data-binary @file in curl, a buffer or a file as the request body) rather than as a stream of unknown length. If your client streams the body itself, compute the size up front and set Content-Length.
FIX-0731-2: tariff name in GET /v1/me comes from the platform edition catalogue, in English
Before
The data.tariff.name field came from a stored tariff snapshot: the licence name as the account reported it, or — when the account reported none — a value from an internal dictionary that had no language awareness. On some accounts it was not a tariff name at all: the response carried either a Russian-language plan name on a non-Russian account, or the licence code itself, such as pro100, presented as a human-readable name.
After
The name is resolved at response time. When the tariff code is known to the platform edition catalogue, the field comes from there in English, so an account now receives Demo period; the name reported by the account is not used in that case, so the label can also change where nothing was wrong. When the code is not in the catalogue, the field still carries the name reported by the account, verbatim and in its own language. Only when there is no name at all, or the only candidate is the licence code itself, is the field null — the licence code is never substituted for a name. The field type is unchanged: string or null, and the code itself is still available in data.tariff.code.
Three edition labels were also renamed to fuller forms: the demo plan is now Demo period, the legacy free plan is now Project — legacy free, and the partner licence is now NFR — partner licence. Their previous values were the short Russian-language forms, so a client comparing this field by string should re-check the comparison.
FIX-0731-3: server user search returns active employees only
Before
GET /v1/infra/servers/:id/b24-users could return inactive users and users who are not employees.
After
The endpoint returns only users confirmed as active Bitrix24 employees. No integration changes are required.
NEW-0731-4: an app now decides for itself whether the placement iframe auto-height is on
Apps gained an optional placementResizeEnabled field (defaults to false). It controls whether the platform serves a wrapper page on a placement open that fits the iframe height to your app's content.
The field is returned by GET /v1/apps and GET /v1/apps/:id and accepted by PATCH /v1/apps/:id. Existing calls keep working unchanged: every existing app carries false, so the placement open behaves exactly as before.
Turn it on only together with a change on the app side. The wrapper opens the app in a nested iframe on the platform origin, so the app gains a new ancestor origin. If your app sends its own Content-Security-Policy header with a frame-ancestors directive, add the platform origin to it — otherwise the browser refuses to open the app. Apps without their own frame-ancestors directive need no change.
If your app already had the auto-height working, turn the field on to keep the previous behaviour. Note that the field is a necessary condition, not the only one: the capability itself is rolled out account by account.
To report its height, the app posts a { type: 'vibe:resize', height } message to the parent window — the contract is described in App inside Bitrix24.
FIX-0731-5: an empty body with the JSON header is no longer rejected on the infrastructure routes
Before
An operation that needs no body answered 400 with the FST_ERR_CTP_EMPTY_JSON_BODY code when a client sent the Content-Type: application/json header with no body. Clients that attach this header to every request do that — axios and PowerShell Invoke-RestMethod, for example. The refusal happened while parsing the body, that is, before the key was checked, so the response gave no way to tell what was wrong with access. It affected DELETE /v1/infra/servers/:id/access-tokens/:tokenId, POST /v1/infra/servers/:id/wake and the rest of the body-less server operations.
After
An empty body is accepted as {}, and the operation answers on the merits — 401 on a wrong key, 404 on a server that does not exist, 200 on success. The former workaround of passing an explicit {} body keeps working. An unparsable body on these routes now returns the INVALID_JSON_BODY code instead of FST_ERR_CTP_INVALID_JSON_BODY — the same code the neighbouring operations of the same server already returned, including deploy, exec and lock.
2026-07-30
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 a portal 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 portal member.
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.
2026-07-29
FIX-0729-1: task comments are no longer refused over key permissions
Before
POST /v1/tasks/:taskId/comments 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, POST /v1/infra/servers/:id/access and DELETE /v1/infra/servers/:id/access/:accessId.
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 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 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 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 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 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 portal 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 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.
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 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 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 and in POST /v1/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 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. 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 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 — 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} and POST /v1/{entity}/search 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} it is a query parameter with exactly two accepted values — true and false; on POST /v1/{entity}/search 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.
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} and POST /v1/{entity}/search 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.
2026-07-28
NEW-0728-1: an erased author's app no longer issues new user tokens
Once an application's author has erased their personal data, the application stops issuing tokens to new users. Such a request previously reached Bitrix24 and created a working token together with the new user's name and email, even though the author is already gone from the system.
The return from GET /v1/oauth/callback then arrives at your redirect_uri with ?error=app_unavailable — the same shape token_exchange_failed, invalid_domain, and profile_fetch_failed already use. POST /v1/oauth/placement-session answers 403 with the APP_UNAVAILABLE code — so does the placement handler Bitrix24 opens the application widget through (it previously answered 401 with the generic USER_AUTH_REQUIRED, which read as a user-authorization problem).
Tokens and sessions already issued for that application are not renewed. Previously working calls are unaffected: while the author is active, both endpoints behave exactly as before.
NEW-0728-2: stuck-lock release is now cross-replica; the DELETE /lock response carries broadcast and localLock
DELETE /v1/infra/servers/:id/lock now broadcasts the release to all platform replicas, so it releases a stuck lock even when it is held on a different replica (a common case under horizontal scaling). The response gains broadcast (the release was broadcast fleet-wide, best-effort) and localLock (whether the lock was held on this replica). The released field now describes only the current replica and is not proof of a fleet-wide release — when the stuck lock is on another replica, released can be false while the lock really was released; do not poll the endpoint until released: true, retry the operation instead. Existing calls keep working unchanged (the fields are additive). Additionally, a stuck exec lock is now guaranteed to be released by a server-side auto-sweep shortly after its TTL expires.
The POST /v1/infra/servers/:id/exec response on 502 EXEC_BUSY for a galaxy app now carries an error.hint with an honest recovery path (the shared host exec channel; escalation to the platform team — DELETE /lock does not help there, as the agent mutex is the blocker). The POST /v1/infra/servers/:id/deploy response on 409 GALAXY_APP_BUSY gains error.hint, retryable: true, retryAfter, and a Retry-After header.
NEW-0728-3: pointers to Open Channels, task checklists and app blueprints in the self-description responses
The GET /v1/guide response gained the data.appBlueprints pointer — a link to the ready-made app spec documentation and the condition behind the 403 BLUEPRINTS_DISABLED response.
For a key with the imopenlines scope the response also carries the data.openLines block: a section overview, links to all seven documentation pages and the split between two endpoint groups. Line configuration and the operator actions are available on every Bitrix24 account. Dashboard statistics answer 422 METHOD_NOT_YET_AVAILABLE until the Bitrix24 update reaches the account, and 403 B24_TARIFF_RESTRICTION without the statistics-access right.
In the GET /v1/me response the api._rules block gained three new pointers — to task checklists, Open Channels and app blueprints.
The fields are additive, existing clients are unaffected. The endpoints themselves did not change.
FIX-0728-4: galaxy deploy now recovers a dropped host tunnel
Before
Deploying a galaxy app onto a host whose tunnel had silently dropped under build load (including a phantom-CONNECTED host — the flag was stale while the tunnel was already dead) looped on GALAXY_HOST_UNREACHABLE / GALAXY_DEPLOY_INTERRUPTED: the platform did not repair the tunnel itself, and the client's retries kept hitting the same dead tunnel.
After
Such a drop on the deploy path now triggers a background repair of the host tunnel, so an honest retry lands on a recovered tunnel and the deploy completes. The error codes and their retryable semantics are unchanged — only the behavior improves (self-healing).
BC-0728-5: unified 404 envelope for nonexistent /v1 routes
Old format supported until: 28.07.2026
The previous body shape is not served — there is no transition period with a dual format, the change takes effect on the publication date.
Before
A request to a nonexistent path or an unsupported HTTP verb under /v1/ answered with the web-server body outside the unified API envelope:
{
"message": "Route GET:/v1/dealz not found",
"error": "Not Found",
"statusCode": 404
}
After
The same request answers in the unified V1 envelope with the new ROUTE_NOT_FOUND code. The HTTP status is unchanged — 404:
{
"success": false,
"error": {
"code": "ROUTE_NOT_FOUND",
"message": "Route GET:/v1/dealz not found. Check GET /v1/guide for available endpoints and verbs."
}
}
ROUTE_NOT_FOUND means "no such route or verb exists" — check the path against the list in GET /v1/guide. Do not confuse it with ENTITY_NOT_FOUND and the domain codes of the *_NOT_FOUND form: there the route exists and the requested object is not found. Outside /v1/ the 404 body shape is unchanged.
What integrators should do
Branch on error.code, not on the body shape. A client that parsed the message, error and statusCode fields of the previous body on /v1/ paths must switch to the unified success and error.code envelope.
BC-0728-6: calendar: an unknown select name returns an error instead of an empty object
Old format supported until: 28.07.2026
Before
GET /v1/calendar-events (as well as GET /v1/calendar-events/{id}, POST /v1/calendar-events/search and calendar-events sub-calls of POST /v1/batch) silently returned id-only objects when select carried an unknown field name (for example dateFrom — no such field exists, the real name is from). The client believed it narrowed the payload while actually losing data.
After
An unknown field name in select returns 400 UNKNOWN_SELECT_FIELD with the list of accepted names (Available: …). Bitrix24-style names (DATE_FROM) and date aliases (updatedAt) are still accepted and project their canonical key — only names that resolve to no schema field trigger the error. Other entities keep the previous behaviour: an unknown name produces a meta.warnings entry, not an error.
What integrators should do
Take field names from GET /v1/calendar-events/fields and remove non-existent names from select (dateFrom/dateTo → from/to). Clients that pass no select or pass valid names are unaffected.
NEW-0728-7: calendar: occurrenceIndex and version fields
Calendar events gained two read-only fields. occurrenceIndex is the zero-based index of an occurrence within an expanded recurring series: the rows of a series share one id, and the id + occurrenceIndex pair uniquely identifies a row of the set. version is a monotonic change counter of the event — it grows on every modification and does not depend on the regional settings of the account. Diff recipe: request GET /v1/calendar-events with select=id,version, compare the pairs against your snapshot, and re-read the changed events by id.
NEW-0728-8: chats: limit clamp echo in meta
Three chat endpoints — GET /v1/chats/recent, GET /v1/chats/:dialogId/messages and GET /v1/chats/:dialogId/users — now, when the passed limit is clamped into the allowed range, extend the response with a meta field carrying the requestedLimit and appliedLimit pair: what was requested and what was applied. When limit is within the range, meta is not added — the envelope is unchanged. For message reading, the real ceiling of cloud Bitrix24 is documented — at most 50 records per call regardless of limit, with continuation read via the lastId cursor.
NEW-0728-9: mutual updatedAt and createdAt date-field aliases
Date fields in the entity catalog carry two naming families: some entities declare updatedAt and createdAt, others updatedTime and createdTime. Pair members are now accepted interchangeably on input: in filter and select — on every entity that declares the partner key, in sort — on entities with a camelCase field schema. For example, updatedTime on an entity with an updatedAt field works as updatedAt, and vice versa. Canonical field names in responses do not change — the alias applies to input only.
NEW-0728-10: POST alias for the Knowledge base search
The Knowledge base 2.0 document search now also accepts POST /v1/note/documents/search with a JSON body { "query": "...", "limit": 20 } — for agents that expect search to be a POST request by analogy with the other entities. The canonical form remains GET /v1/note/documents/search with query parameters. Both forms accept only query and limit, and when a parameter is passed both in the body and in the query, the body wins.
NEW-0728-11: feed: limit up to 200 records per request
GET /v1/posts accepts a limit from 1 to 200. The Bitrix24 feed page is fixed at 50 records — for a limit above 50 the platform stitches up to four pages into one response. A value above 200 answers with the previous 400 INVALID_LIMIT. The meta object gained a returned field — the actual number of records in the response — and on a multi-page read meta.nextOffset is derived from the response window so page chaining continues as before.
FIX-0728-12: calendar: honest offset and hasMore, deterministic order
Before
GET /v1/calendar-events returned the head of the same set at any offset: Bitrix24 delivers the requested range as one unpaginated array, so every "page" repeated the first one, meta.hasMore stayed true, and the tail of the set beyond the first page was unreachable.
After
The full set is sorted deterministically — by the event start from, ties by id, then by occurrenceIndex — and an honest window from offset to offset + limit is returned from it. meta.total is the number of occurrences in the set: recurring events are expanded per occurrence, and the rows of a series share one id. meta.hasMore answers true only while records remain beyond the window. The element order in the response is now deterministic and may differ from the previous one.
Impact on integrators
Walking the set via offset now yields the whole range. Clients that deduplicated repeating pages on their own need no changes — there are no duplicates anymore.
FIX-0728-13: select accepts declared Bitrix24 names and warns about unknown fields
Before
The select parameter understood only the canonical field names from GET /v1/{entity}/fields. A name in any other spelling — the original Bitrix24 name (DATE_FROM, UF_DEPARTMENT) or a different letter case — silently dropped out of the projection: the field was absent from the response with no error signal, and a request made of such names alone degenerated into objects with a single id field. Batch calls did not apply select at all: both the global POST /v1/batch and the per-entity POST /v1/{entity}/batch returned full objects.
After
select accepts canonical names case-insensitively, declared original Bitrix24 names (DATE_FROM projects from, UF_DEPARTMENT projects departmentId) and the mutual date-field aliases — the response carries the field under its canonical key. An unknown name is no longer lost silently: list, search and get-by-id responses add a meta.warnings array with { "code": "UNKNOWN_SELECT_FIELD", "field": "<name>" } entries — up to 10 warnings per response. Both batch calls now apply select to list and search operations the same way single endpoints do; get-by-id inside the global batch does not apply select. The global call additionally reports unknown-name warnings in the per-call meta. The per-entity call carries no warnings and performs no hard rejection of unknown names — an unknown name there is still simply absent from the response.
Impact on integrators
Single-endpoint responses are only extended. In batch calls, a client that passed select while reading fields outside of it will now receive only the requested fields — drop select from the call or list every field you need in it.
FIX-0728-14: windowed search fails fast on an account-side timeout
Before
A POST /v1/{entity}/search with a wide date range is split into time windows. A Bitrix24 timeout on the first window was skipped, and the remaining windows each ran into their own timeout: the response took 60–75 seconds and then arrived as 503 BITRIX_TIMEOUT. When later windows succeeded, a partial 200 with incomplete data was possible after the same minute of waiting.
After
A first-window timeout (BITRIX_TIMEOUT) now ends the request immediately: the 503 response with the BITRIX_TIMEOUT code and a Retry-After header arrives in about 15 seconds, and the remaining windows are not executed. A partial 200 after a first-window timeout is no longer possible — a deliberate trade-off: the windows are identical in shape, a timeout on the first predicts timeouts on the rest, and a partial response after a minute of waiting fed retry storms. A timeout on any later window is handled as before — the window is skipped and the response may be partial.
Impact on integrators
Retry the request per the Retry-After header. Clients that relied on a partial response under account overload now get a fast 503 — narrow the date range or retry later.
FIX-0728-15: Galaxy .zip deploy: honest archive-extraction error instead of EMPTY_BUILD_CONTEXT
A Galaxy app deploy from a .zip now returns the real (sanitized) extraction-failure reason in the 502 buildLog instead of a misleading "EMPTY_BUILD_CONTEXT" / generic message.
FIX-0728-16: the Bitrix24 operation-time-limit pushback now returns `OPERATION_TIME_LIMIT` with `Retry-After`
Before
When an account rejected a method that had exhausted its operating-time budget, the platform
answered 429 RATE_LIMITED with Retry-After: 2 and replayed the call up to three times.
Those retries could not help against an addressed refusal lasting minutes, and Retry-After: 2
was misleading: a client came back two seconds later and got the same refusal.
After
That pushback now returns the code OPERATION_TIME_LIMIT — the same code the account itself
emits — with Retry-After derived from the known lift time and a plain-language userMessage.
Retries are off: the restriction is addressed to one account-plus-key-plus-method triple, exactly
as Bitrix24 itself applies it, and until it expires the platform rejects calls of that method
itself, without contacting the account. Other methods of the account — and the same method under
a different key — are unaffected. Other 429 refusals (including RATE_LIMITED and
QUEUE_OVERFLOW) are unchanged.
Honor Retry-After: the same call cannot succeed sooner. Spread heavy reads over time or
narrow them — fewer fields, smaller pages, POST /v1/batch.
FIX-0728-17: The * value in select returns every field
Before
The familiar Bitrix24 form select: ["*"] (and ["*", "UF_*"]) produced the opposite result on single endpoints: * matches no declared field, so only id remained in the response. There was no error signal — the record simply came back empty.
After
* and UF_* (in any letter case) are recognised as a request for every field: no field selection is applied and the full record is returned. An unknown name passed next to the wildcard is not rejected — the response carries an UNKNOWN_SELECT_FIELD warning instead. This works the same way in list, search, get-by-id and both batch calls — POST /v1/batch and POST /v1/{entity}/batch. On calendar events, where an unknown field name returns a 400 error, the * value is not treated as an error.
Impact on integrators
Nothing to change. A client that carried select: ["*"] over from Bitrix24 portal code will start receiving full records instead of objects with a single id.
FIX-0728-18: windowed search stops once it has the requested rows and reports an incomplete window
Before
POST /v1/{entity}/search over a wide date range splits the range into time windows and merges their results. The walk went to the end of the range even when the requested limit rows had already been collected: a search with limit: 50 over a range of several months read the whole range through — it answered slowly and put a load on the Bitrix24 account out of all proportion to the size of the answer. When a single window held more matching rows than one window read returns, the window returned only the beginning of its set, and did so silently: no signal in the response, and no way to read the remainder (windowed search rejects offset > 0 with the UNSTABLE_OFFSET_PAGINATION code).
The second cause of incompleteness was silent too, on accounts where windows are read in packs: once 5000 rows are collected the search stops sending the remaining windows and returns a cut-off prefix — the response said nothing about that either.
After
The walk over windows stops as soon as it has collected more unique rows than limit asked for. The response still carries at most limit rows, and hasMore carries the "there are more" signal. On an early stop meta.total equals the collected count, so it is a lower bound on the number of matching rows rather than a full count over the range — exactly how this search already behaved on accounts that read windows in packs, and the behaviour is now uniform.
An incomplete answer is no longer silent — for neither of the two causes, and no matter whether windows are read one by one or in packs, on entities whose list method Bitrix24 serves page by page. The response gains a { "code": "WINDOW_TRUNCATED", "field": "…", "message": "…" } warning in the meta.warnings array, where field is the range field the split was keyed on. The warning code is the same in both cases, so you can branch on it without parsing the text; message names the cause that fired: one window held more rows than a single window read returns, or the 5000-row ceiling was reached and the remaining windows were never sent.
The exceptions are named outright. Three entities will never get the warning, because Bitrix24 does not serve their list method page by page: files and folders (/v1/files, /v1/folders) and workgroups (/v1/workgroups). There the request goes out as a single call, limit is not forwarded to Bitrix24 at all, and the account returns a page of its own of about 50 rows: a window holding 200 disk objects comes back with 50 and stays silent, exactly as before this change. On pages (/v1/pages), sites (/v1/sites) and calendar events (/v1/calendar-events) the list method is not page-based either, but it returns the whole requested set in one call — there the warning is simply unreachable while nothing goes missing.
The load this search puts on a Bitrix24 account is reduced further: a degenerate lower bound on id is no longer forwarded to Bitrix24. Two forms are dropped, and they rest on different things. >= with a value of 0 or less, and > with a negative value, exclude negative ids only — they are tautological under a single assumption of non-negativity. > with exactly zero (filter[>id]=0 — the one a cursor walk sends at its start) excludes the record with id = 0, so it additionally rests on Bitrix24 numbering records from one by auto-increment; that is the target case of this change, and it is dropped deliberately. The bound >=id=1 is kept — the guard is deliberately narrow and looks only at values of 0 and below.
The set of records returned does not change on any entity where Bitrix24 really applies a filter on id. One known exception — pipelines (/v1/categories): one of them carries id: 0 ("General"), but the pipeline list method ignores filter entirely, so both before and after this change the answer carries the full set of pipelines. The shape of the response is unchanged.
Impact on integrators
Do not read meta.total as an exact count of matching rows over a wide date range — on an early stop it is a lower bound; branch on hasMore instead. A windowed search cannot be read page by page (offset > 0 is rejected), so "there are more" is answered either by a larger limit (up to 5000) or by a narrower date range.
Check meta.warnings for WINDOW_TRUNCATED: it marks an incomplete result, and paging does not cure that one either — narrow the date range or add filters so the search stops hitting a ceiling. hasMore alone is not enough for this: window splitting only engages at offset === 0, so a request for the next page goes out as a non-windowed one and yields a different result. A search over a narrow range, which is not split into windows, is unaffected.
Mind one boundary of windowed search: sorting applies WITHIN a window, not across the union of windows. Windows are built oldest to newest and concatenated in that same order, and the result is then cut down to limit — there is no global sort over the union. So a request with a descending sort and a small limit over a wide date range returns the OLDEST matching records, not the newest. The behaviour itself is not new, but the early stop rules out a post-merge sort as the way to fix it (records of later windows are no longer read at all), so it is stated here outright. For a true "last N by date" either narrow the range so that window splitting does not engage, or take the range in one request with a large limit and sort on your own side.
2026-07-27
NEW-0727-1: app favicon in one line — /_gw/icon
The browser-tab favicon is now set with a single static line that carries no server id:
<link rel="icon" href="/_gw/icon">
/_gw/icon is a platform-served path on the app's own origin; it always returns the currently uploaded icon. One upload via POST /v1/infra/servers/:id/icon drives both the Bitrix24 catalog card and the favicon: re-upload the icon and the favicon refreshes on its own (~5 minutes), no rebuild needed. You no longer need to ship your own static icon file in the app. The line is compatible with one-shot app creation (no id required).
Additionally, the POST /v1/infra/servers/:id/deploy response and the one-shot create POST /v1/infra/servers response now return a warnings[] entry when no icon has been uploaded yet — with the exact upload endpoint (the icon is a separate request, since the id only exists after create). The same warnings[] still flags a missing displayName/description. A successful deploy that left the app icon-less or unnamed is no longer silently treated as finished.
NEW-0727-2: source-at-create: SOURCE_AT_CREATE_GALAXY_ONLY now carries a hint with a path to a galaxy
A POST /v1/infra/servers refusal with source that could not be placed in a galaxy (a both-mode portal with no open galaxy host, or a standalone-only portal) now additionally carries error.hint — an actionable path: how to get a galaxy host and/or how to deploy to a dedicated server in two steps. The error code and message are unchanged.
FIX-0727-3: galaxy apps: PATCH /sleep and PATCH /port now return 400 — manage them from the Galaxies page
Before
For an app hosted in a galaxy (GALAXY_APP), PATCH /v1/infra/servers/:id/sleep returned 200 and wrote sleepAfterMinutes, and PATCH /v1/infra/servers/:id/port returned 404/409 — contradicting the documented /v1/me (deployment.galaxyApp) contract, where no V1 lifecycle action applies to a galaxy app.
After
Both calls for a galaxy app return 400 with error.code = "GALAXY_APP_USE_GALAXY_ROUTE" and change nothing. Configure the app's auto-sleep via the galaxy route; a galaxy app's port is host-pinned and not settable. Standalone-server behavior for /sleep and /port is unchanged.
FIX-0727-4: galaxy app recovers after its host tunnel drops
Before
When a galaxy host's secure tunnel dropped (the server still RUNNING, but the connection lost), deploying and running commands for a galaxy app returned 502 GALAXY_HOST_UNREACHABLE, and a logs request returned an empty response with a hint. The host stayed unreachable until a manual repair: retrying the same request hit the same error indefinitely.
After
The platform now restores the host's tunnel in the background without holding up the response. Retrying the same request succeeds as soon as the host reconnects (usually within a minute). Parallel deploys/commands against one shared host do not start a duplicate recovery.
Impact
The error code and response shape are unchanged — 502 GALAXY_HOST_UNREACHABLE (for deploy and exec) is still marked retryable, and logs still return an empty list with a hint. What changed is that a retry now succeeds instead of failing forever. Keep retrying on that code and on the empty logs response with your usual policy.
Affected endpoints: POST /v1/infra/servers/:id/deploy, POST /v1/infra/servers/:id/exec, GET /v1/infra/servers/:id/logs
NEW-0727-5: warning when a deploy changelog has nowhere to be published
The POST /v1/infra/servers/:id/deploy response now carries an entry in warnings[] when the request included a changelog but the deploy produced no new source version. A release note is bound to a version, so without one the text is stored nowhere and never reaches the app's channel feed in the Bitrix24 messenger.
The reason is visible in the source field of the same response: source storage is disabled (feature-disabled-platform or feature-disabled-portal), the save failed (save-failed), or the uploaded bytes matched the previous version. Such a deploy used to answer with a plain success, leaving no way to learn the note was lost. The warning is delivered both in JSON mode and in the done event when ?stream=true is used.
FIX-0727-6: bizproc-templates list without an explicit select now returns every field, including id
Before
GET /v1/bizproc-templates and POST /v1/bizproc-templates/search without an explicit select returned only the documentType field. Without id a client could not issue a follow-up update/delete — the list was useless without a second request carrying an explicit select.
After
Both calls without a select return the full declared field set (id, moduleId, entity, documentType, autoExecute, name, description, modified, isModified, userId). An explicit select works as before. The request format is unchanged.
NEW-0727-7: uploading a file to a knowledge-base document now returns assetMarkdown right away
An upload via POST /v1/note/documents/{documentId}/files used to return only { id }, so getting the ready-to-embed block meant a second call to GET /v1/note/documents/{documentId}/files/{id} — or assembling the [[image fileId=N]] markup by hand.
The response now carries the whole file object — id, documentId, name, size, mimeType, assetType, assetMarkdown — the same shape GET returns. Upload the image, take assetMarkdown from the response, put it into the document body and call PATCH: no second request needed.
The change is additive: id stays in place with the same value, so a client reading only that field keeps working unchanged. If a particular Bitrix24 account returns an object without assetMarkdown, the platform invents nothing — the response carries exactly what Bitrix24 sent.
FIX-0727-8: `ttlSeconds` bounds for access tokens in the machine-readable schema now match the behaviour
Before
The OpenAPI schema for POST /v1/infra/servers/{id}/access-tokens advertised ttlSeconds between 60 seconds and 30 days. The platform, from day one, accepted 300 seconds up to 315,360,000 (ten years) and rejected anything outside that with 400 INVALID_TTL. So a client — or a client library generated from the schema — that took the minimum straight from the schema hit a hard error on a value the schema itself offered, while the "unlimited" option from the interface looked unavailable through the API. The prose documentation was correct all along; only the machine-readable schema disagreed.
After
The schema reads its bounds and default from the very constants the request is validated against, so the two can no longer drift: minimum 300, maximum 315,360,000, default 86,400.
What this means for integrators
The endpoint's behaviour did not change — only what the machine-readable schema says about it. If you generate a client from the OpenAPI document and it validates ttlSeconds on your side, regenerate it: the old client would reject valid values above 30 days and allow values below 300 seconds that the platform always refused.
NEW-0727-9: A placement app can auto-resize its iframe
An app embedded in a placement can now report its content height to the platform, which grows the iframe to fit it — previously the height was fixed by Bitrix24 and tall content was clipped. The app posts a message to its parent window: window.parent.postMessage({ type: 'vibe:resize', height: <pixels> }, '*'). The accepted type is vibe:resize or vibe:setHeight with a numeric height field; targetOrigin must be '*' — the browser checks it against the window's immediate parent. Recompute the height when the content changes, for example with a ResizeObserver. A full description and recommendations are in the "iframe auto-height" section of the app runtime guide.
The feature is activated per Bitrix24 account on the platform side; if the resize does not take effect yet, it is not active for your account.
FIX-0727-10: list offset now counts records, not pages
Before
Bitrix24 returns lists in pages of 50 and reads the offset as a page ordinal, not a record count. We forwarded offset verbatim, so it was silently floored to a multiple of 50: offset=0, offset=7 and offset=49 all returned the same first page — no error, no warning. Walking a selection in steps smaller than 50 records looped on the first page, while a step of exactly 50 worked and made the parameter look healthy.
After
offset counts records on the generic list endpoints: GET /v1/{entity}, POST /v1/{entity}/search, POST /v1/batch (action list) and GET /v1/{entity}/{id}/activities. offset=7 starts at the 8th record. Offset and limit are independent: ?limit=2&offset=51 returns exactly two records starting at the 52nd. Bitrix24 still pages by 50 — Vibecode fetches the page covering the requested position and trims the head; the cost is at most one extra Bitrix24 page per request.
Two consequences are fixed alongside. meta.hasMore accounts for the offset: previously, for entities whose list Bitrix24 returns under a named key — deals, contacts, companies, leads, tasks, orders, products, invoices and others, two dozen in all — it compared only the page length against the total and stayed true on the last page whenever offset was non-zero. GET /v1/{entity}/{id}/activities now honours limit: the underlying Bitrix24 method accepts no size limit, so the whole page used to come back regardless of the requested value.
When the requested position lands past what Bitrix24 returned for the fetched page — because it filters after paginating — the response carries meta.warnings with code OFFSET_BEYOND_FETCHED_PAGE; this used to surface as an unexplained empty list.
For deep paging over large selections a keyset cursor (filter[>id] with order[id]=asc) is still more reliable than an offset: it is depth-independent and stable under concurrent writes.
Impact on integrators
Nothing to change: an offset that is a multiple of 50 behaves exactly as before, and code that already used it that way keeps working untouched.
Two things are worth checking. First, GET /v1/{entity}/{id}/activities with an explicit limit: it used to return the whole page (up to 50 records) regardless of the value, and now returns exactly what was asked for. If your code relied on getting more than it requested in one call, raise the limit or page through the selection. Second, walking a selection in steps smaller than 50: that used to loop on the first page and now moves forward, so a loop that leaned on an extra counter-based exit will start returning new records.
Residual exceptions where the offset is NOT row-exact. First, four generic-layer entities whose Bitrix24 method rounds the offset down to a page boundary and whose selection cannot be widened beyond the requested size without risking lost records: calendar-events, calendar-sections, telephony-lines, workgroups. The offset there is unchanged. Second, individual endpoints with their own handlers, which were out of scope for this change: /v1/warehouses, /v1/bookings, /v1/posts, /v1/requisite-links, /v1/lists, /v1/timeline-logs and the stage history under /v1/crm-extras. Their behaviour is unchanged.
The four generic-layer entities above keep their previous offset behaviour, but their meta.hasMore is now more accurate: on the last page with a non-zero offset it could previously say "no more" while records remained.
FIX-0727-11: a closed feedback ticket no longer reports itself as "in progress"
Before
A ticket caught by probe-campaign detection reported status: NEW to its author regardless of what had actually happened to it. The list filter runs on the real status, so a closed ticket both passed the "Solved" tab filter and rendered an "in progress" badge — the same ticket contradicting itself. Affected GET /v1/feedback and GET /v1/feedback/{id}.
After
Only the state the mask exists for is masked: the auto-archive. A closed ticket returns RESOLVED, a ticket in progress returns its real status, and an auto-archived one still arrives as NEW. Keys with feedback access (management, vibe:feedback scope) see the real status as before.
What this means for integrators
A client that read status and expected NEW for such a ticket now gets its actual status — that is the fix. In addition, on a detection-flagged ticket that is already closed, a withdrawal (PATCH {"status":"WITHDRAWN"}) and an author reply are now rejected with 409 FEEDBACK_CLOSED, like on any closed ticket; they used to be accepted because the gate consulted the masked status.
FIX-0727-12: Connect keys: stored scopes are authoritative — vibe:ai / vibe:search are no longer added automatically
Before
A key issued via Vibecode Connect automatically gained the platform scopes vibe:ai and vibe:search on every request, even when they were neither requested nor consented. Such a key could reach the AI endpoints (/v1/chat/completions, /v1/ai/*) and the Search endpoint (/v1/search), and the spend was charged to the account the portal is bound to.
After
The scopes stored on a key are now authoritative — the platform no longer widens them automatically. A key issued via Connect reaches the AI and Search endpoints only when the corresponding scope is actually present on the key; otherwise the response is 403. Rotating a key preserves this property. Keys created in the dashboard are unchanged.
FIX-0727-13: Apps: a derived key and app-scope sync grant no more than the calling key holds
Before
Calling POST /v1/apps with an authoritative key (issued via Vibecode Connect, or derived from one) minted the app's paired key with the full set of default platform scopes (vibe:infra, vibe:ai, vibe:search, vibe:storage) — even when the calling key held none of them. Likewise PATCH /v1/apps/:id could write a vibe:* scope into the app declaration that the key does not hold. In both cases the derived key gained the ability to reach AI and Search, charged to the account the portal is bound to.
After
A derived key receives exactly the platform scopes the calling key actually holds. When an authoritative key lacks a scope, a request carrying it in the body returns 403 SCOPE_GRANT_REQUIRES_CONSENT, with the unconsented scopes listed in error.details.unconsented. Scopes the key does hold (for example a consented vibe:storage) pass through as before. The derived key inherits the caller's expiry. App-scope synchronization to paired keys no longer adds vibe:* to an authoritative key — scope narrowing still applies. Keys created in the dashboard are unchanged: they still receive the default platform scopes.
FIX-0727-14: Bitrix24 call timeout is now configurable, the internal retry after a timeout is removed
Before
The platform always aborted an HTTP call to Bitrix24 at the 15-second mark, and for read methods issued one internal retry after the abort — up to ~30 seconds before the 503 BITRIX_TIMEOUT response. The retry also started a second concurrent execution of the same call on the portal: dropping the connection does not stop Bitrix24 from processing the request.
After
- The time limit of a single Bitrix24 call is now platform-configurable (default stays 15 seconds). On portals where Bitrix24 responds slowly the platform can raise the limit — requests that previously ended in a stable
503 BITRIX_TIMEOUTnow live to the real answer and return data. - The internal retry after a timeout is removed for all methods.
503 BITRIX_TIMEOUTon reads arrives roughly twice as fast (~15 seconds instead of ~30), and the request is no longer executed on the portal twice.429(rate limit) retries are untouched. - The error text
Bitrix24 did not respond within 15snow carries the actual limit (e.g.within 60s) — do not rely on the constant in the text.
2026-07-25
NEW-0725-1: deploy: optional changelog field
POST /v1/infra/servers/:id/deploy accepts a new optional changelog field — plain text up to 2000 characters describing what changed in this version. When a new app version ships, the text is posted to subscribers in the app's Bitrix24 messenger channel feed; if the field is omitted, only the version number is posted. The field works in both JSON and multipart deploy modes. Existing deploy calls keep working unchanged.
NEW-0725-2: model calls with a short-lived token issued to a partner system
Before
The AI router endpoints /v1/chat/completions, /v1/embeddings, /v1/audio/transcriptions and /v1/models accepted a regular platform key only.
After
The same endpoints (and their /v1/ai/... aliases) now also accept a short-lived token of a new type. A Bitrix24 partner system obtains it over its own signed channel; the token is bound to one account and one employee, lives for one hour, and is admitted to exactly these eight routes — any other path answers SCOPE_FORBIDDEN. Spend on such a token is metered by the platform itself and charged synchronously against the account AI quota, so an exhausted quota is refused before the model is called. Only models enrolled in the quota program are available — /v1/models under the same token returns that list.
Refusal codes this route can now return: TOKEN_INVALID, SCOPE_FORBIDDEN, MODEL_NOT_IN_QUOTA_PROGRAM, CREDENTIAL_NOT_PLATFORM, ACCOUNT_FROZEN, PORTAL_DELETED, PORTAL_BLOCKED, PORTAL_SUSPENDED. The envelope is unchanged: success plus error with code and message.
Regular platform keys are unaffected: without a token of the new type the responses are byte-identical to before.
2026-07-24
FIX-0724-1: activities: an unrecognized filter field name is now rejected with 400 UNKNOWN_FILTER_FIELD
Before
GET /v1/activities and POST /v1/activities/search silently dropped an unknown filter key: the request returned 200 success with the filter trimmed to empty — that is, it returned the whole (owner-scoped or entire) activity set. For example {"filter":{"ownerTypeId":2,"ownerId":3,"bogusField":123}} ignored bogusField and returned every activity of the parent deal. This diverged from the documentation and from other CRM entities (companies, invoices), where such a filter was already rejected.
After
A filter field name that is not in the activity schema — and is not a UF_* custom field, the id key, or a special token — is now rejected before the Bitrix24 call with 400 UNKNOWN_FILTER_FIELD and a list of available fields, the same as companies/quotes/contacts already do. The full list of filterable fields is returned by GET /v1/activities/fields.
Impact on integrators
Filtering by real activity fields (in camelCase or the native Bitrix24 UPPER case), by UF_* fields, operators (>=, <, !, etc.), ranges, and AND/NOT all work as before. If you relied on the silent drop of an unrecognized key, remove it from the filter.
NEW-0724-2: bizproc activities and robots /fields now return field labels and descriptions
Before
GET /v1/bizproc-activities/fields and GET /v1/bizproc-robots/fields described each field only by its type and the readonly flag, with no human-readable labels.
After
Each of the 12 fields now carries a label and a description in English, which makes building forms and hints easier. Field types are unchanged.
FIX-0724-3: Downloading your own storage objects no longer returns 403
Before
GET /v1/storage/objects/:key could return 403 for an object the key owner legitimately owns but that physically lives under a different storage "family" prefix (for example, server files visible in a developer's listing). The object appeared in the listing, yet downloading it, minting a presigned URL or issuing a HEAD failed.
After
The scoped credentials account for the object's actual family (the portal stays bound to the caller's context), so download (?download), streaming (?inline) and HEAD work for your own objects regardless of family. Ownership checks are unchanged — someone else's object still returns 404.
FIX-0724-4: GET /v1/workflows honours the limit parameter
Before
GET /v1/workflows accepted limit but silently ignored it — the whole page of running workflows (up to 50) was always returned regardless of the requested size.
After
limit is respected: the response holds at most the requested number of records. Values above 50 are gathered page by page (capped at 500); meta.total still reports the total number of running workflows.
FIX-0724-5: deploy: an app wrapped in a single archive folder no longer fails with ENOENT package.json
Before
Deploying (POST /v1/infra/servers/:id/deploy) to a standalone server an archive that wraps the project in a single top-level folder (e.g. myapp/package.json instead of package.json at the root) failed at the install step:
npm error enoent Could not read package.json ... open '/opt/app/package.json'
The uploaded archive was left beside the extracted content, so the auto-flatten of a single wrapping directory did not fire (the root held two entries — the archive and the folder) and package.json stayed nested.
After
The uploaded archive is removed before the flatten step, so a single wrapping directory collapses, package.json lands at the deploy root, and the install proceeds normally.
Impact
No client action required. Flat archives (files at the archive root) work as before; to be safe you can package flat: tar -czf build.tar.gz -C <project_dir> ..
FIX-0724-6: /v1/users* endpoints no longer return 403 and 500 on keys with user access
Before
On a read-only (READONLY) key with user access, GET /v1/users/me returned 403 WRITE_BLOCKED_READONLY_KEY even though it is a read endpoint. Separately, GET /v1/users, GET /v1/users/:id, POST /v1/users/search and GET /v1/users/fields returned 500 INTERNAL_ERROR on accounts where one of the user fields (UF_*) had an empty definition.
After
GET /v1/users/me works on a read-only key. The other /v1/users* endpoints return data and skip the field with an empty definition instead of failing.
Impact on integrators
No action required — existing calls keep working, and the previously failing scenarios now respond correctly.
NEW-0724-7: bizproc activity and robot callback delivery to a Black Hole app
Registering a bizproc activity or robot whose handler points at your Black Hole deploy server now reliably delivers the execution callback (event token, auth block, code and properties) to the app. Previously that callback could be lost: Bitrix24 online events are not retried, and a sleeping or waking server dropped the call. The platform intercepts the handler at registration, queues the call durably, and retries delivery with a server wake-up and backoff.
Affects POST /v1/bizproc-activities and POST /v1/bizproc-robots (and their update). New error code SERVER_APP_MISMATCH (400): the Black Hole server behind the given handler must belong to the same app that registers the activity. Registering such a handler through /v1/batch is not supported — use a single request (BIZPROC_CALLBACK_BATCH_UNSUPPORTED). Additionally, POST /v1/bizproc-robots now requires code, name and handler upfront — omitting any returns 400 MISSING_REQUIRED_FIELDS instead of a raw Bitrix24 error (as activities already did).
The capability is rolling out gradually and is enabled per account: until it is on for your account, registration behaves as before, without managed delivery. Once it is on, batch registration behaviour changes: an attempt to register a BH handler via /v1/batch starts being rejected (BIZPROC_CALLBACK_BATCH_UNSUPPORTED) — move such registrations to a single POST /v1/bizproc-activities or /v1/bizproc-robots.
2026-07-23
NEW-0723-1: app blueprints library — spec via API key
Ready-made specs for popular apps are now available via a key: GET /v1/app/blueprints/:slug?locale=ru|en returns the raw spec markdown (Content-Type: text/markdown). The endpoint requires Authorization: Bearer <key>; an unknown or hidden blueprint returns 404 BLUEPRINT_NOT_FOUND. The copy-paste "AI prompt" shown when creating a key carries the spec link — the AI agent fetches the spec with the same key. The former anonymous path /api/public/blueprints/:slug.md has been removed.
NEW-0723-2: sources of a deleted server: access, cleanup, and an honest answer for purged bytes
Sources outlive their server — a long-standing platform guarantee — but the API gave you no way to reach them: the whole /v1/infra/servers/:id/sources* surface answered 404 for a deleted server, so the owner could neither list their versions, nor untag them, nor delete them. For a version tagged published or manual that was a dead end: untagging is the only sanctioned way past 409 PROTECTED_BY_TAG, and untagging was exactly what you could not do.
Read and cleanup verbs now work on a deleted server: version list, version metadata, download, tag, PATCH, DELETE and cleanup. Saving a new version (POST /sources) still answers 404 — a dead server accepts no new deposits.
So that a deleted server can be found at all, GET /v1/infra/servers accepts ?includeDeleted=true. The default listing is unchanged. Every row now carries a deletedAt field (null for live servers).
Separately: a version whose bytes were already purged from storage now answers 410 with code SOURCE_VERSION_BYTES_PURGED instead of 404. The difference matters — 404 claimed the version did not exist, when in fact its record is alive and the recovery is different: re-upload the archive rather than look for it elsewhere. The code arrives on download and on deploy by {"source": {"versionId": "vN"}}.
Affected endpoints: GET /v1/infra/servers, POST /v1/infra/servers/:id/deploy, GET /v1/infra/servers/:id/sources, GET /v1/infra/servers/:id/sources/:versionId/download — the sources contract lives on the Source storage page
FIX-0723-3: galaxy app deploy: a mid-build tunnel drop is no longer masked as "host unreachable"
Before
If the host tunnel blipped during the build and no healthy container of this deploy resulted (the build was interrupted, the container never came up, the exec channel was busy, or the app crashed), the deploy (POST /v1/infra/servers/:id/deploy) returned 502 GALAXY_HOST_UNREACHABLE advising "retry once it reconnects". The host was often reachable — the advice was misleading, and the caller never saw the real cause (for example, their own app failing to start).
After
When the host is reachable after the blip but the deploy did not bring the app up, the deploy returns a new code 502 GALAXY_DEPLOY_INTERRUPTED — "the host is reachable, but the deploy was interrupted before the app started: re-send the same deploy; if the app repeatedly fails to start, fix it first (the start command, port, dependencies, environment variables, or memory limit)". It stays retryable — the slot is intact, no need to delete and recreate it. The GALAXY_HOST_UNREACHABLE code now stays only for a genuinely unreachable host (no probe reached it). If the app truly crash-loops, that is reliably surfaced on the retry by the normal liveness check (code GALAXY_APP_START_FAILED).
NEW-0723-4: deploy step timeout error now carries a recovery hint
The DEPLOY_TIMEOUT error from POST /v1/infra/servers/:id/deploy now carries a structured error.hint object (reason, recovery, recoveryAction) tied to the timed-out step (error.step). For user commands (install, preStart) the hint explains that the command did not finish within its time budget and advises making it non-interactive and self-exiting, and launching long-running services from the start command or detached (docker compose up -d). For the runtime install step (runtime — a platform step, not a user command) and other service steps it points at a possible tunnel stall and POST /v1/infra/servers/:id/repair. The field is additive: the existing error.code, error.message and error.step are unchanged, no integration change is required; the hint arrives in both JSON mode and the SSE error event.
FIX-0723-5: a failed deploy step now shows the real cause, not a benign warning
Before
When a deploy step failed, data.steps[].stderr (and hence error.message) could carry only a benign one-stream warning, losing the real cause of the failure.
After
Both streams are returned together, labelled stderr: and stdout:; the real cause is no longer hidden. The response shape and field name are unchanged.
FIX-0723-6: list sorting by a non-unique field no longer drops records on the second page
Before
A list or search sorted by a non-unique field (for example the date field begindate), over more than 50 records, could silently return fewer records than exist: at the page boundary some records sharing the same sort-field value were lost. The response was a 200 with no incompleteness signal. Affected the CRM smart-process-backed entities — deals, leads, contacts, companies, quotes, invoices and smart-process items /v1/items/{entityTypeId} — on GET /v1/{entity}, POST /v1/{entity}/search, inside POST /v1/batch sub-calls and per-entity POST /v1/{entity}/batch. The same instability class affected numeric aggregation (POST /v1/{entity}/aggregate with sum/avg/min/max/groupBy): the record fetch for the aggregate ran with no order, so over more than 50 records some rows could be lost and skew the result.
After
A secondary id key is appended to the sort, making the order fully deterministic, so paginated reads no longer drop or duplicate records regardless of the sort field. Your sort stays the primary key; records with an equal sort-field value are ordered by ascending id. No request changes are needed.
FIX-0723-7: reopening a ticket via a comment no longer leaves the resolution stamp
Before
A team comment through POST /v1/feedback/:id/comments that moved a ticket from RESOLVED or WITHDRAWN back into an active status (NEW, REVIEWING, AWAITING_USER, NEEDS_REVIEW) did not reset resolvedAt and resolvedBy. They lingered from the prior close, so on reads (GET /v1/feedback/:id, GET /v1/feedback) a reopened ticket looked both active and resolved.
After
Such a comment clears resolvedAt and resolvedBy — on reads an active ticket no longer carries a resolution date. resolution is left as is: it mirrors the comment body. A comment that sets RESOLVED still stamps the fields; moving to ARCHIVED and a plain transition between active statuses leave the stamp untouched. This aligns the behaviour with the clear already applied on PATCH /v1/feedback/:id.
FIX-0723-8: list filters on statuses / departments / storages / currencies / products are no longer silently ignored
Before
GET /v1/statuses, /v1/departments, /v1/storages, /v1/currencies, /v1/products run on legacy Bitrix24 methods (crm.status.list, department.get, disk.storage.getlist, crm.currency.list, crm.product.list) that silently ignore non-filterable keys and operators. An unknown or unsupported filter field — e.g. filter[system] on statuses, filter[module] on storages, or filter[price] on products — as well as operators $gt / $contains / $ne returned 200 with the whole table. The client received the full set instead of the expected subset — a silent failure with wrong data.
After
For these entities the filter is validated before the Bitrix24 call: only fields the method actually filters on (live-verified) are allowed. Any other field, operator, or empty set returns 400 UNSUPPORTED_FILTER listing the filterable fields. Allowed fields per entity: statuses — id, entityId, statusId, name, sort, semantics, categoryId; departments — id, name, parentId, headId; storages — id, name, code, entityType, entityId; products — id, name, code, xmlId, active, sectionId, sort. crm.currency.list filters on nothing — any filter on /v1/currencies returns 400 with a hint to filter client-side.
BC-0723-9: POST /v1/apps no longer returns the prefix and suffix fields in the create response
Old format supported until: 21.07.2026
Before
The POST /v1/apps create response carried two vibe_app_ values — a short prefix and the full rawKey. The short prefix was mistaken for the key, and a request using it returned 401.
After
The create response carries one vibe_app_ value — the working rawKey. The prefix and suffix fields remain on GET /v1/apps and GET /v1/apps/:id for masked-key display.
What integrators should do
Use the rawKey field from the create response as X-Api-Key. If you need the masked prefix, read it from GET /v1/apps or GET /v1/apps/:id instead of the create response.
FIX-0723-10: POST /v1/batch now persists phone and email on lead and contact create and update
Before
Through the shared POST /v1/batch the phone and email (multifield) values were dropped on a lead or contact create or update. The call returned success but the field was not saved. The same payload through the single POST /v1/leads or PATCH /v1/contacts/:id and through POST /v1/{entity}/batch saved correctly.
After
The shared POST /v1/batch serializes multifields the same way the single calls do. Phone and email persist on create and update.
FIX-0723-11: Search and research no longer return 402 INSUFFICIENT_BALANCE on a funded account
Before
POST /v1/search and POST /v1/research with the platform-managed engine (bitrix-search) on a personal key could return 402 INSUFFICIENT_BALANCE even with enough Vibe balance on the account. The pre-flight balance check looked the account up by the key owner, while the account is now one per portal — so it was not found.
After
The balance check and charge always resolve the account by the portal. With a positive balance the request runs and is charged correctly; 402 is returned only on a genuine shortfall. The request and response shape are unchanged.
FIX-0723-12: POST /v1/apps reports the plan requirement clearly on the cloud-shared path
Before
Creating an app via the cloud-shared key-issuance path (the unified cloud↔box scheme, rolled out per portal cohort) on a portal without the required Bitrix24 plan made POST /v1/apps return an opaque 502 CONNECTOR_APP_INSTALL_FAILED with no cause.
After
A plan-access denial is now classified up front: before calling the connector, POST /v1/apps checks the portal's access state and, when it's missing, returns 403 INT_TARIFF_REQUIRED right away, with a readable message. Other cloud-shared issuance failures (module not installed, forbidden by the portal admin, other errors) are classified as before.
Impact on integrators
No action required, successful calls are unchanged. If you handled 502 CONNECTOR_APP_INSTALL_FAILED on app creation, also handle 403 INT_TARIFF_REQUIRED and prompt the user to upgrade their Bitrix24 plan.
FIX-0723-13: starting a server no longer errors when the machine is already running
Before
POST /v1/infra/servers/:id/start for a server in the error state
asked the cloud to start the machine and surfaced any rejection as 502 PROVIDER_ERROR. When the
machine was already running — for example, brought back by automatic recovery after preemption —
the cloud rejected the call with "instance already in RUNNING state", and the endpoint returned an
error for an operation that had in fact succeeded. Clients saw a 502 and could not tell it apart
from a genuine failure.
After
That rejection is now treated as an idempotent success: when the machine is already running or in a
transitional state, the call returns 200 and the server moves to provisioning, exactly as on a
normal start. Genuine failures — insufficient permissions, exhausted quota, machine not found — still
return 502 PROVIDER_ERROR.
This is the same idempotency criterion the agent start endpoint and the internal server wake path already applied.
BC-0723-14: the /v1/me deployment.standalone.requiredFields.create shape is now an object + documents the name slug
Old format supported until: 22.01.2027
Before
In the GET /v1/me response, the per-kind sub-block deployment.standalone.requiredFields.create was an array ["provider", "name", "plan", "region"] — the format of name was not stated; the sibling deployment.galaxyApp.requiredFields.create said only "required" for name. A name with non-Latin or uppercase characters was rejected by POST /v1/infra/servers with 400 INVALID_REQUEST, but self-discovery never surfaced that constraint.
After
deployment.standalone.requiredFields.create is now an object (like its sibling deployment.galaxyApp.requiredFields.create), and in both sub-blocks name carries its format: a lowercase-Latin slug matching ^[a-z][a-z0-9-]*$, 2–63 characters long. Put a human-readable label in the optional displayName field.
What integrators should do
The flat deployment.requiredFields["POST /v1/infra/servers"] (the array ["provider","name","plan","region"]) is unchanged — if you read it, no action is needed and the set of required fields is the same. If your code parsed the per-kind sub-block deployment.standalone.requiredFields.create as an array (.forEach / .includes("name") / .length / [0]), switch to reading it as an object: the keys are field names (provider/name/plan/region) and the values are their descriptions.
NEW-0723-15: GET /v1/contacts/fields now returns label and description for every field
The GET /v1/contacts/fields response now carries human-readable label and description for all 28 static contact fields. Previously the base fields (name, lastName, typeId and others) came back with only type and readonly, with no explanation of their meaning. Labels come in English. You can read a field's semantics programmatically from the response instead of cross-referencing the static documentation. In addition, GET /v1/openapi.json exposes these labels and descriptions (in English) as title and description annotations in the Contact and ContactInput schemas.
NEW-0723-16: smart-processes: relations and linkedUserFields fields in the input schema
The relations (parent/child CRM entity links, e.g. linking a smart process to deals) and linkedUserFields fields are now declared in the input schema and shown in GET /v1/smart-processes/fields. Pass them in POST /v1/smart-processes and PATCH /v1/smart-processes/:entityTypeId to link a smart process to other CRM entities and surface it in user fields. Filtering and sorting by these fields are not supported — they are nested write structures, not query fields.
FIX-0723-17: bizproc-activities and bizproc-robots: documentType type in /fields corrected to array
Before
GET /v1/bizproc-activities/fields and GET /v1/bizproc-robots/fields reported documentType as type object, while the field is a three-element array ([moduleId, entity, documentType]), as already declared for bizproc-templates.
After
The documentType type in /fields is now array across all three entities — consistent with the real contract.
FIX-0723-18: PATCH /v1/bizproc-templates returns a numeric id
Before
PATCH /v1/bizproc-templates/:id returned data.id as a string ("1215"), while POST returns a number (1215). A client comparing the id from the create response with the update response saw a false mismatch.
After
The PATCH response returns data.id as a number (1215) — the same as POST.
FIX-0723-19: userfields: label type in the create schema corrected to string
Before
The OpenAPI schema for POST /v1/userfields/{entity} declared label as an object. B24 crm.<entity>.userfield.add accepts LABEL as a string only, so an SDK generated from the spec (where label was an object) sent the wrong type and failed. The OpenAPI spec is a public contract — clients generate SDKs from it, and anyone whose type was "object" had a broken client.
After
label in the create schema is declared as string (portal default-language label). Multilingual labels are set via editFormLabel / listColumnLabel / listFilterLabel (PATCH after create). No runtime change — only the generated spec was corrected.
FIX-0723-20: sleep-now on a galaxy app now returns 400 — manage it from the Galaxies page
Before
POST /v1/infra/servers/:id/sleep-now on a galaxy-hosted app (GALAXY_APP) put the container to sleep and returned 200. This diverged from the session route, which already rejected such an app, and could desync the container state from its host.
After
The same call on a galaxy app returns 400 with error.code = "GALAXY_APP_USE_GALAXY_ROUTE" and changes no state: the container stays RUNNING. Manage the app's lifecycle through the galaxy routes instead. Standalone servers keep their existing sleep-now behavior.
FIX-0723-21: a task comment is no longer served under a foreign task
Before
On legacy portals GET /v1/tasks/:taskId/comments/:id returned 200 with the comment even when the comment did not belong to task :taskId: the same comment was served under any task, and the response taskId was a plain echo of the path.
After
The comment is checked against the task in the path before it is returned. If the comment does not belong to :taskId, the endpoint responds 404 with code NOT_FOUND and message "Comment not found". The response taskId now matches the real parent task. Fetching a comment through its real task keeps working unchanged.
FIX-0723-22: company type is a single field typeId, not companyType
Before
The "company type" field name differed across layers. POST /v1/companies with a companyType field silently ignored the type — the company was created with the default type; the only way to set it was the typeId field. Reads (GET, search) always returned the type in the typeId field. The filter, however, accepted companyType but not typeId.
After
Company type is a single typeId field across all operations: create and update, read and search, filter (filter[typeId]) and grouping (groupBy: typeId). The values are unchanged — CUSTOMER, SUPPLIER, COMPETITOR (list: GET /v1/statuses?filter[entityId]=COMPANY_TYPE). The field is now described in GET /v1/companies/fields.
Impact on integrators
Set the type with the typeId field. Reads are unchanged — the type was always returned in typeId. On create and update companyType is no longer documented (it never persisted a value). In filters and grouping typeId now works, while companyType returns 400 (UNKNOWN_FILTER_FIELD in filters, INVALID_AGGREGATION_FIELD in grouping) — replace the name with typeId.
2026-07-22
BC-0722-1: server icon is now served as PNG
Old format supported until: 21.07.2026
The server icon is now served as a 256×256 PNG (Content-Type: image/png) — the platform renders it from your uploaded SVG. The upload endpoint (POST /v1/infra/servers/:id/icon) still only accepts SVG, and may now return 400 ICON_RASTERIZE_FAILED when the file can't be rasterized to PNG.
NEW-0722-2: scheduled wake (wake-schedules) is now available on every Bitrix24 account
The wake-window CRUD — GET|POST /v1/infra/servers/:id/wake-schedules, PATCH|DELETE /v1/infra/servers/:id/wake-schedules/:scheduleId — is now available on every Bitrix24 account for standalone servers (kind: "STANDALONE"): the call no longer returns 403 WAKE_SCHEDULE_DISABLED. The platform brings a sleeping server up by a given cron-expressed moment, and the task itself is fired by the app's own cron once the machine is already up. Galaxy apps (kind: "GALAXY_APP") are not part of this rollout yet and still return 403 WAKE_SCHEDULE_GALAXY_DISABLED. The GET /v1/infra/servers/:id response and the server list now include the additive fields nextScheduledWakeAt (the next scheduled wake time, ISO 8601 or null) and wakeScheduleCapable — existing fields are unchanged.
NEW-0722-3: deploy 409 SERVER_NOT_READY now signals it is retryable
POST /v1/infra/servers/:id/deploy now adds error.retryable: true, error.retryAfter (seconds) and a Retry-After header to the 409 SERVER_NOT_READY response returned when the server is already restoring its connection (a repair started by a concurrent deploy or repair). It is a machine signal that retrying is worthwhile: wait the given interval and re-issue the deploy. Existing clients are unaffected — the error code and message are unchanged, the fields are additive.
NEW-0722-4: New endpoint to activate the Marketplace trial for the key's portal
POST /v1/portals/:id/activate-market-trial activates the one-time Bitrix24 Marketplace trial for the portal the calling key belongs to. Previously activation was only possible from the Vibecode dashboard — API keys had no programmatic path.
:id must match the key's portal, otherwise 403 PORTAL_MISMATCH. No request body. Rate-limited to 3 requests per hour per portal.
Success response: { "success": true, "data": { "status": "activated", "trialEndsAt": "..." } } (or "status": "already_active" when the trial/access already exists). Errors: 403 WRITE_BLOCKED_READONLY_KEY (read-only key), 403 PURPOSE_KEY_FORBIDDEN (special-purpose key may not activate the trial), 404 NOT_FOUND (portal not found), 409 ALREADY_ACTIVATED (trial already activated), 409 TRIAL_ACTIVATION_UNAVAILABLE (trial not available for this portal), 503 TRIAL_ACTIVATION_RETRY (transient error, retry later).
FIX-0722-5: the portal plan probe no longer stops at a scope-limited developer key
Before
On a portal with several developer-key holders the plan state could stay unread: if the first probed key answered with portal data but without the full licence block (a scope-limited key), the probe stopped there and never reached a key able to read the rest. The plan state stayed unknown, so a portal entitled to create servers could receive 402 from POST /v1/infra/servers while GET /v1/me reported capabilities.servers.create.available: false. Calling GET /v1/me?refresh=tariff did not clear the denial.
After
The probe keeps going until a key returns the full licence block. An entitled portal is allowed to create servers and capabilities.servers.create.available becomes true. Response shapes are unchanged and no client action is required. The state refreshes with the portal's next plan refresh (within an hour) or immediately via GET /v1/me?refresh=tariff.
Integrator impact
No action required. A client branching on 402 at server creation keeps working — on affected portals that response simply stops arriving.
FIX-0722-6: the `region` field in infrastructure responses always returns a region id
Before
For a server created and running on the international segment, GET /v1/infra/servers and GET /v1/infra/servers/:id could return an internal placement-zone identifier in the region field rather than a catalog region id. The value matched no id in GET /v1/infra/providers/:providerId/regions, so a server could not be matched to its catalog region through that field, and it exposed internal placement detail.
After
region always carries a neutral region id from the same namespace as the catalog — for example bc-eu-central. The value equals the id of the corresponding GET /v1/infra/providers/:providerId/regions entry, so a server maps to its catalog region directly. The placement zone is an internal detail: server creation takes a region, and the platform picks the zone inside it.
What integrators should do
Nothing, if you pass a region value taken from the regions catalog — that flow is unchanged. If your code compared a server response's region against a string obtained earlier from a server response rather than from the catalog, that comparison is now reliable: both ends are in one namespace. Identifiers from the previous catalog are still accepted on input.
2026-07-21
FIX-0721-1: built-in search engine name and description, cloud provider name
The public name of the platform search engine now matches the product name: GET /v1/search/providers
and /v1/me return Bitrix24 AI Search in the name field (latin locales). The provider identifier
bitrix-search is unchanged — no client action is required.
The source-citation claim was removed from the same provider's description: the capability depends on
the engine bound to your instance and is declared machine-readably in capabilities.output.citations
of the same response.
GET /v1/infra/providers on the international segment now returns the Bitrix24 Cloud brand in the
name field instead of Bitrix Cloud. The provider identifier bitrix-cloud is unchanged.
Before
"name": "Bitrix AI Search" · "description": "Platform AI search with agentic mode and source citations" · "name": "Bitrix Cloud"
After
"name": "Bitrix24 AI Search" · "description": "Platform AI search with agentic mode" · "name": "Bitrix24 Cloud"
FIX-0721-2: creating a business-process template now accepts the template file
Before
POST /v1/bizproc-templates answered 422 Incorrect field TEMPLATE_DATA! for any body — creating a template was impossible: the field holding the .bpt file content was not in the entity schema and never reached Bitrix24.
After
The templateData field (a .bpt file as a [filename, base64 content] array) is accepted and forwarded to Bitrix24, and the template is created. The field is required on create: without it the request is rejected with 400 MISSING_REQUIRED_FIELDS before Bitrix24 is called (previously the raw Incorrect field TEMPLATE_DATA! error came back). It is write-only and surfaces in the field reference at GET /v1/bizproc-templates/fields, but is not returned on reads.
NEW-0721-3: Aggregated source registry — GET /v1/me/sources
A new endpoint GET /v1/me/sources — the programmatic twin of the dashboard "Application sources" page. It returns source snapshots across every server and application the key owns (an account-admin key sees the whole account), with pagination (page/limit/search) and the standard { success, data, total, page, limit } envelope. Each row carries a drill-in pointer — listEndpoint and latestDownloadEndpoint — plus reachableViaApi and, for server rows, blackholeStatus. Unlike GET /v1/infra/servers, which is scoped to the calling key's own servers, this registry also spans a server bound to another key of the same owner.
The server-scoped source endpoints (POST /v1/infra/servers/:id/sources and the sibling list/download/tag/cleanup routes) are now documented, and the versions[].serverContext field ({ serverId, serverName, serverDisplayName, linkedApp }) is formalized in the contract.
NEW-0721-4: optional error.b24Code field in 422 BITRIX_ERROR responses
422 BITRIX_ERROR responses now include an optional error.b24Code field — the raw Bitrix24 error code for programmatic handling (for example, PERIOD_REQUIRED, INVALID_FILTER). The change is additive: existing clients that parse only error.code and error.message are not affected.
NEW-0721-5: Open Channels statistics — 6 dashboard methods
A new API section for contact-center dashboards: POST /v1/openlines/stats (period aggregates), GET /v1/openlines/operators (real-time operator load), POST /v1/openlines/sessions/search, POST /v1/openlines/sessions/stats, POST /v1/openlines/sessions/transfers, POST /v1/openlines/ratings/search. Requires the imopenlines scope and the report_open_lines plan feature (otherwise 403 B24_TARIFF_RESTRICTION).
Rolling out — the methods ship with the Bitrix24 update imopenlines 26.700.0 and are not yet available on every account. Until the update reaches your account, the methods return 422 METHOD_NOT_YET_AVAILABLE with the target version in the response — this indicates the rollout, not an integration error.
FIX-0721-6: a Bitrix24 plan refusal is returned as 403 B24_TARIFF_RESTRICTION on every endpoint
Before
A Bitrix24 refusal caused by a plan restriction arrived as 422 BITRIX_ERROR with an opaque message — there was no way to tell it apart from other Bitrix24 errors programmatically.
After
Such a refusal is returned as 403 with the B24_TARIFF_RESTRICTION code. The rule is platform-wide, not limited to Open Channels: any endpoint that calls a Bitrix24 method unavailable on the Bitrix24 account plan now answers with this code.
Impact on integrators
Clients with generic error handling keep working unchanged — the refusal is still an error, only a more precise one. If you branched on 422 specifically for plan refusals, move that branch to 403 and error.code === 'B24_TARIFF_RESTRICTION'. This code does not mean the integration is broken: the capability is not included in the Bitrix24 account plan, and retrying is pointless until the plan changes.
FIX-0721-7: Versioned deploy runtimes install the advertised version on Ubuntu 24.04
Before
The node20 runtime installed Node.js 18 (Ubuntu 24.04 has no Node 20 package), and python311 plus the RAG runtimes (node20-rag, python311-rag) failed at the install step because the packages are absent from the distribution. The GET /v1/infra/runtimes packages field advertised postgresql-14 while PostgreSQL 16 was installed.
After
node20 now installs Node.js 20 (with a major-version check), python311 installs Python 3.11, and the RAG runtimes install PostgreSQL 16 with the pgvector extension in the application database. The packages field reflects the real version (postgresql-16). Runtime identifiers and the /deploy request format are unchanged.
FIX-0721-8: server repair status is correct when polled
Before
Polling GET /v1/infra/servers/:id/repair-status on a multi-node backend could briefly return idle even while the repair was still running — when the request landed on a different serving node than the one running the repair. A client polling the status in a loop could therefore wrongly conclude the repair had finished before it even started.
After
The endpoint reliably returns the real repair progress (running / done / failed) regardless of which node the poll lands on.
Impact on integrators
The response shape is unchanged and no client action is required.
FIX-0721-9: apps on a standalone server no longer run with administrator privileges
Before
An app deployed to a standalone Black Hole server ran with administrator privileges and no isolation. Any vulnerability in the app itself (remote code execution, for example) immediately meant full control of the whole virtual machine: the server's connection keys, its service configuration and system files.
After
The app runs under a dedicated unprivileged account and sees only its own directory (extractTo, /opt/app by default), which it owns. System directories are read-only to it and privilege escalation is blocked. Ports below 1024 still work — the platform grants the specific permission needed.
Deploy commands (install, preStart) and /exec still run with administrator privileges. Nothing changed there and no sudo is needed.
No action required: if your app genuinely needs administrator privileges to start, the deploy automatically restores the previous mode, finishes successfully and adds a warning explaining why. To skip that attempt outright — relevant for nginx as the start command, MySQL over the root system socket, and Docker-driven starts — pass "hardening": "off" in the deploy body.
Two new step values appear in data.steps[]: service_user for handing the deploy directory to the unprivileged account, and hardening for the warning that the app was restored to the previous mode. Clients that switch on step names should account for them.
BC-0721-10: a broken image_url candidate no longer fails the whole request
Old format supported until: 21.01.2027
Before
A content array could carry several image_url parts. If any one of them held
something other than an image — for example a Base64-encoded HTML error page
declared as image/png — the platform forwarded it to the model unchanged. The
model could not decode it and the entire request failed with 502 and code
ai_provider_unavailable, even when the remaining images were valid.
Separately, parts with an unsupported MIME type, malformed Base64, or above the
20 MiB limit were rejected with 400 invalid_image_payload — also for the whole
request.
After
Before calling the model, the platform inspects the actual content of every
image_url part by its byte signature rather than its declared MIME type. A part
whose content is a web response (HTML, XML, JSON, an HTTP response) or does not
decode is replaced in place with the text placeholder
[image unavailable: <reason>]. The remaining images are processed normally and the
request succeeds.
Positions are preserved: the length of the content array does not change, so any
candidate numbering on your side stays correct.
Rejected parts are visible in the response — warnings gains an entry with code
IMAGE_CONTENT_REJECTED, and the X-Image-Parts-Rejected header carries their
count. For streaming responses the header arrives with the start of the stream.
400 is now returned only for structural errors: a missing url field, a string
that is neither a URL nor a data URI, an unsupported scheme, and http:// in
production.
What integrators should do
If your code relied on 400 invalid_image_payload to detect a rejected image, read
warnings or the X-Image-Parts-Rejected header instead. The request now succeeds,
and no image is dropped silently — every substitution is reflected in the response.
Model-side failures changed too. Previously any non-2xx provider response arrived as
502; now the status reflects the cause:
- a model response of
400or422→400with codeai_provider_rejected. Retrying such a request unchanged will not help; - a model-side rate limit (
429) →429with aRetry-Afterheader. Retry it after the stated delay. A streaming response cannot carry the header, so the delay arrives as aretryAfterfield in the error frame; - a model-side timeout (
408) →503with codeai_provider_timeout.
Responses 401, 403 and 5xx still arrive as 502. This affects
POST /v1/chat/completions and
POST /v1/embeddings.
Model-side errors now additionally carry a providerStatusCode field — the raw
HTTP status of the model response. It tells a model-side 429 apart from the
platform's own rate-limit 429 (which has no such field): both keep the same
rate_limit_exceeded code so SDKs retry uniformly, and the new optional field is
the distinguisher.
Data URIs may now also carry parameters between the type and ;base64 —
data:image/jpeg;name=photo.jpg;base64,... is no longer rejected.
2026-07-20
NEW-0720-1: Application responses now return the publication status
Application responses — list, app data, create, publish, and unpublish — now carry two new fields: catalogStatus (PRIVATE / PUBLISHED / UNPUBLISHED) and publishedAt (the publication date, ISO 8601, or null). Previously the publication status could not be read via V1 — you had to guess from the placements array, which is unreliable: an unpublished application may keep its previously bound codes, and PRIVATE and UNPUBLISHED are indistinguishable by placements. The fields are additive — existing calls keep working.
FIX-0720-2: chat rename no longer reports a false success to a non-participant
Before
PATCH /v1/chats/:chatId called by a portal administrator who is not a member of the chat returned { "success": true, "data": true } even though the chat title never changed: Bitrix24 answered such a call with a false success. The response gave no way to tell it apart from a real rename, so an integrator treated the operation as done. The caller could not even read that chat.
After
Membership is now checked before the rename. If the caller is not a member, the response is 404 CHAT_NOT_FOUND_OR_NO_ACCESS — the same as for any other non-participant — and no rename is attempted. The false success is gone. A rename by a member who has the rights works exactly as before.
NEW-0720-3: streaming chat-completions now ends a stalled upstream response with an explicit error
When a streaming call (POST /v1/chat/completions with stream: true) receives headers but the upstream model then goes silent mid-response and sends no new data for longer than the idle window, the proxy now terminates the call and emits a terminal error frame before data: [DONE]:
data: {"error":{"code":"stream_idle_timeout","type":"server_error","retryable":true,"retryAfter":<seconds>}}
Previously such a call hung indefinitely (the agent stayed in a "receiving stream response" state). The error is retryable — read the stream through to [DONE] and retry, honoring retryAfter. Normal (non-stalled) streams and reasoning models that continuously emit thinking tokens are unaffected.
NEW-0720-4: tasks: new timeSpentInLogs field
The timeSpentInLogs field (actual tracked time in seconds, summed from the time-tracking log) is now declared in the tasks schema — available in select, filter, and sort on GET /v1/tasks and POST /v1/tasks/search, and present in GET /v1/tasks/fields.
Previously the field was returned only when select contained BOTH spellings at once (timeSpentInLogs and TIME_SPENT_IN_LOGS); either one alone now works. The field is read-only — recorded via the time-tracking endpoint, not via a task update.
FIX-0720-5: GET /v1/files/:id?include=folder now returns the folder
Before
GET /v1/files/:id with ?include=folder answered 200 but without the _included block, even though GET /v1/files/fields advertised folder as includable — the include silently did nothing.
After
?include=folder attaches the folder under _included.folder, as /fields promises.
Impact on integrators
No action required. Clients reading _included.folder now get the object instead of nothing.
FIX-0720-6: /v1/me: infra block now accurate for server cap, provider id, and breakdown
Before
GET /v1/me returned infra.limits.max: 3 regardless of the enforced per-key server cap; infra.providers could return an internal provider id (disagreeing with GET /v1/infra/providers) with possible duplicates; infra.limits.breakdown counted managed-bot VMs under direct instead of bots.
After
infra.limits.max reflects the enforced per-key server cap; infra.providers returns the public provider id, consistent with GET /v1/infra/providers, with duplicates removed; infra.limits.breakdown counts bot VMs under bots. No integration change is needed — the values are simply correct now.
FIX-0720-7: deal-categories: unknown filter fields are rejected, sort by id respects direction
Before
GET /v1/deal-categories with a filter on an unknown field silently returned the WHOLE pipeline table with 200 — Bitrix24 ignores unknown filter keys of the legacy method and returns the entire list. And the sort ?sort=id&order=desc ignored the direction, always returning the same order.
After
A filter on an unknown or unsupported field (as well as operator prefixes >/>=/!/… and operator objects) is rejected before the Bitrix24 call with 400 UNSUPPORTED_FILTER; the message lists the filterable fields (id, name, sort). Exact-match and $in filters on those fields work as before. The sort ?sort=id now correctly respects asc/desc.
FIX-0720-8: openline-configs: unrecognised fields in a write body no longer vanish silently
Before
POST /v1/openline-configs (and PATCH) silently ignored unrecognised body fields — Bitrix24 drops unknown keys of the imopenlines.config.* method. A body of only unknown fields then created a configuration with default values and returned 200.
After
If the body has NO known field, the request is rejected with 400 VALIDATION_ERROR before the Bitrix24 call, and the message lists the unrecognised fields. If a known field is present but some fields are unrecognised, the write proceeds as before, and the response carries meta.warnings listing the ignored fields (previously they disappeared without a trace). Read-only fields (id, queue, dateCreate and others) in a write body are now rejected with 400 READONLY_FIELD — previously they passed as "known" and could lead to a configuration created with default values.
FIX-0720-9: GET /v1/{entity}/fields signals partial metadata on a Bitrix24 failure
Before
When the dynamic field fetch to Bitrix24 (*.fields) failed (rate limit, QUERY_LIMIT_EXCEEDED, queue timeout), the endpoint silently returned 200 with only the static schema fields — many of which have no human-readable label. The response looked complete, so a client could not tell it apart from a correct one and built non-deterministic field mappings.
After
On a field-fetch failure the response is still 200 with the static fields, but now carries meta.warnings: [{ "code": "fields_partial", "message": "..." }] so the client can detect the incomplete set and retry. The warning is an object { code, message } — the same channel and shape as meta.warnings on list/search, so a single warning.code parser works across every endpoint. The failure is now also logged on the Vibe side.
2026-07-19
BC-0719-1: Cowork/Code: tier lineup renamed — Free / Pro / Max / Ultra, Ultra price reduced
Old format supported until: 18.07.2026
Before
The tier field in GET /v1/cowork/state and GET /v1/cowork/me responses (as well as recommendation.upgrade.nextTier and the tiers[] catalog) took the values FREE, START, PRO, MAX. The MAX tier (×20) cost 40,000 Ꝟ/mo.
After
The lineup is renamed with a shift: START → PRO, PRO → MAX, MAX → ULTRA; the value set is now FREE, PRO, MAX, ULTRA. Tier multipliers are unchanged: PRO ×1 (base), MAX ×5, ULTRA ×20; FREE is 5% of PRO. The ULTRA price (former MAX, ×20 volume) dropped from 40,000 to 20,000 Ꝟ/mo. Window volumes were recalibrated against real usage: the five-hour window doubled on every tier while monthly volumes were reduced; limits of an already-paid period do not change — the new values apply from the next renewal. The rename applies atomically at release time: the START value is no longer returned, and the ULTRA value was added. Clients branching on the string values of tier / nextTier must update their mapping and mind the meaning shift (PRO is now the base tier, MAX the middle one); clients that render the server-provided multiplier / feeVibes as-is keep working unchanged.
FIX-0719-2: Cowork/Code tier prices on the international version now use the dollar scale
Before
On the international version the tier catalog in GET /v1/cowork/state returned prices on the Russian-version scale: feeVibes 2000 / 10000 / 20000 for Pro / Max / Ultra. At 1 Vibe credit = 1 US dollar this read as 2000–20000 dollars per month.
After
The tier catalog on the international version returns the dollar grid: Pro — feeVibes: 20, Max — 100, Ultra — 200 per month; the three quota windows are scaled consistently, so tier capacity in requests is unchanged. The Russian version is not affected.
Impact on integrators
If your client reads tiers[].feeVibes from GET /v1/cowork/state on the international version, the values dropped ×100 and now match the price actually charged on activation. No code changes are required.
2026-07-18
FIX-0718-1: deploy reuses the application's server instead of creating a duplicate
Before
POST /v1/infra/servers with source always created a new server even when the application that owns the key already had one — a second, idle server was billed. And POST /v1/infra/servers/:id/deploy returned WRONG_KEY when the calling key differed from the one that created the server (for example, an application has both a personal key and an authorization key).
After
When the calling key belongs to an application that already has a live server, POST /v1/infra/servers returns that server with reused: true instead of creating a new one. If the same request carries source and the reused server is a galaxy app (kind: "GALAXY_APP") that has no running container yet (never deployed, or its previous deploy failed), the source is deployed to its own server right away (the response carries reused: true and deploying: true, and the server's status is provisioning while the build runs) — just like an ordinary create with source: poll GET /v1/infra/servers/:id until status is running, no second deploy call is needed. If the galaxy app is already serving, the response carries reused: true and next: "deploy" — deploy your source with a separate POST /v1/infra/servers/:id/deploy call (so the live container is untouched while the build runs). For a plain server, or a request without source, the response carries reused: true and next: "deploy" — deploy your source with a separate POST /v1/infra/servers/:id/deploy call. POST /v1/infra/servers/:id/deploy now accepts any key of the same application and deploys to its server.
Integrator impact
No changes required. Duplicate servers are no longer created. Previously a one-shot create with source against an application's existing server returned next: "deploy" and dropped the supplied source — now the source is deployed right away. Both a deploy and a status read (GET /v1/infra/servers/:id) of the application's server work under any of the application's keys, regardless of which one you call the API with.
FIX-0718-2: DELETE /lock now releases a stuck lock even on a deleted server
Before
DELETE /v1/infra/servers/:id/lock returned 404 NOT_FOUND when the server had been deleted — even though the operation lock remained in the platform's memory and kept the server busy. So the "previous server deleted, lock stuck, next deploy fails with EXEC_BUSY" scenario had no way out: such a lock could not be released via the API.
After
DELETE /lock releases a stuck lock even on a deleted server — as long as it still belongs to your API key (ownership remains the only check; the lock holds no data and no cloud resources). A successful call returns 200 with data.released: true. 404 NOT_FOUND now means only "the server does not exist or belongs to another key".
NEW-0718-3: connector app-install error codes are now also possible on cloud portals (phased rollout)
POST /v1/apps on a cloud portal can now also install the application through the vibecodeconnector module and therefore return the same connector error codes that were previously only possible on self-hosted portals: 403 CONNECTOR_APP_INSTALL_FORBIDDEN (the Bitrix24 portal administrator forbade the user from installing applications), 409 CONNECTOR_MODULE_NOT_INSTALLED (the vibecodeconnector module is not installed on the portal), and 502 CONNECTOR_APP_INSTALL_FAILED (other install failures). The change is additive: the successful-install response is unchanged, and the rollout is phased — on most cloud portals the install path is unchanged for now. Clients that already handle these codes on self-hosted portals need no change; clients that did not should add handling.
2026-07-17
FIX-0717-1: deploying to a sleeping galaxy host now answers before client timeouts
Before
POST /v1/infra/servers/:id/deploy on a sleeping shared host held the connection open for up to ~6.5 minutes while the host woke. HTTP clients with a typical response-headers timeout (~300 seconds — the Node fetch default) dropped the connection before the platform answered: the deploy surfaced as an opaque fetch failed network error with no error code and no recovery steps. A failed wake also put the host back to sleep, so every retry restarted the host boot from zero.
After
The platform wakes the host in the background and waits for it to connect for at most ~4 minutes. If the host connects in time, the deploy completes in one call as before. If not, the platform immediately returns a retryable 502 GALAXY_HOST_UNREACHABLE with a hint (re-send the same request in 1-2 minutes, do not delete the slot), and the host keeps booting in the background — a re-sent deploy joins the boot already in progress instead of restarting it. deployment.galaxyApp._rules and deployment.standalone._rules (GET /v1/me) now recommend an HTTP client timeout of at least 690 seconds — strictly above the platform's 660-second window.
Impact on integrators
No request changes are required. If a deploy to a sleeping galaxy host previously ended for you in a network error with no platform response, you will now get either a success or a 502 with retry instructions.
NEW-0717-2: server user search explains an empty list
The GET /v1/infra/servers/:id/b24-users endpoint now returns an additional hint field when the list is empty because there is no Bitrix24 access — the app is not yet authorized on the account, or the key was revoked. Existing calls are unaffected: the field is additive and absent on a successful response.
NEW-0717-3: the catalog field references now report nullable fields
The GET /v1/catalog-prices/fields, GET /v1/catalog-sections/fields and GET /v1/catalog-products/fields references now add "nullable": true to fields that can return null: for prices these are quantityFrom, quantityTo and extraId, for sections — iblockSectionId, xmlId, code and description, for products — iblockSectionId, code, weight, purchasingPrice, purchasingCurrency, quantity and quantityReserved. The same flag arrives in data.entities[].fieldsDetailed of the GET /v1/guide response, which an OAuth-application key reads without a session, and in the machine-readable GET /v1/openapi.json spec such fields are described with a union type like ["number", "null"]. A client building a typed model from the reference now gets the correct nullability and no longer breaks on the first null. The field set, the types and the response values are unchanged.
NEW-0717-4: EXEC_BUSY tells you when to retry
The 409 EXEC_BUSY response (another operation holds the server lock) on POST /v1/infra/servers/:id/exec and POST /v1/infra/servers/:id/deploy now carries a retry hint: the Retry-After response header (in seconds) plus two new error-body fields — retryable: true and retryAfter (in seconds). The retryAfter value is a short poll interval (keep retrying with it until the lock clears), not the full time until the lock auto-expires; the full upper bound stays in error.hint.autoExpiresInSeconds. The change is additive: the code, message, and hint are unchanged, so clients matching on error.code keep working without changes.
FIX-0717-5: deploying a galaxy app with an oversized archive returns 413, not "host unreachable"
Before
POST /v1/infra/servers/:id/deploy with a source.content larger than the upload limit returned 502 GALAXY_HOST_UNREACHABLE — a message that blamed an unreachable host and advised retrying "once it reconnects". The host was fully reachable, and re-sending the same archive failed identically: the integrator was stuck in an endless loop of useless retries.
After
The same case now returns 413 GALAXY_UPLOAD_TOO_LARGE with a structured error.hint. The cause is deterministic (the archive is too large), not transient, so re-sending it unchanged will not help. The hint advises shrinking the archive — exclude node_modules, .git and build artifacts (the platform installs dependencies on the host). A galaxy application accepts the inline source.content only (source.url is rejected with 400 GALAXY_DEPLOY_CONTENT_ONLY), so shrinking the archive is the only recovery. A separate adjacent case: a request body exceeding the platform's hard outer limit (500 MB on the base64 body ≈ ~375 MB binary archive) is now rejected at the edge with a coded 413 PAYLOAD_TOO_LARGE (on the Vibe-REST /v1/ routes — deploy/upload/create; OpenAI-compatible AI routes return their error in their own envelope) instead of raw HTML — previously the client got an undecodable response.
Impact on integrators
No change is required: successful deploys are unaffected. Clients that branched on the error code for this failure now see an honest 413 GALAXY_UPLOAD_TOO_LARGE instead of the misleading 502 GALAXY_HOST_UNREACHABLE — the latter remains for a genuine tunnel drop during the build.
NEW-0717-6: displayName and description on deploy set the Bitrix24 catalog card
POST /v1/infra/servers/:id/deploy accepted two new optional body fields — displayName and description. POST /v1/infra/servers (server creation) accepted an optional description. The values become the app's name and description on the Bitrix24 catalog card. If a deploy omits displayName and description, the response includes a warnings: string[] entry nudging the client to set them; the one-shot galaxy create-and-deploy (a POST /v1/infra/servers body carrying source) likewise returns warnings in its 201 response when the fields are absent. Backward-compatible — requests without the new fields keep working as before.
FIX-0717-7: galaxy deploy returns 503 on transient database overload
Before
On a brief database connection-pool exhaustion during a galaxy app deploy, POST /v1/infra/servers/:id/deploy returned a generic 502 GALAXY_APP_DEPLOY_FAILED — the same code as a real build failure. A client could not tell a transient overload from a terminal error and often treated the response as final.
After
A transient database overload is now shed as 503 POOL_EXHAUSTED with a Retry-After header (seconds to wait before retrying). A genuine build failure stays 502 GALAXY_APP_DEPLOY_FAILED.
Integrator impact
No action required. If your client retries, a 503 now carries an explicit backoff signal via Retry-After instead of an opaque 502.
FIX-0717-8: filtering and sorting of the task comment list work on both cards
Before
A GET /v1/tasks/:taskId/comments request with a filter parameter or with a sort other than ID on a Bitrix24 account with the new task card returned 200 and an empty list, even when the task did have comments. No error arrived, so there was no way to tell "nothing matched the filter" from "the filter did not work".
After
Such a request returns the matching comments. Filtering and sorting work by the fields ID, AUTHOR_ID and POST_DATE, and in a filter the field name may carry a !, >, >=, < or <= prefix. Filtering by AUTHOR_NAME and sorting by AUTHOR_NAME or AUTHOR_EMAIL on the new card answer with 400 and the code UNSUPPORTED_FILTER_FIELD or UNSUPPORTED_SORT_FIELD and point at AUTHOR_ID, on the old card those fields are still accepted. An offset parameter is added — it is honoured on the new card for a request with filter or a sort other than ID, and ignored on the other read paths. The INVALID_FILTER code now also arrives when filter is a scalar or an empty array instead of an object (0, false, "", []), the value of the field ID or AUTHOR_ID is not a number, the value of POST_DATE does not parse as a date, or a field value is an object or an array instead of a scalar. A truncated field with the value true is added to meta — the scan window of the history was exhausted, and some comments stayed beyond its edge.
Impact on integrators
Nothing to change: a request that used to hand back an empty list starts handing back data. Mind three edges. First — filtering by AUTHOR_NAME and sorting by AUTHOR_NAME or AUTHOR_EMAIL on a Bitrix24 account with the new card now answer with 400 instead of an empty 200, move such a request to AUTHOR_ID, an employee identifier by name comes from GET /v1/users. Second — meta.total for a filtered request against the new card counts the matching comments within the scanned window rather than across the whole task history, and with meta.truncated: true it is an incomplete number. Third — the value of POST_DATE on the new card is compared against createdAt in UTC, so a result on a day boundary may differ from the selection on the old card. On Bitrix24 accounts with the old card the behaviour is unchanged.
2026-07-16
BC-0716-1: json_object on a reasoning model recovers JSON; the 422 body is refined
Old format supported until: 15.01.2027
Before
A POST /v1/chat/completions request with a json_object response_format on a reasoning model (e.g. bitrix/bitrixgpt-5.5-agent) consistently returned 422 structured_output_truncated, even when the model finished on its own (finish_reason: "stop") and placed a complete valid JSON into the service reasoning_content channel — the answer was lost. Every such error body carried error.suggestedMaxTokens and error.param, and the text claimed "finish_reason=length" regardless of the real stop reason.
After
If a model on json_object finished on its own and valid JSON sits in reasoning_content, the platform recovers it and returns 200 with that JSON in content (in streaming mode — as a content chunk before the terminal finish_reason chunk). The 422 body is now truthful: error.suggestedMaxTokens and error.param are present only on a genuine truncation (finish_reason: "length"); on any other stop reason (stop, etc.) those fields are omitted and the text names the actual finish_reason. json_schema behaviour is unchanged — there the strict schema is enforced model-side.
What to do
Nothing, if you handle 422 by error.code. If your code unconditionally reads error.suggestedMaxTokens or error.param on a structured_output_truncated error, make the read optional: those fields are now absent when finish_reason !== "length". Streaming clients using response_format should accumulate content across all deltas up to data: [DONE].
FIX-0716-2: env sent as a file in a multipart deploy is no longer silently ignored
Before
In multipart/form-data for POST /v1/infra/servers/:id/deploy, an env field sent as a file or Blob was silently ignored — the deploy reported success but the app started without environment variables.
After
Such a request returns 400 with code VALIDATION_ERROR and a hint to send env as a text field containing a JSON string.
Impact on integrators
The correct approach (a text env field with a value like {"KEY":"value"}) is unaffected. Anyone who sent env as a file or Blob now gets an explicit error instead of a false success.
FIX-0716-3: large source.content deploys no longer fail with Gateway HTTP 413
Before
On some accounts POST /v1/infra/servers/:id/deploy with source.content (a base64 archive) failed at the download step with { "code": "DEPLOY_FAILED", "message": "Gateway HTTP 413", "step": "download" } whenever the base64 body exceeded ~1 MB (about 768 KB of raw tar.gz) — despite the documented 500 MB limit. The workaround was uploading via source.url.
After
The 500 MB inline-upload limit (source.content and multipart) applies on all accounts. source.url keeps working as before.
BC-0716-4: POST /v1/infra/servers rejects unknown body fields
Old format supported until: 15.01.2027
Before
An unknown field in the request body was silently ignored. A request with deployMode: "STANDALONE" (a non-existent field) returned 201 and created a galaxy app instead of the expected dedicated server — the real field is placement: "dedicated".
After
POST /v1/infra/servers rejects a body that carries an unknown field with 400 UNKNOWN_PARAM. details.unknownFields lists the extra fields, details.suggestions proposes the correct name (deployMode → placement), and details.validParams is the full list of accepted fields.
What integrators should do
Remove fields that are not in the create parameter list, or fix the typo using details.suggestions. The placement model is set via the placement field (auto by default, dedicated for a dedicated virtual machine).
NEW-0716-5: /v1/sites/fields now describes the allowed values of the type field
GET /v1/sites/fields now returns the allowed values of the type field in type.enum with labels: PAGE (landing), STORE (online store), KNOWLEDGE (Knowledge Base 2.0), plus VIBE (a site from the constructor) and SMN (a link with the "Site Management" module). The VIBE and SMN values appear in responses only and are read-only — a site of that type cannot be created through the API.
NEW-0716-6: Task favorite and pin without edit permission
Four new endpoints for per-user task actions that Bitrix24 allows with read access only (not edit): POST /v1/tasks/:taskId/favorite adds the task to favorites, DELETE /v1/tasks/:taskId/favorite removes it, POST /v1/tasks/:taskId/pin pins the task in the current user's task list, and DELETE /v1/tasks/:taskId/pin unpins it. Previously the only way to change a task was PATCH /v1/tasks/:id, which requires edit permission and returned "No access to edit the task", so an allowed per-user action was unreachable.
NEW-0716-7: preemptible-plan advisory in scheduled-wake responses
The POST /v1/infra/servers/:id/wake-schedules and PATCH /v1/infra/servers/:id/wake-schedules/:scheduleId responses now also carry two additional top-level fields: preemptibleAdvisoryCode and preemptibleAdvisory. When the server runs on a preemptible plan, preemptibleAdvisoryCode is "PREEMPTIBLE_BEST_EFFORT" and preemptibleAdvisory is a short English explanation of the same fact: waking such a server by the window's moment is not guaranteed, and the window may be skipped when no free capacity is available. For a server on a non-preemptible plan, both fields are null. The advisory does not block window creation or updates — it follows the same non-blocking pattern already used by the tzWarning/tzWarningCode fields. Existing integrations that don't read the new fields keep working unchanged.
2026-07-14
NEW-0714-1: OpenAPI spec: 3.1 validity, field semantics, and a scope slice
Before
The machine-readable spec GET /v1/openapi.json carried the 3.0 nullable keyword (invalid under 3.1), left {entityTypeId} undeclared on batch/aggregate/fields/products, shipped no property descriptions/allowed-values/examples, described only vibe_app_ keys, and served one monolith.
After
The spec is valid under OpenAPI 3.1: nullable fields use the union type ["<type>","null"] and every path parameter is declared. Properties now carry title/description, allowed values (x-enumValues plus an in-description decode), and examples. Security schemes name all three key families (vibe_api_/vibe_app_/vibe_live_), every operation carries x-required-scope, and the root exposes an x-scopes catalog. Root tags/externalDocs, a webhooks block for bot events, and a multifield anyOf were added. GET /v1/openapi.json?scope=<scope> (e.g. ?scope=crm) returns a single-scope slice so it fits an agent context window. Pointers to the spec were added to /v1/me and /v1/guide. A full port of per-operation curl examples is a separate follow-up.
FIX-0714-2: Generic 5xx errors on AI endpoints now use the OpenAI-compatible envelope
AI endpoints (/v1/ai/*, /v1/models, /v1/chat/*, /v1/audio/*) are documented with an OpenAI-compatible error format. Pool-exhaustion errors already followed this, but a generic (unexpected) 5xx on these endpoints returned the plain V1 envelope instead.
Before
A generic 5xx on an AI endpoint: {"success": false, "error": {"code": "...", "message": "..."}} — not the shape OpenAI SDK clients expect for these paths.
After
The same case now returns {"error": {"message": "...", "type": "...", "code": "..."}} — one envelope for every error on AI endpoints, including generic 5xx.
Impact on integrators
Clients on the OpenAI SDK that already read error.type/error.code (the standard path for these endpoints) see no change. Code that expected top-level success/error.code specifically on a generic 5xx from an AI endpoint should switch to error.type/error.code.
FIX-0714-3: `versions` in the app source-history list is capped at 500 entries
GET /v1/apps/:id/sources returned the entire version history with no limit — for apps with very long histories this was an unbounded read.
Before
data.versions — the entire version list with no size limit; data.totalVersions always equaled data.versions.length.
After
data.versions holds at most the 500 most recent versions (by savedAt, descending). data.totalVersions and data.totalSizeBytes are still computed over the full set — aggregate accuracy doesn't depend on the cap.
Impact on integrators
For apps with up to 500 versions of history, behavior is unchanged. For apps with more than 500 versions, data.versions.length may now be smaller than data.totalVersions — code that relied on them being equal should use data.totalVersions/data.totalSizeBytes for aggregates and not treat data.versions as the complete list.
FIX-0714-4: Telephony: `userId`/`duration` are now genuinely validated, not just checked for truthy
POST /v1/calls/register, /v1/calls/:callId/show, /v1/calls/:callId/hide, /v1/calls/:callId/finish accepted userId (and duration on finish) without checking its type or shape — any truthy value (for example the string "abc" or an object) was forwarded to Bitrix24 and failed there with an opaque upstream error.
Before
{"userId": "abc"} (or any other truthy value that wasn't a positive integer) passed validation and was sent to Bitrix24; the Required: userId (number) error only appeared for a fully empty/falsy value.
After
userId is accepted as a positive integer or a numeric string ("42"), otherwise a clean 400 MISSING_PARAMS with the refined text Required: userId (positive integer) (plus phoneNumber for register). duration on finish follows the same rule — a non-negative number or a numeric string, otherwise 400.
Impact on integrators
Correct calls (userId as a number or numeric string) are unchanged. Calls that previously "got through" with an invalid userId/duration (not a number, not a numeric string) now get an explicit 400 instead of an opaque Bitrix24-side error.
FIX-0714-5: Creating a comment on a missing task — an explicit error instead of a false success
POST /v1/tasks/:taskId/comments and its batch counterpart POST /v1/tasks/:taskId/comments/batch (action: create) call Bitrix24 to create a comment on the legacy task card. If the task doesn't exist or isn't accessible to the key, Bitrix24 doesn't create the comment and returns no identifier.
Before
Both endpoints responded with success and an empty identifier — the single call as 201 {"success": true, "data": {"id": null}}, the batch item as {"success": true, "id": null}. No comment was created, but the integrator couldn't distinguish this from the normal case.
After
The single call now returns 404 TASK_NOT_FOUND. The batch variant marks the corresponding item as {"success": false, "error": "TASK_NOT_FOUND"} without cancelling the rest of the batch. A separate, unrelated case is unchanged: on the new task card the comment goes through chat, and if the system couldn't recover its id via a follow-up search, the response is still success: true with id: null — the comment was genuinely created in that case.
Impact on integrators
Code that checks data.id / data[i].id for null as an error signal keeps working unchanged and now gets a more precise error code. Code that relied on a silent success with id: null for an inaccessible task should handle 404 / TASK_NOT_FOUND explicitly.
FIX-0714-6: Rate limit for `/v1/search`, `/v1/research`, `/v1/batch` is now per-portal
Before
The limits (/v1/search 60/min, /v1/research 20/min, /v1/batch 30/min) were effectively keyed by source IP, not by portal. A portal using multiple API keys (or several portals behind one shared egress IP) could exceed the documented cap, and the limit was bypassable by IP rotation.
After
The limit is now keyed per portal: all API keys of one portal share a single bucket (60 / 20 / 30 requests per minute respectively). The documented per-tenant cap is now enforced correctly and cannot be bypassed by using more keys or rotating IPs.
Integrator impact
If your portal spread /v1/search / /v1/research / /v1/batch traffic across several API keys, the effective ceiling is now the single portal limit, not the sum across keys. On exceeding it you get 429 with a Retry-After header (as before).
FIX-0714-7: Calendar: working section batch-delete, clean update errors, no leaked sync fields
Before
POST /v1/calendar-sections/batchwithaction: "delete"had no channel for thetype/ownerIdthatcalendar.section.deletemandates — every item failed on both platforms.PATCH /v1/calendar-sections/{id}withouttype/ownerId/nameforwarded to Bitrix24 and returned a raw422leaking the internal method name.GET /v1/calendar-sectionsreturned the undocumented raw Bitrix24 fieldsGAPI_CALENDAR_ID,CAL_DAV_CON,SYNC_TOKEN,PAGE_TOKEN,EXTERNAL_TYPE(three of them sync tokens).GET /v1/calendar-eventsandGET /v1/calendar-events/{id}leaked the internalattendeesEntityListfield — the schema tried to strip it but no-op'd on a key-casing mismatch.
After
- Section batch-delete reads
type/ownerIdfrom the body alongsideidsand threads them into every delete command. A missing anchor is a clean400 MISSING_REQUIRED_PARAMSbefore the Bitrix24 call. - Section partial-update requires the
type/ownerId/nameanchors (sections have no get-by-id to backfill) — a clean400, no raw422, no leaked method name. - Both calendar read paths strip the listed internal/sync fields from the response.
Affected endpoints:
POST /v1/calendar-sections/batchPATCH /v1/calendar-sections/{id}GET /v1/calendar-sectionsGET /v1/calendar-events
FIX-0714-8: PATCH catalog-product-properties works again (was an inescapable catch-22)
Before
Updating a product property was impossible under any body: PATCH without iblockId → 422 ("Required fields: iblockId" — B24 mandates it on every update), and PATCH with iblockId → 400 READONLY_FIELD (create-only field). The entire UPDATE verb was dead — no field could be changed after create.
After
iblockId is now carried over automatically from the existing record (a pre-fetch, like catalog-sections), so PATCH {name:"…"} reaches B24 with the required iblockId and returns 200. You still don't send iblockId in the body (and it is still rejected as read-only if you do) — the service supplies it.
Integrator impact
If your product-property PATCH always failed 422/400, now send only the fields you're changing (PATCH {name:"…"}); iblockId is not required.
FIX-0714-9: entityTypeId validation: junk forms → 400 instead of silent truncation
Before
Five surfaces parsed entityTypeId (the smart-process / dynamic-entity TYPE selector) leniently — with unanchored parseInt or coercing Number() — and a junk form silently turned into a DIFFERENT (within-portal) entity type:
- The path
entityTypeId(/v1/items/:entityTypeId/...,/v1/categories/:entityTypeId/...— CRUD and/aggregate):GET /v1/items/1058abctruncated to1058,1e3→1,1.5→1. - Global
POST /v1/batch:params.entityTypeIdviaNumber()accepted fractions (1.5), hex ('0x10'→ 16), overflow toInfinity, plus the array form[1058]→ 1058. /v1/items/:entityTypeId/userfields/*: its own parser —2abcresolved the userfields of type2.POST /v1/smart-processes/batch:ids: ['1030abc']truncated to1030— delete/update silently ran against a REAL, DIFFERENT type; a fractional number (1030.5) passed too.POST /v1/triggers/fire(entityType="item"):entityTypeId: '1038abc'→ the automation trigger fired against type1038.
After
All five surfaces require the canonical positive-integer form (/^[1-9]\d*$/ for strings, Number.isInteger for numbers): any other form → 400 with each surface's existing error code (INVALID_DYNAMIC_PARAM / INVALID_ENTITY_TYPE_ID / BATCH_ITEM_VALIDATION / MISSING_PARAMS) BEFORE any Bitrix24 call. Aggregate now shares the CRUD routes' validator instead of an inline copy.
Integrator impact
Forms that previously coerced to a correct value and were served — 007 → 7, %20-spaces, +2 → 2, the array form [1058] in batch — are now rejected with 400 as well: the value must be a canonical integer with no prefixes, suffixes or leading zeros. A boolean in batch was rejected before too (it coerced into a reserved type); only its error code changes — now INVALID_DYNAMIC_PARAM. Correct calls are unchanged.
FIX-0714-10: Reopening feedback clears the resolution fields
Before
PATCH /v1/feedback/:id (and the admin endpoint PATCH /api/platform/feedback/:id, which the admin UI reopens through) moving a ticket back to an active status (NEW, REVIEWING, AWAITING_USER, NEEDS_REVIEW) from RESOLVED/WITHDRAWN did not reset resolvedAt, resolvedBy, or resolution — they lingered from the prior close, so a reopened ticket looked both active and resolved.
After
Moving a ticket out of RESOLVED/WITHDRAWN into an active status clears resolvedAt, resolvedBy, and resolution. RESOLVED/WITHDRAWN still set the resolution stamp; ARCHIVED leaves the fields untouched (archiving preserves the resolution history). A plain transition between active statuses (e.g. AWAITING_USER → REVIEWING) leaves the fields alone — on active tickets resolution mirrors the last team comment. An explicit resolution in the same request still wins over the clear.
Integrator impact
If you read resolvedAt/resolution on a reopened ticket and got the prior close's values, they are now null for an active ticket.
FIX-0714-11: `INVALID_JSON_BODY` no longer quotes the engine parser text
Before
Six route groups (/api/billing/*, /v1/apps*, /v1/bots*, /v1/keys*, /v1/note*, /v1/infra/servers/* deploy/exec/upload) answered malformed JSON with 400 and a message like Invalid JSON: Unexpected token } in JSON at position 41 — raw V8 engine text (a runtime fingerprint and an implementation detail). The deploy/exec/upload group set no error code at all.
After
All six now return the single static message Request body is not valid JSON. — matching /v1/<entities> (the same class is closed there by a separate fix). On V1 surfaces the code is INVALID_JSON_BODY (deploy/exec/upload now sets it too); the 400 status is unchanged.
Integrator impact
If your code parsed the message text (e.g., extracted the error position), rely on the INVALID_JSON_BODY code instead; the position is no longer reported.
FIX-0714-12: Quote GET response returns amount, currency, and dates again (were null)
Before
Reading a quote (GET/list/search /v1/quotes) returned null for amount, currency, beginDate, closeDate — the values leaked only under the raw Bitrix24 keys (opportunity, currencyId, begindate, closedate). Writes worked, but the READ projection dropped every aliased field: a quote's total and currency were 100% invisible via the documented API.
After
The READ branch now reverse-maps the declared aliases (mirroring the write mapping): opportunity → amount, currencyId → currency, begindate → beginDate, closedate → closeDate, with type coercion. The raw Bitrix24 keys no longer appear in the response.
Integrator impact
If you read a quote's amount/currency and got null, they are now populated. Code that worked around it by reading the raw opportunity/currencyId from the response will no longer find them there — switch to the documented amount/currency.
FIX-0714-13: Write-path validation: phantom checklist → 404, garbage types and junk `:id` → 400
Before
POST /v1/tasks/:taskId/checklistagainst a nonexistent task returned201with a plausibleid, yet nothing was created (the item was never GETtable).POST /v1/warehousesaccepted non-stringtitle/address(numbers, objects) and forwarded them to Bitrix24 with unpredictable results;POST /v1/doc-templateslikewise passed non-stringname/regionand non-numericnumeratorIdthrough.- A non-numeric
:idon entities with a typed numeric id (GET/PATCH/DELETE /v1/quotes/abc,/v1/deals/1.5,/v1/leads/1e3) went to Bitrix24 verbatim — returning an opaque B24 error instead of a clear code. On smart-processes12abcwasparseInt-truncated to12and hit the WRONG type.
After
- Checklist: the parent task is verified before the item is created; a missing (or invisible-to-the-key) task →
404 TASK_NOT_FOUND. - Warehouses and document templates: a wrong-typed value →
400 INVALID_PARAMSwith no Bitrix24 call (a numeric string innumeratorIdis still accepted). - Entities with an explicitly typed numeric id: a non-canonical-integer
:id→400 INVALID_PARAMSbefore any Bitrix24 call (id0— the main deal pipeline ofcategories— stays valid). Smart-processes keepINVALID_ENTITY_TYPE_IDand now reject12abcon GET/PATCH/DELETE instead of truncating it to12. Entities whose id type is not declared in the schema keep their prior pass-through behavior.
Affected endpoints: POST /v1/tasks/:taskId/checklist, POST /v1/warehouses, POST /v1/doc-templates + GET/PATCH/DELETE on entities with a typed numeric id.
Integrator impact
If your code relied on the phantom checklist 201 or sent garbage-typed values hoping for the best, you will now get an explicit 4xx with a code. Correct calls are unchanged.
FIX-0714-14: Five silent false-success / hint defects: an honest response instead of a fake success
Before
PATCH /v1/userfields/{entity}/{id}withlabelwas a silent no-op on update:200, but the field label never changed (Bitrix24crm.*.userfield.updateignoresLABEL).POST /v1/humanresources/nodes/{id}withparentIdfaked a successful reparent:namewas applied,parentIdwas silently ignored, and the response echoed the stale parent.POST /v1/chats/messages/bulkdid not resolve thedialogId: "me"alias inside the bulk loop (single routes do) → messages went to the wrong dialog.POST /v1/bots/{botId}/chats/{dialogId}/users— theUSERS_NOT_ADDEDsafety-net was dead code (it never matched the v2 method's response shape) → a failed add passed as success.crm.item.listerrors received an irrelevant "Maximum 50 records…" hint even when the cause was something else (e.g. "entity type does not exist").
After
labelon update fans out to the realEDIT_FORM_LABEL/LIST_COLUMN_LABEL/LIST_FILTER_LABELparams — the label actually changes.parentIdandtypeare now create-only: onPATCHthey are rejected with400(reparent viaPOST /v1/humanresources/nodes/{id}/move) instead of faking success.- Bulk message reads resolve
dialogId: "me"per item, like the single routes. - The bot-chat add safety-net fires again: users that were not added come back in
warning.USERS_NOT_ADDED. - Known-limitation hints are gated on the error message's relevance, not the method name alone.
Affected endpoints:
PATCH /v1/userfields/{entity}/{id}PATCH /v1/humanresources/nodes/{id}POST /v1/chats/messages/bulkPOST /v1/bots/{botId}/chats/{dialogId}/users
FIX-0714-15: GET /v1/task-time now honestly returns more than 50 rows when limit>50
Before
GET /v1/task-time with a limit above 50 returned only 50 rows even though meta.limit echoed the requested value and meta.hasMore could mislead. A client paginating with a step above 50 silently lost rows.
After
The request now returns up to limit rows (max 500), collected page-by-page on the backend; meta.total and meta.hasMore match the window actually returned. With a limit above 50, offset now points at the correct position instead of shifting onto the first pages.
NEW-0714-16: GET /v1/companies/fields and catalog-prices system fields now carry label and description
GET /v1/companies/fields now returns human-readable label and description for every company field. GET /v1/catalog-prices/fields adds the same metadata to the system fields extraId, priceScale and timestampX. Labels come in English. A field's meaning can now be read programmatically from the response instead of cross-referencing static documentation.
FIX-0714-17: /stop and /reboot distinguish a missing server from a wrong status
Before
POST /v1/infra/servers/:id/stop and POST /v1/infra/servers/:id/reboot on a server that was not in running status (e.g. sleeping) returned a flat 404 NOT_FOUND reading "Running server not found" — from which you could not tell the server still existed, so an agent concluded it had been deleted.
After
Both routes now behave like /start and /wake: 404 SERVER_NOT_FOUND only when no server with this id exists; 422 SERVER_WRONG_STATE when the server exists but is not in running status. error.currentState carries the current state and error.availableActions lists the actions available now (wake/start/repair/delete).
FIX-0714-18: clearer /fields error for task comments and task time entries
Before
GET /v1/tasks/:taskId/comments/fields returned a confusing 400 INVALID_PARAMS reading "taskId and id must be positive integers" (you asked about fields, the answer was about an id), and GET /v1/tasks/:taskId/time/fields leaked a raw Bitrix24 error exposing an internal PHP method and an HTML tag.
After
Both entities recognise the fields segment and return a clear 400 WRONG_PATH: they have no /fields method (the field schema is documented in /v1/guide), and the message lists the valid routes. A non-numeric or fractional id on the by-id routes is now rejected as 400 INVALID_PARAMS before the Bitrix24 call — no internal error leaks out.
FIX-0714-19: `/v1/ai/credentials*` rate limits are now per-portal; `CREDENTIAL_NOT_FOUND` carries a hint
Before
- The BYOK route limits (
POST /v1/ai/credentials,/:id/test,/:id/fetch-models— 10/min;/:id/modelsadd/delete — 30/min) were effectively keyed by source IP: credential probing was bypassable by IP rotation, and tenants behind one shared egress IP shared a bucket.PATCH /:id(which verifies the key upstream whencredentialsis sent — the same oracle as/:id/test) had no limit at all. - The
404 CREDENTIAL_NOT_FOUNDfrom/v1/searchand/v1/researchcarried only the provider slug — no pointer to how to configure a key.
After
- The limit is keyed per portal: all API keys of one portal share a single bucket; IP rotation and key count no longer affect the cap. On exceeding it you get
429withRetry-After. AdditionallyPATCH /:id(which verifies the key upstream, like/:id/test) previously had NO limit — it is now also 10/min per portal. - The
CREDENTIAL_NOT_FOUNDresponse now includes ahintfield with the exact recipe:POST /v1/search/credentials {provider, apiKey}; provider list —GET /v1/search/providers.
FIX-0714-20: PATCH for bizproc templates, robots and activities via /v1 now applies changes
Before
PATCH /v1/bizproc-templates/:id, /v1/bizproc-robots/:code and /v1/bizproc-activities/:code with metadata fields (name, description, autoExecute) returned 422 BITRIX_ERROR "No fields to update." — updates were impossible (the same in batch requests). An autoExecute value sent as a number was additionally rejected as Incorrect field AUTO_EXECUTE!.
After
Fields are applied correctly (including autoExecute sent as a number); the endpoint confirms success and returns the id of the updated entity. Both single PATCH and batch requests work. Creation (POST) is unchanged.
FIX-0714-21: Transcription: wallet check before the recognition call
Before
POST /v1/audio/transcriptions (and /v1/ai/audio/transcriptions) did not check the wallet before calling the upstream: a PREPAY account past its overdraft (but not yet frozen by the background sweep) still triggered recognition and slid the balance deeper negative. Chat and embeddings already rejected such calls up front; a fully frozen account was always blocked globally (ACCOUNT_FROZEN).
After
Same as chat and embeddings: the wallet check runs before the Whisper call. An exceeded overdraft → 402 insufficient_balance, recognition never starts. BYOK keys (USER scope) are free — no check, no behavior change.
FIX-0714-22: app deletion is no longer blocked by a galaxy host on its key
Before
DELETE /v1/apps/:id returned 409 APP_HAS_ACTIVE_SERVERS when a galaxy host (shared account infrastructure) happened to sit on the application's key. The app could not be deleted, and the response gave no explanation.
After
A galaxy host is excluded from the blocking-servers check: it is managed at the account level, not the key level, so it must not block app deletion. Standalone application servers (including application containers) still block deletion with 409 APP_HAS_ACTIVE_SERVERS — rebind them to another key first.
FIX-0714-23: OpenAPI: per-entity batch endpoint body is now documented correctly (action + items/ids/calls)
Before
The spec (GET /v1/openapi.json) documented every per-entity batch body as {create:[], update:[], delete:[]}. The runtime (shared batch handler) requires {action, items|ids|calls} and returns 400 INVALID_BATCH_ACTION for the documented shape. A client generated from the spec (codegen / AI agent) got 100% batch-write failure across all ~45 per-entity batch endpoints. The feature itself works — only the spec was wrong (the global POST /v1/batch, /v1/tasks/{taskId}/comments/batch, and /v1/guide already documented the correct shape).
After
The spec generator emits the correct shape: a single action (create/update/delete/list/get/fields); create/update send items, delete sends ids, reads send calls. Matches the runtime and the global /v1/batch.
Integrator impact
If you generated a client from openapi.json and batch-write failed with INVALID_BATCH_ACTION, regenerate it: the body is now {action:"create", items:[…]} instead of {create:[…]}. Hand-written clients that already sent {action,…} are unaffected.
FIX-0714-24: Currencies: fullName, format, and decimals now persist on a flat write
Before
POST/PATCH /v1/currencies with flat fullName, formatString, decimals, decPoint, thousandsSep returned success but silently dropped the values — Bitrix24 stores them per-language (LANG) and the API sent them flat. The documented workaround "send a raw LANG" did not work either: LANG is a read-only field, so the request was rejected.
After
The API packs the flat localizable fields into your language's localization (the API key's language) before the Bitrix24 call, so a flat write persists and reads back (POST {fullName:"…",decimals:3} → GET returns them). This works on every write path: single-route, POST /v1/currencies/batch, and the global POST /v1/batch. A raw LANG in the body is still rejected as read-only. Setting different values for several languages at once via the API is not yet supported. The write language is the API key's locale (ru or en) and may differ from the currency's display language on the portal — on portals with another locale (de/pl/ua…) the edit lands under en.
Integrator impact
If you worked around the bug with a raw LANG (and hit 400 READONLY_FIELD), drop it and send the flat fields. Flat requests that already worked now also persist the values.
FIX-0714-25: Aggregate enforces required filters; infra validation no longer leaks raw Zod
Before
POST /v1/<entity>/aggregateon an entity that mandates a filter (e.g.catalog-productsneedsiblockId) let an empty request reach Bitrix24 and returned a raw422, while GET-list/search return a clean400on the same condition.POST /v1/infra/servers/:id/{deploy,exec,upload,logs}put a multi-line JSON-serialized Zod issue array intoerror.messageon a body-validation error (a raw validator fingerprint).
After
- Aggregate checks required filters/params before the Bitrix24 call: a missing required filter →
400 MISSING_REQUIRED_FILTER(e.g.catalog-products→iblockId); a missing required list-param →400 MISSING_REQUIRED_PARAMS(e.g.calendar-events→type,ownerId;humanresources-nodes→type). Like list/search. - Infra validation formats the error compactly (
field: message; …), matching the siblinginfra.ts. The code (VALIDATION_ERROR) and400status are unchanged.
Integrator impact
If you caught a raw 422 from a filter-less aggregate, you now get 400 MISSING_REQUIRED_FILTER. If you parsed infra error.message as JSON, it is now a flat field: message string.
FIX-0714-26: Batch: per-entity batch works for items, and folder creation via batch
Before
POST /v1/items/{entityTypeId}/batchreturned404— the per-entity batch route for dynamic-param entities (items,categories) was mounted at the param-less path (/v1/items/batch), so the documented path didn't resolve and theentityTypeIdnever reached the Bitrix24 command. The globalPOST /v1/batchfallback worked.POST /v1/folders/batchwithaction: "create"failed every item withERROR_ARGUMENT: batch-create sentfields[...], butdisk.folder.addsubfolderexpects the parent folder as a top-levelidand the rest underdata[...].
After
- The per-entity batch route for
items/categoriesis mounted with the:{entityTypeId}segment and threads the validatedentityTypeId(positive integer; dedicated-API ids likedeals=2are rejected with a pointer, same as the single routes) into every command — for all actions:create/update/deleteand the read actionslist/get/fields. - Folder batch-create mirrors the single-route shape:
id=<parent>&data[...]. A missingparentIdis a clean per-item400before the Bitrix24 call.
Affected endpoints:
NEW-0714-27: recover a stuck server exec channel
A new endpoint POST /v1/infra/servers/:id/unstick force-frees a Black Hole server's stuck command channel when a deploy or exec keeps returning EXEC_BUSY ("Another command is running") even after DELETE /v1/infra/servers/:id/lock. It releases the platform-side lock and bounces the agent tunnel — on reconnect the agent finishes the stuck command and frees its mutex. The server is not rebooted.
If a legitimate operation (a deploy, exec, or harden) is still running on the server when you call it, the endpoint returns 409 OPERATION_IN_PROGRESS by default and leaves it alone — only a genuinely stuck channel should be unstuck. Retry with ?force=true if you are certain the command channel is hung.
Response: { success: true, data: { backendLockReleased, agentBounced, reconnected } }. Error codes: 404 SERVER_NOT_FOUND, 409 CONFLICT (a recovery is already running), 409 OPERATION_IN_PROGRESS (an operation is running on the server — retry with ?force=true), 409 GALAXY_UNSTICK_UNSUPPORTED (not supported for galaxy hosts or galaxy apps), 502 GATEWAY_ERROR. The /exec error (EXEC_BUSY) and a /deploy failure (code DEPLOY_FAILED, message "Another command is running") now also carry a hint pointing at this endpoint.
NEW-0714-28: server description in `PATCH /v1/infra/servers/:id`
Before
PATCH /v1/infra/servers/:id accepted only displayName. There was no description field in the contract, and GET responses did not expose one.
After
PATCH /v1/infra/servers/:id accepts an optional description field (string, up to 500 characters; an empty string or null clears the description; omitting the field leaves the current value unchanged). The value is synced to the application's catalog card. The description field is now returned in GET /v1/infra/servers, GET /v1/infra/servers/:id, and in the PATCH response. Existing requests without description keep working unchanged.
FIX-0714-29: galaxy app deploy returns an honest error instead of a false success when the connection drops mid-build
Before
If the connection to the host dropped during a galaxy app build (common under heavy-build load), POST /v1/infra/servers/:id/deploy could return 200 with status running and a [recovered] note in buildLog, even though the new version never built or started — the previous container kept running. A retry hit the same drop and again reported a false success.
After
A deploy is treated as recovered only if it completed fully: the container running under the app's name is the one this attempt started, and the deploy ran to the end. If the connection dropped during the build — or at any point before the deploy completed — and the new version did not come up fully, the endpoint returns a retryable 502 with code GALAXY_HOST_UNREACHABLE and a hint to re-send the same deploy without deleting the server — instead of a false 200. Only if the deploy completed fully and just the final response was lost does recovery to 200 work as before.
BC-0714-30: renaming a catalog app reaches Bitrix24, publish loses menuTitle
Old format supported until: 14.01.2027
Before
PATCH /v1/apps/:id with a title field on an app that is in the catalog returned 200 but changed nothing the user could see: the catalog card and the placement bindings on the Bitrix24 account (the left-menu item, CRM tabs) kept the old name. There was never a failure — the call always succeeded.
For POST /v1/apps/:id/publish the body was not validated, and the placement title was set by a separate menuTitle field.
After
For an app in the catalog, title is a single operation on the display name: the name is synchronized into the catalog card (catalogTitle is written together with title) and re-bound into the placements on the Bitrix24 account. A call that always returned 200 can therefore now fail honestly:
400 NO_USER_TOKEN— the app is not authorised on the Bitrix24 account, so there is nothing to re-bind the placements with;400 TITLE_TOO_LONG_FOR_CATALOG— a catalog app name is capped at 100 characters, whiletitleallows 255;502 BITRIX_PARTIAL_REBIND— Bitrix24 rejected the binding. The name is not written in that case, and repeating the same request repairs the state.
The body of POST /v1/apps/:id/publish is now validated, and the menuTitle parameter is removed: the placement title is always the app's display name. An empty body still works — publication takes the values from the app record.
What integrators should do
- Drop
menuTitlefrom the publish body: the field is ignored, the menu item name comes fromtitleandcatalogTitle. - Keep a catalog app name within 100 characters.
- Handle the rejections on a rename: on
NO_USER_TOKENauthorise the app on the Bitrix24 account, onBITRIX_PARTIAL_REBINDrepeat the request. - Note the side effect: renaming through
titlenow also writescatalogTitle— for an app in the catalog the two fields are kept in sync.
NEW-0714-31: GET /v1/tasks/:taskId/comments/fields — task-comment field schema
Task comments gained a /fields method like every other entity: GET /v1/tasks/:taskId/comments/fields returns a static 5-field schema (id, taskId, authorId, message, createdAt) with type, read-only flag, and segment-localized label and description. The method makes no Bitrix24 call. This path previously returned 400 WRONG_PATH — the field set was only available from the static docs.
FIX-0714-32: per-entity batch with action list now applies filter
Before
POST /v1/{entity}/batch with action: "list" ignored filter: field names were not mapped to their Bitrix24 names (e.g. a leads statusId was not turned into stageId), and operators $gt / $contains / $in and others had no effect. The call returned 200 with the whole table — a silent failure with wrong data. The global POST /v1/batch and POST /v1/{entity}/search filtered correctly.
After
Per-entity batch runs filter through the same translator search and the global /v1/batch use. Field aliases and operators ($gt, $gte, $lt, $lte, $ne, $contains, $in, $nin, prefix >=, <=, !, etc.) are applied. An invalid filter (an unknown field on an entity with a complete schema, an unsupported operator, the @ / !@ prefixes, or $or / $and logic tokens) now returns 400 naming the call index instead of silently returning the whole set — matching the single endpoints.
FIX-0714-33: auto-pagination no longer duplicates records across page boundaries
Before
For Bitrix24 list methods without sort support (e.g. the storage-object list) a record on a page boundary could shift between the fetches of adjacent pages and land in both — with limit > 50 the response carried a duplicate that occupied a slot, and the client processed the same record twice.
After
After all pages are stitched, the result is deduplicated by id (the first occurrence is kept). For stably-sorted methods nothing changes (no duplicates — a no-op).
FIX-0714-34: requisite-preset field list no longer comes back empty; create no longer returns a foreign record
Before
GET /v1/requisite-presets/:presetId/fields could return 200 with an empty data: [] even when the preset had fields: the Bitrix24 method returns result sometimes as an array [{…}] and sometimes as an object-map {"0":{…},"1":{…}}, and the handler accepted only the array form. On create (POST …/fields) the echoed record could be a DIFFERENT existing field — the created row was read back by the id from the add response, and on some portals a read by that id returned another field.
After
The list normalizes both Bitrix24 response shapes (array and object-map) — fields are no longer dropped. The created record is echoed only when its fieldName matches the one that was created; on any mismatch the response carries { id } (the row object is not substituted), so a client never receives a foreign record.
Integrator impact
A preset field's id is a Bitrix24 positional identifier: it can change after write operations on the preset and, on some portals, is not a stable key. Do not cache id across preset mutations — re-fetch the field list before a get/update/delete on a specific field.
FIX-0714-35: placement bind for earlier-created apps is no longer rejected over the handler
Before
POST /v1/placements/bind could return 400 with code PLATFORM_HANDLER_UNRESOLVABLE for an app created before the switch to the single platform handler (such an app kept its own technical address as the handler). The bind was rejected even when the platform handler /v1/bitrix-handler was available — the app could not be re-published through the API.
After
The bind succeeds: the placement handler is registered on the platform /v1/bitrix-handler, and the response carries handlerRewritten: true plus requestedHandler with the original value. PLATFORM_HANDLER_UNRESOLVABLE is now returned only when the platform handler is genuinely unavailable. POST /v1/placements/unbind removes such a placement by the same address.
2026-07-13
FIX-0713-1: OAuth app key platform scopes now sync on create and edit
Before
A key issued together with an OAuth app via POST /v1/apps did not receive the platform scopes (vibe:infra, vibe:ai, vibe:search, vibe:storage) that a key created in the dashboard gets. As a result POST /v1/infra/servers under such a key returned 403 INFRA_SCOPE_REQUIRED. Adding vibe:infra to the app's scopes via PATCH /v1/apps/:id changed only the app, not the paired key — so it had no effect on access.
After
The paired key created via POST /v1/apps now receives the same default platform scopes as a key created in the dashboard. A vibe:* scope change via PATCH /v1/apps/:id (both add AND remove) now propagates to the app's active keys. A read-only (READONLY) key is rejected with 403 WRITE_BLOCKED_READONLY_KEY on three write operations: server creation (POST /v1/infra/servers), app scope change (PATCH /v1/apps/:id), and READWRITE key issuance (POST /v1/apps with mode: "READWRITE").
Impact on integrators
Apps created via the API can now manage infrastructure without re-creation. A key whose app already declares vibe:infra while the key itself lacks it (the old divergence) is fixed with one edit: remove vibe:infra from the app's scopes and add it back via PATCH /v1/apps/:id — the second edit syncs the key; or re-create the app. A READONLY key can still create a READONLY app (mode: "READONLY").
FIX-0713-2: product-sections: unsupported filters return 400 instead of the whole table
Before
Operators (>, <, !, %, $ne, $contains, $nin) and non-exact-match fields (sort, unknown) in the filter of GET /v1/product-sections and POST /v1/product-sections/search were silently ignored — the code 200 and the whole list were returned unfiltered.
After
Such filters are rejected with 400 UNSUPPORTED_FILTER. Filter by exact match or $in on id, name, xmlId, code, catalogId, sectionId. Sorting (order/sort) is unchanged.
Impact on integrators
Calls with operators or filter[sort] that previously returned 200 with unfiltered data now return 400 — switch to exact match or $in.
FIX-0713-3: the reason for a failed first server provisioning is now visible
Before
If a server failed to come up on its first creation (a preemptible plan evicted, no free capacity), it silently ended up in sleeping status with no explanation. A client polling GET /v1/infra/servers/:id saw sleeping and could not tell why the deploy was blocked.
After
Such a server now moves to status: "error" with a populated provisionError (a human-readable reason) and a new provisionErrorCode field — a machine-readable failure category (PREEMPTIBLE_EVICTION / PROVISION_TIMEOUT / NO_CAPACITY / GENERIC). The provisionErrorCode field is added to GET /v1/infra/servers/:id and GET /v1/infra/servers next to provisionError (additive, null when there were no errors).
Integrator impact
No action needed: error is an already-existing status. Recover such a server via POST /v1/infra/servers/:id/start or /repair (not /wake: on error status it returns 422; the availableActions field in the response points to the available action).
NEW-0713-4: POST and PATCH /v1/tasks/:taskId/time accept createdDate
An optional createdDate field is now forwarded on POST /v1/tasks/:taskId/time and PATCH /v1/tasks/:taskId/time/:itemId — the time entry lands on the given date instead of the current moment (backfilling last week's tracks). ISO 8601 with offset, bare ISO, and YYYY-MM-DD are all accepted and passed to CREATED_DATE verbatim. When the field is omitted the behaviour is unchanged — the date equals the creation moment.
FIX-0713-5: meta.hasMore no longer stays stuck at true with filter + offset
Before
When paginating a filtered list (for example GET /v1/deals with filter and offset), meta.hasMore stayed true at every offset — even well past meta.total. A while (meta.hasMore) { offset += limit } loop ran forever.
After
When Bitrix24 ignores an offset beyond the filtered set and returns the whole set, meta.hasMore is derived from the request window (offset + limit < meta.total) instead of being forced to true. At offset=0 the response is unchanged; once the window passes total, hasMore becomes false. Applies to list and POST /search across all entities.
FIX-0713-6: GET /v1/lists and /v1/lists/:iblockId/elements honour offset
Before
GET /v1/lists/:iblockId/elements and GET /v1/lists silently ignored offset — ?limit=50&offset=50 returned the same first page, and a client paginating by offset never reached rows 51 and beyond.
After
offset is mapped to start, the native pagination param for these methods, so offset-based paging works. An explicit start keeps priority when both are supplied.
NEW-0713-7: New preserveEnv flag keeps .env across cleanDeploy
The POST /v1/infra/servers/:id/deploy body gained an optional boolean preserveEnv (default false). When cleanDeploy: true (which wipes the app directory including the .env file) and preserveEnv: true, the existing .env is read before the wipe and restored afterwards unless this request also passes env (a supplied env wins). Without the flag a re-deploy with cleanDeploy could boot the app with no environment variables — on its default port.
NEW-0713-8: failed galaxy app builds now carry a `buildHint`
Before
A galaxy app build failure returned only GALAXY_APP_BUILD_FAILED
(and GALAXY_APP_START_FAILED) with a buildLog tail — you had to read the log
by hand to find the cause. GET /v1/infra/servers/:id for an ERROR app returned a
short provisionError but no ready recommendation.
After
The response now carries the parsed cause. The error body of
POST /v1/infra/servers/:id/deploy (502) additionally includes error.category
(a machine category such as MODULE_NOT_FOUND, INSTALL_AUTH, RESOURCE) and
error.buildHint — a localized recommendation line naming the concrete next step,
whenever the failure is classifiable. GET /v1/infra/servers/:id for an ERROR app
adds the same data.buildHint field. The fields are additive: when the cause is
unrecognized they are absent (buildHint is null), and provisionError/buildLog
keep arriving as before — existing requests are unaffected.
FIX-0713-9: concurrent identical source saves no longer mint a duplicate version
Before
Two simultaneous saves of identical bytes to the same server (POST /v1/infra/servers/:id/sources, and the auto-save on deploy) could, under rare timing, create two byte-identical versions instead of one — content deduplication was best-effort.
After
Deduplication is deterministic: identical bytes submitted concurrently always converge on a single version. On POST /v1/infra/servers/:id/sources both responses return the same versionId and the losing request gets deduplicated: true; the deploy auto-save converges on that same single version (the deploy response shape is unchanged).
2026-07-12
NEW-0712-1: The AI quota exhaustion response now points to the top-up path
The 402 ai_quota_exhausted error with reason: wallet_empty now additionally returns the hint and topupUrl fields. hint is a short English message: the monthly AI quota and the account balance are exhausted, and an account admin can top up the balance to resume. topupUrl is a link the admin opens to top up. The fields are additive: the existing response fields (reason, resetAt) are unchanged, and the reason: breaker and reason: wallet_off branches do not carry them. This lets an agent client relay the top-up path to the human. The error is returned by model calls, see POST /v1/chat/completions.
FIX-0712-2: `sleep-now` on a server with a long-past computed wake now sleeps honestly
Before
For a server with an enabled wake schedule, POST /v1/infra/servers/:id/sleep-now could return { data: { slept: false, reason: "WAKE_IMMINENT" } } forever when the denormalized "next wake" was left far in the past (the server was woken outside the scheduler and the stamp was never recomputed). The server never slept and kept its plan running around the clock.
After
A stamp older than a dynamic window (grace = margin + typical lead) is treated as stale rather than "imminent": sleep-now sleeps the server honestly and answers { success: true }, after which the scheduler quietly rolls the anchor forward to the future window without waking it. WAKE_IMMINENT stays a normal response only for a genuinely near wake.
2026-07-11
FIX-0711-1: galaxies: GALAXY_HOST_UNREACHABLE is now actionable — structured hint and a precise provisionError
Before
When a galaxy host was temporarily unreachable, POST /v1/infra/servers/:id/deploy answered with a bare 502 GALAXY_HOST_UNREACHABLE, and a slot created in one call via POST /v1/infra/servers with source moved to an error status with the text "Deploy failed unexpectedly — please retry; details are in the server logs". Neither the cause nor the recovery path was reported — clients deleted the slot and created a new one, which does not help: the fresh slot lands on the same host.
After
The 502 GALAXY_HOST_UNREACHABLE response — on deploy, exec and delete (DELETE /v1/infra/servers/:id) — carries a structured error.hint: the condition is transient, the slot and its data are intact, retry the same request in 1–2 minutes, do not delete the slot. On the create-with-source path, provisionError now contains the real cause ("Galaxy host … became unreachable during build …") and the same advice to re-send the deploy to the existing slot. Additionally: when several servers work under one OAuth application, a saved source version is no longer lost to a version-numbering conflict — neither on the deploy auto-save nor on an explicit save via POST /v1/infra/servers/:id/sources.
Impact on integrators
The change is additive: response codes and statuses did not change; the error.hint field was added and the provisionError text became precise. No client updates required; AI agents should read hint.recovery — it states exactly what to do.
NEW-0711-2: distinct error code when the Vibecode Connector module is not installed on the portal
Issuing an application key through the connector module (POST /v1/apps) now returns 409 with code CONNECTOR_MODULE_NOT_INSTALLED and a clear "install the module" message when the portal has no vibecodeconnector module, instead of the previous opaque 502 CONNECTOR_APP_INSTALL_FAILED. This is a legitimate, permanent state (especially for a self-hosted portal), not a transient failure — retrying will not help, the module must be installed on the portal. Other issuance error codes are unchanged.
FIX-0711-3: GET /v1/ai/quota pacing can now be populated by the platform by default (gradual rollout)
The platform can now enable pacing (AI-quota day/week smoothing) by default for an account — no admin action required. The rollout is staged (pilot accounts → all accounts): until an account falls under the platform default-on, the data.pacing field in the GET /v1/ai/quota response stays null, same as before. Once an account is under default-on: mode: "ignore" is an informational mode, and active: false means window limits do not reject requests (a 429 ai_pacing_limited rejection is impossible). The response shape is unchanged; integrators who already treat data.pacing as an optional field need no changes.
2026-07-10
NEW-0710-1: wake-schedule window management (wake-schedules)
New CRUD contract on Black Hole servers: GET/POST /v1/infra/servers/:id/wake-schedules, PATCH/DELETE /v1/infra/servers/:id/wake-schedules/:scheduleId. Lets you declare one or more recurring wake windows (cronExpr + a required IANA timezone, optional label/lead/enabled) — the platform wakes a sleeping server ahead of the window, and the app's own in-VM cron runs the task from there.
Rolling out gradually and not yet available on every portal — until enabled on a given portal, the call returns 403 with code WAKE_SCHEDULE_DISABLED. Available only for BLACKHOLE-mode servers (otherwise 400 BLACKHOLE_ONLY) and not yet supported for galaxies (400 GALAXY_NOT_SUPPORTED on both the host and a nested app — coming later). The minimum cadence between occurrences and the per-server window cap (50) are platform-configured; violating either returns 400 CADENCE_TOO_LOW and 403 WAKE_SCHEDULE_LIMIT respectively. The create/update response additionally carries a tzWarning field — a heads-up that the VM's in-VM timezone may have drifted from the window's timezone if the server hasn't been redeployed since. PATCH replaces the whole window (PUT semantics): omitted optional fields reset to their defaults — enabled→true, label/lead→null. Send the full window object on update.
Affected endpoints: GET|POST /v1/infra/servers/:id/wake-schedules, PATCH|DELETE /v1/infra/servers/:id/wake-schedules/:scheduleId.
NEW-0710-2: machine-readable tz-warning code in wake-schedules responses (`tzWarningCode`)
The wake-schedule create/update response (POST/PATCH /v1/infra/servers/:id/wake-schedules[/:scheduleId]) now additionally carries a tzWarningCode field alongside the existing text tzWarning — "SINGLE_ZONE" / "MULTI_ZONE" / null (when the server ends up with zero enabled windows after the mutation). It's a machine-readable companion to the same advisory, letting clients localize the message themselves instead of rendering the raw English tzWarning text. The field is additive — tzWarning is unchanged and stays for backward compatibility.
Affected endpoints: POST|PATCH /v1/infra/servers/:id/wake-schedules[/:scheduleId].
FIX-0710-3: wake-schedules: schedules rejected on always-on servers
POST/PATCH /v1/infra/servers/:id/wake-schedules now reject creating or updating a wake window on an always-on (24/7) server — the call returns 400 with code ALWAYS_ON_CONFLICT. Such a server never auto-sleeps, so a wake schedule would silently break the paid always-online guarantee. Ordinary sleeping Black Hole servers and preemptible agents/bots are unaffected. Turn off always-on to declare wake windows.
Affected endpoints: POST /v1/infra/servers/:id/wake-schedules, PATCH /v1/infra/servers/:id/wake-schedules/:scheduleId.
FIX-0710-4: wake-schedules can now be declared on nested galaxy apps
Before
POST/PATCH /v1/infra/servers/:id/wake-schedules returned 400 GALAXY_NOT_SUPPORTED for any galaxy-family server — both the host itself and nested apps (kind=GALAXY_APP).
After
Nested galaxy apps (kind=GALAXY_APP) are now accepted — you can declare a wake window on a specific app, and the platform wakes it (and the host, if needed) ahead of the window. The galaxy host itself still returns 400 GALAXY_NOT_SUPPORTED — schedule the individual apps, not the host.
Affected endpoints: POST|GET /v1/infra/servers/:id/wake-schedules, PATCH|DELETE /v1/infra/servers/:id/wake-schedules/:scheduleId.
FIX-0710-5: sleep-now declines to sleep when a scheduled wake is imminent
Before
POST /v1/infra/servers/:id/sleep-now always put the server to sleep immediately, even when the next scheduled wake was a minute away — the server woke straight back up.
After
When the server has an enabled wake schedule and the next wake falls within the guard margin, the call does not sleep the server and responds 200 with { "success": true, "data": { "slept": false, "reason": "WAKE_IMMINENT" } }. Otherwise the server sleeps and the scheduler wakes it at the next window.
Impact on integrators
The data block with slept: false arrives only when the call declines to sleep the server — check for its presence if you rely on the server always sleeping after this call. On a successful sleep the response is just success: true. Existing calls to servers without a schedule are unchanged.
FIX-0710-6: non-streaming chat/completions and embeddings no longer abort at 360 seconds
Before
A non-streaming (stream:false) POST /v1/chat/completions (and POST /v1/embeddings) request whose generation took longer than ~2 minutes reliably aborted at ~360 seconds with {"code":"ai_provider_unavailable","message":"This operation was aborted"} — regardless of the client's own timeout. The doomed generation also burned triple the compute.
After
Such a request now runs within a single ~850-second budget (one attempt for the whole budget, no compute tripling). If generation still does not finish in budget, a meaningful 503 with code ai_provider_timeout is returned, with a localized userMessage and a hint (reduce the request size or use streaming mode stream:true) — without a Retry-After header (the timeout is not transient). A client disconnect now immediately cancels the provider-side generation. Streaming mode (stream:true) was never affected by this limit.
FIX-0710-7: requisite-presets/:presetId/fields and requisite-links: list sorting, filtering, and parameter validation
Before
GET /v1/requisite-presets/:presetId/fields ignored sort/order and any filter[...] — it always returned the full list in Bitrix24 order. GET /v1/requisite-links and POST /v1/requisite-links/search accepted only plain equality; comparison operators ($gte, >, etc.), sort/order, and unknown fields were silently dropped, so the whole table came back.
After
Both lists now honour sort/order (including the order[field]=asc|desc form) and filter. requisite-links supports comparison operators ($gt/$gte/$lt/$lte, $in/$nin, >=/> prefixes). An unknown filter or sort field now returns 400 (UNKNOWN_FILTER_FIELD / UNKNOWN_SORT_FIELD), a top-level logical operator ($or/$and) returns 400 INVALID_FILTER_OPERATOR, and filtering by entityId without entityTypeId returns 400 MISSING_ENTITY_TYPE_ID instead of a raw "Access denied".
Impact on integrators
Requests on documented fields keep working and now actually sort/filter. If you relied on an unknown parameter being silently ignored, it now returns 400 — drop the typo or use a field from the response.
FIX-0710-8: Bank details sorting by id honors the direction
Before
Listing bank details (GET /v1/bank-details) sorted by id descending (?sort=-id) returned records in ascending order — the sort direction was silently ignored.
After
?sort=-id (and ?sort=id) sorts by the identifier in the requested direction.
Integrator impact
No code change required — requests that relied on sorting by id now return the expected order.
NEW-0710-9: GET /v1/quotes/fields — ~26 quote fields declared with human-readable labels
The quotes entity schema now declares ~26 fields that Bitrix24 returned but that were undeclared: quoteNumber, updatedBy, lastActivityBy, lastActivityTime, content, terms, leadId, storageTypeId, storageElementIds, personTypeId, webformId, lastCommunicationTime, contactIds, contacts, locationId, taxValue, actualDate, mycompanyId, utmSource/utmMedium/utmCampaign/utmContent/utmTerm, lastCommunicationCallTime/lastCommunicationEmailTime/lastCommunicationImolTime/lastCommunicationWebformTime. They now appear in GET /v1/quotes/fields with readable labels (instead of service names like STORAGE_TYPE_ID/UTM_SOURCE), and their values are type-coerced (numbers, ISO dates) in list/get responses. Labels were also added to the previously label-less stageId, opened, closed.
FIX-0710-10: a PATCH with no writable field is rejected with an explicit error
Before
PATCH /v1/{entity}/:id with an empty body — or a body carrying no recognized writable field (for example because of a typo in a field name) — reached Bitrix24, which silently ignored the request and returned success. The wrapper then replied 200 with the unchanged object, so the client believed the edit had applied while nothing actually changed — a silent-drift risk, especially for AI agents. This affected all three update surfaces: single PATCH /v1/{entity}/:id, per-entity POST /v1/{entity}/batch and global POST /v1/batch.
After
An update with an empty body returns 400 EMPTY_UPDATE_BODY (a per-item error in batch calls) on all three surfaces, before Bitrix24 is called. For /v1/catalog-products/:id a strict check is added: a PATCH that carries no recognized writable field returns 400 NO_RECOGNIZED_UPDATE_FIELDS. A meaningful update always carries at least one field — send a recognized field (custom properties propertyNNN and UF_* fields are accepted too). Entities that already validated their fields behave as before.
Catalog products additionally expose in GET /v1/catalog-products/fields, and now accept for explicit select, filter and sort, the fields from the product-update contract — code, xmlId, sort, vatId, height, length, width, previewText, detailText and others (filtering by them previously returned 400 UNKNOWN_FILTER_FIELD). Filter and sort reliably by the indexable fields (code, xmlId, sort, vatId, dimensions); for the full-text ones (previewText, detailText) Bitrix24 may ignore the filter. List responses without an explicit select are unchanged.
FIX-0710-11: leads, companies, quotes — /fields date keys are now createdTime and updatedTime
Before
GET /v1/leads/fields, GET /v1/companies/fields and GET /v1/quotes/fields advertised createdAt and updatedAt, while list/get/search responses always returned createdTime and updatedTime — the value was never readable under the createdAt/updatedAt name. Filter and sort accepted the createdAt/updatedAt names.
After
These entities' /fields advertise the real keys createdTime and updatedTime — like contacts, invoices and items. The keys in the response body are the same (createdTime/updatedTime were always returned), but the value is now normalized to ISO-8601 in UTC (2026-04-15T07:00:00.000Z) — previously it arrived in the raw Bitrix24 form with the account offset (2026-04-15T08:00:00+01:00). Same instant, only the representation changes.
Impact on integrators
Read dates from createdTime and updatedTime (the keys did not change). If you compare the date string byte-for-byte or cache by it, account for the offset → Z shift (same instant). In filter and sort use createdTime/updatedTime; the former createdAt/updatedAt now return 400 UNKNOWN_FILTER_FIELD (filter) and 400 UNKNOWN_SORT_FIELD (sort). On write, createdAt/updatedAt are no longer rejected as read-only — they are ignored as unknown fields (like contacts/invoices/items); the creation/update timestamp still cannot be set.
FIX-0710-12: sleep settings: flipping to always-on is now rejected while wake windows are active
PATCH /v1/infra/servers/:id/sleep now rejects setting sleepAfterMinutes: null (always-on, 24/7) on a server that still has enabled wake-schedule windows — the call returns 400 with code ALWAYS_ON_CONFLICT. This is the reverse direction of an existing gate: creating a wake window on an always-on server was already rejected, and now the opposite transition is rejected symmetrically — otherwise the server would keep sleeping on schedule, silently breaking the paid always-online guarantee. Delete or disable the wake windows first, or keep a sleep timeout instead of "Never", to turn always-on on.
Affected endpoints: PATCH /v1/infra/servers/:id/sleep.
NEW-0710-13: placement.bind on a self-hosted Bitrix24 returns a clear SESSION_REQUIRES_ADMIN for a non-administrator
On a self-hosted (BOX) Bitrix24, binding a placement via the developer-key path requires the key's user to be an account administrator. Previously a non-administrator request returned an opaque 502 BITRIX_UNAVAILABLE.
Now POST /v1/placements/bind recognises the access-denied signal from Bitrix24 and returns 403 SESSION_REQUIRES_ADMIN with a hint: bind from an account-administrator account, or ask an administrator to grant those rights. The requirement is visible up front in GET /v1/me — the placements.bindPrerequisite block for self-hosted accounts now includes the SESSION_REQUIRES_ADMIN code.
FIX-0710-14: GET /v1/me capabilities now reflect read-only (READONLY) mode
Before
For a READONLY-mode key, GET /v1/me returned capabilities.managedBots.create, agents.create, servers.create and apps.* as available: true, even though every write is blocked with 403 WRITE_BLOCKED_READONLY_KEY. An agent saw "can create" and hit the rejection.
After
For a READONLY key these write capabilities are returned as available: false with reason: "WRITE_BLOCKED_READONLY_KEY" and a hint to switch the key to read+write. Read-only capabilities and the AI Router are unchanged. For READWRITE keys the response is unchanged.
FIX-0710-15: the feedback author can reply to their own ticket again without the vibe:feedback scope
Before
POST /v1/feedback/:id/comments rejected the ticket author with 403 FEEDBACK_SCOPE_REQUIRED when the key lacked the vibe:feedback scope — even though the author branch was documented. The author could not reply to their own AWAITING_USER ticket, and it stalled.
After
The author check now runs before the scope gate: the author replying with the same key that created the ticket reaches the author branch (authorType=USER, ball-court rule AWAITING_USER → NEEDS_REVIEW) even without the vibe:feedback scope. A key that is neither the author nor scoped still gets 403 FEEDBACK_SCOPE_REQUIRED.
NEW-0710-16: AI quota pacing: pacing field in the response and 429 ai_pacing_limited error
The GET /v1/ai/quota response gained a pacing field — the state of monthly AI quota pacing (peak smoothing via a daily and a weekly limit layered on top of the overall monthly limit; enabled by the portal admin from the /ai dashboard page). null when pacing is off at the platform level or not configured for the account; otherwise an object { mode, active, day, week }: mode is the enforcement behavior on a tripped limit (wallet/block/ignore), active signals whether tripping a window will reject a call right now (false in observation mode, and always false in ignore mode — windows are tracked as informational only, 429 is never returned), day and week are each { pctUsed, resetAt } as a percentage of that window's OWN limit. The response is still cached for 30 seconds, so pacing state can lag by that long.
When the daily or weekly limit trips, calls to POST /v1/chat/completions, POST /v1/embeddings, and POST /v1/audio/transcriptions can now return 429 with the body { success: false, error: { code: "ai_pacing_limited", type: "rate_limit_exceeded", message, reason, overageDenied, resetAt, retryAfter } } and a Retry-After header. reason names the tripped window (day_window or week_window); overageDenied names why paid overage was refused (wallet_empty, breaker, wallet_off), or is null under the hard-block mode. Retrying before Retry-After/resetAt will not help — the quota is not any more available in the meantime. Pacing is off by default — the platform enables it.
FIX-0710-17: GET /v1/pages, /v1/sites and POST /v1/{pages,sites}/search now honour offset
Before
Listing pages or sites with an offset (GET /v1/pages?offset=50, POST /v1/pages/search with offset) returned 422 BITRIX_ERROR "Unknown parameter: start". The first page (no offset) worked.
After
offset for pages and sites is handled correctly: the requested window [offset, offset+limit) is returned, with meta.total and meta.hasMore computed from the row count. No client change is needed — calls without offset behave as before.
NEW-0710-18: /fields of documents, catalogs, prices, telephony lines, basket items and leads gain richer metadata
GET /:entity/fields of several entities now carries more complete schema metadata. Documents (GET /v1/documents/fields), catalogs (GET /v1/catalogs/fields), catalog prices (GET /v1/catalog-prices/fields) and telephony lines now expose a human-readable label and description per field, returned in English.
For basket items (GET /v1/basket-items/fields) the fields weight, vatRate, measureCode, measureName, dimensions, productXmlId, catalogXmlId are marked with a nullable flag — they may come back empty. For leads (GET /v1/leads/fields) the same flag marks secondName, sourceDescription, comments, and the previously-undeclared fields originatorId, dateClosed, lastCommunicationTime and the tags utmSource/utmMedium/utmCampaign/utmContent/utmTerm are now declared — filter and sort work on them, and dateClosed is normalized to ISO-8601.
For landing sites the group-by dimensions are declared, so POST /v1/sites/aggregate with groupBy (type, active, deleted, lang, tplId, domainId, createdById, modifiedById) no longer answers Available: .. For documents aggregation is disabled (every numeric field is an identifier): POST /v1/documents/aggregate returns 404.
Existing calls keep working unchanged — this is additional field metadata.
NEW-0710-19: Idempotent server creation — Idempotency-Key header
POST /v1/infra/servers now accepts an optional Idempotency-Key header when creating a standalone server. A retry with the same key — for example after a lost response or a network drop — does not create a second server: it returns the same server the first request created, with status 201 and an Idempotent-Replayed: true response header. The key is a 1–255 character string from [A-Za-z0-9_.:-]; it is scoped to your API key.
On a replay the one-time SSH credentials (ssh.privateKey / ssh.password) are NOT re-exposed — they are null in the response body and a note field explains this. Keep the credentials from the first create response.
New error codes: 400 INVALID_IDEMPOTENCY_KEY (the key fails validation), 400 IDEMPOTENCY_UNSUPPORTED_WITH_GRADUATION (the key together with graduateFrom for a dedicated server is not supported), 409 IDEMPOTENCY_KEY_ALREADY_USED (the key was already used for a server that has since been deleted), 409 IDEMPOTENCY_CONCURRENT_RETRY (a concurrent request with the same key is still in progress — retry shortly).
The header applies to standalone servers only. On galaxy-placement portals a well-formed key is ignored without an error, and the retry protection does not extend to that path.
Additionally: the create response now returns the canonical server name (suffixed when a name collision is resolved) instead of the name from the request — these previously diverged on a same-name collision.
NEW-0710-20: localized message on OPEN-mode switch denials
Responses from PATCH /v1/infra/servers/:id/mode with codes OPEN_MODE_DISABLED (OPEN mode is disabled platform-wide) and OPEN_MODE_NOT_ALLOWED (OPEN mode is not allowed by portal policy) now additionally carry an error.userMessage field — a localized, human-readable message pointing to the Deploy API as the supported replacement for direct SSH. The field is additive: error.message (English technical string), error.code and the HTTP status are unchanged. It matches the shape of the existing error.userMessage on the OPEN_MODE_REQUIRES_COMMERCIAL denial. The userMessage text depends on the key owner's locale.
BC-0710-21: Open Channels config fields normalized to camelCase and described in /fields
Old format supported until: 10.01.2027
Before
GET /v1/openline-configs, GET /v1/openline-configs/{id} and POST /v1/openline-configs/search returned most configuration fields in the Bitrix24-native form — upper case with underscores (CRM_CREATE, WELCOME_MESSAGE, QUEUE_TIME, and others). The GET /v1/openline-configs/fields reference described only 6 fields, so the rest were invisible to programmatic discovery.
After
All configuration fields are normalized to a single camelCase form (crmCreate, welcomeMessage, queueTime, and so on), and /fields describes the full field set with human label and description values. Filtering and sorting by the new camelCase names work. On write (create/update) both cases are still accepted — existing calls that send upper-case names in the body keep working.
What integrators should do
Read response fields by their camelCase names: config.crmCreate instead of config.CRM_CREATE. The mapping is a direct transliteration from upper case to camelCase (WELCOME_BOT_ID → welcomeBotId, WORKTIME_TO → workTimeTo, LINE_NAME → name). The full list of new names is in the /fields reference.
NEW-0710-22: Added GET /v1/warehouses/fields — warehouse field schema
A new GET /v1/warehouses/fields endpoint returns the schema of the 19 warehouse fields: for each field — its type, a read-only flag (readonly), a human label, and a description. Warehouses are a custom route (no entity schema), so they previously lacked the field reference that auto-generated entities have. The response is { success: true, data: { fields: { … } } }. Requires the catalog scope.
FIX-0710-23: app publish recovers from a placement drift
Before
On publish (POST /v1/apps/:id/publish) or a placement update (PATCH /v1/apps/:id), if a placement was registered on the Bitrix24 side but missing from the app (drift after an unpublish), the bind failed with "Handler already binded" and the placement stayed out of sync.
After
On that error the platform sweeps the stale binding once and retries — the placement syncs automatically. Recovery fires only on the confirmed conflict, so a live placement is never stripped by mistake; placements that need non-portable OPTIONS (chat widgets, the background worker) are excluded from auto-recovery and still surface a warning.
FIX-0710-24: storage: object sha256 is populated on direct upload
Before
The sha256 field in the storage-object upload response was always null for user objects, even though the schema described it as "computed on upload".
After
For direct upload (POST /v1/storage/objects/upload, files up to 10 MB) sha256 now carries the SHA-256 hash of the object content — usable for integrity checks and duplicate detection (identical content yields an identical hash). Presigned and multipart uploads send bytes straight to storage bypassing the platform, so sha256 stays null there for now.
FIX-0710-25: pacing.active in GET /v1/ai/quota no longer reports an active limit when the quota is zero
Before
With pacing enabled on a Bitrix24 account whose monthly quota is zero (a hard stop via the monthlyVibes = 0 override, or usage not yet initialized), data.pacing.active returned true — even though a 429 ai_pacing_limited rejection is structurally impossible in that state: requests are rejected by the monthly limit, not by a pacing window.
After
data.pacing.active returns true only when exceeding the day or week window can actually produce 429 ai_pacing_limited. With a zero quota the field honestly reports false. The response shape is unchanged; clients that built backoff logic on active need no changes — the signal is simply more accurate.
2026-07-09
FIX-0709-1: transient DB overload now returns 503 with Retry-After instead of 500
Before
In a rare form of transient DB overload (connection exhaustion), some requests (including POST /v1/infra/servers) returned 500.
After
Such requests return 503 with code POOL_EXHAUSTED and a Retry-After header. The error is transient — retry the request with backoff.
Impact on integrators
Clients that retry on 5xx must now handle 503 and honor Retry-After. POST /v1/infra/servers is non-idempotent — a retry can create a duplicate server, so retry with backoff rather than immediately.
FIX-0709-2: placements/bind reports the Bitrix24 plan requirement clearly
Before
Binding a placement with an app key on a portal whose Bitrix24 plan does not permit developer-key REST calls made Bitrix24 deny access, and POST /v1/placements/bind returned an opaque 502 BITRIX_UNAVAILABLE with no cause. The plan is checked on the developer-key path, but GET /v1/me gave no advance signal about this prerequisite.
After
The denial is now classified: POST /v1/placements/bind returns 403 with code INT_TARIFF_REQUIRED and a readable userMessage — the developer-key path needs a commercial Bitrix24 plan on the portal. An app key's GET /v1/me gains a placements.bindPrerequisite block that describes the Bitrix24-side prerequisite up front and lists the error codes. Other bind failures (unknown clientId, stale embedding) still return 502 BITRIX_UNAVAILABLE.
Impact on integrators
No action required, successful calls are unchanged. If you handled 502 BITRIX_UNAVAILABLE on bind, also handle 403 INT_TARIFF_REQUIRED and prompt the user to upgrade the portal to a commercial Bitrix24 plan.
FIX-0709-3: bindPrerequisite in GET /v1/me now reflects the portal plan
Before
The placements.bindPrerequisite block on an app key described the bind prerequisite identically for every portal: subscriptionRequired: true, a remedy that does not apply to a portal whose access is granted by the Bitrix24 plan, and an errorCodes list carrying codes such a portal never receives. On it the bind denial arrives as INT_TARIFF_REQUIRED.
After
The block now depends on the portal. On a portal whose access is granted by the Bitrix24 plan, subscriptionRequired is false, the note describes the commercial-plan requirement, and errorCodes carries INT_TARIFF_REQUIRED and BITRIX_UNAVAILABLE — only the codes that portal can actually receive.
Impact on integrators
Successful calls are unchanged. If you read errorCodes from bindPrerequisite as an exhaustive list, note that it is now narrowed to the codes reachable on the calling portal. The codes themselves and the behavior of POST /v1/placements/bind are unchanged.
BC-0709-4: CRM, task and landing fields aligned with the real Bitrix24 contract
Old format supported until: 09.01.2027
Before
GET /v1/deal-categories returned isLocked as the string "Y"/"N", and GET /v1/payments returned paySystemIsCash as the string "Y"/"N". GET /v1/leads exposed the internal searchContent field (Bitrix24's full-text search index) in every response. The payment fields paySystemXmlId, dateMarked, dateResponsibleId passed through as-is, with no date normalization.
After
isLocked (pipelines) and paySystemIsCash (payments) are now boolean (true/false). searchContent is no longer returned by GET /v1/leads. Payments now declare paySystemXmlId (string), dateMarked and dateResponsibleId (datetimes normalized to a single ISO form with a Z suffix). Tasks gained changedBy/closedBy/statusChangedBy (read-only — writing them returns 400 READONLY_FIELD). GET /v1/currencies/fields now returns human-readable label values and a lang field; GET /v1/deal-categories/fields returns label values; GET /v1/sites/fields sets a nullable flag on fields that may come back empty.
What integrators should do
Read isLocked and paySystemIsCash as boolean instead of comparing to the string "Y". If your code relied on the leads searchContent field, stop: it was an internal, undocumented field.
FIX-0709-5: creating an entity with an empty body is rejected with an explicit error
Before
Creating an entity with an empty body (or no body at all but a Content-Type: application/json header) reached Bitrix24 and silently created an entity with default values — including deals, leads, contacts, companies, invoices and smart processes. A retry or a bodyless client thus spawned junk records in CRM. This affected all three create surfaces: single POST /v1/{entity}, per-entity POST /v1/{entity}/batch and global POST /v1/batch.
After
Such a request returns 400 EMPTY_CREATE_BODY (a per-item error in batch calls) before Bitrix24 is called, on all three surfaces. A meaningful create always carries at least one field — send the fields you need in the request body. Entities that already validate their required fields behave as before.
FIX-0709-6: releasing a server lock accepts an empty JSON body
Before
DELETE /v1/infra/servers/:id/lock with a Content-Type: application/json header and an empty body returned 400 (empty JSON body). Releasing a stuck lock required sending an explicit {}, and a client unaware of that hit a dead end.
After
An empty body with that header is accepted as {}; a bodyless request works and releases the lock. An explicit {} still works too.
FIX-0709-7: portal events now wake a sleeping galaxy app
Before
A Bitrix24 event sent to a sleeping galaxy app's subscription did not wake it — delivery retried and was lost after the attempts ran out.
After
The platform wakes a sleeping galaxy app on event delivery and delivers the event once it is up — same as for a standalone server.
NEW-0709-8: camelCase keys inside communications when creating an activity
POST /v1/activities now accepts the nested keys of communications items in camelCase (type, value, entityTypeId, entityId) — consistent with the rest of the API. Previously the nested keys were accepted only in Bitrix24 UPPER case (TYPE, VALUE, ENTITY_TYPE_ID, ENTITY_ID), and the camelCase form was silently dropped — communications: [{ "type": …, "value": … }] returned 422 "COMMUNICATIONS is not defined or invalid", while [{ "TYPE": …, "VALUE": … }] created the activity. UPPER case still works; if both forms of the same key appear in one object, the UPPER-case one wins.
FIX-0709-9: unpublishing removes the tab across all of the app's handlers
Before
POST /v1/apps/:id/unpublish unbound the placement only by the handler matching the assumed platform address. If the tab was bound to the app's own technical address, Bitrix24 did not find it and did not remove it — the tab stayed stuck in the CRM card and could no longer be removed via the API.
After
Unpublishing now removes the placement across all of the app's handlers, including one bound to the server's technical address — the orphaned tab disappears.
NEW-0709-10: Key self-description in GET /v1/guide
The GET /v1/guide response now carries a data.keysAuth block. It describes the two self-description endpoints — GET /v1/me and GET /v1/guide — and links to their documentation: the response contract of each, the key access mode, and the overview of key types.
Both endpoints work with the X-Api-Key header alone, no session token is required.
The field is additive: existing clients are unaffected.
Affected endpoints: GET /v1/guide
FIX-0709-11: GET /v1/{entity}/:id now honours ?select=
The ?select= field projection was ignored on single-record reads by id: the response always came back with every field, even though /v1/me states ?select= works "on list, get by id, and POST /search". Get-by-id now projects the response the same way list and search do, bringing the behaviour in line with what was documented.
Before
GET /v1/leads/42?select=id,title returned the full record (all fields).
After
GET /v1/leads/42?select=id,title returns only id and title. The comma (?select=id,title), array (?select[]=id&select[]=title) and indexed (?select[0]=id&select[1]=title) forms are all accepted; id is always included in the response. An unknown field name is silently skipped — it is not an error. When ?select= and ?include= are combined, the included relation is preserved in the response. The indexed form (?select[0]=…) previously returned 500 on the list endpoint GET /v1/{entity} too — it now projects correctly there as well.
FIX-0709-12: /fields for orders, basket items, requisite presets and document templates now matches the real B24 contract
Before
GET /:entity/fields (and the OpenAPI schema generated from it) declared fields that Bitrix24 never returns: provider on document templates; reserved, sumPaid, dateBill, datePayBefore, datePaid, empPaidId, userEmail, userName at the order top level; module, fUserId, lid, dateRefresh, subscribe, reserved, reserveQuantity on basket items; originatorId on requisite presets. Filtering and sorting by those fields silently did nothing. Meanwhile real fields were left undeclared: requisiteLink on the order, type/properties/reservations on the basket item. Requisite presets accepted countryId/entityTypeId on update, where Bitrix24 silently ignores them.
After
The non-existent fields are removed from /fields and OpenAPI. The real fields are declared: on the order — requisiteLink (object requisiteId/bankDetailId/mcRequisiteId/mcBankDetailId, read-only); on the basket item — type, properties, reservations (read-only). On requisite presets, countryId and entityTypeId are marked create-only: an update returns 400 READONLY_FIELD instead of a silent no-op.
Impact on integrators
list/get responses are unchanged — the removed fields were never returned. If a request filtered or sorted by a removed field it now returns 400 — use the real fields from /fields (for example, an order's payment dates and sums live inside the payments array, not at the top level). Updating countryId/entityTypeId on a requisite preset is now explicitly rejected — set those fields on create only.
Affected endpoints: GET /v1/orders/fields, GET /v1/basket-items/fields, GET /v1/requisite-presets/fields, GET /v1/doc-templates/fields
NEW-0709-13: GET /:entity/fields returns human-readable labels and descriptions for deals, leads, invoices, activities, statuses and timeline comments
The response of GET /v1/deals/fields, /v1/leads/fields, /v1/invoices/fields, /v1/activities/fields, /v1/statuses/fields and /v1/timelines/fields now carries a human-readable label and description for every field, returned in English. Fields with magic-number codes gained enum dictionaries: deals — stageSemanticId (P — in progress, S — success, F — failure); activities — typeId, direction, priority, status, notifyType and descriptionType. Existing calls keep working unchanged — these are additive metadata fields, the response shape does not change.
FIX-0709-14: POST /v1/batch — unified sub-call error shape and totals only for list/search
Before
A Bitrix24 error inside a successful 200 POST /v1/batch response (for example, a get of a missing element) landed in data.errors[<id>] in Bitrix24's native { error, error_description } form — not the V1 envelope { code, message } used by validation errors and every other API response. Meanwhile data.totals[<id>] was populated for any action, including get/create/update/delete, where a lone number next to a single record is meaningless.
After
A sub-call error is mapped to { code, message } (error → code, error_description → message), consistent with all other errors. data.totals[<id>] is populated only for list and search actions, where a match count actually has meaning.
FIX-0709-15: GET /v1/openline-configs — empty-value normalization in the response
Before
GET /v1/openline-configs and GET /v1/openline-configs/:id returned service fields in shapes awkward for clients: KPI_FIRST_ANSWER_LIST, KPI_FURTHER_ANSWER_LIST, DEFAULT_OPERATOR_DATA came back as null (client .map/.length broke on them); AUTO_CLOSE_TEXT for an unset value came back as "" in the card but null in the list; WORKTIME_HOLIDAYS/WORKTIME_DAYOFF for an empty set came back as [""] (an array with one empty string).
After
Lists are normalized: null → []. AUTO_CLOSE_TEXT converges on a single null for an unset value in both the list and the card. WORKTIME_HOLIDAYS/WORKTIME_DAYOFF return [] for an empty set. The list and the card now return the same shape for these fields.
FIX-0709-16: GET /v1/users/fields returns the possible values (items) for enumeration user-fields
Before
A custom enumeration user-field (UF_USR_* of the "list" type) came back from GET /v1/users/fields as { "type": "string", "label": "…" } — with no list of possible values. Reason: for UF fields the user.fields method returns only a label, not the type or the variants, so an enumeration was indistinguishable from a string.
After
Such a field now carries its real type and the list of variants: { "type": "enumeration", "label": "…", "items": [ { "ID": "…", "VALUE": "…", "DEF": "…", "XML_ID": "…" }, … ] }. The values are read from user.userfield.list — the key needs the user.userfield scope for this; without it the field is still returned with its label but without items (graceful degradation). Other UF fields (money, date, …) likewise now show their real type instead of string.
FIX-0709-17: the empty-queue hint now appears after sustained emptiness
Before
GET /v1/bots/:botId/events incremented the empty-response counter on exactly every request, and the hint field appeared strictly after the fifth consecutive empty response. The number in the hint text matched the number of requests made.
After
The empty-response counter is updated periodically rather than on every request, so hint appears after the queue stays empty for a sustained period — at the recommended 2–5 second polling interval, after roughly a couple of minutes of continuously empty polling. The number N in the text reflects the count of observed empty periods, not the exact number of requests made. The "an event was delivered → counter and hint reset" rule is unchanged.
Impact on integrators
No code change is needed. If you relied on hint appearing strictly on the fifth request or read N as an exact request count — use persisted and the presence of events as the primary signal, and treat hint as a diagnostic hint.
2026-07-08
FIX-0708-1: GET /v1/doc-templates and POST /search now honour order and offset
Before
The order and offset parameters on GET /v1/doc-templates and POST /v1/doc-templates/search were silently ignored: the list always came back sorted ascending by id, and offset did not move the window. The underlying Bitrix24 method returns templates as an id-keyed object, and the requested order was lost while unwrapping the response.
After
Sorting (order[field]=asc|desc, including multi-field) and windowing (offset/limit) are applied on the Vibecode side: the full template set is fetched, sorted, and sliced to the requested window. total and hasMore are computed from the actually collected set.
Impact on integrators
If you relied on the implicit ascending-by-id order at offset=0 with no sort, nothing changes — that stays the default. POST /v1/doc-templates/batch (batch list) is unaffected. String order (name, region) is byte-wise, not locale-aware.
NEW-0708-2: GET /v1/apps/:id/sources — new linkedServerSources field
GET /v1/apps/:id/sources now additionally returns a linkedServerSources field — source versions stored under a server (via POST /v1/infra/servers/:id/sources or auto-save on deploy), grouped by server, each with its own serverContext. Such versions previously did not appear in this response when saved under a personal key — they are now visible.
The field is additive: versions, totalVersions, currentVersionId and totalSizeBytes are unchanged and still list only app-scoped versions. Alongside it come linkedServerSourcesTruncated (a truncation flag for very large histories) and linkedServerHint, which points to GET /v1/infra/servers/:serverId/sources — the authoritative full list and download for those versions. The section is populated for the app author (personal key) and the portal administrator; it is empty when called with an OAuth-app key, and is not computed on a ?sha256= probe.
NEW-0708-3: preemptible field on the server plans list response
The GET /v1/infra/providers/:id/plans response now formally documents the preemptible field on each plan. A preemptible plan is cheaper, but the cloud force-restarts such a machine roughly once a day — it is not suitable for continuous 24/7 workloads. For a server, agent or bot that must run without interruption, pick a non-preemptible plan (preemptible is false or absent).
The field was already returned by the runtime — this entry formalizes it in OpenAPI and the docs; no change to existing integrations is required.
NEW-0708-4: GET /v1/models/:id now reports a retired model's successor price and a replaced_by field
For a model that has been retired, the by-id detail request now returns a replaced_by field with the identifier of the successor model that actually serves the calls, and the pricing field shows that successor's price — the price the request is billed at. Previously pricing showed the retired row's own zero price, making the model look free while a paid successor served the calls. Ordinary models and existing calls are unchanged.
FIX-0708-5: list auto-pagination preserves row order for limits above 550
Before
Auto-paginated list reads — GET /v1/{entity}?limit=… and POST /v1/{entity}/search — returned rows out of order when limit exceeded ~550: internal result pages were merged in the wrong sequence relative to what Bitrix24 returned, so the order parameter was not honored across the final array. When the collection held more rows than limit, the window trim could drop rows from the middle of the sorted set while keeping later ones.
After
Pages are merged strictly in Bitrix24 return order: rows arrive in the requested sort for any limit, and the limit trim no longer drops rows from the middle of the set because of wrong merge order.
Impact on integrators
No changes required. If you re-sorted large result sets on your side as a workaround, that is no longer needed.
FIX-0708-6: auto-pagination no longer silently drops a page when a batch sub-request fails
Before
With limit > 50 the list is assembled from batch sub-requests of 50 records each. If Bitrix24 rejected one sub-request (most often on its request limit — QUERY_LIMIT_EXCEEDED), that page silently fell out of the middle of the set: the response stayed 200 with an undetectable 50-record hole in the data (e.g. records 1–200 and 251–600 without 201–250), while meta.total and meta.hasMore looked self-consistent.
After
For lists, plain search, batch sub-calls and aggregations the result is always a contiguous prefix of the set: records past the failed page are dropped, meta.hasMore stays true, and the response carries meta.pageErrorSample { code, message } with the failure reason — mirroring windowed search's meta.windowErrorSample. The field is added to list responses (e.g. GET /v1/deals), to POST /v1/{entity}/search (e.g. deals), to per-sub-call meta of POST /v1/batch and to data.meta of POST /v1/{entity}/aggregate (where it explains why recordsProcessed is below totalRecords). In windowed search (a wide date range) the set is assembled from windows, so a page lost inside one window can leave that window's tail missing — there the incompleteness signal is meta.pageErrorSample itself, not meta.hasMore. In all cases the field appears only when the returned page is actually shorter than limit: a full page is never flagged with a false alarm. Incomplete responses are never cached: a retry goes straight to Bitrix24.
Impact on integrators
No client changes required: result sets that could previously contain an invisible hole are now correct, and the shortfall reason is visible in meta.pageErrorSample. Fetch the remainder by retrying with offset equal to your original offset plus the number of records received — except for windowed searches over a wide date range (offset is not supported there: narrow the range or retry later).
BC-0708-7: structured output: a truncated or empty result now returns 422 instead of an empty 200
Old format supported until: 08.07.2026
Before
POST /v1/chat/completions with response_format (json_object or json_schema) could return 200 with content: null (or a truncated, unparseable JSON string) plus a warning that clients ignored, when generation was cut off. This happened most often on reasoning models: the reasoning phase consumed the whole max_tokens budget before the model produced the JSON. The response looked successful but could not be parsed.
After
Such a request now returns 422 with code: "structured_output_truncated", plus finishReason, suggestedMaxTokens (a larger max_tokens to retry with), and param: "max_tokens". In streaming mode a {"error":{"code":"structured_output_truncated"}} frame is emitted before data: [DONE] — read the stream through to [DONE]. Additionally, for free reasoning models given a too-small max_tokens the platform raises the budget to a safe minimum and tags the successful response with a MAX_TOKENS_RAISED warning. The truncated attempt still consumes and bills tokens.
What integrators should do
Handle 422 structured_output_truncated in your error branch and retry with a larger max_tokens (you can use the suggestedMaxTokens value). For strictly deterministic JSON, set a generous max_tokens or use a non-reasoning model.
2026-07-07
FIX-0707-1: smart-processes: linkedUserFields accepts Y/N and boolean values
Before
POST /v1/smart-processes and PATCH /v1/smart-processes/:entityTypeId with linkedUserFields only worked when the flag value was strictly "true"/"false". A value in the "Y"/"N" convention (used by every other smart-process field) or a boolean true/false was silently ignored: the request returned success: true, but the display in the user field was not enabled.
After
linkedUserFields values are normalized the same way as the nested relations[].isChildrenListEnabled flag: true/"Y"/"yes"/1 → enabled, false/"N"/"no"/0 → disabled. Existing calls with "true"/"false" keep working unchanged.
Impact on integrators
No action needed — calls that previously "silently did nothing" with "Y" are now applied correctly.
BC-0707-2: Order-card nested fields normalized
Old format supported until: 06.01.2027
Before
GET /v1/orders/{id} returned the nested clients, payments, basketItems arrays in raw Bitrix24 shape: boolean fields as "Y" and "N" strings (payments[].paid, clients[].isPrimary, basketItems[].vatIncluded, and others), dates inside payments and basketItems with a +03:00 offset, and companyId set to 0 when no company is bound. The accountNumber field was silently ignored on create and update.
After
Nested Y/N fields now arrive as boolean (true or false); nested dates are normalized to UTC (ending in Z); companyId is null when no company is bound instead of 0; accountNumber became read-only — sending it on create or update returns 400 with code READONLY_FIELD.
What integrators should do
Read nested Y/N fields as boolean instead of comparing to the string "Y"; treat null instead of 0 as "no company bound"; stop sending accountNumber in the create and update body — the number is assigned automatically.
FIX-0707-3: The /v1/openapi.json spec now matches actual runtime
Before
The machine OpenAPI spec was generated from static entity metadata and drifted from real responses: no field was marked nullable, nested order-card arrays were typed as a string, list methods were missing filter and select, and operations declared only success codes and 403.
After
The spec now reflects the contract. Nullable fields are emitted as type: ["<type>", "null"]. Object and array-of-object fields are typed honestly, including the nested clients, payments, basketItems, propertyValues of GET /v1/orders/{id}. List methods declare the filter and select query params. Operations carry the standard error codes 400, 401, 404, 422 in the single { success:false, error:{ code, message } } envelope. *Input schemas declare create-required fields. Additionally GET /v1/orders/fields returns clients as an array instead of object. An SDK generated from the spec now types responses correctly.
BC-0707-4: /search: auto-windowed search now returns the real Bitrix24 error on total failure
Old format supported until: 07.09.2026
Before
Any failed auto-windowed POST /v1/{entity}/search returned 502 { "error": { "code": "WINDOWED_SEARCH_FAILED" } } with a generic "add autoWindow:false".
After
The response matches the same query at a narrow range — the real code and message: a rejected filter/sort field → 400 UNKNOWN_FILTER_FIELD / 400 INVALID_PARAMS; no access → 403; request limit / queue overload → 429 + Retry-After; timeout → 503; Bitrix24 unavailable → 502 BITRIX_UNAVAILABLE. Partial window failure (status 200) now carries meta.windowErrorSample { code, message }.
What integrators must do
If you branched on error.code === "WINDOWED_SEARCH_FAILED" (e.g. to retry with autoWindow:false) — branch on the real codes instead. The autoWindow:false workaround remains; it is useful where it actually helps (the 429 QUEUE_TIMEOUT hint names it). The total-failure response no longer carries the meta block (autoWindowed/windowCount/windowErrors) — the signal is now in the error code/message itself; meta.windowErrorSample remains on partial failure (status 200).
FIX-0707-5: lists on a portal without the module return 409 consistently, not 429
Before
On a portal where the Universal Lists module is disabled, calls to /v1/lists returned the clear 409 LISTS_MODULE_NOT_ENABLED only for the first few requests. After that the built-in error-loop protection tripped and every subsequent call returned 429 ERROR_LOOP_DETECTED, hiding the real cause (the module is not installed).
After
The "method unavailable on this portal" signal is no longer counted by the error-loop protection, so lists.* calls on a module-off portal return 409 LISTS_MODULE_NOT_ENABLED consistently no matter how many times they repeat. The response stays actionable: enable the module on the portal and retry.
NEW-0707-6: error.hint on the 400 for a server create missing source and provider/plan/region
POST /v1/infra/servers, when rejected with 400 INVALID_REQUEST because provider/plan/region are missing (and no source was passed) on a galaxy-placement portal, now additionally returns an error.hint object with reason (why the request was rejected on this portal), recovery (the recommended one-shot path plus the working two-step alternative) and example (a paste-ready one-shot body skeleton). error.code and error.message are unchanged — the hint is strictly additive; portals without galaxy placement get the previous response, without hint.
The hint is also returned on 400 RUNTIME_PARAM_REMOVED (a create with runtime but no source on a galaxy-placement portal), and a body carrying placement: "dedicated" gets a separate hint variant — for a dedicated server, keeping the intent and adding the missing provider/plan/region tuple, instead of steering the caller into a galaxy container.
FIX-0707-7: Galaxy deploy checklist in /v1/me now matches the actual contract
Before: step 2 of deployment.galaxyApp.checklist instructed POST /v1/infra/servers { name } with no source and no provider/plan/region — that call always failed with 400 INVALID_REQUEST. The CREATE rule did not explain that the two-step path requires the full provider/plan/region tuple, and newAppPlacement.note promised a dedicated standalone VM where the create actually returns a galaxy slot with next: "deploy". The favicon guide pointed to the same broken order; the never-deployed-slot reap window was stated as "~12-20 min" versus the actual ~20-25.
After: the recommended path is a single call — POST /v1/infra/servers { name, source, runtime, start } (omit provider/plan/region). The two-step path is documented truthfully: a create without source requires the full provider/plan/region tuple (values are informational for a galaxy — the app inherits its host), and on galaxy placement returns a slot with next: "deploy"; a never-deployed slot is reaped to ERROR after ~20 min (swept every 5 min). Favicon: the primary path is a self-hosted /icon.svg inside the archive (no id needed); the platform-hosted URL remains an alternative via the two-step order or a re-deploy. The status-polling step gained a status=error branch → provisionError/buildLog → re-deploy.
Impact on integrators: agents following the checklist now deploy on the first call. Endpoint behavior is unchanged — only the /v1/me texts and the /v1/openapi.json description were updated; existing integrations keep working as is.
NEW-0707-8: Company AI quota is available via the API
The new GET /v1/ai/quota endpoint returns the monthly AI quota state of your Bitrix24 account: the percentage of the limit consumed (pctUsed, an honest value — above 100 on overspend), the exhaustion flag (exhausted), the reset date (resetAt, a rolling 30-day window), and a per-model breakdown — request counts, tokens, and each model's share of the monthly limit (byModel[].pctOfLimit). Absolute limit values in Vibe credits are not exposed — percentages only, same as the dashboard. Requires the vibe:ai scope.
2026-07-06
NEW-0706-1: pricing.perCall and pricing.perMinute fields in the model catalog
GET /v1/models and GET /v1/models/{model} responses gain optional fields in the pricing object: perCall — the cost of a single request in Vibe credits, perMinute — the cost of one minute of audio in Vibe credits (for speech-to-text models). The fields appear only for models whose corresponding base price is above zero; for all other models the pricing object is unchanged — existing requests keep working as before.
NEW-0706-2: New 402 error code ai_quota_exhausted on AI endpoints
When the portal monthly AI quota control is active, POST /v1/chat/completions, POST /v1/embeddings and POST /v1/audio/transcriptions may return 402 with { success: false, error: { code: "ai_quota_exhausted", type: "insufficient_quota", reason, resetAt? } }. The reason field distinguishes three cases: breaker — the hourly over-quota spending limiter fired, wallet_empty — the quota is exhausted and the portal balance has no funds, wallet_off — over-quota usage is not available for this portal. resetAt is when requests will pass again (may be absent for a permanently disabled portal). While the portal quota is not exhausted, endpoint behavior is unchanged.
FIX-0706-3: Over-quota AI usage is charged at the model's base catalog price
Before
With the portal monthly AI quota control active, over-quota requests to POST /v1/chat/completions, POST /v1/embeddings and POST /v1/audio/transcriptions were charged to the portal balance at internal quota-program rates — with off-peak discounts applied; the effective price was not visible in the model catalog.
After
Over-quota usage is charged at the model's base price from the public catalog — the same one returned in the pricing field of GET /v1/models, including the new perCall and perMinute for non-token models. Off-peak discounts apply only to quota consumption, not to the money balance. Usage within the quota is still not charged to the portal balance.
Impact on integrators
No changes required. The cost of over-quota usage can now be computed upfront from the model's catalog price.
NEW-0706-4: The bitrix/embeddings model is available in the API
The POST /v1/embeddings endpoint is now served by the bitrix/embeddings model — turning text into vector representations for semantic search, clustering, and retrieval (RAG). The model is free and billed on input tokens only. For the list of embedding-capable models see GET /v1/models.
FIX-0706-5: Deploy reports an honest error when the new build did not bind the port
Deploy via POST /v1/infra/servers/:id/deploy now verifies the port is held by the NEW service. If a previous process keeps listening and the new build crash-loops with EADDRINUSE, the deploy fails honestly instead of falsely succeeding; a port held by a leftover process of the same app is freed automatically where provable.
Before
The old version kept answering 200, the deploy reported success, and the new build never came up — with no error and no hint.
After
The healthcheck step returns an error naming EADDRINUSE and the port, and the stop_existing step frees the port from a leftover process of the app (or warns and proceeds when it cannot).
NEW-0706-6: filter: $nin (NOT IN) operator to exclude a set of values
Before
There was no way to select records whose field is NOT in a set of values: the $in (IN) operator existed, but its inverse did not. The Bitrix24-native field-name prefixes @ (IN) and !@ (NOT IN) ({ "!@categoryId": [1, 3] }) were not translated — such a deal filter returned 400 UNKNOWN_FILTER_FIELD.
After
Added the $nin operator: { "filter": { "categoryId": { "$nin": [1, 3] } } } returns records of every value except those listed (NOT IN). Symmetric to $in. The Bitrix24-native @ / !@ field-name prefixes are still unsupported, but now return a clear 400 INVALID_FILTER_FIELD hinting to switch to $in / $nin, instead of a confusing error or a silently-ignored (thus full-set) filter.
BC-0706-7: dedicated error code for an oversized exec command
Old format supported until: 06.01.2027
Before
A command longer than 10000 characters at POST /v1/infra/servers/:id/exec was rejected with the generic VALIDATION_ERROR code, with no cause and no way out.
After
Such a request returns 400 with the dedicated COMMAND_TOO_LONG code and a structured hint: ship large payloads and scripts via POST /v1/infra/servers/:id/upload, then run them with bash /path/script.sh. All other schema violations still return VALIDATION_ERROR.
What integrators should do
If your client handles VALIDATION_ERROR of this endpoint as the catch-all validation case — add handling for the COMMAND_TOO_LONG code (or treat any 400 uniformly).
NEW-0706-8: hint in the exec timeout error
The EXEC_TIMEOUT error of POST /v1/infra/servers/:id/exec now carries a structured hint field (reason / recovery / recoveryAction): why the process was stopped (at timeout the whole process group is terminated forcibly, with no grace period) and what to do — run long operations as a detached background job and monitor it via GET /v1/infra/servers/:id/logs, raise timeout up to 600 seconds, or use ?stream=true. The field is additive: the previous code / message shape is unchanged, and the hint arrives both in JSON mode and in the SSE error event.
2026-07-05
FIX-0705-1: Chat message send — a clear error when the text is empty
The message text goes in the message field. Previously a call to POST /v1/chats/{dialogId}/messages that carried the text under an unknown field name (e.g. {"text": "hi"}) silently dropped that field, and Bitrix24 returned a 422 BITRIX_ERROR about an empty message — even though content was supplied.
Before
{"text": "hi"} → 422 BITRIX_ERROR about an empty message, with no hint at the cause.
After
The same call now returns 400 MESSAGE_REQUIRED right away and lists the unrecognized field(s), pointing at the message field. Empty text is still allowed together with an attach block (attachment-only message).
Impact on integrators
Correct calls that use the message field are unchanged. The error for a wrong field name is now precise.
FIX-0705-2: API key in the Authorization: Bearer header — a clear error instead of INVALID_SESSION
The API key goes in the X-Api-Key header. Previously, if the key was mistakenly placed in Authorization: Bearer (that slot is for an OAuth app session token), the server returned 401 INVALID_SESSION, so integrators chased an OAuth-session problem when the real cause was the wrong header.
Before
A vibe_app_* key in Authorization: Bearer → 401 INVALID_SESSION.
After
The same request now returns 401 WRONG_AUTH_SCHEME with a hint: an OAuth app key (vibe_app_*) goes in X-Api-Key, and Authorization: Bearer carries the session token (vibe_session_*) from POST /v1/oauth/token; a client that can only send Bearer can use a personal key (vibe_api_*). Session tokens and personal keys in Bearer are unaffected.
Impact on integrators
Correct calls with the key in X-Api-Key and the session in Authorization: Bearer are unchanged.
FIX-0705-3: multipart/create rejects XSS-prone content types for PUBLIC objects
Before
For PUBLIC objects the content types text/html, application/javascript, application/x-javascript and image/svg+xml were rejected on direct and presigned uploads, but not when starting a multipart upload. Calling POST /v1/storage/objects/multipart/create with visibility = PUBLIC and such a type created a session, and once completed the object was served inline in the browser.
After
POST /v1/storage/objects/multipart/create with visibility = PUBLIC and an XSS-prone content type now returns 415 STORAGE_FORBIDDEN_CONTENT_TYPE, the same as direct and presigned uploads. No multipart session is opened. PRIVATE objects still accept any content type.
Impact on integrators
Behavior now matches what the Storage reference documents: XSS-prone content types are not allowed for PUBLIC objects on any upload path. To multipart-upload such a file, use visibility = PRIVATE or a safe content type.
FIX-0705-4: binding a placement to a server's technical URL now routes through the platform handler
Before
POST /v1/placements/bind accepted a handler pointing at the app's technical Black Hole URL (app-*.vibecode…) and registered it with Bitrix24 verbatim. Bitrix24 posted the placement iframe straight to that URL, bypassing the platform: no session was minted, and on a server restricted to Bitrix24 users the placement looped on the login gate (on a public server the app returned its own 404 error).
After
Such a handler is automatically rewritten to the app's platform handler (/v1/bitrix-handler) — the placement opens and authenticates normally. The response gains handlerRewritten: true and requestedHandler with the original value. External (non-Black Hole) handlers are left unchanged. If the app's platform handler cannot be resolved, the bind is rejected with code PLATFORM_HANDLER_UNRESOLVABLE instead of registering an unusable URL. Additionally GET /v1/placements flags an already-misbound handler: data.handlers[].misbound: true plus a textual warnings[].
Additionally, if the placement is already registered in Bitrix24 but missing from the app's list (drift — e.g. after an unpublish that didn't unbind on Bitrix24), the bind no longer fails with "Handler already binded": the platform clears the stale binding and retries the request once, healing the drift. If the bind fails for another reason (for example a commercial Bitrix24 plan is required), the working placement is left in place.
NEW-0705-5: New 402 error code ai_quota_exhausted on AI endpoints
When the portal monthly AI quota control is active, POST /v1/chat/completions, POST /v1/embeddings and POST /v1/audio/transcriptions may return 402 with { success: false, error: { code: "ai_quota_exhausted", type: "insufficient_quota", reason, resetAt? } }. The reason field distinguishes three cases: breaker — the hourly over-quota spending limiter fired, wallet_empty — the quota is exhausted and the portal balance has no funds, wallet_off — over-quota usage is not available for this portal. resetAt is when requests will pass again (may be absent for a permanently disabled portal). While the portal quota is not exhausted, endpoint behavior is unchanged.
2026-07-04
BC-0704-1: Overload rejections: 429/503 instead of 504
Old format supported until: 31.07.2026
Overload rejections changed HTTP status codes (body codes are UNCHANGED — only the status changes). Rule of thumb: 429 — the request was NOT processed, safe to retry after Retry-After (the header is now always set); 503 — the platform or upstream is unhealthy, retry later; for write operations verify whether the change was applied before retrying. Application-level 504 is removed from the API.
What changed: QUEUE_OVERFLOW 503→429; QUEUE_TIMEOUT 504→429; BITRIX_TIMEOUT (Bitrix24 did not respond within 15 seconds — the write may have been applied, re-read the entity before retrying) →503; ai_provider_timeout 504→503; UPSTREAM_TIMEOUT (web search /v1/search — the upstream provider did not respond in time) 504→503; RUNTIME_TIMEOUT / GATEWAY_TIMEOUT / WAKE_TIMEOUT 504→503.
NEW-0704-2: New AI overload code: 429 ai_congested
AI requests to the platform cluster now pass through an admission gate: on pool overload the response is 429 with body code ai_congested and a Retry-After header. Retry is safe (the request never ran, no charge). BYOK keys and external providers are not affected. Disabled by default — enabled by the platform.
BC-0704-3: read-only (READONLY) keys can no longer write on hand-written endpoints
Old format supported until: 03.07.2026
Before
An APP key with the accessMode: READONLY access mode still reached the write to Bitrix24 on a number of hand-written endpoints (requisites and presets, user fields, timeline pin/note/bind, telephony, mail, disk, business processes, user invite and deactivation, and others) — the guard checked only the scope, not the key access mode.
After
Any write attempt with a read-only key returns 403 with the WRITE_BLOCKED_READONLY_KEY code. Read endpoints are not affected.
What integrators should do
If your integration was writing with a read-only key, switch the key to read and write mode on the key management page.
FIX-0704-4: galaxy app deploy now unpacks wrapped archives correctly and reports an empty source.content clearly
Before
POST /v1/infra/servers/:id/deploy for a galaxy app (kind=GALAXY_APP) whose source.content project files sat inside a single wrapping folder (a typical macOS-built zip) built the app with an empty build context and failed at runtime with npm error enoent Could not read package.json. When source.content did not unpack into any files at all (a versionId, a path, or an empty archive), the deploy raised the same confusing build error.
After
Such archives now deploy correctly — the app files end up at the build-context root. And when source.content unpacks to an empty context, the deploy returns GALAXY_APP_BUILD_FAILED right away with a clear empty-build-context message, hinting that source.content must be a base64 archive (tar.gz or zip) of your project files, not a versionId or a path.
2026-07-03
FIX-0703-1: binding a placement via a developer key no longer returns 500 after an unpublish/republish cycle
Before
POST /v1/placements/bind for a developer-key-managed app (type local.*) could consistently return 500 (BITRIX_UNAVAILABLE, INTERNAL_SERVER_ERROR) when binding a placement (e.g. CRM_DEAL_DETAIL_TAB or CRM_CONTACT_DETAIL_TAB) after the app was unpublished and published again. The previous placement registration remained on the Bitrix24 side, and registering a new one on top of it failed with an internal error. Retries returned the same error.
After
Before registering the placement, the request now clears its previous registration on the Bitrix24 side, so the bind succeeds even after an unpublish/republish cycle. No change to your call.
FIX-0703-2: /v1/sites — filtering by knowledge-base (KNOWLEDGE) and group (GROUP) type is no longer ignored
Before
POST /v1/sites/search (as well as GET /v1/sites and POST /v1/sites/aggregate) with filter[type]=KNOWLEDGE or filter[type]=GROUP silently returned ordinary landing sites (PAGE / STORE / VIBE) instead of knowledge bases or group pages. The cause is on the Bitrix24 side: the landing.site.getList method binds the TYPE filter to an internal area (scope), and without the scope parameter the KNOWLEDGE / GROUP types are not part of the default area — so the type filter was silently dropped. The only workaround was to add scope manually (see List sites).
After
When the filter names one such type and scope is not passed explicitly, Vibecode supplies the matching area itself (type=KNOWLEDGE → scope=KNOWLEDGE, type=GROUP → scope=GROUP) — and the request returns exactly the knowledge bases / group pages. An explicitly passed scope always wins and is never overridden. If the type is given as a list or operator (for example {"type":{"$in":["KNOWLEDGE","PAGE"]}}), where a single area cannot be chosen, meta.warnings carries a hint with code TYPE_REQUIRES_SCOPE.
Impact on integrators
No action required. Requests with filter[type]=PAGE / STORE / VIBE and requests without a type filter work as before. MAINPAGE is an area, not a site type (its sites have type VIBE), so MAINPAGE is not derived from the type filter. Behavior for /v1/pages is unchanged.
FIX-0703-3: page and site aggregation returns count again
Before
POST /v1/pages/aggregate and POST /v1/sites/aggregate returned count: 0 and meta.totalRecords: 0 even when pages and sites existed — in every form: without a filter, with a filter, with a count expression, and as the top-level count alongside groupBy. Per-group counts under groupBy were already correct.
After
count and meta.totalRecords reflect the actual number of records; the top-level count under groupBy equals the sum of the group counts.
Integrator impact
No action required — the response is now correct.
NEW-0703-4: Quality and timestamp parameters in audio transcription
Audio transcription POST /v1/audio/transcriptions accepts five new optional fields. Recognition quality: prompt — a context hint (conversation topic, style, correct spelling of terms, up to 2000 characters), hotwords — a comma-separated list of special words (rare terms, brands, names, up to 500 characters), vad_filter — a silence filter applied before recognition (fewer hallucinations on recordings with pauses). Output control: temperature — decoder temperature from 0 to 1, timestamp_granularities[] — timestamp granularity word/segment (only with response_format=verbose_json; with word, each segment gains a words array with per-word timing and probability). The fields are passed in multipart/form-data alongside file and are compatible with the OpenAI contract. Invalid values are rejected with the invalid_prompt, invalid_hotwords, invalid_temperature, invalid_vad_filter, and invalid_timestamp_granularities codes.
FIX-0703-5: Apps created via the API now open correctly as placements
Before
Some apps created via POST /v1/apps failed to open when their placement was invoked in Bitrix24 — instead of the app interface the user saw an app-resolution error.
After
Created apps now resolve correctly and open as placement widgets in Bitrix24. The create response is unchanged — the app is immediately ready for publishing and binding placements.
Integrator impact
No action required. Recreate a previously non-opening app (delete and create it again) — the new app opens correctly.
NEW-0703-6: key deletion is blocked while an agent or bot is linked
DELETE /v1/keys/:id now returns 409 with code KEY_HAS_LINKED_AGENT when the key is the control key of a live AI agent or managed bot.
Before
Deleting such a key orphaned the agent and cascade-deleted the bot together with its token — the bot's Bitrix24 identity was lost irrecoverably, with no warning.
After
Response body: { success: false, error: { code: "KEY_HAS_LINKED_AGENT", message, details: { linkedAgentCount, linkedBotCount, agents: [{ id, name, status }] } } }. Rebind the resources to another key or delete the agent/bot first; to restore access for an orphaned agent, use the "Restore access" action in the dashboard. The check runs before Bitrix24 synchronization — on a 409 the Bitrix24-side credentials are left untouched. Sibling of the existing KEY_HAS_ACTIVE_SERVERS.
NEW-0703-7: Task change history and kanban stages
Two read-only endpoints added (scope task). GET /v1/tasks/:taskId/history returns a task's full change history in a single call: kanban stage moves, sprint and backlog moves, statuses and other events. Filter by event type via ?field=STAGE (comma-separate several, e.g. ?field=STAGE,MOVE_TO_SPRINT); sort via ?order=asc or ?order=desc (created-date ascending by default). Each entry carries id, createdDate, field, a value object with the previous and new value, and a user with the author id. GET /v1/tasks/stages/:entityId returns the current kanban columns of a workgroup (N) or personal plan (0).
FIX-0703-8: bizproc-activities: clear error instead of "Wrong handler URL" when handler is missing
Before
POST /v1/bizproc-activities without a handler field (or with the handler URL mistakenly sent under handlerUrl) returned the opaque core error 422 BITRIX_ERROR: Wrong handler URL.
After
code, name, and handler are validated before the Bitrix24 call: a missing handler now returns 400 MISSING_REQUIRED_FIELDS with the message Body field "handler" is required to create bizprocActivity, pointing at the correct field name. Successful calls that include a valid handler are unaffected.
FIX-0703-9: POST /v1/batch — update and delete responses are now normalized like create and get
Before
In the global batch call POST /v1/batch an update sub-call returned its response in a raw wrapper (a nested object instead of the flat record), and a delete sub-call returned an empty array with no success signal. This diverged from create and get in the same endpoint and from single PATCH /v1/{entity}/:id, which return a flat normalized record.
After
An update sub-call returns the flat normalized record (camelCase fields) — like create, get and single PATCH. A delete sub-call returns a success signal of the form { id, deleted: true }.
FIX-0703-10: POST /v1/{entity}/batch — create, update and delete work again for CRM entities
Before
A per-entity batch call to POST /v1/{entity}/batch with a create, update or delete action for deals, contacts, companies, leads, quotes and invoices returned a per-item error "Could not find value for parameter {entityTypeId}", and the record was not created, updated or deleted. Single calls (POST /v1/{entity}) and the global POST /v1/batch worked on the same entities.
After
Per-entity batch create, update and delete for these entities now succeed — the same way single calls and the global batch endpoint do.
FIX-0703-11: Omitting the model field in chat again falls back to the default model
Before
POST /v1/chat/completions without a model field returned 400 no_default_model on portals where no default model was explicitly set — even when a free model was available on the portal. Meanwhile GET /v1/me could report a defaultModel that could not be called.
After
When the model field is omitted, the request automatically uses the portal's first callable model — as the docs describe. GET /v1/me now always reports a callable defaultModel, the same one chat will use.
FIX-0703-12: include of related entities again returns the entities themselves, not null
Before
GET /v1/deals/:id?include=contact,company returned _included.company = null and _included.contacts as relation metadata only (sort/isPrimary/roleId) without the entity's own fields, even though the deal referenced an existing company and contacts.
After
_included.company carries the full company object, and _included.contacts carries the full contact objects (id, name, …) alongside the relation metadata. The fix applies to include across CRM entities (deals, leads, quotes, and others).
FIX-0703-13: deal search rejects an unknown filter field instead of silently returning every row
Before
GET /v1/deals and POST /v1/deals/search with an unknown filter field (a misspelled name, a field not in the schema) silently forwarded it to Bitrix24, which ignores unknown filter keys and returns the whole deal set with a 200. A client that sent a filter with a typo in the field name got neither an empty result nor a 400, but the full table — as if the filter had applied.
After
An unknown filter field is now rejected before the Bitrix24 call with a 400 and code UNKNOWN_FILTER_FIELD, listing the available fields in the message — the same behavior contacts, companies, leads, quotes, invoices, and items already have. Declared fields (including aliases such as amount), custom fields (UF_CRM_* and ufCrm*), and id work as before.
FIX-0703-14: Catalog: listing and search return a clean 400 when iblockId is missing
Before
Listing and search GET /v1/catalog-products, POST /v1/catalog-products/search, GET /v1/catalog-sections and POST /v1/catalog-sections/search without iblockId in the filter reached Bitrix24 and returned a murky 422 BITRIX_ERROR ("Field iblockId is not specified in the filter").
After
Catalog listing and search require iblockId in the filter — when it is missing they return 400 MISSING_REQUIRED_FILTER with an example and never reach Bitrix24.
Integrator impact
No change needed — correct requests (with filter[iblockId]) work as before. Only the error code and its clarity changed for requests that already failed.
FIX-0703-15: Contacts — real date fields createdTime/updatedTime instead of phantom createdAt/updatedAt
Before
GET /v1/contacts/fields advertised createdAt and updatedAt, but they never appeared in contact responses — Bitrix24 returns the dates under createdTime/updatedTime, and those are what the contact body carried. Meanwhile filter and select by createdTime (the name a client actually sees in the response) were rejected as an unknown field, while the phantom createdAt "worked" even though the field itself was never readable.
After
The schema declares the real keys createdTime and updatedTime (datetime, read-only): they appear in /fields, filter and select on them work, and the value is normalized to ISO-8601 in UTC. The phantom createdAt/updatedAt are no longer declared — filter or select on them returns 400 UNKNOWN_FILTER_FIELD.
Integrator impact
Reads are unchanged — the createdTime/updatedTime keys were already in the response body, now also normalized. If you filtered or projected contacts by createdAt/updatedAt, rename them to createdTime/updatedTime.
FIX-0703-16: Open Channels config list now honors the limit parameter
Before
GET /v1/openline-configs and its search counterpart ignored limit: the underlying Bitrix24 method returns the whole set of configurations and the wrapper forwarded every row. The hasMore flag was wrong too — false even when records remained beyond the requested limit.
After
The response is clamped to limit on the wrapper side. hasMore: true when Bitrix24 returned more records than the requested limit (a next page exists), otherwise false. total is the record count in the current window.
Integrator impact
A request with limit now returns no more than limit records. Clients that relied on the whole set coming back regardless of limit will see a truncated list — use offset for the next page.
2026-07-02
NEW-0702-1: filter and sort tasks by real status (realStatus)
GET /v1/tasks, POST /v1/tasks/search and POST /v1/tasks/aggregate now accept realStatus in filter (list and search also in sort) — filtering by the task's actual stored status: 1 — new, 2 — pending, 3 — in progress, 4 — awaiting control, 5 — completed, 6 — deferred, 7 — declined. Previously filter[realStatus] was silently ignored and the request returned the whole set.
Unlike filter[status], which Bitrix24 treats as a virtual (meta) filter (values −1 overdue, −2 unviewed, −3 almost overdue) that does not match the status field value in the response, realStatus filters by the stored status. The field is read-only (change the status via status) and is used only in filter/sort — the response already exposes the task's real status in the status field.
FIX-0702-2: creating an app reuses a failed slot with the same name
Before
Repeating POST /v1/infra/servers with the same name after a failed deploy created a new app slot. Failed slots piled up and were only removed by auto-cleanup after 7 days.
After
If the key owner already has a slot with the same name on the portal in error status (or one that was created but never received a deploy), the repeated call returns that same slot: its id is preserved, the error and build log are reset, and the status goes back to provisioning — deploy into it. Slots that never received any code are now removed by auto-cleanup after 24 hours instead of 7 days (slots with a failed build are still kept for 7 days together with their build log).
Impact on integrators
No request changes are required. If your flow re-created a slot with the same name after a failure, you will start receiving the previous id instead of a new one — this is expected: deploying into the returned slot works as usual. Slots owned by other portal users and running apps are never picked up for reuse.
FIX-0702-3: read-only keys can no longer write through /v1/bots
Before
A read-only API key (accessMode: READONLY) could perform write operations through the bot endpoints (POST /v1/bots, sending and deleting messages, adding chat members, registering and deleting a bot, and others) — the call returned 200 instead of 403. Every other Bitrix24 proxy surface already blocked such writes.
After
A write through /v1/bots/* with a read-only key returns 403 with code WRITE_BLOCKED_READONLY_KEY. Read operations are unaffected, including fetching a message context (GET /v1/bots/:botId/messages/:messageId/context) and downloading a file (GET /v1/bots/:botId/files/:fileId).
Impact on integrators
If your bot integration needs to write, switch the key to read+write mode in the /keys section.
FIX-0702-4: folders include=storage now resolves
Before
GET /v1/folders/:id?include=storage (and the list form GET /v1/folders?parentId=...&include=storage) did not add _included to the response, even though GET /v1/folders/fields advertises includable: true for the storage relation.
After
The related storage now resolves: the response includes _included.storage with the storage record looked up by storageId. The relation is described in GET /v1/folders/fields.
FIX-0702-5: PAGE_BACKGROUND_WORKER: bind no longer fails with 500
Before
POST /v1/placements/bind for the PAGE_BACKGROUND_WORKER placement filled in the options.errorHandlerUrl field Bitrix24 requires only when the call went through an OAuth session. When an app bound via a developer key or on a self-hosted portal, the field was not added and Bitrix24 answered 500 (BITRIX_UNAVAILABLE, "Field errorHandlerUrl is empty"), even though other placements bound fine.
After
For PAGE_BACKGROUND_WORKER the options.errorHandlerUrl value now defaults to handler regardless of the bind path. An explicit options.errorHandlerUrl still takes precedence. The response options field now reflects the effective value (with the defaulted errorHandlerUrl).
Impact on integrators
No action required — a call that previously returned 500 now succeeds.
FIX-0702-6: POST /search reports missing required parameters with a clean error
Before
POST /v1/{entity}/search for entities whose Bitrix24 list method requires mandatory parameters did not check for them and forwarded the request to Bitrix24 as-is. A raw Bitrix24 error leaked out (BITRIX_ERROR, e.g. "Invalid value of parameter [ $id ]" or "required parameter type is not set"), whereas the equivalent GET list already returned a clear 400 MISSING_REQUIRED_PARAMS in the same case. Affected POST /v1/calendar-events/search (needs type), POST /v1/files/search (needs folderId) and POST /v1/folders/search (needs parentId).
After
POST /v1/{entity}/search validates the required parameters before calling Bitrix24 — the same guard the GET list has had for a while. A missing parameter returns 400 with code MISSING_REQUIRED_PARAMS and the list of missing fields, with no call to Bitrix24. The required parameter can be passed inside filter, and the parent parameter (folderId for files, parentId for folders) may also be passed at the top level of the request body.
NEW-0702-7: Research price in the key self-description and the required top-up in the 402 body
GET /v1/me now returns cost for every provider in the webResearch.providers[] block, mirroring the webSearch block. The field carries the research-mode price in Ꝟ (cost.research) and the currency (cost.currency), so an agent sees the deep-search price directly in the key self-description, without a separate call.
The 402 response on insufficient balance (INSUFFICIENT_BALANCE, as well as BILLING_FROZEN) on POST /v1/search and POST /v1/research now carries a required field — the amount in Ꝟ needed for the request. The existing userMessage and hint fields are unchanged.
Clients with strict additionalProperties schema validation need to account for the new response fields.
NEW-0702-8: POST /v1/triggers/fire supports invoices (SmartInvoice)
The POST /v1/triggers/fire endpoint accepts a new entityType value — invoice. Pass entityType: "invoice" and the invoice entityId (returned by GET /v1/invoices) to fire an automation trigger for a smart invoice. Existing values (deal, lead, contact, company, quote, item) keep working unchanged.
Previously firing a trigger for an invoice was impossible, and trying it through entityType="item" with entityTypeId=31 was rejected with a message that led to a dead end. Now item with a reserved entityTypeId (including 31) points to the matching entityType — for invoices, that is invoice.
NEW-0702-9: GET /v1/ai/usage returns transcribed audio duration per model
GET /v1/ai/usage now returns an audioSeconds field in the byModel[] block — the total number of audio seconds sent for transcription per model over the selected period. The field is populated for speech-to-text (Whisper) calls and is 0 for text models, where audio duration does not apply.
The field is additive — existing integrations keep working unchanged.
BC-0702-10: categories: code and isDefault marked read-only (were phantom-writable)
Old format supported until: 01.10.2026
Before
GET /v1/categories/:entityTypeId/fields advertised code and isDefault as writable (readonly: false), but writing them via crm.category.add/update was silently ignored (values not persisted, 200 returned).
After
Both fields are marked readonly: true. /fields now reports them as read-only, and an attempt to write code or isDefault returns 400 READONLY_FIELD instead of silently dropping the data.
What integrators must do
Previously, sending code/isDefault in a POST/PATCH /v1/categories/:entityTypeId body was accepted (200, values silently ignored). Now such a request returns 400 READONLY_FIELD. Remove code and isDefault from your category create/update request bodies — these fields are no longer accepted on write.
BC-0702-11: telephony-lines: crmAutoCreate normalized to boolean and now listed in /fields
Old format supported until: 01.10.2026
Before
GET /v1/telephony-lines/fields returned only number, serverName, name. The CRM auto-create flag leaked into list responses under the raw UPPER name CRM_AUTO_CREATE as a "Y"/"N" string — the only UPPER field among camelCase ones — and was absent from /fields. On write, camelCase crmAutoCreate was silently dropped.
After
The field is declared as crmAutoCreate (boolean). It now appears in /fields, comes back normalized (true/false) in list responses instead of raw "Y"/"N", and is accepted as a camelCase boolean on create/update (the raw UPPER name is still accepted on write for compatibility). Clients reading data[].CRM_AUTO_CREATE should switch to data[].crmAutoCreate (boolean).
NEW-0702-12: workgroups: aggregate operation and groupBy fields exposed
Before
POST /v1/workgroups/aggregate worked but was never advertised: the operation was missing from the machine index at /v1/guide, and groupBy returned 400 on any field (Available: .) because the aggregatable list was empty.
After
An aggregatable list is declared: membersCount (numeric sum/avg/min/max) plus categorical active, isProject, ownerId for grouping. The operation is now visible in /v1/guide and /fields, and groupBy over these fields works.
FIX-0702-13: /fields: metadata completeness for doc-templates and bookings
Before
GET /v1/doc-templates/fields omitted isDefault and productsTableVariant even though they appear in list responses. On GET /v1/bookings/fields the mandatory resourceIds and datePeriod were not flagged required, so their obligatoriness was invisible in the schema.
After
doc-templates: isDefault and productsTableVariant are declared (read-only) — /fields now matches the responses. bookings: resourceIds and datePeriod are marked required: true, so the requirement is visible in /fields.
FIX-0702-14: orders: /fields synced with responses, pseudo-key order removed, companyId filterable
Before
GET /v1/orders/fields carried a spurious pseudo-key order (an artifact of parsing sale.order.getFields) and omitted fields that actually appear in responses: companyId, clients, dateMarked, personTypeXmlId, statusXmlId, version. Because companyId was not in the schema, filtering by it failed with UNKNOWN_FILTER_FIELD.
After
/fields is now built from the schema: the order pseudo-key is gone and the six missing fields are declared (companyId — writable number; clients — read-only object returned by get; dateMarked/personTypeXmlId/statusXmlId/version — read-only). Filtering and sorting by companyId now work.
FIX-0702-15: users: limit > 50 now honored, meta.hasMore is accurate
Before
GET /v1/users?limit=500 returned only 50 records even though meta.total reported more. meta.hasMore was always false, so the documented hasMore-based pagination silently dropped everything past the first page.
After
The entity is backed by the legacy user.get method (no .list suffix), so the auto-paginator never engaged. A paginateViaStart flag was added (same as departments): for limit > 50 it now walks pages via start, and meta.hasMore reflects whether more records actually exist.
FIX-0702-16: POST /v1/batch rejects disabled write operations and forwards required list parameters
Before
A global POST /v1/batch call with action create, update or delete for an entity whose operation is disabled (for example openline-configs — writes live behind dedicated routes) went straight to Bitrix24, bypassing normalization, and could silently create or modify a record. Separately: action list or search for an entity with required method parameters (for example calendar-events — type and ownerId) returned AUTO_PAGINATION_FAILED "missing required parameter", even though a direct list request with the same parameters worked. In addition, batch list for folders and files sent the parent folder under the name parentId or folderId, which the disk.folder.getchildren method ignores, so the list silently came back for the wrong folder; and batch list for calendar-events with a leftover filter key forwarded it to Bitrix24 with no error, so the method returned the whole calendar.
After
A disabled write operation in a sub-call is rejected with ACTION_NOT_SUPPORTED before any Bitrix24 call — the same as POST /v1/{entity}/batch. Required list parameters and the method's top-level parameters are forwarded to Bitrix24 under their original names, so batch list behaves like the direct list, and when they are missing a clear MISSING_REQUIRED_PARAMS is returned instead of a raw Bitrix24 error. For folders and files the parent folder is now renamed to the id key the method expects, so batch list comes back for the right folder. For entities whose method has no filter envelope (calendar-events), a leftover filter key is now rejected with UNSUPPORTED_FILTER before any Bitrix24 call — the same as the direct list.
Integrator impact
No action needed. If a batch sub-call previously relied on a disabled write operation running, switch it to the entity's dedicated route. For batch list on entities with required parameters (calendar-events), pass type and ownerId in the sub-call params. If a calendar-events batch list used filter, drop it or move it to the top-level parameters, otherwise the sub-call returns UNSUPPORTED_FILTER.
NEW-0702-17: GET /:entity/fields now returns field label and description
The GET /v1/{entity}/fields response can now carry a human-readable short name label and an explanatory description per field — previously a field was described only by the {type, readonly} pair. This lets an AI agent or UI show a field's name and purpose without consulting the documentation. The text is returned in English. The keys were added for the following entities: Departments, Smart Processes, Storages, Folders, Files, Workgroups, Document Templates, Bookings, Calendar Events, Tasks, Requisites, Users. For Users the label resolution was additionally fixed: GET /v1/users/fields now returns the real Bitrix24 field names from user.fields instead of the technical codes. The change is additive: the new keys appear alongside the existing ones, and existing calls keep working unchanged.
FIX-0702-18: a vibe:*-only key now issues instead of failing
Before
Issuing an API key via POST /v1/keys whose every requested scope is an internal Vibecode vibe:* scope (for example only vibe:infra), on a dev-key portal (self-hosted Bitrix24 or a connected cloud portal), was rejected with 502 DEVKEY_MINT_FAILED. vibe:* scopes are never sent to Bitrix24, so the Bitrix24 webhook scope set came out empty and Bitrix24 rejected the mint, requiring at least one scope.
After
A vibe:*-only key now issues successfully. No Bitrix24 webhook is created for it — none is needed, such a key never calls the Bitrix24 REST — and the key works with Vibecode's internal features per its scopes.
Impact on integrators
No action required: the request that previously failed now returns the created key.
FIX-0702-19: /v1/storages pagination — limit over 50 returns all records, meta.hasMore is correct
Before
GET /v1/storages with limit over 50 returned at most 50 records, and meta.hasMore was always false — even when the portal held more. A client requesting ?limit=50 against 489 storages saw hasMore: false and could not tell it needed to fetch the next page. Same on POST /v1/storages/search and in /v1/batch.
After
limit over 50 goes through auto-pagination (like every other list) and returns the requested number of records, and meta.hasMore equals (offset + returned count) < meta.total even for limit of 50 or less. The change applies to GET /v1/storages, POST /v1/storages/search and the storages list in /v1/batch.
Note: the correct meta.hasMore calculation for list responses with limit of 50 or less now applies to all entities (GET /v1/{entity} and POST /v1/{entity}/search), not only storages — previously meta.hasMore was always false on this path.
FIX-0702-20: infra: Cyrillic in displayName on application create
Before
On POST /v1/infra/servers with a Cyrillic displayName, the name could be stored as a run of question marks (??????) — an encoding corruption while sending to Bitrix24.
After
The name is transmitted as UTF-8 and Cyrillic is preserved correctly.
Impact on integrators
No action required. Cyrillic names are no longer mangled.
FIX-0702-21: items: filtering by parentId relation fields
Before
Filtering by a dynamic relation field (for example parentId2 — the linked deal) on GET /v1/items/:entityTypeId and POST /v1/items/:entityTypeId/search was rejected with 400 UNKNOWN_FILTER_FIELD, even though the field is present in GET /v1/items/:entityTypeId/fields and returned in responses.
After
Fields shaped like parentId<N> are accepted in the filter and forwarded to the request as-is. Finding a smart-process item linked to a specific parent entity now works directly through the items wrapper.
Impact on integrators
No action required. Requests that previously returned 400 now succeed.
FIX-0702-22: smart processes: enumeration values and the userfield display name now persist
Before
On POST /v1/items/:entityTypeId/userfields an enumeration field (userTypeId: enumeration) was created but its value variants were not saved (the list came back empty), and a display name passed as a plain string stayed blank in the interface.
After
Value variants are accepted both as enum and as list and are saved correctly. A name passed as a plain string is automatically wrapped into a language map and fills the edit-form, list-column and list-filter labels.
Impact on integrators
No action required. The enumeration values and the name that were previously dropped silently now persist.
FIX-0702-23: req-family: address and preset-field /fields now return camelCase keys
Before
GET /v1/addresses/fields and GET /v1/requisite-presets/:presetId/fields/schema returned the field description with raw UPPER_SNAKE_CASE keys (TYPE_ID, ADDRESS_1, FIELD_NAME, IN_SHORT_LIST), even though the data of these entities (GET /v1/addresses, the preset field list) already came back in camelCase — the schema did not match the real field names in the data.
After
Both endpoints normalize the description keys to camelCase (typeId, address1, fieldName, inShortList), like the rest of V1. The inner field descriptors (type, isRequired, isReadOnly, title) are unchanged.
Impact on integrators
The /fields response keys now match the field names in the data. A client that read the camelCase names from the data gets a consistent schema; no action required.
2026-07-01
FIX-0701-1: Disk file upload accepts files larger than 1 MB
Before
POST /v1/files/upload rejected a request body larger than ~1 MB with FST_ERR_CTP_BODY_TOO_LARGE. The file is sent as base64 in the JSON body, and base64 inflates the size by roughly a third — so even a 1.1 MB file did not pass. There was no way to upload a larger file.
After
This route's body limit was raised to 70 MB — enough for a file of about 50 MB once base64 and the JSON envelope are accounted for (call recordings, typical attachments). Bitrix24 still enforces its own Disk file-size cap: exceeding it returns an error in the standard envelope. Files in the hundreds of MB need a separate upload path (multipart / presigned), which is not implemented yet.
NEW-0701-2: server app icon: SVG upload, anonymous serve, favicon
You can now set an app icon for a server. POST /v1/infra/servers/:id/icon (multipart/form-data, field file, SVG only up to 256 KB, no scripts, event handlers or external references) uploads the icon; it is served anonymously at the stable GET /api/server-icons/:id and shown in the Bitrix24 app catalog. To make it the browser-tab favicon, add <link rel="icon" type="image/svg+xml" href="<base-URL>/api/server-icons/:id"> to your app HTML at build time — after that, re-uploading the icon refreshes both the catalog and the favicon automatically. Format, requirements and procedure: App icon.
NEW-0701-3: The current-user profile reports administrator rights
GET /v1/users/me now returns a working isAdmin field: true — the user is a portal administrator, false — not, null — could not be determined (a transient failure; the profile is still returned). Use it for server-side permission checks in your backend. GET /v1/users/:id and the user list still do not expose it — the verdict is available only for the current session user.
NEW-0701-4: Extended static field contract in /v1/guide and a schema-discovery pointer in /v1/me
Each entity in the GET /v1/guide response gains a data.entities[].fieldsDetailed field — an extended static field contract: type, readonly, required, createOnly, and enum decoding (for example, the status and priority values on tasks). It is available with the X-Api-Key header alone, without a session, for mappings and code generation before a user session exists. The compact fields field is unchanged.
The GET /v1/me response for an authorization key without a user session (no Authorization: Bearer header) now carries a schemaDiscovery block — a pointer to where the static schema lives without a session (/v1/guide) and how to get live and custom fields (a Bearer session or a personal key). The GET /v1/<entity>/fields and GET /v1/userfields/* endpoints are unchanged.
BC-0701-5: categoryId in the posts response is now an array of numbers
Old format supported until: 01.01.2027
Before
GET /v1/posts returned categoryId with a type that depended on the number of a post's categories: null for none, a number (11) for one, a comma-separated string of IDs ("5,7,9") for several. A typed client with a categoryId: number | null field worked on single-category posts but broke on posts with two or more.
After
categoryId is always an array of numbers number[]: [] for none, [11] for one, [5, 7, 9] for several. The type is uniform regardless of how many categories a post has.
What integrators should do
Read categoryId as an array: post.categoryId.length instead of a null check, post.categoryId[0] for the first category. The old "number or string" branch can be removed.
FIX-0701-6: unified token and scope error codes across the Disk section
Before
Custom Disk operations — POST /v1/files/:id/moveto, copyto, POST /v1/files/upload, GET /v1/files/:id/download and the folder counterparts — returned 401 NO_TOKENS when portal tokens were missing and 403 SCOPE_MISSING when the disk scope was absent. The generated CRUD operations of the same section (list/get/create/update/delete) already returned 401 TOKEN_MISSING and 403 SCOPE_DENIED for the same conditions — so within one section a client saw two different codes for the same error.
After
All Disk operations return unified codes — 401 TOKEN_MISSING and 403 SCOPE_DENIED, matching the rest of the V1 API. The HTTP statuses (401 and 403) are unchanged.
Impact on integrators
If your code branched on the NO_TOKENS or SCOPE_MISSING strings on the moveto/copyto/upload/download operations, switch to TOKEN_MISSING / SCOPE_DENIED (or check the HTTP status). No change is needed otherwise.
FIX-0701-7: CALL_CARD placement removed from the allowed list
Before
The CALL_CARD placement code was treated as valid: POST /v1/placements/bind passed it through validation and forwarded it to Bitrix24, and GET /v1/placements/available listed it. But no Bitrix24 module registers this placement, so placement.bind failed with an internal error that the platform surfaced as INTERNAL_SERVER_ERROR.
After
CALL_CARD is removed from the allowed list: it no longer appears in GET /v1/placements/available, and POST /v1/placements/bind with it is rejected up-front with a clear VALIDATION_ERROR, without calling Bitrix24.
Impact on integrators
Binding CALL_CARD never worked (it returned an opaque 500 error), so no working integration breaks. For the call-card apps panel, use the current placements from GET /v1/placements/available.
FIX-0701-8: workday open/close/pause: the userId field is now applied
Before
The documented body field userId in POST /v1/workday/open, POST /v1/workday/close and POST /v1/workday/pause was silently ignored: the operation always ran against the key's token owner, even when another employee was passed. The response was success, but the action affected the wrong user.
After
userId is translated to the Bitrix24 USER_ID parameter, so the operation runs against the specified employee (given admin or manager rights). A non-existent userId now returns a Bitrix24 error instead of a false success. An invalid userId (not a positive integer) is rejected as 400 INVALID_PARAMS. This matches the already-working GET /v1/workday/status.
Impact on integrators
Callers that did not pass userId see no change — the operation still applies to the token owner. Callers that passed userId now get the correct action against the specified employee.
2026-06-30
NEW-0630-1: POST /v1/cowork/deploy-key — get a project deploy key from Cowork/Code
The Cowork/Code key (vibe:cowork) is data-plane only and is blocked from the infrastructure control-plane with 403 INFRA_FORBIDDEN_FOR_COWORK_KEY. The new POST /v1/cowork/deploy-key endpoint lets an agent self-serve a separate project deploy key: call it with the same Cowork key, take the top-level key field from the response (a bare object, not wrapped in data), and use it as the X-Api-Key header for deploy/provision/exec under /v1/infra/*.
The returned key carries the vibe:infra + vibe:storage scopes (no vibe:cowork), expires in 7 days, and is bound to the Cowork key's owner and portal. Each call returns a fresh key and revokes the previous project key (exactly one is active). Requires the vibe:cowork scope and an active Cowork/Code subscription; rejection codes are 403 INSUFFICIENT_SCOPE / 403 COWORK_NOT_ACTIVATED / 503 DEPLOY_KEY_DISABLED / 503 INFRA_DISABLED.
NEW-0630-2: Self-hosted placement now receives a one-time authorization code on appUrl
For an app with its own appUrl (outside Black Hole) opened as a placement, the platform now appends a one-time authorization code (?code=...) to the appUrl redirect instead of an internal gateway token. The app exchanges this code for a vibe_session via the existing POST /v1/oauth/token — redirect_uri must exactly match the configured appUrl. Previously such apps received a non-redeemable token and could not authorize the user.
NEW-0630-3: New endpoint POST /v1/oauth/placement-session for self-hosted apps
A self-hosted app (on its own server, not on Black Hole) opened as a placement (iframe) in Bitrix24 can now exchange the Bitrix24 user token it received in the placement callback on its own handler for a vibe_session. The request POST /v1/oauth/placement-session with body { app_key, access_token, member_id, domain } (optionally refresh_token, expires_in) is server-to-server — the session token never reaches the browser. The authorization section of the docs covers both placement topologies.
FIX-0630-4: PATCH on an OAuth-app key's scopes: honest rejection instead of false access
Before
PATCH /v1/keys/:id adding a Bitrix24 scope to an OAuth-app key (vibe_app_*), and PATCH /v1/apps/:id widening an app's scopes, returned 200 and persisted the new scope set. But an OAuth app's scopes are fixed at issue time and such an edit never reaches the Bitrix24 side, so GET /v1/me then reported a scope Bitrix24 had not granted and the actual call was rejected.
After
Adding a Bitrix24 scope to an OAuth-app key or to an app is now rejected with 403 OAUTH_SCOPE_CHANGE_REQUIRES_REISSUE. Removing scopes and changing vibe:* scopes still work. To gain a new scope, issue a new authorization key with the scopes you need.
FIX-0630-5: a key with the `tasks` scope now actually unlocks Tasks methods
Before
A key issued with the tasks scope (plural — the spelling the UI picker offers) minted, on a dev-key portal, a webhook that Bitrix24 accepted but never bound to the Tasks REST methods. GET /v1/me reported tasks, yet Tasks method calls were rejected on the Bitrix24 side. The task scope (singular) worked.
After
On key issue and edit the scope set is canonicalised to the spelling Bitrix24 actually binds methods to (tasks → task), so the key unlocks Tasks methods regardless of the chosen spelling. Already-issued keys are not changed retroactively — re-issue the key.
FIX-0630-6: a malformed FILES field on a timeline comment is now rejected with 400
Before
POST /v1/timelines and PATCH /v1/timelines/:id accepted the FILES field in any shape and answered 200/201. When the shape was anything other than an array of [[fileName, base64Content]] pairs — a flat array of strings or a single pair without the outer array — the comment was created but the file was attached as garbage (random name, unreadable content) or silently dropped, with no error.
After
A FILES value that is not an array of [[fileName, base64Content]] pairs is rejected before the Bitrix24 call with 400 INVALID_FILES_SHAPE and a hint about the correct shape. An empty FILES ([]) and an omitted field are still accepted. The check applies to single POST/PATCH and to batch (POST /v1/batch, POST /v1/timelines/batch).
Impact on integrators
Callers passing FILES in the documented [[fileName, base64Content]] shape are unaffected. Callers relying on other shapes now get an explicit 400 instead of a silently broken attachment, and can fix the request.
FIX-0630-7: empty bot and user fields now return null/[] instead of false/{}
Before
In bot-card responses (GET /v1/bots/:botId, POST /v1/bots, PATCH /v1/bots/:botId) the unset fields inside users[] came back as the wrong primitive: the datetimes lastActivityDate, mobileLastDate, desktopLastDate were returned as boolean false, and an empty phones list was also false. The same lastActivityDate field in GET /v1/users came back as an empty object {}. As a result new Date(lastActivityDate) silently produced the epoch and phones.map(...) threw a type error.
After
An unset datetime is now encoded uniformly as null on every path, and an empty phone list as []. A populated datetime is still an ISO string and a populated list is still an array.
Impact on integrators
No action required — the types are now correct. Code that relied on comparing empty values to false will stop matching: check the datetime against null and treat the phone list as an array.
Affected endpoints: GET /v1/bots/:botId, POST /v1/bots, PATCH /v1/bots/:botId, GET /v1/users
NEW-0630-8: Relink an app's OAuth credentials without deleting it
A new endpoint POST /v1/apps/:id/relink-oauth updates bitrixClientId and bitrixClientSecret on an existing application without deleting it. This is needed when the local OAuth application is recreated in the Bitrix24 account and its client_id changes: previously the only path was to delete the app (which broke the linked bot, Open Channels config and bindings) and create it anew.
Request body: { bitrixClientId, bitrixClientSecret } (both required). The paired key, bot and bindings are preserved. If that client_id is already linked to another application — 409 OAUTH_CLIENT_ID_IN_USE. It cannot be called with the OAuth-app's own key — 403 OAUTH_APP_KEY_CANNOT_RELINK (use a personal key or the dashboard). After relinking, reinstall the app in the account — this restores the event subscription.
NEW-0630-9: Web search: full page text, images, news mode, and research domain filters
Before
POST /v1/search accepted include_raw_content as a boolean flag, but the full text of the found pages did not reach the response. There were no parameters for news mode or for requesting images. POST /v1/research accepted include_domains and exclude_domains but silently dropped them.
After
POST /v1/search gained two new optional parameters: topic (general or news, default general) and include_images (boolean, default false). The boolean include_raw_content now actually returns the full text: each result gained a rawContent field (the full page text, size-capped). The response added top-level images (an array of objects with a url field) and ignored_filters (a string array — the passed filters the provider could not apply). These fields arrive in both the synchronous response body and the done streaming frame. The X-Search-Filters-Ignored header is kept and may now list topic and include_images. POST /v1/research now applies include_domains and exclude_domains (up to 20 each) for capable providers and reports overflow via ignored_filters in the done frame. Which capabilities a selected engine offers is returned by GET /v1/search/providers. Clients with strict schema validation via additionalProperties should account for the new response fields.
NEW-0630-10: Read AI client-call transcripts via the API
The new GET /v1/activities/:activityId/transcript endpoint returns the ready-made AI transcript of a client call by the ID of its CRM Call activity. The method only reads an existing transcript — it does not trigger generation. It requires the crm scope. When there is no transcript for the call yet, the data.transcription field is null — a normal response, not an error.
2026-06-29
FIX-0629-1: org structure node search now searches by name
Before
POST /v1/humanresources/nodes/search proxied to humanresources.node.list: the node type was set inside filter, there was no name search at all, and a top-level { "type": ..., "name": ... } body (or a request with no body) returned 400 or 500.
After
The endpoint now wraps humanresources.node.search. Two fields are required at the top level of the body — type (DEPARTMENT or TEAM) and name (a substring of the name). Optional are parentId and pagination.limit (default 50, maximum 200). It returns nodes whose name contains name, in a flat data with meta (total, hasMore). The filter, order and select fields are no longer accepted.
Integrator impact
Send { "type": "TEAM", "name": "<substring>" } at the top level instead of the former { "filter": { "type": "TEAM" } }. To enumerate all nodes of a type without name search, use GET /v1/humanresources/nodes with ?type=....
NEW-0629-2: AI quota off-peak hours schedule
Added the GET /v1/off-peak endpoint — the off-peak (Time-of-Use) discount schedule for the AI quota. The response carries the price multiplier right now (currentMultiplier), the next window when it gets cheaper (nextWindow), a 24×7 grid by hour and weekday (grid), the current grid cell (nowCell), and the schedule timezone (timezone). The discount applies to quota-metered usage only — the quota drains slower during these hours; wallet pay-per-token charges are unaffected. The optional model=<id> parameter returns a specific model's schedule instead of the platform default. Requires the vibe:ai scope. While off-peak is not enabled, the response is { "enabled": false }.
BC-0629-3: vibe-search provider slug removed
Old format supported until: 26.12.2026
Before
The provider field in POST /v1/search and POST /v1/research accepted the vibe-search slug — a separate platform engine added on 2026-06-06. It was also listed among the slugs in GET /v1/search/providers.
After
The vibe-search slug is removed. The platform search engine on every instance is bitrix-search — which upstream backs it is instance-dependent. A request with provider: "vibe-search" now returns 400 INVALID_REQUEST (the value fails validation). research support for bitrix-search is likewise instance-dependent — see GET /v1/search/providers.
What integrators should do
If your request explicitly passed provider: "vibe-search", replace it with bitrix-search or omit the provider field to use the instance default engine (shown by the defaultProvider field in GET /v1/me). The vibe-search slug was not the default engine on any production instance, so only integrations that hardcoded it are affected.
FIX-0629-4: A null field value via POST /v1/batch no longer writes the string "null" into the field
Before
In a composite POST /v1/batch, a create or update with a field value of null (for example {"entity":"deals","action":"update","entityId":123,"params":{"comments":null}}) wrote the literal string "null" into the field.
After
The field receives an empty value, which Bitrix24 interprets by field type: text is cleared, numeric becomes 0, a date is left unchanged. The literal string "null" is no longer written and no error is raised. This matches the behavior of a single PATCH /v1/{entity}/:id with null. The per-entity /v1/{entity}/batch path still skips a null field entirely (leaves the value unchanged for every type).
FIX-0629-5: Search and list with a null filter now return more than 50 rows
Before
A POST /v1/{entity}/search or GET /v1/{entity} request with a filter on an empty value (for example {"filter": {"closedDate": null}}) and a limit above 50 returned at most 50 records, even though meta.total reported the real match count and meta.hasMore was true. Auto-pagination silently stopped after the first page, so the common "read while rows equal limit" loop got an incomplete result with no error.
After
Such a request now returns up to limit records, the same as with any other filter. A null filter value is treated as "field is empty" consistently across every page of the result set.
Impact on integrators
Clients that paged manually via offset in steps of 50 to work around the truncation no longer need to — up to 5000 records can be fetched in a single call.
FIX-0629-6: port change and deploy auto-routing now work out of the box on new app servers
Before
A regular "Publish app" server booted its agent with a fixed port, so PATCH /v1/infra/servers/:id/port and the deploy auto-routing step returned 409 PORT_NOT_APPLIED (NO_SCANNER), and the public URL served the Black Hole service page while the app listened on a non-default port.
After
New regular app servers boot with port auto-detection: a service on any port is reachable through the tunnel immediately, and port change plus deploy auto-routing succeed. Agent and galaxy-host servers are unchanged.
FIX-0629-7: app install via /v1/apps returns a precise error code instead of the generic BOX_APP_INSTALL_FAILED
Before
On an OAuth-app install failure, POST /v1/apps always returned 502 BOX_APP_INSTALL_FAILED, with the full raw Bitrix24 response echoed into error.message.
After
The failure response is now classified: 403 B24_INSUFFICIENT_SCOPE (the service integration lost its rights on the portal), 410 STALE_DEVELOPER_KEY (access was changed or removed and cannot be auto-recovered), 502 RECOVERY_FAILED (transient, retryable) or 502 DEVKEY_MINT_FAILED (other). error.message no longer carries the raw Bitrix24 body — the diagnostic moves to the redacted error.details.b24Body field.
Impact
Existing "non-201 means install failed" handling keeps working unchanged. If your code branched specifically on BOX_APP_INSTALL_FAILED, add handling for the new codes above.
FIX-0629-8: date-range search no longer returns empty for wide ranges
Before
POST /v1/deals/search (and likewise for leads, contacts, companies, quotes, invoices, items) with a date filter and a lower bound (>= / >) spanning more than 14 days returned 200 with an empty data and meta.total: 0, even when records existed in that range.
After
The request returns all matching records. GET /v1/deals, narrow ranges (≤ 14 days), and the autoWindow: false parameter were unaffected.
NEW-0629-9: New error code CONNECTOR_APP_INSTALL_FORBIDDEN on app install
POST /v1/apps on a self-hosted portal now returns 403 with code CONNECTOR_APP_INSTALL_FORBIDDEN when the Bitrix24 portal administrator has forbidden the user from installing applications. The error.message field carries a clear localized explanation pointing the user to ask their portal administrator. Previously this denial surfaced as a generic 502 CONNECTOR_APP_INSTALL_FAILED with no cause; that code is still used for other install failures.
NEW-0629-10: Transfer bot ownership to another key
Added POST /v1/bots/:botId/transfer — moves a bot's ownership to another API key of the same Bitrix24 account and the same user (or an account admin). Resolves the case where a bot is orphaned after the app is recreated: the owning key is revoked and the bot's runtime stops working. Body: { "targetApiKeyId": "<id>" }. The target key must be active, in the same account, with the imbot scope. After the transfer, verify the new key's B24 binding via POST /v1/bots/:botId/reauth.
2026-06-28
FIX-0628-1: editing a key's scopes now applies them to the Bitrix24 webhook
Before
PATCH /v1/keys/:id with a scopes array saved the new set in Vibecode but, on self-hosted Bitrix24 portals, did not propagate it to the portal webhook. The webhook kept its old scope set, so a call to a freshly added scope was rejected by Bitrix24 (403) even though Vibecode already reported the key as carrying it.
After
A scopes change is now applied to the Bitrix24 webhook in the same request. If the sync cannot be performed, the key is not updated (Vibecode and Bitrix24 stay on the previous set) and the response carries an error code: INVALID_SCOPES (400) — the portal does not grant one of the requested scopes, STALE_DEVELOPER_KEY (410) or RECOVERY_FAILED (502) — the portal access key is invalid, BOX_NO_DEVELOPER_KEY (400) — the key owner has no portal access key, DEVKEY_SCOPE_SYNC_FAILED (502) — any other Bitrix24 rejection.
Impact on integrators
No action needed — scopes added via PATCH /v1/keys/:id now take effect immediately. If the response returns one of the codes above, the scope set was left unchanged: fix the cause and retry.
2026-06-27
NEW-0627-1: Catalog product properties — read and manage the property schema
Added the /v1/catalog-product-properties section — definitions of a trade catalog's custom properties (id, name, type). List, get, create, update, delete, search, and the field reference are supported. A product's list-type properties come back in /v1/catalog-products as fields named propertyNNN, where NNN is the property id. The new section maps that id to the property name and type. The filter[iblockId] filter scopes the selection to one catalog, with the id taken from /v1/catalogs. Requires the catalog scope.
2026-06-26
FIX-0626-1: catalog-prices: system fields priceScale, extraId, timestampX declared in the schema
Before
GET /v1/catalog-prices and GET /v1/catalog-prices/:id returned the priceScale, extraId, and timestampX fields, but they were not declared in the schema: they passed through without normalization (the timestampX field arrived in an offset format, e.g. 2024-06-17T16:53:24+05:30) and were absent from the GET /v1/catalog-prices/fields response.
After
The three fields are declared as read-only. They are now listed in GET /v1/catalog-prices/fields, and timestampX is normalized to ISO 8601 UTC (2024-06-17T13:53:24.000Z), consistent with the other datetime fields.
Impact on integrators
The instant in timestampX does not change — only its string representation does (UTC instead of a local offset). Clients that parse the value with a standard date parser keep working unchanged.
FIX-0626-2: Server and app source download: the signed URL no longer returns 403
Before
GET /v1/infra/servers/:id/sources/:versionId/download and GET /v1/apps/:id/sources/:versionId/download returned 200 with a signed URL, but downloading from that URL failed with 403 AccessDenied when the request used a personal key (vibe_api_*). Listing versions worked and the file was physically present in storage.
After
The signed URL is now bound to the object's actual storage location, so the download returns the archive content. The fix also covers snapshots whose storage key belongs to a different family (legacy app snapshots linked to a server).
Impact on integrators
The endpoint contract is unchanged — this restores the documented 200 + working signed URL behavior. No client-side changes are required.
NEW-0626-3: api-bearer token refresh and Gateway rejection reason
The new endpoint POST /v1/infra/servers/:id/access-tokens/:tokenId/refresh mints a fresh JWT (up to 10 minutes) for an existing api-bearer token — without creating a new record, and without consuming the active-token limit or the hourly mint limit. A long-running client (CI, AI agent) refreshes the token before jwtExpiresAt instead of minting a new one. A single revoke kills the original and all refreshed JWTs of one token.
The 401 BH_LOGIN_REQUIRED response that the Gateway returns on an app subdomain when it rejects the Authorization: Bearer header now carries a reason field with the specific cause: expired, signature, subdomain, type, revoked, malformed, or invalid. The field is additive — existing clients ignore it.
2026-06-25
FIX-0625-1: neutral provider, plan and region identifiers in the infrastructure API
Before
On the international (.com) surface, GET /v1/infra/providers, GET /v1/infra/providers/:id/plans and GET /v1/infra/providers/:id/regions returned provider, plan, disk-type and region identifiers in the underlying infrastructure's raw form rather than in the neutral brand namespace.
After
The same fields now use the neutral Bitrix Cloud namespace: provider bitrix-cloud, plans bc-small/bc-medium/bc-large/bc-xlarge, diskType: "network-ssd", regions bc-eu-central/bc-eu-west/bc-us-east/bc-us-west/bc-ap-southeast. The name and country fields stay human-readable (for example "Frankfurt (EU Central)", DE). The same values are returned in the provider/plan/region fields of GET /v1/infra/servers and GET /v1/infra/servers/:id.
Integrator impact
The standard flow needs no changes: an identifier read from the catalog is still passed to POST /v1/infra/servers as-is. Server creation accepts both the new ids (bitrix-cloud/bc-small/bc-eu-central) and the ids from the previous catalog, so existing integrations keep working. Only update code that compares response identifiers against hard-coded strings from the previous provider, plan, or region catalog.
FIX-0625-2: offset inside /v1/batch sub-calls now pages through results
Before
In POST /v1/batch, list and search sub-calls silently ignored offset in params: Bitrix24 received an unknown offset key instead of start, so every page returned the same first set of records. Three contacts.search sub-calls with offset 0, 50 and 100 all returned the identical first page.
After
offset in a list/search sub-call is mapped to Bitrix24's start (matching the single POST /v1/{entity}/search endpoint). The same three sub-calls now return three distinct, non-overlapping pages. Single-endpoint behavior is unchanged.
FIX-0625-3: Files and folders: deletedBy on non-deleted objects is now null
Before
GET /v1/files/:id, GET /v1/files and the folder endpoints returned deletedBy: 0 for a non-deleted object, although the field is declared as number | null with "null — the object is not deleted". The sibling field deletedAt correctly returned null, so two fields with the same contract behaved differently, and a deletedBy !== null check wrongly treated every active object as deleted.
After
deletedBy is normalized to null for non-deleted objects (Bitrix24 stores zero in the DELETED_BY column to mean "no user"; user id 0 does not exist). For deleted objects the field still carries the id of the deleting user.
Impact on integrators
The behavior now matches the documented number | null contract. Clients that checked deletedBy === null / deletedBy !== null now get the correct result for active objects.
NEW-0625-4: Knowledge Base 2.0 reads — list bases, documents, tree, search
Read access to Knowledge Base 2.0: list the knowledge bases you can access (cursor pagination), get a knowledge base and a document by id (the document includes its Markdown content), the document tree of a knowledge base, and full-text search across documents. Scope note.
Affected endpoints: GET /v1/note/collections (list), GET /v1/note/collections/:id (one base), GET /v1/note/collections/:collectionId/documents (tree), GET /v1/note/documents/:id (document with Markdown), GET /v1/note/documents/search (search by query).
FIX-0625-5: Knowledge Base 2.0 methods (create, update, file upload) now work
Before
Creating and updating knowledge bases and documents (POST /v1/note/collections, PATCH /v1/note/collections/:id, POST /v1/note/documents, PATCH /v1/note/documents/:id) returned 400 with a Bitrix24 validation error, and attachment upload (POST /v1/note/documents/:documentId/files) stored the file but did not return its id in data.id.
After
The methods work: create and update return 200, and create responses for knowledge bases, documents, and files carry the id in data.id. Archive, delete, and file retrieval already worked.
FIX-0625-6: /v1/me: storage supportedVisibilities are now uppercase
Before
GET /v1/me returned supportedVisibilities: ["private","public"] (lowercase) in the storage block, but the upload endpoints accept only PRIVATE/PUBLIC (uppercase). An agent copying the value from the manifest hit STORAGE_INVALID_VISIBILITY.
After
supportedVisibilities is returned as ["PRIVATE","PUBLIC"] — exactly the values the visibility upload parameter accepts.
Impact on integrators
If a client took the visibility value from /v1/me and uppercased it itself, nothing changes. If it passed the value as-is, uploads now succeed without an error.
2026-06-24
FIX-0624-1: galaxy app logs return container output
Before
GET /v1/infra/servers/:id/logs for a galaxy app (kind=GALAXY_APP) returned only the host system journal (journalctl), not the app container's own logs — you could not see the stdout/stderr of a crashed app.
After
For a galaxy app the endpoint reads the container's stdout/stderr (docker logs). The read is read-only: if the galaxy host is asleep or unreachable, the response is an empty data.logs plus data.hint, and the host is not woken. The since parameter for galaxy apps accepts only a duration (10m) or an RFC3339 timestamp — the human-readable journalctl forms ("1 hour ago") are allowed only for Black Hole servers.
NEW-0624-2: GALAXY_APP_START_FAILED deploy error code
POST /v1/infra/servers/:id/deploy for a galaxy app returns 502 GALAXY_APP_START_FAILED when the app builds successfully but crashes or enters an OOM restart loop right after start. This is a distinct code from GALAXY_APP_BUILD_FAILED (a build error): it shows the build succeeded and the problem is at runtime (for example, exceeding the memory limit). The tail of the container logs is delivered in the buildLog field.
FIX-0624-3: blocking server wake window raised to ~5 minutes
Before
A blocking wake — POST /v1/infra/servers/:id/wake with ?wait=true and the auto-wake of a sleeping server on POST /v1/infra/servers/:id/deploy — waited for readiness (status RUNNING plus a connected tunnel) for up to about 3 minutes, then returned 504 WAKE_TIMEOUT.
After
The main wait phase is raised from ~3 to ~5 minutes, and including the reboot phase the full ceiling before 504 is about 6.5 minutes. A deeply cold host (for example, a galaxy that has slept for several days) gets enough time to boot and connect instead of hitting a false timeout. The error code, response shape, and the proxy-side ceiling are unchanged.
Impact on integrators
If your client sets its own timeout for these calls, budget about 6.5 minutes instead of 3. Everything else is unchanged — you do not need to rewrite the integration.
NEW-0624-4: placement and graduateFrom parameters on server create
POST /v1/infra/servers gained two optional parameters. placement — auto (default, unchanged behavior) or dedicated: on a portal with the galaxies-only placement model, the value dedicated creates a standalone virtual machine instead of a Galaxy application, passing the same checks as a normal server create — the serverCreation policy and the per-user server quota. graduateFrom takes the identifier of your Galaxy application (kind=GALAXY_APP): after the dedicated server is created, that application is deleted. graduateFrom is owner-scoped — a foreign or non-Galaxy identifier returns 404 and deletes nothing.
This is additive: without placement, or with placement: "auto", the request behaves exactly as before.
In addition, when a Galaxy application fails with OOM, the response of POST /v1/infra/servers/:id/deploy with the code 502 GALAXY_APP_START_FAILED now carries a structured hint error.hint with recoveryAction: "graduate-to-dedicated-vm" — how to re-create the application on a dedicated server via placement: "dedicated" and graduateFrom. The hint is added only when the failure cause is OOM (exceeding the container memory limit), not a plain crash.
FIX-0624-5: server creation with code in source.content accepts archives up to 500 MB
Before
POST /v1/infra/servers with an inline archive in source.content (one-shot Galaxy app creation) returned 413 FST_ERR_CTP_BODY_TOO_LARGE for archives above ~750 KB, even though the documented limit for source.content is 500 MB per request body. The route inherited the global 1 MB body limit.
After
The route accepts a request body up to 500 MB — matching POST /v1/infra/servers/:id/deploy and POST /v1/infra/servers/:id/upload. The documented limit now actually applies.
NEW-0624-6: group requisites by tax IDs (INN/OGRN/KPP) in aggregate
POST /v1/requisites/aggregate now accepts groupBy on the requisite's string identifiers: rqInn, rqKpp, rqOgrn, rqOgrnip, rqOkpo, rqVatId, rqResidenceCountry, rqCompanyName, plus presetId, entityTypeId, active. Previously grouping by these fields returned 400 INVALID_PARAMS with an empty list of available fields.
Grouping by rqInn is the fastest way to find duplicate requisites in a single call: groups with count > 1 hold the repeated values. Existing calls (groupBy on entityTypeId/presetId) keep working unchanged. Numeric functions (sum/avg/min/max) remain unavailable on these string fields — they are for grouping only.
FIX-0624-7: a sleeping server's availableActions includes wake/start; repair-status reports `running` immediately
Before
For a sleeping server with no tunnel (SLEEPING + blackholeStatus: DISCONNECTED — the normal state of a stopped server) the availableActions field in the 422 SERVER_WRONG_STATE response and in GET /v1/me (infra.unhealthyServers) listed only ["repair","delete"] — no obvious way to bring the server back. Separately: right after POST /v1/infra/servers/:id/repair, a repair-status poll in the first milliseconds could return {status:"idle"}, and the poll loop exited prematurely.
After
availableActions for any sleeping server now contains ["wake","start","repair","delete"] — both actions are genuinely accepted by the /wake and /start endpoints. And repair-status sets running synchronously when the repair starts, so the very first poll sees running, not idle. Existing calls keep working unchanged.
FIX-0624-8: stage-history?entityType=invoice now returns smart-invoice history (31)
Before
GET /v1/stage-history?entityType=invoice returned the history of the deprecated old invoice (entityTypeId 5, status-based: statusId/statusSemanticId). The current smart invoice (31) was reachable only under entityType=new-invoice. A client working with invoices via /v1/invoices (type 31) who queried history under invoice got a foreign, deprecated type.
After
entityType=invoice returns the current smart invoice's history (entityTypeId 31, stage-based: stageId/stageSemanticId/categoryId) — consistent with /v1/invoices. The new-invoice key is kept as an alias for 31 for backward compatibility, so nothing needs to change.
NEW-0624-9: bitrix/embeddings text embeddings endpoint
An OpenAI-compatible POST /v1/embeddings endpoint converts text into vector representations (embeddings) for semantic search, clustering, deduplication, and finding similar CRM records. The bitrix/embeddings model is free and platform-provided — no provider key of your own is required. The input field accepts a string or an array of strings, and the response is returned in the raw OpenAI format: an object field set to list, a data array of objects shaped like { object: "embedding", embedding, index }, and a usage block. The optional encoding_format (float or base64) and dimensions parameters are supported. The list of available models and their capabilities is at GET /v1/models, and the embeddings model has the embeddings capability set.
FIX-0624-10: calendar event field types now match real responses
Before
GET /v1/calendar-events/fields declared rrule as string and dateCreate and updatedAt as datetime, while on read rrule comes back as an object and dateCreate and updatedAt come back as a string in the portal's regional format (not ISO 8601). The schema listed an ownerType field that responses never return. The rrule object carried internal keys ~UNTIL and UNTIL_TS, and recurring-event list items carried an internal RINDEX key.
After
/fields declares rrule as object and dateCreate and updatedAt as string. The phantom ownerType field is removed from the schema. The internal keys ~UNTIL, UNTIL_TS, and RINDEX no longer reach responses.
Impact on integrators
Documented fields did not change — a client that read only them keeps working. Use from and to (ISO 8601) for an absolute timestamp. Do not parse dateCreate and updatedAt with a fixed parser — their format depends on the portal's regional settings.
NEW-0624-11: provisionReason field in the server responses
GET /v1/infra/servers and GET /v1/infra/servers/:id now return a provisionReason field with the value oom, crash or null — a structured reason for a galaxy app failure. Previously it was exposed only on the dashboard session routes, so the Vibecode API had to parse the free-text provisionError. The oom value signals to graduate the app to a dedicated server: create one with placement set to dedicated and graduateFrom. The field is optional and additive — existing integrations keep working unchanged.
FIX-0624-12: the graduation signal fires for any galaxy app memory shortage
Before
A Galaxy app that ran out of container memory — both one that hit the limit and kept restarting at the edge and one that exhausts memory right at startup (for example, loading a big model) — was classified as an ordinary crash: GET /v1/infra/servers/:id returned provisionReason crash, and the POST /v1/infra/servers/:id/deploy 502 GALAXY_APP_START_FAILED response carried no graduation hint. Conversely, a non-OOM crash-loop (an unhandled exception) could be wrongly marked oom.
After
A real memory shortage in any shape — an instant boot-OOM or a grow-into-the-limit loop — now reliably yields provisionReason oom and an error.hint with recoveryAction graduate-to-dedicated-vm. Build errors and apps that never started (a broken start command) stay crash with no graduation.
Impact on integrators
No action needed: the field value and the hint now reflect the memory shortage more accurately. An agent can reliably detect provisionReason oom for any memory exhaustion and graduate the app to a dedicated server.
2026-06-23
NEW-0623-10: push-delivery setup hint in event-subscription errors
The 400 NOT_OAUTH_APP and 400 NO_USER_TOKEN errors of POST /v1/infra/servers/:id/event-subscriptions now include an error.hint field — a text instruction on how to get a server backed by an authorization key (vibe_app_) for push delivery: create an authorization key via POST /v1/apps, authorize the app on the portal, then create a new server under that key. There is no separate "migration" of an existing server from a regular key. The field is additive — existing clients are unaffected.
FIX-0623-1: bizproc activity list
Before
GET /v1/bizproc-activities returned each activity code as an object with per-character numeric keys — for example {"0":"D","1":"i", …} instead of the string "DiskRead". The Array.includes(code) check did not work.
After
The endpoint returns activity codes as an array of strings, as documented.
FIX-0623-2: duplicate-search response keys in camelCase
Before
POST /v1/duplicates/find returned the data object keys in upper case (LEAD, CONTACT, COMPANY), unlike the rest of the API which uses camelCase.
After
Keys come back in camelCase (lead, contact, company); the values (arrays of ids) are unchanged.
FIX-0623-3: Scrum epic files field as an array of ids
Before
GET /v1/scrum/epics/:id returned the files field as a raw Bitrix24 UF object (with VALUE_RAW, USER_TYPE_ID and other internal metadata).
After
files is an array of attachment ids ([417]) or an empty array, consistent with the rest of the API.
BC-0623-4: API key creation is admin-only
Old format supported until: not applicable, the restriction takes effect immediately
Before
Any portal user could create an API key (POST /v1/keys).
After
Key creation is available only to portal administrators, others are rejected.
What integrators should do
Create keys under an account with Bitrix24 administrator rights.
FIX-0623-5: Retry-After header on rate limiting
Before
On a 429 (rate limit exceeded) response the Retry-After header was not returned, so the integrator did not know when to retry.
After
The 429 response carries Retry-After with the interval in seconds. Use it as the pause before retrying.
FIX-0623-6: a deleted server returns 404 again
Before
GET /v1/infra/servers/:id returned 200 with a full body and status: "deleted" for a soft-deleted server, although the documentation promises 404. A client polling the endpoint and expecting 404 as deletion confirmation never received it.
After
The endpoint returns 404 NOT_FOUND for a deleted server — the same as the list and delete, and as described in the documentation.
Impact on integrators
If your code relied on 200 with status: "deleted", switch to checking for 404 (or for the server's absence from the list) as the deletion signal.
NEW-0623-7: Universal Lists — full REST API
A new Lists section (scope lists): programmatic access to Bitrix24 Universal Lists — the lists themselves, their fields, sections, and elements. 24 endpoints under /v1/lists over the lists.* methods.
A list is addressed by infoblock type (iblockTypeId — lists, lists_socnet, or bitrix_processes, default lists) and an identifier: a numeric path segment is treated as IBLOCK_ID, a non-numeric one as the symbolic IBLOCK_CODE. Fields, sections, and elements are available under nested paths.
If the Universal Lists module is not enabled on the portal, the call returns 409 LISTS_MODULE_NOT_ENABLED — a sign the module is off, not an integration error.
Affected endpoints: /v1/lists, /v1/lists/:iblockId, /v1/lists/:iblockId/fields, /v1/lists/:iblockId/sections, /v1/lists/:iblockId/elements
FIX-0623-8: smart-process relation isChildrenListEnabled flag
Before
The nested relation flag isChildrenListEnabled was accepted only as true/false. A Y/N value, like the other smart-process flags, was silently saved as disabled.
After
POST /v1/smart-processes and PATCH /v1/smart-processes/:entityTypeId coerce Y/N (and 1/0, yes/no) to true/false for isChildrenListEnabled in relations.
FIX-0623-9: deal custom field filter and select
Before
When filtering and selecting deal custom (UF) fields in the UF_CRM_* form, the field was rejected with UNKNOWN_FILTER_FIELD in the filter and silently dropped from select.
After
Deal custom fields are given in camelCase (ufCrmCheckOut) and work unchanged in filter and select.
Affected endpoints: GET /v1/deals, POST /v1/deals/search
2026-06-22
NEW-0622-1: deal-to-contacts relation
Manage a deal's contact set: read, add, replace the whole set, delete. PUT replaces the entire set at once. Scope crm.
Affected endpoints: GET/POST/PUT/DELETE /v1/deals/:id/contacts — Deal contacts
BC-0622-2: model list contains only GA models
Old format supported until: not applicable, experimental models were not part of the stable contract
Before
GET /v1/models and the model list in /v1/me included experimental non-GA models.
After
The public list contains only GA models. Experimental ones are excluded from the list and rejected on call.
What integrators should do
Pick a model from the current GET /v1/models response, do not hardcode experimental model ids.
FIX-0622-3: department auto-pagination
Before
GET /v1/departments returned only the first page when limit > 50.
After
Auto-pagination collects all departments into a single response.
2026-06-19
FIX-0619-1: document creation
Before
POST /v1/documents returned 422 and did not create the document.
After
The endpoint creates a document from a template and returns the record.
FIX-0619-2: warehouse auto-pagination
Before
GET /v1/warehouses and warehouse stock returned only the first page when limit > 50.
After
Auto-pagination collects every record into a single response.
Affected endpoints: GET /v1/warehouses, GET /v1/warehouses/:id/stock
FIX-0619-3: list-type custom field values preserved
Before
When creating and updating a "list" (enum) custom field, the list values were lost.
After
List values are preserved on create and update.
Affected endpoints: create, update a custom field
FIX-0619-4: partial update of a basket item
Before
PATCH /v1/basket-items/:id did not perform a partial update of the item.
After
Partial update works, the quantity field is required in the body.
FIX-0619-5: Open Channels config filter and sort
Before
For Open Channels configs, filter and sort did not work for all fields, and values were not normalized on write.
After
Filter and sort respect the field schema, boolean values are coerced to the Bitrix24 format (Y/N) on write.
2026-06-18
NEW-0618-1: grouping in deal aggregation
POST /v1/deals/aggregate accepts groupBy: "stageSemanticId" — a breakdown by stage semantics (in progress, won, lost) for funnel analytics.
NEW-0618-2: pagination and filter for stage history
GET /v1/stage-history supports pagination (meta.total, meta.hasMore) and a filter by entity type entityTypeId.
FIX-0618-3: task time tracking does not reassign the author
Before
PATCH /v1/tasks/:taskId/time/:id accepted a userId field, but Bitrix24 does not reassign the record author — the value was silently ignored.
After
The userId field is rejected with 400 — the author of a time-tracking record cannot be changed.
Impact on integrators
Do not pass userId when updating a time-tracking record.
FIX-0618-4: calendar event time zone
Before
PATCH /v1/calendar-events/:id could save the time in the Bitrix24 user's time zone rather than the event's own.
After
The event time zone is preserved on update.
2026-06-17
NEW-0617-1: Knowledge Base 2.0 (note.*)
Knowledge base collections, documents, and attachments: create, update, archive, and delete knowledge bases and documents, plus attachment upload. Scope note.
Affected endpoints (write methods): POST /v1/note/collections, POST /v1/note/documents, POST /v1/note/documents/:documentId/files, plus the paired PATCH and DELETE. Read methods are added in a separate entry.
2026-06-16
NEW-0616-1: AI follow-up for completed calls
AI follow-up for completed calls. Scope call.
Rolling out — the methods ship in Bitrix24 update call 26.600.0 and are not yet available on every portal. Until the update reaches a portal, the method returns 422 METHOD_NOT_YET_AVAILABLE with the target version in the response — this is a sign of the rollout, not an integration error.
Affected endpoints: POST /v1/calls/followups/list, GET /v1/calls/followups/:callId
FIX-0616-2: transcription response format
Before
POST /v1/audio/transcriptions always returned a JSON object, even with response_format=text, srt, or vtt.
After
text, srt, vtt return the raw body as text/plain, SubRip, or WebVTT. json and verbose_json return a JSON object.
2026-06-12
BC-0612-1: order payed field is read-only
Old format supported until: not applicable, the field is now read-only
Before
payed was accepted in the order create and update body.
After
payed is read-only — it is rejected on write.
What integrators should do
Remove payed from the body of POST /v1/orders and PATCH /v1/orders/:id.
Affected endpoints: POST /v1/orders, PATCH /v1/orders/:id
2026-06-11
NEW-0611-1: Scrum API
Epics, linking tasks to epics, and reading the task chat. Scope tasks.
Affected endpoints: /v1/scrum/epics, /v1/scrum/epics/:id, /v1/scrum/tasks/:taskId — the Scrum section.
NEW-0611-2: call rating on completion
POST /v1/calls/:callId/finish accepts a rating for the completed call and forwards it to Bitrix24.