# API changes: September 21, 2026

[← Changelog](/docs/changelog) · [September 2026](/docs/changelog/2026-09)

### FIX-0921-1: the mailbox batch example now shows a sub-call

**Before**

The reference card for `POST /v1/mail/mailboxes/batch` printed the body `{"action":"list"}`, with no `calls` array. Copied as printed, that call dispatched no sub-calls and came back with an empty result, which read as "this account has no mailboxes" even when it had them. The door accepts sub-call parameters only inside `calls[].params`, and there was nowhere to put them. In the machine-readable specification the sub-call was described as an arbitrary object, and whether the door lifts the required parameters of a sub-call was not published at all.

**After**

The example shows the working form: `{"action":"list","calls":[{"params":{"limit":5}}]}`. The specification describes the `calls` item with its `params` field and publishes the `x-batch-list-lifts-params` extension, as the neighbouring batch doors already do. The operation's responses and behaviour are unchanged: same address, same request body, same status codes, same required scope. In the specification the operation moved from the `mail` tag group to `mail-mailboxes`, which is where the other mailbox operations live. If you generate a client from `GET /v1/openapi.json`, your next generation renames the class this method lands in; the call itself needs no change.

### FIX-0921-2: image generation honours output_format and negative_prompt

**Before**

[POST /v1/images/generations](/docs/ai/images) accepted `output_format` and `negative_prompt`, but the generation service never received them. A request with `output_format: "jpeg"` returned a PNG image, and the `output_format` field in the response also came back as `png`. A request with `negative_prompt` produced the same image as one without it.

**After**

Both fields reach the generation service. `output_format: "jpeg"` returns JPEG, `"webp"` returns WebP, and the `output_format` field in the response names the actual format. Without the field the format is still PNG. `negative_prompt` affects the result.

**Impact on integrators**

No action needed. If you were sending `output_format: "jpeg"` and relying on a PNG response, read the format from the response `output_format` field instead of assuming a constant one.

### FIX-0921-3: `/exec` no longer corrupts multi-byte characters in output

**Before**

In the `POST /v1/infra/servers/:id/exec` response, and in streaming mode `?stream=true`, a multi-byte UTF-8 character that landed on an internal read-buffer boundary arrived as `�`: for example `—` could arrive as `—��`, and the next fragment could start with `�`. Single-byte output was unaffected.

**After**

Output is assembled on character boundaries: an incomplete character at a fragment boundary is held and completed by the next fragment, so `stdout` and `stderr` arrive intact in both streaming and JSON modes. The response remains HTTP 200 and `exitCode` is unchanged.

### FIX-0921-4: application external API survives a short tunnel reconnect

**Before**

An `ANY /v1/applications/:id/api/*` call that landed during a short application tunnel reconnect window could immediately receive `503 APP_API_UNAVAILABLE` with `Retry-After`, even when the application became reachable a few seconds later.

**After**

During that window, the Vibecode platform now briefly holds the call and, if the tunnel returns within the overall request budget, delivers it to the application. If the tunnel does not return in time, the response remains `503 APP_API_UNAVAILABLE` with `Retry-After`.

### FIX-0921-5: write-operation 403 schemas now allow every published code

**Before**

V1 write operations that documented the `WRITE_BLOCKED_READONLY_KEY` refusal also named sibling 403 codes in the same description: `SCOPE_DENIED`, shared API-key gate refusals, and Bitrix24 refusals. The response body schema still closed `error.code` to the single `WRITE_BLOCKED_READONLY_KEY` value, so an SDK generated from OpenAPI treated the other codes as impossible.

**After**

That response schema now allows the standard V1 envelope `{ success: false, error: { code, message } }` next to the strict `WRITE_BLOCKED_READONLY_KEY` branch. Typed `details` for the READONLY refusal stay available, while the other published 403 codes are no longer rejected by the schema.

### BC-0921-7: A field the entity description does not declare is checked on write by its Bitrix24 type

> Old format supported until: not provided

**Before**

The write-shape check covered only the fields declared in the entity description. A field an entity returns on read but does not declare (`utmSource` and `locationId` on deals, `birthdate`, `honorific` and `categoryId` on contacts, `employees` on companies, `statusDescription` on leads) and the custom `UF_*` fields were invisible to it: `{"locationId": {"a": 1}}` on [POST /v1/deals](/docs/entities/deals/create) answered `201` while the record stored the string `Array`; `{"categoryId": "abc"}` on a contact was stored as `0` and `{"categoryId": {"a": 1}}` as `1`, moving the contact to another category; an object in `birthdate` was stored empty. All seven write doors behaved this way — the single `POST` and `PATCH`, the per-entity and the global [batch write](/docs/batch), import.

**After**

Such a field is checked against what Bitrix24 itself reports about it in its field list: an object or an array in a field of a string, numeric, boolean or date Bitrix24 type and a non-numeric string in a numeric field are refused with `400` and the `INVALID_PARAMS` code before the write reaches Bitrix24, and the message names the field and the type Bitrix24 reports for it. An empty string in a link field (employee, contact, company, lead, deal) or in a custom numeric field is not refused — that is how Bitrix24 unlinks and clears; in a category it is refused, because it moves the record to category 0. Fields Bitrix24 reports as multi-value or read-only, fields immutable after creation on an update, fields of types outside those listed (file, enumeration, money), names absent from the Bitrix24 field list, and entities whose field list carries no multiplicity flag (tasks) are not checked — as before. The field list is requested only when the body carries such a name and is kept in memory for five minutes; if Bitrix24 did not return it, the write proceeds as before, and for the next thirty seconds the list is not requested again before a write. The boundaries are in the [INVALID_PARAMS](/docs/errors/request#invalid_params-400) section.

**What integrators should do**

Review the places where fields from `GET /v1/<entity>/fields` that are absent from the entity description receive a structure or a string assembled from an external system: such a request used to answer with success while the data was lost or distorted, and now it returns an honest error naming the field. Values of the right type pass unchanged.

### BC-0921-8: the contact and deal schemas are completed: twelve response fields declared read-only, thirteen service and empty keys removed

> Old format supported until: not provided

**Before**

[GET /v1/contacts/{id}](/docs/entities/contacts/get) (and the list, search, `include` and the re-read record in create and update responses) returned eight keys that were in neither the schema, nor the reference, nor the OpenAPI description: `phoneWork`, `phoneMobile`, `phoneMailing`, `imol`, `address`, `shortName`, `login` and `entityTypeId`. [GET /v1/deals/{id}](/docs/entities/deals/get) — seventeen: `isWon`, `isLose`, `isWork`, `hasProducts`, `receivedAmount`, `lostAmount`, `begindateShort`, `closedateShort`, `dateCreateShort`, `dateModifyShort`, `eventDateShort`, `eventId`, `eventDate`, `eventDescription`, `orderStage`, `productId` and `entityTypeId`. An explicit `select` of any of them answered `400 UNKNOWN_SELECT_FIELD`, a sort on contacts `400 UNKNOWN_SORT_FIELD`, while on deals `?sort=` by any of them travelled to Bitrix24 verbatim (`200`; by `entityTypeId` — a raw `422 BITRIX_ERROR`). A value sent to any of these keys on create, update, import or in a batch was stored by Bitrix24 on no door, yet the platform answered success (`201`/`200`): create and update with an `UNRECOGNIZED_WRITE_FIELD` warning, import silently. An unfilled value of any of the six contact fields (phones, open-channel contact, address, short name) came back as an empty string, not `null` like the described fields.

**After**

Six contact fields are declared, all read-only: `phoneWork`, `phoneMobile`, `phoneMailing` (the `phone` multifield by type), `imol` (the open-channel contact), `address` (the legacy address column — filled only by the legacy `crm.contact.add`, an address is edited in the requisites) and `shortName` (last name with initials, computed by Bitrix24). Six deal fields computed by Bitrix24 are declared, all read-only: `isWon`, `isLose`, `isWork` (flags by the stage semantic), `hasProducts` (whether product rows exist), `receivedAmount` and `lostAmount` (the deal amount in the account's accounting currency — converted at the rate when the deal's currency differs — while it is won or lost, `0` otherwise). All twelve are described on the [contact fields page](/docs/entities/contacts/fields) and the [deal fields page](/docs/entities/deals/fields), in `GET /v1/contacts/fields` and `GET /v1/deals/fields`, in the reference and in the OpenAPI description; types come from a live measurement. An explicit `select` of these fields works; filter and sort by `phoneWork`, `phoneMobile`, `phoneMailing`, `address`, `shortName` and by `receivedAmount`/`lostAmount` work (by `imol` — accepted), `receivedAmount`/`lostAmount` are also accepted in `sum`/`avg`/`min`/`max` of `POST /v1/deals/aggregate`; the four deal flags are not accepted in `filter` (`400 UNKNOWN_FILTER_FIELD`) — a boolean filter travels to Bitrix24 as `Y`/`N`, while Bitrix24 matches these flags only against a boolean value: for the three stage flags the string `Y` answers the inverted set (filter on `stageSemanticId`), for `hasProducts` it matches no record (no substitute filter); sorting by the flags works. The six computed deal fields and the contact's `shortName` are calculated by Bitrix24 only for a list or search with an explicit `select` without `*`; a single `GET`, a list without `select` and the `list` action of the entity batch return `null` for them. A value sent to any of the twelve fields — which Bitrix24 never stored — is now refused: create and update answer `400 READONLY_FIELD`, import `400 IMPORT_ITEM_VALIDATION`, the entity batch `400 BATCH_ITEM_VALIDATION`, and the global batch `READONLY_FIELD` under the call in `data.errors`. Unfilled values of the six contact fields come back as `null`, like every other empty string of the record. Thirteen keys are removed from the responses: on contacts `entityTypeId` (the type constant, 3) and `login` (empty on every account, written by no door); on deals `entityTypeId` (the constant 2), the five short dates `begindateShort`, `closedateShort`, `dateCreateShort`, `dateModifyShort`, `eventDateShort` (the same dates a second time) and the five empty legacy columns `eventId`, `eventDate`, `eventDescription`, `orderStage`, `productId` (empty on every account, filled neither through the API nor through the legacy methods). The removed keys are not accepted in `select`, `filter` or sorting — `?sort=`, `?order[]`, the search body `order` (`400 UNKNOWN_SELECT_FIELD` / `400 UNKNOWN_FILTER_FIELD` / `400 UNKNOWN_SORT_FIELD`) — on deals `?sort=` by them used to travel to Bitrix24 verbatim. The prior exception stays: the `params.order` of a `POST /v1/batch` sub-call on contacts and deals still travels to Bitrix24 verbatim.

**What integrators should do**

If you sent any of the twelve fields on create, update, import or in a batch — drop it from the body: Bitrix24 never stored it, phones are written through `phone`, the address in the requisites, the flags and amounts are computed by Bitrix24. If you read `entityTypeId`, `login`, the short dates or `eventId`/`eventDate`/`eventDescription`/`orderStage`/`productId` — these keys no longer arrive: the entity type is given by the endpoint itself, take the dates from `begindate`/`closedAt`/`createdAt`/`updatedAt`, the rest were always empty. If you checked `phoneWork`, `phoneMobile`, `phoneMailing`, `imol`, `address` or `shortName` against an empty string — check for `null`. If you need `isWon`/`isLose`/`isWork`/`hasProducts`/`receivedAmount`/`lostAmount` or `shortName` — request them with an explicit `select` (without `*`) on the list or search.

**Affected endpoints:** [GET /v1/contacts/{id}](/docs/entities/contacts/get), [GET /v1/contacts](/docs/entities/contacts/list), [POST /v1/contacts/search](/docs/entities/contacts/search), [GET /v1/contacts/fields](/docs/entities/contacts/fields), [POST /v1/contacts](/docs/entities/contacts/create), [PATCH /v1/contacts/{id}](/docs/entities/contacts/update), [GET /v1/deals/{id}](/docs/entities/deals/get), [GET /v1/deals](/docs/entities/deals/list), [POST /v1/deals/search](/docs/entities/deals/search), [GET /v1/deals/fields](/docs/entities/deals/fields), [POST /v1/deals](/docs/entities/deals/create), [PATCH /v1/deals/{id}](/docs/entities/deals/update), [POST /v1/deals/{id}/move](/docs/entities/deals/move), [POST /v1/{entity}/import](/docs/import), [POST /v1/{entity}/batch](/docs/batch), [POST /v1/batch](/docs/batch).

### FIX-0921-9: Cowork plans return terms to an unknown account too, once terms are open to everyone

**Before**

`GET /v1/platform/cowork/plans` returned the 3-, 6- and 12-month terms only for an account the platform already knows. An account with `portal.known: false` got a single one-month entry in `price.terms[]` with a `0` discount — whether or not terms were open to everyone.

**After**

An unknown account is given the terms when term sales are open to **every** account. While a partial rollout is under way it still gets one month: the term price is not valid for everyone at that point, so it must not be promised.

A known account answers as before, by its own rollout.

**What integrators should do**

Nothing. The response shape is unchanged, the term selector is still built from `price.terms[]`, and a single term in the array remains a normal state. The only change is that a first-time buyer — an account the platform does not know yet — now sees the same set of terms as everyone else.

### NEW-0921-10: the Cowork employee list now breaks parked seats down by plan and term

**What is new**

The `seats` block of the `GET /v1/platform/cowork/members` response now carries `parkingBreakdown` — the same parked seats, row by row:

```json
"seats": {
  "parking": 3,
  "parkingBreakdown": [
    { "plan": "PRO", "count": 2, "validUntil": "2026-11-30T21:00:00.000Z" },
    { "plan": "ULTRA", "count": 1, "validUntil": "2026-10-15T21:00:00.000Z" }
  ]
}
```

Rows are grouped by the pair «plan + the moment the term ends» and ordered by plan rank, then by term ascending. The sum of `count` across rows always equals the `parking` counter — one pass computes both.

`validUntil` has the same shape as on an employee row: an exact ISO instant, not a calendar date. The day is worked out by whoever knows the buyer's timezone — a seat expiring on 30 November at 23:30 UTC expires on 1 December in Moscow.

Seats bought in one purchase share the instant and collapse into one row; seats bought separately get their own rows.

**Why**

A single number cannot produce the line «2 Pro seats parked, expiring 30 November» — neither the plan nor the date is in it.

**What integrators should do**

Nothing, the field is additive. The `parking` counter stays where it was and means what it meant.

### NEW-0921-11: server deletion reports how the request to delete the machine in the cloud ended

The response of [DELETE /v1/infra/servers/{id}](/docs/infra/servers/delete) now carries a `cloudDelete` field: `confirmed` — the cloud accepted the request to delete the machine, `already-gone` — the machine was already gone, `failed` — the cloud did not accept the request, `not-applicable` — the server has no machine in the cloud, for example a Galaxy application. `success` is still `true` in all four cases: the server record is deleted and billing is stopped. The response did not show this before: on `failed` the server record was deleted anyway while the machine could keep running — invisible in the response. On `failed` do not treat the resource as destroyed.

### NEW-0921-12: DISK_FULL code in a server's provisionErrorCode after a failed repair

If a server repair (`POST /v1/infra/servers/{id}/repair`) fails for lack of disk space, the server
in `GET /v1/infra/servers` and `GET /v1/infra/servers/{id}` now gets the new value
`provisionErrorCode: "DISK_FULL"` and a clear message in `provisionError`: retrying will not help,
free up disk space first. A successful repair clears `DISK_FULL`. An existing
`AGENT_NEVER_CONNECTED` or `GUEST_NOT_BOOTING` verdict is never replaced by `DISK_FULL`. The server
status does not change, and existing requests keep working as before.

### NEW-0921-13: activities now support include: responsible, author, editor

An activity now declares three relations to employees, requestable through the `include` parameter on [GET /v1/activities](/docs/entities/activities/list), [GET /v1/activities/{id}](/docs/entities/activities/get) and [POST /v1/activities/search](/docs/entities/activities/search): `responsible` — the assignee (via the `responsibleId` field), `author` — the author (via `authorId`), `editor` — the last editor (via `editorId`). Previously the entity declared no relations at all, so every name was rejected with `400 INVALID_INCLUDE` and the list of accepted names in the refusal text was empty.

A request such as `GET /v1/activities/{id}?include=responsible,author` answers `200` and places the employee cards under `_included`. A request without `include` works as before. The relation reads an employee card, so the key must carry the `user` permission — otherwise the request answers `403 SCOPE_DENIED`. The available names are published by [GET /v1/activities/fields](/docs/entities/activities/fields) and by the OpenAPI specification.

A name outside that list is still rejected, but the refusal now names the available relations: `Unknown include 'owner'. Available: responsible, author, editor`. The parent of an activity (`ownerTypeId` + `ownerId`) and `communications` are deliberately not declared as relations: the target entity there changes from record to record, and a single numeric identifier cannot tell which card to read; both values already arrive as fields of the record itself.

### FIX-0921-14: structured output is checked for parseability, not only for truncation

**Before**

With `response_format` of type `json_object` or `json_schema` the platform only checked that the model had not been cut off and that the answer was not empty. A non-empty answer that does not parse as a JSON document — prose around the object, an answer wrapped in a markdown code fence, a bare value — arrived as an ordinary HTTP 200 response, and `JSON.parse` failed inside the integration.

**After**

The platform parses the answer before handing it over. The `200` response is unchanged: a valid JSON object or array arrives exactly as before, byte for byte. If the answer is non-empty but does not parse, the call is refused with `422`, code `structured_output_invalid_json`, a `finishReason` field and a `hint`; it carries no `param` and no `suggestedMaxTokens` — a larger token budget does not change the result. On a stream the refusal arrives as a `{"error":{"code":"structured_output_invalid_json"}}` event before `data: [DONE]`, and the chunks already delivered should be discarded. The `structured_output_truncated` refusal keeps covering truncation and an empty answer. A call that reached the model is billed exactly as before.
