# API changes: August 27, 2026

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

### NEW-0827-1: aggregation marks a partial total next to the number itself

**Before**

`POST /v1/{entity}/aggregate` with `sum` / `avg` / `min` / `max` computes over the first 5000 records matching the filter. The incompleteness marker lived in `data.meta.truncated` alone, while `data.aggregates.amount` looked like `{ "sum": 1234567 }` — a plain number with nothing attached. A client reading only the total received an understated result under code `200` with nothing to branch on: `data.count` meanwhile reported the full number of records.

**After**

When the selection is truncated, every field object in `data.aggregates` and in `groups[].aggregates` additionally carries `truncated: true` — `{ "sum": 1234567, "truncated": true }`. The same condition adds a warning with code `AGGREGATE_TRUNCATED` to `data.meta.warnings`. The `sum` / `avg` / `min` / `max` values stay numbers, nothing wraps them into an object. On a complete selection the response does not change at all: the field object carries no `truncated` key and no `meta.warnings` appears. The 5000 ceiling and `data.count` behave as before.

Grouping gains one more field — `groups[].truncated`: on a truncated answer the group object itself carries this marker next to its counter. The marker says the answer is a sample; whether the group counter is exact is shown by `meta.aggregatePath` — on the ordinary walk `groups[].count` is computed over the slice that was read, while under the stage-count mode (`meta.aggregatePath: "fanout"`) it comes from a separate probe and is exact. On a count-only grouping it is the only marker next to a number: a `count` expression produces no field object, and both `aggregates` bags come back empty. `data.count` and `data.meta.totalRecords` stay exact at any size and never carry it.

Alongside the incompleteness marker the response now always reports the size of the gap. Previously `meta.recordsShortfall` arrived only when the truncation was NOT caused by the ceiling: on a selection above 5000 records the response raised `truncated: true` with no gap-size field at all. That case is the most common one: on a pipeline of 20,000 deals a client saw "this answer is a sample" and zero as the size of what was missing. Now every reason for truncation carries a quantifier next to the marker — `meta.recordsShortfall` (how many records never reached the numbers) or `meta.pageErrorSample` (when the slice was cut short by a sub-page error). On a complete selection neither key appears in the response, as before.

A false alarm is removed at the same time. The truncation marker used to be raised from the comparison "more than 5000 records in total", which is only an indirect sign that the read will be cut. On entities where the limit never reaches Bitrix24 — pages and sites (`landing.*`), Open Channels configs — nothing is cut and every row is read. Such a response still declared itself a sample. The marker is now raised from the fact: fewer rows read than promised, then and only then. A fully read selection of any size counts as complete again.

This also closes a gap in grouping deals by stage: when a stage walk returned fewer records than its probe had counted (usually because of access rights), the response used to arrive with `data.meta.truncated: false` and looked complete. Such a response now raises `truncated` honestly — and with it comes both the marker next to the number and the warning.

### BC-0827-2: runtime installation order and nginx startup have changed

> Old format supported until: not provided

**Before**

During a redeploy, the runtime was installed after the previous app version had been stopped. A failed installation could leave the app stopped. For `static`, `php83`, and `php83-mysql`, package installation could implicitly start and enable the system `nginx.service`, and a request could rely on that process.

**After**

[`POST /v1/infra/servers/:id/deploy`](/docs/infra/deploy/deploy) installs the runtime before stopping the app and replacing its files. If installation fails, the previous app is not stopped. Installing nginx for `static`, `php83`, and `php83-mysql` does not start the system `nginx.service` or leave it newly enabled. The existing service enablement state is preserved.

**What integrators should do**

For `static`, `php83`, and `php83-mysql`, make sure the command in the `start` field starts the process that listens on the application port inside `app.service`. Do not rely on an implicit start of the system `nginx.service`. Requests using other runtimes do not need to change.

### NEW-0827-3: unfinished BOX account reason and employee identity transfer between accounts

The BOX account card with an unfinished connection (`accessPending: true`) in the `GET /api/portals` response now carries a reason in the optional `accessPendingReason` field: `transfer_requested` — the employee already has a pending request to move their link to this Bitrix24 account from another Vibecode account, `incomplete` — the connection is simply unfinished. Such requests are managed by the new `/api/box-transfers/*` routes: the current holder lists their pending requests, opens one via the email link, and approves or declines the transfer.

### FIX-0827-4: unfinished BOX account connection is visible in the list

**Before**

`GET /api/portals` silently hid a BOX account whose connection was unfinished.

**After**

The account card is returned with the new `accessPending: true` field.

### FIX-0827-5: user invitations preserve all profile fields

**Before**

[POST /v1/users/invite](/docs/entities/users/invite) accepted ten writable profile fields in the regular Vibecode API format but forwarded them without converting their names. The user was created while the photo, external ID, time zone, and personal details remained empty.

**After**

[POST /v1/users/invite](/docs/entities/users/invite) converts these fields to the Bitrix24 format. Clients do not need to change their requests, and the previous Bitrix24 field format keeps working.

### FIX-0827-6: single comment lookup finds a comment with a string identifier

**Before**

[GET /v1/tasks/:taskId/comments/:id](/docs/entities/task-comments/get) could return `404 NOT_FOUND` when Bitrix24 returned the found message identifier as a string. The same comment was still present in the task comment list.

**After**

The endpoint normalizes the numeric identifier from the Bitrix24 response and returns the found comment with numeric `id` and `authorId` values.

**Impact on integrations**

No client changes are required.

### FIX-0827-7: contact select accepts typed email addresses again

**Before**

[POST /v1/contacts/search](/docs/entities/contacts/search) and a contact sub-call in [POST /v1/batch](/docs/batch) rejected `emailWork`, `emailHome`, and `emailMailing` with `UNKNOWN_SELECT_FIELD`, even though contact responses already contained these values. [GET /v1/contacts/fields](/docs/entities/contacts/fields) did not list these names.

**After**

Both operations accept the three names in `select` and return a successful response with the selected values. [GET /v1/contacts/fields](/docs/entities/contacts/fields) describes them as nullable read-only fields.

### FIX-0827-8: tariff headers are returned for infrastructure requests

**Before**

Responses from the `/v1/infra/*` endpoint family did not contain `X-Tariff-Checked-At` or `X-Tariff-Is-Commercial`, even when the tariff check had completed successfully.

**After**

Responses from `/v1/infra/*` contain tariff headers under the same rules as `/v1/me`: the time of the last successful check and the commercial-tariff indicator are returned when the corresponding account data is available. The response status and body are unchanged.

**Impact on integrators**

No request changes are needed. A missing header once again means that the corresponding tariff-check data is unavailable, rather than that the infrastructure route was skipped.

### NEW-0827-9: array element schemas are available in field contracts

[GET /v1/orders/fields](/docs/entities/orders/fields), [GET /v1/basket-items/fields](/docs/entities/basket-items/fields), and [GET /v1/guide](/docs/keys-auth/guide) now return `itemSchema` for arrays with a declared element shape. The schema recursively describes `type`, `readonly`, `nullable`, `properties`, and nested `itemSchema` values. Clients can optionally read the new field, and existing integrations continue to work without changes. The separate `items` key continues to contain the raw Bitrix24 value directory for enumeration fields.

### FIX-0827-10: lists without a total no longer look complete too early

**Before**

If Bitrix24 did not return the total record count, a list request with a limit above 50 stopped after the first page. A full window of up to 50 records could also look like the last page because its size was treated as the collection size.

**After**

The platform continues reading after each full page and stops at a short page or the request limit. When the collection end is not yet proven, the total remains unknown and `meta.hasMore` or `truncated` reports that more records may exist.

**Impact on integrations**

No request changes are required. Requested windows larger than one page are no longer silently truncated; `GET /v1/mail/messages` now always includes a boolean `truncated` signal and omits an unknown `total` instead of returning `null`.

**Affected endpoints:** [GET /v1/humanresources/nodes](/docs/humanresources/nodes/list), [GET /v1/mail/mailboxes](/docs/mail/mailboxes/list), [GET /v1/mail/messages](/docs/mail/messages/list).

### NEW-0827-11: the self-description now points at the Cowork/Code promo code docs

The `GET /v1/guide` and `GET /v1/me` responses now name the desktop application's promo code pair. The `cowork` section of `guide.ts` carries the new `docs.couponPreview` and `docs.couponRedeem` pointers to the `POST /v1/cowork/coupon/preview` and `POST /v1/cowork/coupon/redeem` pages, and the `/v1/me` rules gained an item about them: which key class is required, how the check differs from the redemption, and why `accessGranted: false` deserves a screen of its own.

The endpoints themselves behave as before — what is new is that an integrator or an agent finds their description without reading the changelog.

### FIX-0827-12: the Cowork/Code rate limits no longer quote a number the client never receives

**Before**

The `GET /v1/guide` reference, the `GET /v1/me` rules and the Cowork/Code documentation pages quoted rate limits as a concrete number: 20 requests per minute for `POST /v1/cowork/coupon/preview`, 10 for `POST /v1/cowork/coupon/redeem`, 30 for `GET /v1/cowork/subscription/preview`, 5 for `DELETE /v1/cowork/key`, 3 per five minutes for `POST /v1/cowork/deploy-key`, 30 for `GET /v1/cowork/applications/defaults` and 6 for `POST /v1/cowork/applications`. None of those numbers ever reached the client: the cap is divided across the platform's processes, and the `x-ratelimit-limit` header returned 7, 4, 10, 2, 1, 10 and 2 respectively. For `deploy-key`, `key` and the create-application wizard the two channels of one endpoint contradicted each other — the page promised one thing while the header returned another.

**After**

The `GET /v1/guide` reference, the `GET /v1/me` rules and the section's documentation pages now name the `x-ratelimit-limit` response header as the source of the current value and ask you not to hard-code a number in client code. The endpoints themselves did not change and the response is unchanged — what was corrected is the description that diverged from it. A client that already read the header changes nothing.

What this entry does NOT cover. The machine schema `GET /v1/openapi.json` still quotes numbers for five operations of the section — `subscription/preview`, `deploy-key`, `applications/defaults`, `applications` and `key` — and those numbers diverge from the header exactly as the others did. A client generated from the schema must still read `x-ratelimit-limit` rather than the value in the operation description. One promise in the schema is accurate: the "three requests per hour" on `POST /v1/cowork/activate-market-trial`, where the cap is multiplied by the process count before the division.

### FIX-0827-13: telephony line creation returns a usable key

**Before**

[POST /v1/telephony-lines](/docs/telephony/lines/create) returned an internal numeric identifier. It could not be used in the path for updating or deleting the created line.

**After**

The `data.id` field contains the created line number. This value can be used in subsequent update and delete requests.

**Impact on integrations**

No action is required. New create responses immediately contain an addressable key.

### NEW-0827-14: time entry lists explicitly reject unsupported filter envelopes

**Before**

An unsupported `filter` in [GET /v1/task-time](/docs/entities/tasks/time/global-list) and [GET /v1/tasks/:taskId/time](/docs/entities/tasks/time/list) could return status `200` with a broader result set than the client expected.

**After**

A non-empty, bracket, JSON, or repeated `filter` returns `400 UNSUPPORTED_FILTER`. For requests with the named `userId`, `taskId`, `from`, and `to` parameters, the response remains HTTP 200 and their behavior is unchanged.

### FIX-0827-15: PUBLIC app robots.txt is available to crawlers

**Before**

Exact `GET` and `HEAD` requests to `/robots.txt` always received the gateway's local denial, even when the app had `accessPolicy=PUBLIC`, was running, and served its own file.

**After**

With `accessPolicy=PUBLIC` and a live tunnel, exact `GET` and `HEAD` requests to `/robots.txt` return the app response: `200 text/plain` with a complete non-empty GET body no larger than 1 MiB (HEAD has no body), or `304`. For every other policy, an unavailable app, or any invalid response, the gateway still returns local `200 text/plain` with `Disallow: /`.

**Impact on integrators**

A PUBLIC app can now control indexing through its own `/robots.txt` file. No change is needed for non-public apps: denial remains fail-closed by default.

### FIX-0827-16: entity filters are no longer lost on list calls

**Before**

`list_entities` could send filters as ordinary query parameters. This made a field named `sort` collide with the sorting directive, while `openline-configs` could return the full list instead of a filtered result. On envelope-based entities, a filter could also be lost in `GET /v1/{entity}/aggregate`, `POST /v1/batch`, and `POST /v1/{entity}/batch`.

**After**

`list_entities` sends fields through `filter[...]`, separately from sorting. Every V1 list path applies the envelope declared by the entity, so a non-empty filter reaches Bitrix24 and a non-matching filter returns an empty result instead of the full collection. The custom bookings list accepts the same bracket form for its required `dateFrom`/`dateTo` window while preserving the existing flat form.

**Impact on integrations**

No request changes are required. Previously over-broad successful responses now match the supplied filter.

### FIX-0827-17: the application access list contains Bitrix24 account employees only

**Before**

`GET /v1/infra/servers/{id}/access` returned every audience row in `users[]`. The element's `id` field is declared required, yet a grant issued to someone outside the server's Bitrix24 account has no account number at all — such an element would arrive with an empty `id`, breaking any client that reads the field as required.

**After**

`users[]` contains only rows carrying an account number. Rows addressed by a network identifier are excluded from this list — they live in a different identifier space, and the two must never be mixed. On today's data the response is unchanged: no such rows exist yet. The same rule applies to the matching dashboard screen.

### FIX-0827-18: the `connect` step of repair status no longer fails for an agent that did connect

**Before**

Server repair counted the `connect` step as complete only when the version of the agent that came
up matched the version set in platform settings. If the agent connected on a different version, the
tunnel was up and `blackholeStatus` read `CONNECTED`, yet
`GET /v1/infra/servers/{id}/repair-status` returned `status: failed`, `step: connect` and
`error: "Agent did not connect"`.

**After**

The `connect` step completes once the agent connects to the Gateway — exactly as the step order is
described on the operation page. The installed agent version is still returned in
`data.agentVersion` and no longer affects the repair outcome.
