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

API changes: September 15, 2026

← Changelog · September 2026

BC-0915-1: Black Hole classifies the app's response by its author, not its status; new code BH_APP_TIMEOUT

Old format supported until: not provided

Before

The gateway intercepted the app's response by HTTP status: 502 and 504 — always, 503 — whenever the response was not Content-Type: application/json. An intercepted response was replaced with a 503 carrying code BH_APP_STARTING and header Retry-After: 3, dropping the app's body and headers.

After

The gateway classifies the response by its author, not its status. A response the app itself wrote reaches the caller verbatim — status, headers, body. That covers 502, 504, and any 503, except two shapes indistinguishable from the agent's own refusal: a 503 with Content-Type: text/html and a 502 with no Content-Type at all are still replaced with the BH_APP_STARTING screen. Other than those two shapes, only a response the app did NOT write is replaced: no frame arrived from the tunnel at all, or the agent answered for itself — reporting that nothing listens on the port, or refusing on its own (its plain-text overload refusal, a 503 Too many concurrent requests with Content-Type: text/plain, matches neither shape and also reaches the caller verbatim — that is not a bug in your app, it is the agent saturated with concurrent requests).

The gateway's own window in which no full response arrived from the app in time (30 seconds with no frame at all, or an already-started response going silent for more than 15 seconds) no longer poses as BH_APP_STARTING — it is now the separate code 504 BH_APP_TIMEOUT with no Retry-After header: the request may have been delivered to the app and executed, so an automatic retry is not safe. It differs from the app's own 504 (which also passes through verbatim) only in the body — the gateway's carries error.code: BH_APP_TIMEOUT in its JSON envelope.

What integrators should do

Handle 504 BH_APP_TIMEOUT separately from 503 BH_APP_STARTING: before retrying a write, verify the operation's effect by a stable identifier — the response carries no Retry-After. If your code assumed every 502/503/504 from your app always arrives as a contentless BH_APP_STARTING, update it: such responses (other than the two shapes above) now carry the app's real body and headers. To keep the gateway from replacing your app's own error, do not answer it with a 503 whose Content-Type is text/html, and do not answer it with a 502 that carries no Content-Type at all.

More detail — App runtime environment and What is safe to retry.

FIX-0915-2: Bitrix24 event subscriptions accept event codes with dots

Before

POST /v1/infra/servers/:id/event-subscriptions rejected official Bitrix24 event codes with dots, such as CATALOG.PRODUCT.ON.ADD, with 400 INVALID_EVENT. Subscribing to such events was not possible.

After

The method accepts both event code formats: without dots (ONTASKADD) and with dots (CATALOG.PRODUCT.ON.ADD). For codes without dots the response remains HTTP 200. The code is not rewritten: dots are kept, and delivered events carry the same code. Lowercase codes still get 400 INVALID_EVENT.

Impact on integrators

No changes are required. You can now subscribe to Bitrix24 events whose codes contain dots.

FIX-0915-3: the bitrixgpt-* family no longer names its base model in an answer

Before

While telling about itself, a model of the bitrixgpt-* family could name a third-party base model and its vendor in the answer text — in both the regular response (choices[].message.content) and the streamed one (choices[].delta.content).

After

The rule recognises a first-person statement the model makes about itself: a copula whose predicate is the name ("I am …"), and self-description forms ("my base model …", "created me …"). In such a statement the base model name is replaced with the public model name and the vendor with Bitrix24. The rule behaves identically for regular and streaming responses and keys off the model that actually served the request, not the model field of the request.

The response stays HTTP 200 and its structure is unchanged. A mention of another model in ordinary answer text (for example "Llama is an open-weights model") is not rewritten: the rule fires only on a statement the model makes about itself and does not parse arbitrary phrasings — it is not a replacement for a product identity setting. Not affected: models whose origin is part of the public identifier, keys with their own providers (BYOK), the reasoning_content and tool_calls fields.

On a request with response_format (json_object or json_schema) the rule does not run at all — identically for regular and streaming responses. The rest of the response handling is unchanged: as before, the router extracts the reasoning block into reasoning_content.

Before

An employee of a self-hosted portal opened a Black Hole app by a direct link without signing in to the platform cabinet. If the email of their Bitrix24 Network account differed from the one on the Bitrix24 account, the X-Vibe-User-Id header reached the app as net_<id>, the same as for a visitor from outside the account. Named access granted by employee ID did not apply to such a visitor.

After

Such a visitor is recognized by the employee ID already stored in their Vibecode account. X-Vibe-User-Id arrives as a number — the same one sent when the app is opened from Bitrix24 — and named access by employee ID applies.

Impact on integrators

No action required. If an app stored data for such visitors under net_<id>, the same people now arrive with a numeric ID — the one they already had when opening the app from Bitrix24.

FIX-0915-6: the frozen-account refusal no longer promises the backup model is paid from the wallet

Before

A Cowork/Code subscription key with an exhausted quota, on an account frozen for non-payment, got a 402 ACCOUNT_FROZEN refusal from POST /v1/chat/completions saying that "the fallback model is paid from the wallet". No such charge exists: a backup-model reply spends neither the subscription quota nor the wallet balance, and the platform pays for that inference itself. An integrator showing this text to the user explained the refusal by a charge that never happens and sent them looking for money where none is taken.

After

The text names the real reason: the subscription quota is exhausted and the backup-model reply is not served while the account is frozen; work resumes once the balance is topped up. The ACCOUNT_FROZEN code, the 402 status and the body shape are unchanged.

Impact on integrators

Nothing to change: the refusal code and the status are the same. A handler that shows error.message to the user will now tell the truth about money.

FIX-0915-7: a platform integration key survives its creator moving to the administrator tier

Before

A /v1/platform/* key was closed on any drop of its creator's platform tier, including a move from superadministrator to administrator. An administrator issues the very same key with the very same access rights, so closing it took away no permissions — it only stopped a working channel: requests started receiving 401, with nothing to check the channel state in advance.

After

A key is closed only when its creator is left without a platform tier at all: removal from the team, a drop to the staff level, a block, an accepted account erasure request, or the expiry of an administrator term. A move between the superadministrator and administrator tiers leaves the key alone, in both directions.

The rest of the earlier entry still holds: a closed key answers 401, the remaining platform administrators are notified by email, and a new key is issued through POST /api/platform/integration-keys. Integrations should keep treating 401 as the "a new key is needed" signal.

FIX-0915-8: a refused Black Hole app now explains which account the platform recognized

Before

A visitor refused access to a Black Hole app saw a refusal page on the app's own domain. It did not name the account the platform had recognized, so "I signed in with the wrong account" was indistinguishable from "I am not on the access list". The only way to tell was to sign out and sign in again.

After

An ordinary browser tab is sent to the platform page "No access to the app". It names the visitor's own name and email — the ones they were recognized by — and the domain of the app's Bitrix24 account when the visitor is a member of that account. The page shows nothing about the app's owner or about the access list.

Opening the app inside Bitrix24 is unchanged: an embedded window keeps the previous refusal page, because an embedded app must not be navigated to another address. Programmatic calls are unchanged as well — they still receive a 403 response with the BH_ACCESS_DENIED code.

Impact on integrators

No action required.

FIX-0915-10: external collaborator keys are bound to their server

Before

A request made with an external-collaborator key could name a server other than the one for which the key was issued. When access to the requested server was current, the API processed the request instead of returning a server-mismatch error.

After

A request whose path names a server other than the one for which the key was issued now receives EXTERNAL_COLLABORATOR_KEY_SERVER_MISMATCH without details about the requested server.

Impact on integrators

Correct calls made with the requested server's own key remain unchanged. When working with multiple servers, use the key for the corresponding external membership.

Affected endpoints: GET /v1/infra/servers/:id, PATCH /v1/infra/servers/:id, DELETE /v1/infra/servers/:id, POST /v1/infra/servers/:id/deploy, POST /v1/infra/servers/:id/exec, POST /v1/infra/servers/:id/upload, GET /v1/infra/servers/:id/logs, POST /v1/infra/servers/:id/wake, GET /v1/infra/servers/:id/sources, POST /v1/infra/servers/:id/sources, GET /v1/infra/servers/:id/sources/:versionId, PATCH /v1/infra/servers/:id/sources/:versionId, DELETE /v1/infra/servers/:id/sources/:versionId, GET /v1/infra/servers/:id/sources/:versionId/download.

BC-0915-11: application publication no longer hides a rejected placement bind

Old format supported until: not provided

Before

Application publication returned HTTP 200 and moved the application to PUBLISHED even when Bitrix24 rejected every requested placement bind. A generic developer-key HTTP 401 in direct binding could be misclassified as INT_TARIFF_REQUIRED.

After

A developer-key HTTP 401 is treated as a credential refusal and is retried through OAuth automatically when an OAuth token is available, unless the Bitrix24 response explicitly names a non-credential cause such as an account-policy refusal or a missing scope. Named non-credential refusals remain authoritative at every HTTP status. If no transport confirms the bind, publication returns HTTP 502 PLACEMENT_BIND_FAILED, does not move the application to PUBLISHED, and returns the safe Bitrix24 machine code and status in error.failures. If removal of an old placement also fails in the same request, its code is returned in error.failedUnbinds. The stored placement list already contains the successfully bound subset. Direct binding without a successful OAuth fallback returns HTTP 403 B24_ACCESS_DENIED, not a tariff paywall.

What integrators should do

Treat publication as successful only on HTTP 200. On PLACEMENT_BIND_FAILED, re-read the application, retain the placements already present in data.placements of that read response, and retry publication only after resolving the listed causes. For B24_ACCESS_DENIED, refresh the credential or supply a live OAuth session. The platform has already decided whether to use the OAuth fallback before returning; retain error.failures for diagnostics, but do not start your own retry from an internal Bitrix24 code.

NEW-0915-12: the application external API is open to every Bitrix24 account

The address ANY /v1/applications/{id}/api/** is available on every Bitrix24 account: the platform accepts an HTTP request from outside — a Bitrix24 business-process robot or another application — delivers it to the application over its tunnel and returns the application's answer as is. GET, POST, PUT, PATCH, DELETE and HEAD are accepted; everything after /api/ reaches the application as its own request path, the query string and the body are passed verbatim. The path is checked both raw and once-decoded, its limit is 2048 characters, and an invalid path or query string form yields 400 APP_API_BAD_PATH.

The key. The address accepts only the external-API key issued for this application on its card on the Vibecode platform. It is passed in the X-Api-Key header or as Authorization: Bearer — the two forms are equivalent. A personal key or an application authorisation key gets 403 APP_API_NOT_GRANTED, and an external-API key on any other platform address gets 403 APP_API_KEY_OUT_OF_SCOPE. A missing or revoked key yields 401 MISSING_API_KEY / 401 INVALID_API_KEY, a read-only key on a mutating call yields 403 WRITE_BLOCKED_READONLY_KEY. Calls are billed to the application owner as API requests: a frozen account yields 402 ACCOUNT_FROZEN, an exhausted quota 429 QUOTA_EXCEEDED.

Conditions on the application side. The channel is open when the "External API" switch is on in the application card, the application has a server and the server does not go to sleep. Otherwise the answer is 409 APP_API_NOT_ENABLED, 409 APP_API_NO_SERVER or 409 APP_API_NOT_ALWAYS_ON; an unknown or deleted application yields 404 APP_API_APP_NOT_FOUND. An enabled switch exposes the whole HTTP surface of the application to key holders: there is no route selection on the platform side, the application authenticates and authorises its own routes itself, and the Authorization header stays free for that — the platform strips it only when it carries the platform's own key.

What the application receives. The original method, path, query string, body and the caller's headers, except the platform key X-Api-Key, Cookie, Host, Content-Length, hop-by-hop connection headers, the whole X-Vibe- prefix and proxy-trust headers — the X-Forwarded, X-Original, X-Rewrite, CF and Fastly families and the individual names libraries read the client address from (Forwarded, X-Real-Ip, Client-Ip and the like). In their place the platform sets X-Vibe-Request-Id, X-Vibe-Caller-Kind: external-api, X-Vibe-Caller-Portal-Id and X-Vibe-Caller-Key-Id; there are no X-Vibe-User-* or X-Vibe-Authorization headers on this path — an external call carries no session.

What the caller receives. The application's status, body and response headers as is, except Set-Cookie, Content-Length, hop-by-hop connection headers and the X-Vibe- prefix; 3xx answers are not followed. A platform refusal is told apart from the application's answer by the X-Vibecode-Proxy-Error: 1 header — it is on every response the platform built, and the application cannot set it. The refusal body is the V1 envelope {"success": false, "error": {"code", "message"}}. The end-to-end call identifier X-Vibe-Request-Id on the response is described in entry NEW-0914-13.

Limits. The request body is 4 MiB, more yields 413 APP_API_PAYLOAD_TOO_LARGE. The application's response body is 4 MiB, more yields 502 APP_API_RESPONSE_TOO_LARGE without Retry-After: a retry will not fix such an answer. The rate is 120 requests per minute per key, read the effective value from the X-RateLimit-Limit header; above it 429 APP_API_RATE_LIMITED with Retry-After. An application that does not answer, an unreachable or a saturated channel — 503 APP_API_UNAVAILABLE with Retry-After in seconds; an answer that did not arrive within the overall call ceiling of 30 seconds — 503 APP_API_TIMEOUT with Retry-After (entry BC-0914-25). An application answer outside the contract, for example a 1xx status, — 502 APP_API_BAD_ENVELOPE. Keep long work outside the call: answer right away and deliver the result separately.

NEW-0915-13: task comment attachments and comment file download

A task comment now carries a list of attachments. The key is always present: with no files it is an empty list. Every attachment has a file identifier and a download path, plus a name and a size whenever Bitrix24 reported them. The path is root-relative — join it with the same API base address you called. A comment whose only content is the attached file is now distinguishable by its non-empty attachment list — previously it arrived with empty text and nothing indicated the file. The set of returned comments is unchanged: such a comment was returned before as well.

A new operation, GET /v1/tasks/:taskId/comments/:id/files/:fileId/download, returns the file bytes. It needs only task access: the key does not have to be widened to the whole Drive, and the operation can return only a file of the very comment that key can already read. A file of a different comment, or one whose identifier merely matches by number, is refused. What comes back are the bytes themselves — the file's address on the account never appears in the response.

The field list at GET /v1/tasks/:taskId/comments/fields gained an attachments entry describing the element shape. The same entry now appears in GET /v1/guide — under entities[task-comments].fields and fieldsDetailed, where the element shape is machine-readable under the itemSchema key. That matters to an agent holding no account token: the live field list is out of its reach, while the guide is served anyway.

NEW-0915-14: catalog price types are available through the API

Price type identifiers depend on the account, and V1 had no way to read them: the catalog scope wrapped only the warehouse. Because of that the docs suggested treating 1 as the base price, and integrations that did so received a 422 from Bitrix24 with a message about the wrong price group.

A read-only entity has been added — GET /v1/catalog-price-types — with list, get by identifier, search and a field reference. The response carries id, name, base, xmlId, sort, createdBy, modifiedBy, dateCreate and timestampX. The base price type of the account is the record whose base equals Y, and its id is not necessarily one. The scope is unchanged, catalog, but it is not enough: in Bitrix24 catalog.priceType.* is available only to a key whose owner has Bitrix24 account administrator rights. Writing is not supported, price types are created in the Bitrix24 interface.

The 422 response from POST /v1/catalog-prices for an unknown catalogGroupId now carries a hint pointing at the new endpoint. The claim that the base price is 1 has been removed from the catalog price documentation.

Affected endpoints: GET /v1/catalog-price-types, GET /v1/catalog-price-types/:id, POST /v1/catalog-price-types/search, GET /v1/catalog-price-types/fields, POST /v1/catalog-prices

BC-0915-15: a read-only key can deploy an application to its own server again

Old format supported until: not provided

Before

Entry BC-0825-2 closed every platform write to a key in read-only mode, including code delivery to the caller's own server. POST /v1/infra/servers/{id}/deploy, POST /v1/infra/servers/{id}/exec, POST /v1/infra/servers/{id}/upload, POST /v1/infra/servers/{id}/icon and POST /v1/infra/servers/{id}/unstick returned 403 WRITE_BLOCKED_READONLY_KEY before the operation ran.

After

Those endpoints now pass for such a key, together with POST /v1/infra/servers/{id}/unstick — releasing a stuck exec lock, the recovery for exactly those operations. ⚠️ This is NOT DELETE /v1/infra/servers/{id}/lock: that one releases a stuck operation lock and was an exception before this change too. The framing of the decision: read-only mode restricts Bitrix24 data, while the caller's own application stays at their disposal. The group is indivisible — deploying an arbitrary archive is the same as executing arbitrary code, so allowing deploy without exec would be an imaginary restriction.

The rest of the mode's behaviour is unchanged. Still returning 403 WRITE_BLOCKED_READONLY_KEY: server creation POST /v1/infra/servers, lifecycle operations (POST /v1/infra/servers/{id}/stop, start, reboot, POST /v1/infra/servers/{id}/wake, sleep and wake schedules), server deletion, and also AI, storage, keys and placements.

⚠️ Do not read that "still refused" list as closed: the central restriction has other exceptions that predate this change. POST /v1/apps answers 201 for such a key — the application and its paired key are created in read-only mode, and the 403 arrives only when read+write is requested. DELETE /v1/cowork/key passes as an emergency exit, exactly like DELETE /v1/infra/servers/{id}/lock above. The full set of platform-gate exceptions arrives in writeRestriction.exceptions of the GET /v1/me response — check that, not this paragraph.

Three effects of such a key are now visible to the Bitrix24 account, and that is the price of the decision. The first deploy of a server with a public subdomain creates the application card in the account catalog, which every employee sees — the icon endpoint changes the picture on that same card. And deploying to a sleeping server wakes the machine on its own, which starts spending the Vibecode balance. The separate wake call stays forbidden: such a key cannot wake a server without deploying anything to it.

And the third one, the one that matters most to whoever issues the keys: read-only mode is no longer a boundary for the owner of a server. Deploy and exec run code inside the container, and the credentials the platform issued to that application live there — the application's personal API key (which is in read+write mode) and the application's tokens to the account. By reading the environment, the holder of a read-only key obtains the right to write Bitrix24 data. This is an accepted price of the decision, not an oversight: the endpoint itself writes no account data, but it grants access to credentials that do.

The list of exceptions to the central restriction arrives in the GET /v1/me response as writeRestriction.exceptions, and per-operation availability in the capabilities block. The capabilities.servers.deploy slot no longer arrives with available: false for a read-only key.

⚠️ The shape of one neighbouring response changed too. GET /v1/infra/servers/{id}/logs on a sleeping galaxy app answers 200 with a recovery block, and recovery.recoveryAction is now CONDITIONAL: it is absent whenever the caller cannot call the wake. ⚠️ That set of conditions is NOT closed — branch on the PRESENCE of the field, not on a list. ⚠️ The converse is not guaranteed: the field's presence only means the caller is not refused by identity or key mode; billing and an administrative wake ban are separate axes, and the address it names may still answer 402 or 403. Today it holds: read-only mode, a Cowork/Code key, the galaxy pilot switched off for the account, an agent maintenance key (log reads are open to it, waking is not), an owner whose account deletion is pending, and reaching the app through its application link while another key owns the server. It used to arrive always and named the wake address, which answered 403 for such a key. An empty string in place of the address is deliberately not sent: a machine would try to execute it, whereas a missing key reads as "there is no wake here, look at the neighbouring fields".

A third field of the same block became conditional too — recovery.poll, the readiness-polling address. It disappears when GET /v1/infra/servers/{id} refuses the caller itself: an agent maintenance key does not carry that route in its allowlist, and for an owner whose account deletion is pending it sits in the frozen surface, unlike this log read. In that state the hint simply says to repeat the log read later.

In that same state recovery.wakeSchedule became a verdict about the CALLER, not only about the app: it arrives available: false, usually carrying the code of the door that refused the wake (WRITE_BLOCKED_READONLY_KEY, INFRA_FORBIDDEN_FOR_COWORK_KEY, GALAXY_DISABLED, AGENT_MAINTENANCE_KEY_OUT_OF_SCOPE, user_self_deletion_pending, NOT_FOUND — the set is open, treat an unknown code as a refusal). It used to judge the app's eligibility alone and could arrive available: true next to a hint saying "a window is refused to you as well" — a client branching on it walked into a guaranteed 403. Creating a window is the same write, refused mostly by the same doors as the wake. The exception is a window door checked before the wake's door: then code and the hint text both name that window door.

⚠️ But not only by those, and "nothing changed for whoever can wake" is wrong: the window has TWO doors of its OWN, and each answers with ITS OWN code. The first: the schedule route may sit outside the set of routes your key may call while the wake itself sits inside it — that is how an external-collaborator key works; code EXTERNAL_COLLABORATOR_KEY_OUT_OF_SCOPE. The second: the schedule route requires the vibe:infra scope, which reading logs and waking do NOT, so an ordinary owner key without that scope reaches this refusal as well; code INFRA_SCOPE_REQUIRED, and the cure is to grant the scope, not to change the key's mode. Either one gives available: false EVEN for a caller who is allowed to wake, and then no field of the response names the schedule address. Branch on available, not on whether the wake is available to you.

A new conditional field recovery.deliveryWakesHost appeared — the second way up. It arrives when waking is unavailable to the caller and the deploy is available to them, carrying action (the deploy call) and cost (its price in words). The reason: this same change ALLOWS a read-only key to deploy to a server it owns, and a deploy to a sleeping galaxy app brings the host up by itself — so for that caller "nothing can bring it up" would be untrue. The price sits next to the address deliberately: it is a code deployment and it spends the balance, not a wake, and it should not be called just to read a log.

⚠️ The field is DOUBLY conditional, and that matters to a client: the carve-out lifts the ACCESS-MODE gate on deploys and nothing else. A Cowork/Code key, an account with the galaxy pilot switched off and an agent maintenance key are refused POST …/deploy by the same doors that refuse the wake — they do not get the field at all. Check for its presence rather than assuming it: no field means no open path.

The TEXT of that response changed along with the fields. The prose in hint and recovery.reason no longer names the wake address, nor the creation of a scheduled window. In place of an address, hint names EXACTLY the condition that applied to this caller (rather than a list joined by "or") plus the second way up together with its cost. For a caller who can call the wake, the text and the set of fields are unchanged — except where creating a window is closed to them separately. Then recovery.wakeSchedule arrives available: false carrying that door's code and no field names the schedule address. There are two such doors, and neither coincides with the wake's: the schedule route sitting outside the set of routes the key may call (that is how an external-collaborator key works — the wake is open to it, the window is not) — code EXTERNAL_COLLABORATOR_KEY_OUT_OF_SCOPE; and the key lacking the vibe:infra scope, which the schedule handler checks on its FIRST line while reading logs and waking require no such scope — code INFRA_SCOPE_REQUIRED. The code comes from the door that actually fired for you, not from its neighbour: the route allowlist is checked before every other door, while the scope is checked after the mode, freeze, Cowork and pilot doors but BEFORE server ownership. So a caller admitted through its application link and lacking vibe:infra gets INFRA_SCOPE_REQUIRED, not NOT_FOUND.

What integrators should do

A client calling those endpoints has nothing to change — the refusal simply stopped arriving.

Read recovery.recoveryAction in the logs response only after checking the key is present: without that check a client dereferences a missing field and ends up either in an exception or in a request to undefined.

Do not branch on recovery.wakeSchedule.available as a verdict about the app: it now accounts for the caller's right too, and for a refused caller it arrives false where it used to arrive true. A client that created a window on true will no longer get the refusal — it will not issue the request at all.

Check for the PRESENCE of recovery.deliveryWakesHost rather than assuming it: the field only arrives for callers the deploy is actually available to. And before calling action, read the cost beside it: the call delivers code and spends the balance. It is not a way to "just wake the app to read a log".

Check for the PRESENCE of recovery.poll — the third conditional field of the same block. The readiness-polling address is dropped whenever GET /v1/infra/servers/{id} refuses the caller itself: an agent-maintenance key does not carry that route in its allowlist, and an owner whose account deletion is pending has it behind the freeze. A client written against the earlier contract, where the field always arrived, dereferences a missing field and ends up either in an exception or in polling the address undefined. In this state readiness is not polled at all — the hint tells you to re-read the log later.

Do not scrape the wake address out of hint with a regular expression: in that state it is not there, and the expression returns an empty result rather than a refusal.

Action is required from the account administrator who issued read-only keys precisely because such a key could not deploy code. Review who holds those keys. The access mode no longer serves that purpose: it restricts Bitrix24 data, not delivery.

Important: removing the vibe:infra scope does NOT close delivery — that scope is checked only on server creation and on wake schedules. Delivery is granted by any one of three branches: the server was created by this key; the server is bound to an application whose primary key this is; the key owner belongs to the server's development team with the code right.

Because of that, only one lever is reliable — revoke the key. Re-binding the server to another key does NOT close delivery: it changes the server owner but touches neither the application binding nor the team membership, so the previous key keeps delivering code through those two branches.

NEW-0915-16: Bitrix24 account balance change notifications

Added balanceChanged and balance.get to synchronize the Bitrix24 account balance with the module. Notifications coalesce, with the next attempt reserved one hour later. Execution delays may shorten the actual interval between sends. Enqueueing after the monetary commit may lose a signal; the next change allows the module to retrieve the current balance. version identifies a notification generation, not a balance revision: the module serializes reads and stores responses, including those with an unchanged generation. The portal-balance-events flag is disabled by default.

FIX-0915-17: the mode-switch address for an application auth key now follows where the application card lives

Before

The WRITE_BLOCKED_READONLY_KEY access-mode refusal for an application auth key (vibe_app_*) always returned the Applications page "/applications" in details.switchUrl. Not every application has a card there, though: an application that is not registered in the Applications section has no card on that page, so the holder of such a key landed on a page with no switch.

After

For an application auth key, details.switchUrl points at the page that carries that particular application's card: the Applications page "/applications" when the application is registered in that section, otherwise the Auth Keys page "/apps". There the card opens from the row menu, item "Info", and the Access mode block sits in it right under the key. The address for a personal key ("/keys") and a management key ("/management-keys") is unchanged. The message text still names the same address as the field, and the refusal code and the response itself stay the same — only the value of switchUrl changed.

Impact on integrators

A client that reads switchUrl from the response needs no change. Details are on the Access mode and Authentication errors pages.

NEW-0915-18: open channels: chats by CRM card and putting an operator into a dialog

Three endpoints were added, all requiring the imopenlines scope and working on every Bitrix24 account.

GET /v1/openlines/crm/chats?crmEntityType=lead|deal|company|contact&crmEntityId=N returns the Open Channel chats bound to a CRM object: { "success": true, "data": [ { "chatId": 2043, "connectorId": "telegrambot", "connectorTitle": "Telegram" } ] }. The optional activeOnly=false adds finished dialogs to the open ones. For an object with no dialogs data is empty. A crmEntityType outside the list is refused with 400 INVALID_PARAMS before Bitrix24 is called.

POST /v1/openlines/sessions/intercept with the body { "chatId": 2043 } moves the dialog to the current operator and answers { "success": true, "data": { "chatId": 2043, "intercepted": true } }. POST /v1/openlines/sessions/join with the same body adds the operator to the dialog as one more participant and answers { "success": true, "data": { "chatId": 2043, "joined": true } }. Both accept chatId as a number and as a string of the chat2043 form, both write into a live conversation with a client and are therefore unavailable to a read-only key — it gets 403 WRITE_BLOCKED_READONLY_KEY.

Until now there was no way to find an Open Channel dialog by a lead, deal, company or contact card, and of the operator actions the API only offered POST /v1/openlines/operator/answer and POST /v1/openlines/operator/finish. Existing requests keep working as before.

Documentation — Chats by CRM card.