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

API changes: September 16, 2026

← Changelog · September 2026

FIX-0916-1: partner accounts get raised free Cowork limits

Before

A free Cowork seat was measured against the shared windows no matter whose account it was. GET /v1/cowork/me and GET /v1/cowork/state returned shares computed from the shared grid, and once a window ran out an AI request got 402.

After

On a partner (NFR) account a free seat is measured against its own windows, set on the platform side, and they only ever raise a ceiling. The response shape is unchanged: quotaPct and windows carry the same fields, but the shares are now computed from the partner ceiling, and the 402 for an exhausted window arrives later. A partner's paid seat is untouched — it is measured against its own tier, and the seat price does not change.

The platform reads the partner mark from the account licence. On a self-hosted portal it appears once the Vibecode module is re-registered there.

Impact on integrators

Nothing to do: the fields and statuses are the same, and a successful response stays successful. A client keeping its own copy of the ceiling, derived from the share and the spend, should recompute it from a fresh response rather than cache it between sessions.

NEW-0916-2: Bitrix24 account members and their Cowork seats

GET /v1/platform/cowork/members is new — Bitrix24 account employees with their current Cowork plans, consumption and a plan recommendation. It is meant for a checkout that draws a seat calculator: the list of employees, their plans and their spend live on the Vibecode platform only.

The Bitrix24 account is addressed by portalNetworkId, its identifier in Bitrix24.Network — the same one carried by a GET /v1/platform/revenue/balances row. The fallback key is portalDomain. An unknown account is not an error: the response comes back with portal.known set to false and empty lists, because that is what a first-time buyer looks like.

The portal block carries usersTotal and usersInVibecode; the seats block carries assigned seats in assigned, seats running out in expiringWithin30Days, and unassigned for seats bought ahead. A data row carries userId (the person's identifier in Bitrix24.Network — the same value that arrives on entry from the checkout), b24UserId, email, name, position, departmentIds, isAdmin, inVibecode, a plan block (code, status, source, months, validFrom, validUntil), a usage block (requestsMonth, quotaVibesMonth, usedVibesMonth, lastActiveAt) and recommendation (plan, reasonCode, confidence, facts). An employee who is not on the Vibecode platform yet comes with userId set to null and is identified by b24UserId.

Selection is filter with the values ALL, PAID, EXPIRING, SUSPENDED, NO_PLAN, NOT_IN_VIBECODE, HAS_RECOMMENDATION; search matches name and position. Paging: limit up to 500 (100 by default), continued by cursor taken from nextCursor; the snapshot time arrives in capturedAt. An unknown query parameter is rejected with INVALID_FILTER instead of being ignored silently.

The degraded field says whether the list is complete. true means the account's employees could not be read and the response holds only those already working on the Vibecode platform; usersTotal is then a lower bound, not an exact count.

Authorization is a platform integration key in the Authorization: Bearer header with the cowork:read scope, granted by a platform administrator when the key is issued. The scope is separate from revenue:* on purpose: the response carries employees' personal data rather than amounts, and an accounting key must not receive it along the way.

FIX-0916-3: a concurrent application key replacement issues exactly one key

Before

The "one replacement per application" limit held only until a neighbouring replacement finished, not for the whole life of a request. Two concurrent calls to POST /v1/cowork/applications/{id}/key carrying DIFFERENT Idempotency-Key values could land in that window and issue a key each: the documented APPLICATION_KEY_REPLACE_IN_PROGRESS refusal never reached the second caller, the card slot went to whichever finished second, and the key issued to the first caller was silently moved to a one-day expiry as a replaced key — its owner was never told.

After

The limit now holds for the whole life of a request. A caller whose card has meanwhile been taken by another replacement gets 409 APPLICATION_KEY_REPLACE_IN_PROGRESS and mints nothing — exactly the refusal already documented for this method. One key is issued, and that key is the one in the slot.

Integrator impact

Nothing to change: the APPLICATION_KEY_REPLACE_IN_PROGRESS code and the advice that goes with it are unchanged, and no new codes were added. What changed is how often it arrives: the refusal now also covers the race where a second key used to slip through. Treat it as a normal outcome of a parallel call — read the application card and check whether another key is still needed. If you fire replacements in parallel against yourself, serialise those calls on your side.

NEW-0916-4: server details show the port detected by the tunnel agent

GET /v1/infra/servers/:id additionally returns detectedPort and detectedPortObservedAt in the full server card: the last reliably observed tunnel target port and the observation time. Both fields are null before the first observation; an unsuccessful observation does not erase an earlier one. The GET itself does not probe the machine, so assess freshness together with the timestamp and connection status.

Impact on integrators

No action is required: the new fields are additive. Use detectedPort to diagnose the tunnel's actual target, and continue treating localPort as configuration or fallback.

FIX-0916-5: the COWORK_HARNESS_DISABLED code no longer arrives on Cowork subscription keys

Before

PATCH /v1/keys/:id and POST /v1/keys/:id/rotate answered 403 with the code COWORK_HARNESS_DISABLED while issuing subscription keys for third-party agents was closed on the platform.

After

That code does not arrive. The remaining issuance checks are unchanged: the key owner's Cowork access, the account administrator's permission for third-party clients and the subscription state — each still answers 403 with its own code, and a successful 200 response stays successful. Nothing has to change in an integration, and handling of the COWORK_HARNESS_DISABLED code can be dropped.

FIX-0916-6: Object reads are limited independently for each Bitrix24 account

Before

GET /v1/storage/objects/:key and HEAD /v1/storage/objects/:key did not limit a flow of repeated requests.

After

Each method now has its own budget of 600 requests per minute per Bitrix24 account, shared by all API keys of that account. Normal successful responses are unchanged. Once the budget is exhausted, the API returns 429 RATE_LIMITED: the effective limit arrives in x-ratelimit-limit, and the retry delay in Retry-After.

Impact on integrations

No changes to normal requests are required. On 429 RATE_LIMITED, wait for the Retry-After delay before retrying.

FIX-0916-7: API reports the effective tunnel state

Before

After a tunnel was lost, server reads and /refresh could keep returning CONNECTED, so clients did not see the repair action and /exec, /upload, and /logs proceeded to a missing-tunnel error.

After

List, detail, and refresh responses report the effective tunnel state, including DISCONNECTED and the repair action. This status correction is not persisted. Before exec, upload, logs, and deploy, the API verifies server availability. Tunnel absence is confirmed only by a non-empty current connection snapshot; an empty or unavailable snapshot preserves the stored status. Exec, upload, and deploy may restore a confirmed-missing tunnel, while GET logs returns 409 SERVER_NOT_READY with an explicit POST /repair instruction and does not start repair. Successful response shapes and their HTTP 200 status remain unchanged.

Affected endpoints: GET /v1/infra/servers, GET /v1/infra/servers/:id, POST /v1/infra/servers/:id/refresh, POST /v1/infra/servers/:id/exec, POST /v1/infra/servers/:id/upload, POST /v1/infra/servers/:id/deploy, GET /v1/infra/servers/:id/logs.

Impact on integrators

Treat DISCONNECTED and repair as the effective state. After 409 SERVER_NOT_READY, retry with normal backoff and honor Retry-After when the header is present. For GET logs, first call the POST /repair endpoint named in hint: the GET does not start repair itself. Response shapes and successful HTTP 200 behavior are unchanged.

NEW-0916-8: Cowork off-peak and quota-relief keys now always arrive

In GET /v1/cowork/me and GET /v1/cowork/state the keys arrive in every successful response: offPeak (both responses), touSavedPct (state), relief (both responses), boostPct and boostExpiresAt (me). Presence checks for these keys can be dropped — the 200 response is still successful, the values inside the blocks are unchanged, and null inside a block keeps its own separate meaning. currentWindowEndsInHours in GET /v1/off-peak arrives in every successful response as before: it never depended on a per-account rollout.

offPeakHint in the 402 cowork_quota_exhausted refusal of POST /v1/chat/completions no longer depends on the capability being enabled for the account, but it still arrives only when the unblock moment falls into a discounted hour. The presence check for that key stays.

FIX-0916-9: storage: a file uploaded again under the same key no longer counts as several objects

Before

Until 2026-09-04, uploading a file again under the same key with a personal developer key or on behalf of a server created one more object instead of replacing it. The GET /v1/storage/objects response listed one file several times under different identifiers, while GET /v1/storage/objects/{key} and DELETE /v1/storage/objects/{key} acted on one of the copies, so a deleted file stayed in the list.

After

Copies of private files are folded: the key keeps one object with the identifier of its earliest non-deleted copy and the metadata of the latest upload (sizeBytes, sha256, contentType, contentUpdatedAt). Deleting by key works on the first call. File bytes are unchanged. Copies of public files stay in place for now, so links already issued for them through GET /v1/public-storage/{portalId}/{objectId} keep working. Objects uploaded with an app key are not affected.

Impact on integrators

No changes are needed. A private file is read and deleted by key, and the key is unchanged. The identifiers of the folded copies disappear from the listing.

FIX-0916-10: a wake window on a server with a run mode is no longer refused as an always-on conflict

Before

POST /v1/infra/servers/{id}/wake-schedules answered 400 ALWAYS_ON_CONFLICT for any server with no idle-sleep threshold — including a machine whose owner had already assigned a scheduled run mode. Adding an extra wake to such a machine was impossible.

After

A server in the scheduled (SCHEDULE) run mode is no longer treated as always-on: its sleep is declared by the owner explicitly, through the schedule windows. An extra wake window on such a machine is created as usual and the 201 response is unchanged. For a machine without a schedule — including one in the ALWAYS run mode — the behaviour is the same as before: 400 ALWAYS_ON_CONFLICT.

BC-0916-11: Unambiguous record type when listing CRM documents

Old format supported until: not provided

Before

GET /v1/crm-documents accepted repeated entityTypeId values with HTTP 200 and could return documents of another record type. Supplying both entityTypeId and entityTypeID was also accepted.

After

Repeated parameters, both names together and bracket forms return HTTP 400 INVALID_ENTITY_TYPE, even when the values agree. For a single bracket form, the error code changes from MISSING_PARAMS to INVALID_ENTITY_TYPE. A single positive integer supplied as entityTypeId or entityTypeID works as before.

What integrators need to do

Supply exactly one entityTypeId or entityTypeID parameter with one positive integer value. Do not use arrays or objects for the record type. Encode user input when building query strings.

NEW-0916-12: calendar events now support include: owner, host, attendee

A calendar event now declares three relations to employees, requestable through the include parameter on GET /v1/calendar-events and GET /v1/calendar-events/{id}: owner — the calendar owner (via the ownerId field), host — the meeting organiser (via meetingHost), attendee — the invitees (via attendeeList). 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/calendar-events/{id}?include=owner,host 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.

A name outside that list is still rejected, but the refusal now names the available relations: Unknown include 'section'. Available: owner, host, attendee. Calendar sections are deliberately not declared as a relation: Bitrix24 offers no read of a section by identifier, it is available only as a list.

NEW-0916-13: employee custom fields via /v1/userfields/users

Employee custom fields (entity users, field-name prefix UF_USR_) can now be created and edited through the Vibecode API. Previously the platform could only read them: an employee field already arrived in GET /v1/users/fields with its type and value list, while creating the same field via POST /v1/userfields/users answered 400 UNKNOWN_ENTITY listing the six CRM entities.

Six routes were added: GET /v1/userfields/users (list), GET /v1/userfields/users/{id} (single field), POST /v1/userfields/users (create), PATCH /v1/userfields/users/{id} (update), DELETE /v1/userfields/users/{id} (delete). The user.userfield scope is required; the bare user scope is not enough for this entity. The create body is the same as for CRM fields: fieldName, userTypeId, label and the other field properties. An employee field name carries the UF_USR_ prefix and is passed in full — the platform does not prepend it.

GET /v1/userfields/users/types answers 400 UNSUPPORTED_ACTION: Bitrix24 has no method listing employee field types, so userTypeId is stated explicitly on create. The existing behaviour of the six CRM entities on /v1/userfields/{entity} is unchanged in every response.

The same entity is now available in MCP: the manage_userfield tool accepts entity: "users".

FIX-0916-14: aggregation refusals explain the limit on reads without pagination

Before

In POST /v1/{entity}/aggregate, the AGGREGATION_LIMIT_EXCEEDED message suggested paginated exports even for entities that this aggregation path reads without pagination.

After

For these entities, the message explains the protection against unbounded reads for numeric and grouped aggregation. It suggests count without groupBy, without promising that counting reads no data. HTTP 422, the error code, and the 5000-record limit are preserved.

Impact on integrators

Only error.message changes for this case. Handle the refusal by its AGGREGATION_LIMIT_EXCEEDED code.

BC-0916-15: previousKey in the application key replacement response

Old format supported until: not provided

Before

POST /v1/cowork/applications/{id}/key returned previousKey on the rotated branch with a mandatory graceUntil date — the moment the previous key stops authenticating.

After

previousKey now carries { id, graceUntil, status }. The graceUntil field can be null, and exactly then status is BLOCKED: the previous key was blocked, it was not working before the replacement either, and no grace period was granted. A live previous key behaves as before — a date and status ACTIVE. The response remains HTTP 201.

What integrators should do

Accept null in graceUntil and print the text by status: on BLOCKED the previous key already stopped working, there is nothing to wait for, and the application needs the new secret right now. Code that reads graceUntil as an always-present date breaks on such a response.

FIX-0916-16: an application with a blocked key can be repaired by replacing it

Before

POST /v1/cowork/applications/{id}/key answered 409 KEY_ROTATE_NOT_ACTIVE when the personal key of the application was blocked. There was no way to free the slot: deletion was refused while a live server held the key, and a blocked key cannot be put back in service because the block is terminal. The only repair was re-creating the application.

After

The replacement goes through. The block is not lifted: the previous row stays blocked, it gets no one-day grace period, and the live access tokens it issued are revoked rather than moved to the new key. The refusal KEY_ROTATE_NOT_ACTIVE now means a revoked or expired key only. The replacement refusal texts have been rewritten: three of the four name the exact cabinet section and the action to take, while the platform-issued key refusal names no section — there is none in the cabinet — and tells you what to do instead: contact support. When those live tokens cannot be revoked, the replacement is not reported as success: the answer is 500 KEY_ROTATE_REVOKE_FAILED with details.orphanKeyId (the id of a key minted but handed to nobody) and no secret. One field decides what to do next. No details.strandedSlots — the slots were moved back to the previous key in full: revoke the key from orphanKeyId and retry with a NEW Idempotency-Key. The field is present — the rollback did not fully succeed and those categories still point at the orphan key: you must NOT retry and must not revoke it either — contact support. One entry there is not a slot category: previousKeyGrace means the previous key, blocked mid-replacement, kept the one-day expiry the rotation gave it and stops working at that deadline. A retry here is worse than useless: it replaces whatever the card points at NOW and can answer 201 with a working secret, while the leaked key's live access tokens are never revoked — the incident stays open behind a screen that says it is done.

Impact on integrators

The successful replacement response is unchanged apart from previousKey (a separate entry). A client that branched on 409 KEY_ROTATE_NOT_ACTIVE as "the key is blocked" must drop that branch: a blocked key is now replaceable, and this code means a revoked or expired key only.

FIX-0916-17: POST /v1/keys/{id}/rotate accepts a blocked key

Before

Rotating a blocked key answered 403 KEY_BLOCKED. A key held by a live server could not be repaired at all: the server blocked deletion and this check blocked re-issuing.

After

The rotation goes through on the same terms as the application key replacement: the block stays, the previous row gets no grace period, and its live access tokens are revoked. When that revocation cannot be confirmed, the rotation is not reported as success: the endpoint answers 500 KEY_ROTATE_REVOKE_FAILED with details.orphanKeyId (the id of a key already minted but never handed over) and no raw secret is returned in that response. When the rollback itself did not fully succeed, the answer also carries details.strandedSlots — what still points at the orphan key; on a clean rollback that field is absent. One entry there is not a slot: previousKeyGrace means the previous key kept the one-day expiry the rotation gave it (which happens when the key was blocked mid-rotation and restoring its earlier expiry failed), so it stops working at that deadline. Unblocking is still impossible — PATCH on a blocked key answers 403 KEY_BLOCKED as before.

Impact on integrators

A client that used to get only a 403 on a blocked key now gets either a 201 or a 500 KEY_ROTATE_REVOKE_FAILED. Branch on details.strandedSlots: absent means the rollback completed — revoke the orphan key from details.orphanKeyId and retry — the endpoint supports no Idempotency-Key, so a retry is a plain new request, and it attempts the revocation again. Present means you must NOT retry: part of the estate stayed on the orphan key, and a retry replaces THAT one while the leaked key's live access tokens are never revoked — the answer may even be a success, but the compromise stays. That key must not be revoked while anything still points at it either — contact support.

NEW-0916-18: department names in the members read

The GET /v1/platform/cowork/members response carries a new departments block — the Bitrix24 account's department directory, department id to name. One block per response rather than a field on every row: the tree belongs to the account, and an employee is not necessarily in it just once.

The department field on an employee row now carries a name when that name is unambiguous: the person belongs to exactly one department and it was found in the directory. For someone in two departments the field stays null — picking the "first" of them would present the result of an unpinned ordering as a fact.

A departmentIdsKnown field appears next to it — whether this employee's department membership was read at all. Without it an empty departmentIds is ambiguous: it arrives both when the person genuinely belongs to no department and when we know nothing about their departments — the row was missing from the Bitrix24 overlay, or the overlay did not arrive at all. The difference matters exactly where the list is grouped by department: with departmentIdsKnown equal to false the empty array means "unknown", and such an employee must not be filed under "no department". Resolve ambiguous cases from the row's departmentIds together with the departments dictionary, but only while departmentIdsKnown is true.

An empty object and null mean different things in the departments field. departments is {} when the company has no departments: department membership was read for every employee in the response, and none of them belongs to any department. departments is null when the names are unknown — the Bitrix24 overlay did not arrive, membership was not read for everyone, the directory could not be read, the rights were insufficient, or Bitrix24 did not answer in time. Collapsing the two answers into one value is not safe: in the first case there is nothing to show, in the second it is worth retrying later.

Existing fields are unchanged, and requests written before this entry keep working.

NEW-0916-19: model catalog: capability filter for Cowork/Code keys

GET /v1/models accepts an optional capability parameter — an explicit selection of models by one declared capability, instead of scanning the full list client-side.

The parameter is read only for a key carrying the vibe:cowork scope. For every other key it is ignored: the response stays the same for any value, unknown ones included. The set of accepted values is closed and grows together with the capabilities the platform serves for Cowork/Code.

The response without the parameter is unchanged for every key.

BC-0916-20: a whitespace-only application name is rejected on every write path

Old format supported until: not provided

Before

A name made only of spaces, tabs or line breaks (for example " ") used to pass the "at least one character" check. The application was created or renamed, and its menu item in Bitrix24 got a blank name.

After

Such a value is rejected with 400 VALIDATION_ERROR before Bitrix24 is called at all — on every request that sets the application's display name or a placement label:

A name with at least one visible character is accepted as before and stored unchanged, including leading and trailing spaces. The rule does not apply to already stored values: publishing an application that carries an old blank name works exactly as before.

What integrators should do

Send title and catalogTitle with at least one visible character. If the name is generated automatically and may come out empty, substitute a meaningful default before sending the request. Pay particular attention to rename and re-publish flows — those used to accept a blank name.

FIX-0916-21: a call to the account's previous address is now refused by name

Before

After a self-hosted Bitrix24 account moved to a new address, a key whose webhook still pointed at the previous one was refused by the origin guard — but the client got 500 with {"success":false,"error":{"code":"INTERNAL_ERROR","message":"Internal server error"}}: no cause, no next step.

After

The same call answers 409:

JSON
{"success":false,"error":{"code":"PORTAL_ADDRESS_CHANGED","message":"Portal address changed: this credential is issued for the portal's previous address. Re-issue the key webhook to continue"}}

The remedy is unchanged: re-issue the key's webhook (Keys → Re-issue). The key string, its id and its scopes stay the same.