# API changes: September 18, 2026

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

### FIX-0918-1: quote filter and sort by the not-returned contacts field answer with the platform's refusal, not a Bitrix24 error

**Before**

On quotes the `contacts` field is marked as not returned (the Bitrix24 item API never returns it, the bound contacts are in `contactIds`), yet `?filter[contacts]=…`, `?sort=contacts` and `?order[contacts]=…` were forwarded to Bitrix24 and answered `422 BITRIX_ERROR` with `Unknown field definition CONTACTS` — Bitrix24's words, whereas companies, leads, deals and invoices answer the same requests with `400 UNKNOWN_FILTER_FIELD` / `400 UNKNOWN_SORT_FIELD`.

**After**

On quotes, `filter`, `sort` and `order` by `contacts` are refused before Bitrix24 is called: `400 UNKNOWN_FILTER_FIELD` and `400 UNKNOWN_SORT_FIELD`, with the accepted names listed in the message — like the other CRM entities. Nothing else about the field changes: a write answers `400 READONLY_FIELD`, `select` answers `400 SELECT_FIELD_NOT_RETURNED`.

**Impact on integrators**

None: those requests never worked. Filter by `contactId` (the primary contact) or by `contactIds`.

**Affected endpoints:** [GET /v1/quotes](/docs/entities/quotes/list), [POST /v1/quotes/search](/docs/entities/quotes/search), [POST /v1/quotes/aggregate](/docs/entities/quotes/aggregate), [POST /v1/{entity}/batch](/docs/batch), [POST /v1/batch](/docs/batch).

### BC-0918-2: A server development-team member gets their own key, and the key block gains the collaborator slot

> Old format supported until: not provided

**Before**

Only the application owner could get an application key, and `slot` in the card's `key` block was
`auth`, `api` or `null`. Server development routes refused a team member.

**After**

A member of the server development team (an employee of the Bitrix24 account with role ADMIN or DEVELOPER) gets
their own key through `POST /v1/cowork/applications/:id/key` and can see another application's
`sources` and `activeOperation`. On the card such a key arrives with `slot: "collaborator"`; the
owner's key is never shown to them. The key works on thirteen development method/path pairs for
its own server, including deploy-lock release and short-lived `api-bearer` access tokens with a
required `ttlSeconds` of at most 600. The server icon is not among them — it belongs to the owner
and an ADMIN. The `share-url` mode is closed with `403 PORTAL_COLLABORATOR_TOKEN_MODE_FORBIDDEN`,
an invalid TTL returns `400 INVALID_TTL`, and only the caller's own token may be revoked (another
one returns `404`). The new response can carry `COLLABORATOR_KEY_REQUIRES_SERVER`,
`COLLABORATOR_MEMBERSHIP_BIND_CONFLICT`, `PORTAL_COLLABORATOR_KEY_OUT_OF_SCOPE`,
`PORTAL_COLLABORATOR_KEY_WRONG_SERVER`, `PORTAL_COLLABORATOR_NOT_A_MEMBER`, and the
`ENV_SYNC_NOT_APPLICABLE_FOR_COLLABORATOR` warning.

**What integrators should do**

If your client parses `slot` as a closed set, add `collaborator` to it or map an unknown value to
`null`. A development-team key calling a method/path pair outside the list below answers
`403 PORTAL_COLLABORATOR_KEY_OUT_OF_SCOPE`.

**Affected endpoints:** `POST /v1/cowork/applications/:id/key`,
[GET /v1/applications](/docs/applications/list),
[GET /v1/applications/:id](/docs/applications/get) and the thirteen pairs the member key works
on: [GET /v1/infra/servers/:id](/docs/infra/servers/get),
[POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy),
[POST /v1/infra/servers/:id/exec](/docs/infra/deploy/exec),
[POST /v1/infra/servers/:id/upload](/docs/infra/deploy/upload),
[GET /v1/infra/servers/:id/logs](/docs/infra/deploy/logs),
[POST /v1/infra/servers/:id/wake](/docs/infra/lifecycle/wake),
[GET /v1/infra/servers/:id/sources](/docs/source-storage/servers),
[POST /v1/infra/servers/:id/sources](/docs/source-storage/servers),
[GET /v1/infra/servers/:id/sources/:versionId](/docs/source-storage/servers),
[GET /v1/infra/servers/:id/sources/:versionId/download](/docs/source-storage/servers),
[DELETE /v1/infra/servers/:id/lock](/docs/infra/deploy/lock),
[POST /v1/infra/servers/:id/access-tokens](/docs/infra/access-tokens/create),
[DELETE /v1/infra/servers/:id/access-tokens/:tokenId](/docs/infra/access-tokens/delete).

### NEW-0918-3: An application created from Cowork no longer has to name its scopes

`b24Scopes` in [POST /v1/cowork/applications](/docs/cowork/applications-create) is now optional. Omit it and the key gets every
Bitrix24 scope this account can grant: the catalog minus the feature-flag-gated scopes, plus the
gated ones this account is allowed, minus the ones Bitrix24 refuses to store on an incoming
webhook. An explicit list is still accepted and is how you ask for LESS; an empty array is
refused as before, because a key with no Bitrix24 scopes cannot be widened later. The widening is
not silent: the response carries `B24_SCOPES_DEFAULTED_TO_ALL` in `warningCodes`, and both audit
rows — application created and key issued — now carry `scopesSource`, either `default` or `explicit`. Omitting the
field and sending the full list explicitly are different bodies: the idempotency fingerprint
hashes the body as it arrived, not the resolved default.

### BC-0918-4: The envSync field on a galaxy application key replacement returns the real delivery outcome

> Old format supported until: 17.03.2027

**Before** — for an application on the Vibecode platform whose server is a Galaxy container,
replacing the key with `syncServerEnv: true` always answered
`{ "status": "skipped", "reason": "galaxy" }`: the platform could not deliver the key into the
container, and the field only reported that.

**After** — the platform delivers the key into the container environment, and the field returns
the outcome of that delivery: `{ "status": "recreated", "variable": "..." }` (the key was
delivered and the container was recreated from the same image; `alsoBakedInImage: true` is added
when the OLD value is ALSO baked into the image and keeps answering until the app is redeployed
from source, with `bakedVariable` naming that image variable), `{ "status": "not_required" }`
(the container environment carries no platform key at all),
`{ "status": "baked_in_image", "bakedVariable": "..." }` (the key comes from the image rather
than from the run: the variable can be overridden, only a rebuild fixes it for good), `{ "status": "recreate_failed", "reason": "..." }`
(delivery failed, see `reason`), `{ "status": "unreachable" }` (the galaxy host or the
application is asleep — nothing was touched), `{ "status": "not_found" }` (the container runs on
a key generation the platform cannot account for), `{ "status": "superseded" }` (delivery was
overtaken by a newer key replacement) or `{ "status": "failed" }` (the platform could not even
work out where to deliver the key; that outcome is not galaxy-specific and is reachable on either
kind of server). The former
`{ "status": "skipped", "reason": "galaxy" }` stays reachable — it arrives when container
delivery is switched off for the account.

**What integrators should do** — handle every `envSync.status` value listed above, not just
`skipped`. A branch treating `{ "status": "skipped", "reason": "galaxy" }` as the only possible
answer for a galaxy application now receives values it does not expect.

1. Treat the key as delivered ONLY on `recreated`. On `recreate_failed`, `unreachable`,
   `baked_in_image`, `failed` and `skipped` the container still runs the previous key; on
   `not_found` the platform cannot tell which key it runs at all — the environment holds a
   generation it never issued.
   In every one of those cases write THIS replacement's key in by hand, and do it before
   `previousKey.graceUntil` from the same response. That deadline is not always a day and can
   be missing: `null` means there is no grace at all (the previous key is blocked) and access
   is already gone.
2. `superseded` is a separate case: this replacement's key has already been overtaken by a newer
   one, so writing it into the container is WRONG — the application would end up on a stale key.
   Follow the `envSync` outcome of that NEWER replacement and use ITS raw key: the secret is handed
   out once, in the response to the replacement itself, and nothing can recover it afterwards — the
   application card carries metadata only. If you no longer have that response, make one more
   replacement with a NEW `Idempotency-Key` and work from its answer.
3. On `recreated` with `alsoBakedInImage: true` and on `baked_in_image`, also rebuild the
   application from source: the old value is baked into the image (named in `bakedVariable`),
   and the image hands it back on every deploy that skips a rebuild. Writing the variable by
   hand, as step 1 says, does work — it overrides the image value — but the override lives only
   until the next deploy: the rebuild is the permanent fix, the manual write only covers the
   `previousKey.graceUntil` window.
4. Treat an unknown `status` as an undelivered key rather than a success: the set of outcomes
   will grow.

The key replacement itself still succeeds regardless of the `envSync` outcome, and recreating
the container restarts the application and drops its open connections for the duration of the
restart. The former `{ "status": "skipped", "reason": "galaxy" }` stays reachable during the
support window too — it arrives when container delivery is switched off for the account.

**Affected endpoints:** `POST /v1/cowork/applications/{id}/key`

### FIX-0918-5: the icon upload tells a permission refusal apart from a missing server

**Before**

`POST /v1/infra/servers/:id/icon` answered `404 SERVER_NOT_FOUND` even when the server does exist in the account but is bound to a different API key. A permission refusal was indistinguishable from a typo in `id` or a deleted server: a key that passes `GET /v1/infra/servers/:id`, deploy and source deposits was told "no such server" on the icon — with no statement of what was wrong or how to fix it.

**After**

The icon answers with the same code as the rest of the control plane: the server exists in your account but another key manages it — `403 WRONG_KEY`, and the body carries `error.hint` with the recovery order (rebind the server in your account, then send the new key in the `X-Api-Key` header). `404 SERVER_NOT_FOUND` stays on exactly its previous grounds: no server with that `id`, it was deleted, or it belongs to another account. Upload rights did not change — the icon still takes the managing key, because it rides on the application's catalog card. Details — [App icon](/docs/infra/app-icon) and [Server access recovery](/docs/infra/server-access-recovery).

### FIX-0918-6: an application's external API now reaches the application instead of answering "not reachable"

**Before**

The `ANY /v1/applications/{id}/api/*` channel answered `503` with code `APP_API_UNAVAILABLE`
and the message "Application is not reachable" on every call that had passed the admission
checks — even when the application was running and opened fine from the cabinet and through an
external-access link. The refusal came back instantly, without an `X-Vibe-Request-Id` header,
and did not depend on where the application was hosted. The "Check schema" button on the
application card, which uses the same channel, reported that the application server was
unavailable.

**After**

The call reaches the application: its answer is returned as is — its own HTTP status, headers
and body are preserved, and a successful call stays successful. A transient `503`
`APP_API_UNAVAILABLE` with `Retry-After` now means exactly what the channel documents — the
application really is not answering, rather than the channel being broken altogether.

### FIX-0918-7: a function-call turn no longer duplicates the reasoning into content

**Before**

When a reasoning model called a function, the non-streaming [POST /v1/chat/completions](/docs/ai/chat/completions) response came with `finish_reason: "tool_calls"`, the reasoning in `reasoning_content` and the same chain in `content` as well: the client received the model's reasoning as the assistant's message. In streaming the reasoning did not reach `content`.

**After**

On a function-call turn `content` is `null` and the reasoning arrives in `reasoning_content` only, as in the [function calling example](/docs/ai/chat/tools). Responses without reasoning and responses with a final text are unchanged.

**Affected endpoints:** [POST /v1/chat/completions](/docs/ai/chat/completions).

### NEW-0918-8: company-parked seats in the Cowork members method

The `GET /v1/platform/cowork/members` response now carries a new `parking` field in the `seats`
block — the number of paid seats parked with the company. A seat lands there when an employee
leaves the Bitrix24 account: it stays with the company, its term keeps running, and an administrator can
hand it to another employee.

Such seats count in neither `assigned` nor `expiringWithin30Days`. A seat whose term has already
ended is not counted: it can no longer be handed to another employee. For an unknown Bitrix24 account the
field comes as zero.

### FIX-0918-9: employees who left no longer appear in the Cowork members list

**Before**

The `data` of the `GET /v1/platform/cowork/members` response also carried people who had left the
Bitrix24 account while a paid seat stayed assigned to them. Such a person looked like an employee without a
plan (`plan.status: NONE`), matched the `NO_PLAN` filter, and was counted in `portal.usersTotal`
and `portal.usersInVibecode`.

**After**

People who left do not appear in `data` and are not counted in `usersTotal` / `usersInVibecode`.
Their seats are not lost: a seat parked with the company is counted in the `seats.parking` field.

### FIX-0918-10: an account whose plan was never asked about no longer gets a "paid plan required" refusal

**Before**

The `403 PORTAL_TARIFF_UNREADABLE` refusal ("the plan could not be read") was only issued when the last licence probe had recorded a failed outcome. An account with no plan code AND no probe outcome at all — never probed, or probed before that mark existed — fell into the previous branch and got a `402` advising to buy a commercial Bitrix24 plan. That advice rested on an empty plan code, and the code is equally empty for an account whose licence the platform never asked about.

**After**

Both "could not ask" states are now told apart from "the account is on a free plan" in the same way: no plan code AND the probe either failed or was never recorded answers `403` with the code `PORTAL_TARIFF_UNREADABLE`, `details.requiredTariffs` is empty, and `details.upgradeUrl` and `alternatives[0].url` point to support. An account whose licence WAS read successfully keeps its behaviour: an empty code after a successful probe still means a free plan and still answers `402` with the previous copy.

### FIX-0918-11: an access refusal now tells an unread subscription from a missing one

**Before**

An account whose subscription state the Vibecode platform could not read was refused with `402` and the code `MARKETPLACE_REQUIRED` on server creation and on key issuance, and was offered to buy a subscription. That advice could be wrong: the subscription state only arrives in the `market` envelope of the licence probe, which runs under a developer key of an account member — Bitrix24 refuses a key with a narrow rights set, and an account with a live subscription then looked exactly like an account without one.

**After**

The two states are now told apart. When the subscription state could not be read, the answer is `403` with the code `PORTAL_SUBSCRIPTION_UNREADABLE`: `details.requiredTariffs` is empty — no purchase clears this refusal — while `details.upgradeUrl` and `alternatives[0].url` point to support. The `userMessage` field names an action that can actually be taken: reconnect the application under the account main administrator so the developer key gets the rights to read the licence. The `MARKETPLACE_REQUIRED` code keeps its own state — the subscription was read and there is none: it still answers `402` with the previous copy. The change grants access to nobody: a refusal stays a refusal, and only the code, the status and the suggested action change.

### NEW-0918-12: a server can be created already in the run mode you need

`POST /v1/infra/servers` accepts an optional `runMode` block of the same shape as
`PATCH /v1/infra/servers/{id}/run-mode`: `{"mode": "ALWAYS"}`, `{"mode": "IDLE", "idleMinutes": 60}`
or `{"mode": "SCHEDULE", "scheduleId": "..."}`. The machine is born in that mode — "create, then
switch" cost an extra call, and the machine hours of default running that were already billed.

The refusal comes before the machine is created, so a failed request leaves nothing behind:
`WORK_SCHEDULE_NOT_FOUND` — no schedule with that id in the account, `WORK_SCHEDULE_EMPTY` — the
schedule has no windows, `RUN_MODE_UNAVAILABLE` — run modes are not enabled for the account yet. The
body is discriminated on `mode`, so a field that does not belong to the chosen mode is rejected
rather than silently dropped. A request without the `runMode` block behaves exactly as before.

### NEW-0918-13: storage and key-count ceilings for an account without a commercial plan

A Bitrix24 account without a commercial plan now has two optional ceilings the platform can switch on: on stored volume and on the number of live account keys. An account on a commercial plan — and any account that ever was on one — gets no ceilings.

A storage write that would take the account past the volume ceiling answers `507` with code `STORAGE_QUOTA_EXCEEDED`. Issuing a key past the key-count ceiling answers `409` with the already existing code `KEY_LIMIT_REACHED`; the `details` object of that response gains a `scope` field set to `portal`, which is what tells the account-wide ceiling apart from the per-employee limit, whose response carries no `scope`. The field is additive: a client that does not read it sees no difference. Requests below the ceiling answer exactly as before.

### NEW-0918-14: a payment carrying a Cowork seat composition now creates seat rights

The composition assembled by the buyer in the Bitrix24 checkout is now applied: the
`metadata.application` block of `payment.paid` turns into paid seat rights, and `rights[]` in
`GET /v1/platform/cowork/members` fills up with plan-and-term pairs. A seat addressed to a
specific employee is reserved for them.

The composition is applied once per order — keyed by `sale_order_id`, at the moment every
receipt of the purchase has arrived.

Composition intake is switched on per Bitrix24 account. Until handing a right to an employee from the
company console ships, it is open only on Bitrix24 accounts agreed for testing. On the rest such a payment
is credited as a plain vibes top-up, and the composition of that payment is not applied.

Worth building into the client: after a Cowork purchase the Bitrix24 account balance does not grow by the
full payment amount. Paid seats are fenced off from the shared wallet right away, so they cannot
be spent on anything else. If the tokens did not cover the whole composition, as many rights are
created as the money covers and the remainder stays on the balance as vibes — in this order:
`seats[]` top to bottom first, then `unassigned[]`.
