For AI agents: markdown of this page — /docs-content-en/changelog/2026-09-21.md documentation index — /llms.txt
API changes: September 21, 2026
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 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 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, 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 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} (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} — 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 and the deal fields page, 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}, GET /v1/contacts, POST /v1/contacts/search, GET /v1/contacts/fields, POST /v1/contacts, PATCH /v1/contacts/{id}, GET /v1/deals/{id}, GET /v1/deals, POST /v1/deals/search, GET /v1/deals/fields, POST /v1/deals, PATCH /v1/deals/{id}, POST /v1/deals/{id}/move, POST /v1/{entity}/import, POST /v1/{entity}/batch, POST /v1/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:
"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} 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, GET /v1/activities/{id} and POST /v1/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 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.