# API changes: August 12, 2026

[← Changelog](/docs/changelog) · [August 2026](/docs/changelog/2026-08)

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

[GET /v1/bots/{botId}/events](/docs/bots/events/polling) 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`](/docs/infra/deploy/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`](/docs/infra/deploy/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](/docs/feedback/submit) 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](/docs/apps/publish) and [Source storage](/docs/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](/docs/entities/deals/products-get) 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](/docs/apps/list), create, read, update, delete, publish, unpublish and OAuth re-link), AI spend and quota ([GET /v1/ai/usage](/docs/ai/consumption/usage), [GET /v1/ai/quota](/docs/ai/consumption/quota)), the off-peak schedule ([GET /v1/off-peak](/docs/ai/consumption/off-peak)), AI follow-ups for finished calls ([POST /v1/calls/followups/list](/docs/calls/followup), [GET /v1/calls/followups/:callId](/docs/calls/followup)) and the line list ([GET /v1/voximplant-lines](/docs/telephony/lines/voximplant)).

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](/docs/entities/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](/docs/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](/docs/keys-auth/me)) and do not repeat the request, it will run into the rate limit.

### FIX-0812-11: a dedicated-machine deploy keeps the build's internal links and checks the application root

**Before**

The platform removed every symbolic link from the extracted tree before starting the app. A prebuilt server-rendering bundle links its own files relatively inside its own directory, so the app lost part of its dependencies and answered HTTP 500 at the root address. The health check polled only the path given in `healthPath`, so a light endpoint such as `/api/health` kept answering 200 and [POST /v1/infra/servers/{id}/deploy](/docs/infra/deploy/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](/docs/infra/deploy/deploy) answered with success. But [exec](/docs/infra/deploy/exec), [upload](/docs/infra/deploy/upload) and [log reads](/docs/infra/deploy/logs) 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](/docs/infra/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](/docs/infra/deploy/operation-status). 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.
