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

API changes: August 12, 2026

← Changelog · August 2026

NEW-0812-1: nextPollAfterMs — the platform can ask you to poll events less often

GET /v1/bots/{botId}/events now returns an optional nextPollAfterMs field — how many milliseconds to wait before the next request. The field only reaches a bot whose events are delivered by webhook (eventMode is webhook): the Event.get queue of such a bot is empty by Bitrix24 design, so frequent polling brings nothing back. A bot in fetch mode never receives the field — keep polling at your current rate.

An absent field means "poll as before", so it never arrives empty or zero. The field is optional: a client that ignores it keeps working unchanged. When the response still has an unread remainder (hasMore is true), drain the queue without waiting out the requested pause — the pause applies to the next empty poll.

FIX-0812-2: `touSavedPct` is now a share of the monthly allowance, not of consumption

Before

GET /v1/cowork/state computed touSavedPct as a share of the undiscounted bill — saved / (saved + used). Such a ratio requires the numerator and the denominator to cover the same window, and the savings counter started mid-period. The field therefore returned null on purpose until the end of the first period — in practice for almost every subscription.

After

The denominator is the subscription monthly allowance, the same one the month quota bar is a share of. The value is meaningful from day one and is no longer suppressed: null arrives only when nothing has been saved yet in the current period. The value is capped at 100.

The two shares now read side by side: "N% of the monthly allowance used" and "off-peak hours gave back M%". The field type and range are unchanged; no client action is required.

NEW-0812-3: The app port is pinned to the server and survives a wake

Before GET /v1/infra/servers/:id returned only localPort, which defaults to 3000 on every server — the response gave no way to tell whether the agent routes strictly to that port or elects one itself. A port set through PATCH /v1/infra/servers/:id/port lived only in the agent's memory: after the server woke up or the tunnel was repaired the election ran again, and the public address could start serving a neighbouring process.

After A successful deploy that passed its healthcheck writes the port into the agent settings on the machine and marks the server as pinned. The GET /v1/infra/servers/:id response carries portPinned: when true, the agent proxies strictly to localPort and never re-elects the port, neither after a wake nor after a repair; when false, the port is still auto-detected from the listening sockets.

On a pinned server PATCH /v1/infra/servers/:id/port applies the port by rewriting the agent settings and restarting the agent instead of switching it on the fly. What this means for a client: 409 PORT_NOT_APPLIED never comes back on such a server; the tunnel drops for a few seconds; verified: true means the settings carry the requested port and the agent is back online, while verified: false means the port is written but the agent's return could not be confirmed (not a failure — the port takes effect once it comes up). port: 0 removes the pin. Two failure codes were added: 502 AGENT_CONFIG_WRITE_FAILED — the machine refused the settings write, 502 GATEWAY_ERROR — the command never reached the machine.

The data.steps[] of POST /v1/infra/servers/:id/deploy gained a port_pin step — the outcome of writing the port into the agent settings. It does not affect deploy success: a warning there means the application is deployed and running but the port was not pinned.

BC-0812-4: context in feedback submissions must be a JSON object

Old format supported until: not provided

Before

POST /v1/feedback accepted context values of any JSON type, including strings, arrays, numbers, booleans, and null.

After

context remains optional. When supplied, its top level must be a JSON object, including {}. A string, array, number, boolean, or null receives 400 VALIDATION_ERROR; the ticket is not created.

What integrators should do

Send an object: {"context":{"endpoint":"/v1/feedback"}}. Senders that used a string, array, number, boolean, or null must switch to an object: the old format stops being accepted with this release.

NEW-0812-5: app publication accepts sources from a named server

POST /v1/apps/:id/publish accepts an optional sourceServerId field (or the X-Source-Server header) — the identifier of the server whose sources are published. This closes the case where a deploy auto-save answered autoSaved: true yet the next publication still returned 409 SNAPSHOT_REQUIRED: when the server does not belong to the published app's OAuth key (for example, the deploy ran under a personal vibe_api_ key), the snapshot is kept with the server rather than the app, and the publish check could not see it. Naming the same server you called deploy with is now enough; saving the sources again through POST /v1/apps/:id/sources is unnecessary.

The platform never picks the server — you pass the identifier. Rights over the server are checked separately from rights over the app: the server-owner key, a personal key of the same user, or a Bitrix24 account administrator all qualify. No such server, a server from another Bitrix24 account, or a deleted one — 404 SERVER_NOT_FOUND; no rights — 403 NOT_AUTHORIZED; an X-Source-Server header that is not in UUID form or is sent twice — 400 VALIDATION_ERROR. The body field takes precedence over the header: when sourceServerId arrives in the body, the header is neither read nor validated. Together with sourceServerId, the sourceVersionId field means a version of that server: a server has its own version numbering.

The published tag and the publication stamp are applied only to the app's own version. Publication does not mark a server's version: on it, those same fields hold the deploy history returned by GET /v1/infra/servers/:id/sources, and the tag means indefinite retention plus a delete block. There is a consequence worth knowing: a published server version has no indefinite retention — it lives by the ordinary cleanup rules and can be removed while the app stays published. That is why warnings carries a line with a ready-made PATCH /v1/infra/servers/:id/sources/vN you can use to tag the version yourself; when the version is already tagged, there is no such line. If source storage is disabled for the Bitrix24 account, the sourceServerId you passed is not used at all — publication proceeds and warnings carries a line saying the selector went unused.

Without sourceServerId the behaviour is unchanged — the check looks for a fresh snapshot of the app. The 409 SNAPSHOT_REQUIRED refusal gained a hint.reason field with four values (app_snapshot_missing, app_snapshot_stale, server_snapshot_missing, server_snapshot_stale), and on the path without a named server a hint.serverKeyedSources block that explains how to publish sources from a server. The message text of that refusal was rewritten: it used to open with the word "Deploy" and offered POST /v1/apps/:id/sources as the only way out.

Details — Publish an app and Source storage.

NEW-0812-6: deal product rows: the response now signals truncation

Before

GET /v1/{entity}/{id}/products (deals, leads, invoices, quotes, smart-process items) returned only the first page of product rows — Bitrix24 pages them at 50 — and gave no indication of it. The response carried success and data with no meta field at all, so an item with 125 rows and an item with 50 rows produced identical-looking responses, and an app lost rows with no way to notice. The limit and offset parameters had no effect on this route.

After

The response now carries a meta block: meta.total is how many product rows the item has in total (as reported by Bitrix24), and meta.hasMore tells whether rows exist beyond the returned page. The success and data fields are unchanged, so a client reading only those keeps working as before.

Alongside that, limit and offset now work on GET /v1/deals/:id/products and its counterparts for leads, invoices, quotes and smart-process items. A limit of up to 5000 returns that many rows in one call, and higher values are clamped to 5000. offset counts rows rather than pages: offset=7 starts at the eighth row. meta.hasMore is correct for any requested window, not just the first one. A call without limit and offset behaves exactly as before. A limit=0 is not a page size: the default applies and meta.warnings carries an entry with code LIMIT_ZERO_IGNORED, the same as on the list routes. The behavior is documented in the knownIssues list of GET /v1/guide.

NEW-0812-7: the machine schema now describes the live app, AI and telephony methods

Eighteen operations that were live and described on the documentation pages were missing from the machine schema at GET /v1/openapi.json. A client checking the schema did not find the method and concluded it did not exist. The schema now covers them: the applications family (GET /v1/apps, create, read, update, delete, publish, unpublish and OAuth re-link), AI spend and quota (GET /v1/ai/usage, GET /v1/ai/quota), the off-peak schedule (GET /v1/off-peak), AI follow-ups for finished calls (POST /v1/calls/followups/list, GET /v1/calls/followups/:callId) and the line list (GET /v1/voximplant-lines).

The methods themselves are neither new nor changed — same addresses, same parameters, same response shapes. Only the description is new, so there is nothing to change in an integration.

Four OpenAI-compatible AI methods answer at two addresses at once: /v1/models and /v1/ai/models, and likewise for chat completions, embeddings and audio transcriptions. Only the short addresses were in the schema. Both are described now, and the /v1/ai/ form is explicitly marked deprecated: its responses carry Deprecation: true and X-Deprecated-Use naming the canonical address. Calls on it keep working, but move to the short address. The exception is GET /v1/ai/usage and GET /v1/ai/quota — they have no short form, they are canonical themselves and carry no deprecation headers.

The schema still leaves out the hint addresses that exist only to answer with a wrong-path error listing the real ones, and the single-model detail route: its identifier contains a slash, which an OpenAPI path parameter cannot express.

FIX-0812-8: the free-tier plan list is returned as the public identifier

Before

On the international surface, allowedPlans in the 402 PLAN_NOT_ALLOWED_ON_TRIAL body and in capabilities.servers.create.limits (GET /v1/me) carried an internal plan identifier — not the one the same plan is given in the GET /v1/infra/providers/:providerId/plans catalog. The same identifier was interpolated in quotes into the human-readable note, so an AI assistant relayed it to the user verbatim, and the neighbouring requestedPlan could echo a plan chosen by the platform itself (agent creation) rather than by the client.

After

Both fields and the note text now carry the same neutral identifier the plan catalog returns, so the gate response and the catalog finally agree. Identifiers from the previous catalog are still accepted as server-creation input, so a client sending back a value it received earlier keeps working unchanged. In the dashboard, the card of a freshly created server no longer shows an identifier in place of the plan name.

FIX-0812-9: Requisite custom-field filtering is corrected

Before

GET /v1/requisites with filter[ufCrm_1698325419] forwarded the custom-field name unchanged. Bitrix24 ignored that key, so the successful response did not narrow the list.

After

Both spellings of one field — filter[UF_CRM_1698325419] and filter[ufCrm_1698325419] — are accepted and filter identically. A spelling Bitrix24 never produces (filter[ufCrm_taxId], for one) is still not applied. No UNKNOWN_FILTER_FIELD is introduced for custom fields.

FIX-0812-10: an unconfirmed Marketplace trial activation now answers pending instead of activated

Before

POST /v1/portals/{id}/activate-market-trial returned two success statuses: activated and already_active. When Bitrix24 switched the trial on but sent no confirmation for it, the call still answered activated with a trialEndsAt field — reporting a completed activation that nobody had confirmed.

After

The unconfirmed outcome has a status of its own — pending, with no trialEndsAt. It means the trial on the Bitrix24 side is already switched on and cannot be switched on again, but the confirmation has not reached us yet. The activated and already_active statuses are now returned for confirmed outcomes only.

Impact on integrators

Review your data.status === 'activated' branch: some successful activations will no longer land in it. Treat pending as a completed call — re-read the account state in a minute (Key self-description) and do not repeat the request, it will run into the rate limit.

Before

The platform removed every symbolic link from the extracted tree before starting the app. A prebuilt server-rendering bundle links its own files relatively inside its own directory, so the app lost part of its dependencies and answered HTTP 500 at the root address. The health check polled only the path given in healthPath, so a light endpoint such as /api/health kept answering 200 and POST /v1/infra/servers/{id}/deploy reported success. Inside Bitrix24 the app still did not open: the error arrived with the app's own X-Frame-Options header, so instead of an error page the user saw the browser's connection-failed message.

After

Links inside the deploy directory are kept. Only unsafe ones are removed: absolute links, links whose target does not exist, and links whose real path leads outside the deploy directory.

In addition, when healthPath is something other than /, the deploy probes the application root as well once that path has passed — the address Bitrix24 opens. A 5xx at the root stops the deploy with an error on the healthcheck step. A 4xx does not stop the deploy and arrives as a separate app_root step with status warning and the reason in stdout. With the default healthPath (/) nothing changes: no extra request is made and no app_root step is returned.

Judging the links costs more than the old blanket removal, so on a very large tree the pass may not fit its time budget. Such a step used to report success anyway — the normalize_windows_paths and cleanup_metadata steps now arrive with status warning and an explanation in stdout instead, and the deploy continues.

Impact on integrators

No call has to change. An app that answers at the root behaves as before. A server with no page at the root — an API-only backend, for instance — still deploys successfully: its 404 arrives as a warning, and to silence that warning point the application at the path it really answers on via PATCH /v1/apps/{id}. A deploy whose root answers with a 5xx now ends with an error instead of a false success — the previous green result was wrong in that case.

BC-0812-12: task time numeric fields are returned as numbers, not strings

Old format supported until: not provided

Before

GET /v1/task-time and GET /v1/tasks/:taskId/time returned id, taskId, userId, seconds, and minutes as JSON strings, while the field reference declared them numeric — the description and the response disagreed.

After

Those five fields are returned as JSON numbers. The source field stays a string. The GET /v1/task-time/fields schema is aligned in the same change.

Integrator action

Update strict schemas on your side: where a string was expected, a number now arrives. The old format is not returned any more, so a comparison such as seconds === "900" stops matching and must compare against a number.

FIX-0812-13: a filter with a reserved field name is rejected instead of returning the whole collection

Before

A filter with a reserved field name — filter[__proto__], filter[constructor], filter[prototype] — was accepted and silently returned the FULL, unfiltered collection (and on /{entity}/aggregate, a count over the whole collection). A client that built the filter field name from user input believed the result was filtered.

After

Such a filter is rejected with 400 UNKNOWN_FILTER_FIELD (like any other unknown field), on every entity and on /{entity}/aggregate.

FIX-0812-14: a key allowed to deploy can also patch the application on that server

Before

A key bound to a server through its application deployed code successfully: POST /v1/infra/servers/:id/deploy answered with success. But exec, upload and log reads with the same key answered 403 WRONG_KEY, and there was no programmatic way to obtain that right — the managing key is changed from the dashboard only. A small runtime fix on a server that accepts deploys was unreachable. The refusal on exec carried no hint object, so it offered no recovery guidance either.

After

Rights on a server split by what the call touches. Operations on the application's content — deploy, exec, upload and log reads — accept either the server's managing key or a key whose application is bound to that server. The machine itself (start, stop, wake, reboot, sleep, delete, repair, mode, access policy, port, SSH), access tokens and the icon upload still require the managing key.

A 403 WRONG_KEY refusal now carries a hint object on all four routes and names both recovery steps: rebind the server in the dashboard and switch the key your client sends — the rebind does not change the secret you are already sending. The response code and its meaning are unchanged: what was refused before is still refused.

Integrator impact

No changes required. Calls that worked keep working; calls that answered 403 WRONG_KEY because of an application link now go through. Details — server access recovery.

FIX-0812-15: room in a shared galaxy is decided by the machine's own measurement, not by a fixed seat count

Before

A request to place a new application into a shared galaxy was refused with GALAXY_FULL as soon as the machine held a fixed number of residents. That number was derived from the memory its plan advertises rather than from what is actually in use, so the refusal also arrived on a nearly idle machine, and another galaxy was raised for the request.

After

The answer is built from two measured conditions of the machine itself: free disk space and a memory forecast. The forecast takes the occupancy the machine reported at its last measurement and adds the cost of the applications that were not running at that moment, against its measured memory. While both hold, the galaxy keeps accepting residents, and GALAXY_FULL means there is genuinely no room.

A machine with no measurement yet, or a stale one, answers as before — by the seat count.

Rollout

The new rule ships disabled and is enabled per account. While it is off, answers do not change: the previous seat count still decides. The measurements are introduced by this same update and fill in gradually, by sweeping awake machines, so there is nothing for the rule to apply to any earlier.

NEW-0812-16: deploy operation id and after-the-fact outcome reconciliation

Deploying an app to a server now issues an operation id BEFORE the work starts, not at the end. It arrives as the X-Vibe-Operation-Id response header in both modes, in streaming mode additionally as a first operation event (a browser EventSource does not expose headers), and is mirrored as an operationId field in the envelope — on success and on failure alike.

That id reads the outcome through a separate request: GET /v1/infra/operations/:operationId. It answers with a status of running, succeeded, failed or unknown (the process died between the start and the outcome write), the step, the start and finish times, and the error code. The handle is read-only and has no side effects.

Why it exists: the outcome used to live only inside the deploy response itself, so a dropped connection left no way to learn how it ended — the only option was to deploy again without knowing whether the first attempt had worked. The id is now issued before the long work begins, so an interrupted call can be reconciled.

An id is issued only for a deploy to a standalone virtual machine (kind: "STANDALONE"). A galaxy application (kind: "GALAXY_APP") gets none — not in the header, not in the frame, not in the body — and reconciling its outcome afterwards is not supported yet.

Outcomes are kept for 7 days and each is addressable on its own, including earlier attempts on the same server. An elapsed id answers differently from "not found" — 410 with code OPERATION_OUTCOME_EXPIRED, meaning "the operation existed, its outcome is no longer stored". There is no operation list yet.

A missing header does NOT mean the deploy never started: if the platform could not open the record, the deploy proceeds without an id. Do not read a missing header as a failure and launch a second deploy on top of the first.

NEW-0812-17: A failed deploy reports its outcome as data, not prose

A dedicated-VM deploy could end like this: the app is deployed and serving, the health check answers with an error, and the platform has already moved the app from restricted privileges back to full ones. Telling that apart from "the deploy never happened" was only possible by reading English prose inside error.message — while the consumer here is a machine that reads fields.

The healthcheck step in data.steps[] now carries three facts about the probe: httpCode — the status of the last attempt (the field is absent when no attempt got one; zero or null are never sent instead), healthPath — the path that was actually probed, and portOwner — service, foreign or unknown. These are facts about the attempt, not a verdict: a step with the error status may legitimately carry httpCode: 200, because the outcome is decided by success and the step status.

The hardening step now also arrives on a failed deploy — previously it appeared only on a successful one. It carries the hardeningRollback field: completed — the revert finished and the app runs with administrator privileges; incomplete — the revert started and did not finish, so the app may still be running with restricted privileges. The same value is mirrored in the error envelope as error.hardeningRollback. The field describes the revert of the unit and of the deploy directory ownership, and nothing else; its absence means "no revert was attempted", not "restricted privileges were never applied" — the latter is answered by the service_user step.

All fields are optional and additive: existing calls keep working unchanged.