For AI agents: markdown of this page — /docs-content-en/changelog/2026-08-05.md documentation index — /llms.txt
API changes: August 5, 2026
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.