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

API changes: August 11, 2026

← Changelog · August 2026

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.

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 Bitrix24 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 Bitrix24 answer.

The answer is appended only when there was one: on a transport failure, where Bitrix24 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 account 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.

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 an account 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.