# API changes: August 6, 2026

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

### NEW-0806-1: 402 for an exhausted Cowork/Code quota carries a Retry-After header

The `402` response with code `cowork_quota_exhausted` on [POST /v1/chat/completions](/docs/ai/chat/completions) now carries a `Retry-After` header — the number of seconds until the exhausted quota window (`5h`, `week` or `month`) resets. Previously the reset moment was visible only in the `resetAt` field of the response body; the header is understood by plain HTTP clients without parsing the body. The `402` response with code `insufficient_balance` does not carry the header — an empty balance has no reset time.

### FIX-0806-2: Web search: the response status distinguishes a provider key rejection from a provider failure

**Before**

Every search provider error on [POST /v1/search](/docs/search/run) arrived as `502 UPSTREAM_ERROR` — a provider rejecting the key (`401`/`403`), provider throttling (`429`), and a genuine failure looked the same. Clients retried requests that could never succeed.

**After**

For a BYOK key, provider `401` and `403` responses keep their status — the provider rejected your key, replace it. A provider `429` keeps its status for any key and carries the `Retry-After` header. The error code stays `UPSTREAM_ERROR` in every case, and the body additionally carries the `upstream_status` field with the provider's original status. Every other provider error, including a rejected platform-engine key, still arrives as `502`. Additionally, the `message` field of the `400 INVALID_REQUEST` response is now length-capped — the received value is no longer echoed in full.

**Impact on integrators**

A handler that retried on any `5xx` keeps working. If you branched on `502` as "any provider error", add branches for `401`/`403` (replace the BYOK key) and `429` (retry per `Retry-After`); the reliable "this is a provider error, not an authorization error" signal is the `upstream_status` field in the body.

### FIX-0806-3: the operation registry in GET /v1/guide now lists what actually works

**Before**

An entity's operation list in [GET /v1/guide](/docs/keys-auth/guide) disagreed with the
set of working endpoints in both directions.

It stayed silent about working operations. No entity declared `fields`, although
`GET /v1/{entity}/fields` answers for 46 of 49 entities. Bookings were missing
`list` and `search`, although `GET /v1/bookings` and `POST /v1/bookings/search` are
served by dedicated handlers — a robot read the entity as write-only and had no way
to obtain an identifier. Open-channel configs were missing `list`, `search`,
`create`, `update` and `delete`, leaving only `getById`, `aggregate` and `batch` in
the registry. The same mechanism hid `create` for document templates, `list` and
`create` for addresses, `search` for org-structure nodes and `delete` for a user,
while the address search was described with the generic windowed-search contract
its handler does not implement.

And it promised what an entity does not have: the batch example named the `create`
action for eight entities, where that action answers `400 ACTION_NOT_SUPPORTED`.

**After**

An operation is declared exactly when its route is really registered. Added:
`fields` for the entities that serve that route, `list` and `search` for bookings
(with the mandatory `dateFrom` and `dateTo` parameters stated in the description),
the full `list` / `search` / `create` / `update` / `delete` set for open-channel
configs, `create` for document templates, `list` / `search` / `create` for
addresses, `search` for org-structure nodes and `delete` for a user. The
descriptions of these operations list the parameters their own handler reads rather
than the generic search contract: windowed search (`autoWindow`, `windowCount`)
does not exist for them.

The batch example names an action the entity accepts, and the key that action
reads: `ids` for delete, `items` for other writes, `calls` for reads. An entity
with no available write operation gets a read example.

The note on such an entity no longer states the read set as one fixed sentence
("accepted: `list`, `get`, `fields`"): it names the actions that really answer with data
for THIS entity, and separately what the envelope does with the rest. There are three
reasons an action stays blind: on an entity with no field-schema method `fields` answers an
empty object (and the note points at `GET /v1/{entity}/fields` when that route exists); on
an entity with no addressed read method `get` answers the collection's first record rather
than the one asked for; on a REST 3.0 entity the batch sub-call does not reach the method at
all and comes back as a per-call error inside the `200`; and on the open-channel config list
the sub-call reaches Bitrix24 without the envelope `imopenlines.config.list.get` requires,
answering `200` with the whole collection while silently dropping the filter.

The same statement is corrected in the two other places a client sees it: the
`400 ACTION_NOT_SUPPORTED` body no longer ends in "Supported batch actions: list, get,
fields" (it now carries the same computed set — previously a client that read the honest
registry and then tripped the refusal got the misleading list back), and the OpenAPI
description of the open-channel config batch read now says which of the accepted actions
answer with data.

The same descriptor's `select` description is corrected too: for the open-channel config
list the comma-separated form (`?select=id,name`) IS read — only the indexed form
(`?select[0]=id`) is not.

An entity that does not have an operation still does not get one: `fields` did not
appear for task comments, for calendar sections or for mail mailboxes. What should not be declared stays
undeclared too: routes that exist only to refuse a call and point at the correct
path, and `aggregate` on an entity where only counting works — the search
description still names that path.

**Impact on integrators**

The change is additive: existing `operations` fields are neither renamed nor
removed. A client that built its list of available calls from this registry now
sees operations it previously had to guess or look up in the documentation. A
client that copied the batch example verbatim will stop receiving
`400 ACTION_NOT_SUPPORTED` on read-only entities.

### FIX-0806-4: the task service fields are accepted on writes — exactly as Bitrix24 itself accepts them

A task carries seven service fields: the creator (`createdBy`), who changed it (`changedBy`), who closed it (`closedBy`), who changed its status (`statusChangedBy`), and the creation, change and closing dates (`createdDate`, `changedDate`, `closedDate`). Bitrix24 accepts and stores all of them — both when a task is created and when it is updated. Vibecode refused six of the seven, which made it stricter than the platform for no gain.

**Before**

[POST /v1/tasks](/docs/entities/tasks/create) and [PATCH /v1/tasks/:id](/docs/entities/tasks/update) answered `400 READONLY_FIELD` for `changedBy`, `closedBy`, `statusChangedBy`, `createdDate`, `changedDate`, `closedDate` and never reached Bitrix24. The upper-case spellings were refused the same way — `CHANGED_BY` and the rest. The refusal also came from a [POST /v1/batch](/docs/batch) sub-call. The seventh field, `createdBy`, worked on creation and was refused on update.

**After**

All seven are accepted on both operations and on all three write surfaces — the single route, the entity batch request and the global batch request. Both spellings, `createdBy` and `CREATED_BY`, are accepted. The value is applied within the permissions of the calling user: when Bitrix24 refuses to edit the task, the refusal arrives as it is — a `422` carrying its own text, with no substitution by an error of ours and no false success.

A change of the creator is written to the task change log, and the real calling user stays visible there. For the other six fields no log entry exists. One more subtlety — for the three dates, a value without a timezone gets the offset from the `X-Vibe-Timezone` header when sent as `createdDate`, while as `CREATED_DATE` it goes through unchanged. Both subtleties are covered on the [PATCH /v1/tasks/:id](/docs/entities/tasks/update) page.

What did NOT change: `id` is still refused — Bitrix24 assigns the identifier itself and ignores a submitted value, so an explicit refusal is more honest than a silent loss. `dateStart`, `activityDate` and `realStatus` stay closed as well, but for a different reason: their behavior on write was not verified, and we will not declare a field open without verifying it.

Pass an existing employee only — in all four fields that carry a user id. Bitrix24 does not check the value for existence and will store any number, and a task whose creator does not exist stops being manageable through the API: a further update and a deletion are both refused, even for an administrator key and even directly in Bitrix24, bypassing us. The warning is on the task-update page.

**Impact on integrators**

Nothing to change: requests that used to be refused now go through. If your code treated `400 READONLY_FIELD` as protection for authorship and history, it never played that role — Bitrix24 itself accepts the same values through its own interface, bypassing our layer. Only the Bitrix24 permission model can restrict overwriting the service fields — that is a separate change on the platform side. For leads and deals the author field stays closed, which has not changed: Bitrix24 silently ignores the value there, so an explicit refusal remains the honest answer.

### FIX-0806-5: a catalog application card opens its subpath instead of the server root

**Before**

An application card in the Bitrix24 catalog always opened the root of its Black Hole server. An application serving its interface from a subdirectory could not be opened from the catalog at all: the click answered HTTP 200 and rendered whatever lives at the root of the same server. The application address (`appUrl`) had no effect on this, and editing it through [PATCH /v1/apps/:id](/docs/apps/update) never reached the card.

**After**

The card now opens the full address of the linked application — subpath, query and fragment included — whenever that address points at the same Black Hole subdomain as the card's server. Everything else keeps the previous server root: a different subdomain, a custom domain, a different scheme, an empty or unparseable address.

Editing `appUrl` through [PATCH /v1/apps/:id](/docs/apps/update) now queues the card for an update, so the new address reaches Bitrix24 on its own. Already-published cards are reconciled platform-side — no integrator action required.

**Integrator impact**

Nothing to change. An application serving its interface from the root behaves exactly as before. An application in a subdirectory no longer needs a manual workaround — it is enough for `appUrl` to carry the subpath.

### FIX-0806-6: speech recognition now reports a temporary provider pause

**Before**

When the cluster was temporarily unavailable, [POST /v1/audio/transcriptions](/docs/ai/audio/transcriptions) could respond with `502 ai_provider_unavailable` without telling the client how long to wait before retrying.

**After**

In this state, the endpoint responds with `429 ai_provider_cooldown` and a `Retry-After` header in seconds. The request is not executed and consumes neither quota nor money. Wait for the stated interval and retry the same request.

### FIX-0806-7: speech recognition now really waits the stated 15 minutes

**Before**

For a long recording, [POST /v1/audio/transcriptions](/docs/ai/audio/transcriptions) could answer `503 ai_provider_timeout` after about 5 minutes, even though the endpoint documents a wait of up to 15 minutes. The error text described a network-layer timeout.

**After**

The endpoint waits the full stated period — up to 15 minutes — and answers `503 ai_provider_timeout` with a `Retry-After` header only once it expires. The limit on recording length is unchanged: for files longer than ~30 minutes, keep splitting the recording into parts.

### FIX-0806-8: galaxy app exec now targets the container by its real name

**Before**

`POST /v1/infra/servers/{id}/exec` on a galaxy app always addressed the container by its `subdomain`. An app restored from a clone runs under a different name, so the command targeted a container that does not exist — the call failed, and in the worst case it could reach a leftover container from an earlier deployment. The rest of the galaxy lifecycle (deploy, migrate, stop) already accounted for the rename; only exec did not.

**After**

The container name is resolved by one shared rule for every operation: the on-host name when the app was renamed, otherwise `subdomain`. The resolved name is validated before it is interpolated into the command; an unusable name returns `409 GALAXY_APP_NOT_READY` with `invalid on-host name` instead of running anything. Apps that were never restored from a clone are unaffected.

### BC-0806-9: V1: server status in JSON is always lowercase

> Old format supported until: 06.09.2026

**Before**

[`GET /v1/infra/servers`](/docs/infra/servers/list) and [`GET /v1/infra/servers/:id`](/docs/infra/servers/get) returned a lowercase `status` (`running`, `sleeping`), while [`POST /v1/infra/servers/:id/wake`](/docs/infra/lifecycle/wake), [`POST /v1/infra/servers/:id/refresh`](/docs/infra/lifecycle/refresh), the `currentState.status` field on 422 responses of [`POST /v1/infra/servers/:id/start`](/docs/infra/lifecycle/start), [`POST /v1/infra/servers/:id/stop`](/docs/infra/lifecycle/stop), [`POST /v1/infra/servers/:id/reboot`](/docs/infra/lifecycle/reboot), and `infra.unhealthyServers[].status` in `GET /v1/me` exposed the stored enum value in UPPERCASE (`RUNNING`, `SLEEPING`, `PROVISIONING`). A client that learned `status === 'running'` from the docs and `GET` broke on the wake and refresh responses.

**After**

Every listed field of the public V1 JSON carries a lowercase server status: `provisioning`, `running`, `stopped`, `sleeping`, `error`, `deleted`. The refresh `data` field is still a **string**, not an object: compare `data === 'running'`, not `data.status`. The `blackholeStatus` field is unchanged — it stays UPPERCASE (`CONNECTED`, `DISCONNECTED`, `NONE`).

**Impact on integrators**

Replace equality checks against `'RUNNING'` / `'SLEEPING'` / `'PROVISIONING'` and the other uppercase values with lowercase ones, or compare case-insensitively. Read the status from the structured fields (`data`, `currentState.status`) rather than from `message` / `userMessage` prose, where an uppercase status may still appear.

### NEW-0806-10: deploying a galaxy app from a link or a saved version

Previously a galaxy app accepted an inline archive only: `source.url` and `source.versionId` were rejected with `400 GALAXY_DEPLOY_CONTENT_ONLY`. [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy) now accepts both forms where the platform has enabled link deploys for your Bitrix24 account; where it has not, `GALAXY_DEPLOY_CONTENT_ONLY` is returned as before, and the inline `source.content` keeps working in all cases.

A link is downloaded by the host itself, so the archive never travels through the request body: the inline size limit (`413 GALAXY_UPLOAD_TOO_LARGE`) does not apply to this path, and it does not consume a concurrent large-request slot (`429 DEPLOY_BACKEND_BUSY`). `source.versionId` deploys a version already held in [source storage](/docs/source-storage): the platform mints the signed link itself and links that version to this deploy, so the history shows exactly what went to production.

On a galaxy app the link points at source storage only — the signed link of a saved version qualifies. Any other address is refused with `400 GALAXY_SOURCE_URL_NOT_ALLOWED`, so a large archive is first saved as a version and then deployed by `source.versionId`. A separate virtual machine has no such restriction.

Creating a server with a source ([POST /v1/infra/servers](/docs/infra/servers/create)) accepts `source.url` on the same terms. `source.versionId` is not accepted there: at create time there is no server yet whose storage would scope the version lookup — deploy a saved version as a second step, via `/deploy`.

The dashboard routes keep accepting the inline archive only.

### FIX-0806-11: the smart-process items field reference no longer shows the contacts field

**Before**

[GET /v1/items/:entityTypeId/fields](/docs/entities/items/fields) listed a `contacts` field of type `crm_contact`. No value for it ever arrived, either in the list or in the item card, and it could not be written: Bitrix24 accepted an empty array only and rejected any non-empty value with an error from its own internal data layer. The field reached the reference through the passthrough of the Bitrix24 schema; the platform never declared it.

**After**

The field is gone from the reference. Linked contacts are read and written through `contactId` and `contactIds`, which are unchanged. Filtering and sorting by `contacts` are still refused with `UNKNOWN_FILTER_FIELD` and `UNKNOWN_SORT_FIELD`.

**Impact on integrators**

No action needed: the field never had a value, so a client reading it always got nothing. If you generated a data model from the reference, drop `contacts` from it and rely on `contactIds`.

### FIX-0806-12: the pages field reference now says which fields can be empty

**Before**

The [GET /v1/pages/fields](/docs/entities/pages/fields) response gave no way to tell a field that always has a value from a field that arrives as `null`. Two descriptions also promised something other than what arrives: `datePublic` was described as "arrives as an empty object", and `dateCreate`, `dateModify` and `datePublic` as dates in a fixed template. A client that wrote its parsing against those descriptions tripped over an empty value, and a date filter in the wrong format returned an empty list with code 200.

**After**

Nine fields Bitrix24 does not always fill are marked with a `nullable` flag: `description`, `xmlId`, `tplId`, `tplCode`, `folderId`, `searchContent`, `initiatorAppCode`, `rule`, `datePublic`. The set was measured over the whole page collection of a live account rather than derived from the Bitrix24 method reference.

`datePublic` is described honestly: the wrapper returns `null`, and that is the usual value even for a published page, so read `active` or `public` to tell whether a page is published. The descriptions of `dateCreate`, `dateModify` and `datePublic` no longer promise a fixed template: the value is a string in the account locale format, identical in the list and in the card. The filter needs the same format: a value in another locale's format, or in ISO, is not recognized by Bitrix24 and returns an empty list with code 200.

The same fields are marked in the generated OpenAPI schema, where the type is now written as `["string", "null"]`, so a client validating the response against the schema no longer fails on an empty value. The write contract is untouched.

**Impact on integrators**

No action needed: the field set, the types and the values are unchanged — only the `nullable` flag was added and the descriptions were made accurate. If you were telling whether a page is published by the presence of `datePublic`, switch to `active` or `public`.

### NEW-0806-13: the key-limit refusal now states the numbers

On a `KEY_LIMIT_REACHED` (409) refusal, `POST /v1/apps` now puts the quota state
into `error.details`: `limit` — how many keys per person the Bitrix24 account
administrator allows, `used` — how many are taken right now.

**Before**

```json
{
  "success": false,
  "error": { "code": "KEY_LIMIT_REACHED", "message": "Maximum number of API keys reached" }
}
```

**After**

```json
{
  "success": false,
  "error": {
    "code": "KEY_LIMIT_REACHED",
    "message": "Maximum number of API keys reached",
    "details": { "limit": 10, "used": 10 }
  }
}
```

The field is additive — clients reading only `code` see no change. `used` also
counts keys the platform issued itself (apps, agents, bots), so it can exceed the
length of the `GET /v1/keys` list.

### NEW-0806-14: the site and employee field maps now carry labels, descriptions and value lists

[GET /v1/sites/fields](/docs/entities/sites/fields) now returns a `label` and a `description` on all 22 fields — previously only the `type` field had them, and the other 21 arrived with nothing but a type and a read-only flag. The descriptions say what the type cannot: that `active` is not settable through the API, that `code` is stored in a slash-wrapped form, that `landingIdIndex`/`landingId404`/`landingId503` are settable on update only, and that `dateCreate` and `dateModify` arrive as a string in the Bitrix24 account locale format rather than ISO 8601.

[GET /v1/users/fields](/docs/entities/users/fields) gained an `enum` of allowed values on gender (`personalGender`: `M`, `F`) and on account type (`userType`: `employee`, `extranet`, `email`), each value with its own `label`. Labels and descriptions also appeared on ten work-details fields that had no label in Bitrix24 at all, where the field name used to arrive in place of one: `WORK_FAX`, `WORK_PAGER`, `WORK_STREET`, `WORK_MAILBOX`, `WORK_STATE`, `WORK_ZIP`, `WORK_COUNTRY`, `WORK_PROFILE`, `WORK_LOGO`, `WORK_NOTES`. When the Bitrix24 account labels such a field itself, its own label is kept unchanged.

Gender also gained a `nullable: true` flag, and in the OpenAPI schema the property type is now declared as `["string", "null"]`. An unfilled gender comes back empty (`null`) — 48 of 50 employees on the measured Bitrix24 account answered that way — while the schema without the flag promised a string and nothing but a string, so a client validating the response against our own published schema failed on almost every record. The flag describes the read only: in the request-body schema the field type is still a plain string.

The value lists also arrive on the two other machine-readable surfaces — [GET /v1/guide](/docs/keys-auth/guide) (the entity's `fieldsDetailed` block) and the OpenAPI schema (`x-enumValues` on the property). Labels and descriptions of declared schema fields are carried by OpenAPI alone (`title` and `description` on the property), so a schema-generated client picks them up with no extra calls; the guide does not carry labels, by design. Labels and descriptions of the ten work-details fields arrive in the field map itself only.

The change is additive: responses gained new keys while the field set and the values stay the same, so existing integrations keep working untouched.

### FIX-0806-15: product sections: the sort field is marked read-only and not-returned

**Before**

`GET /v1/product-sections/fields` advertised `sort` as writable, [POST /v1/product-sections](/docs/entities/product-sections/create) and [PATCH /v1/product-sections/:id](/docs/entities/product-sections/update) accepted it without an error, and Bitrix24 did not save the value. Meanwhile no read response — the card, the list, search, the create echo — carried the field, even when it was requested explicitly in `select`. The client got a success and went on believing the order had been set.

**After**

The field is marked read-only and `notReturned: true`. Sending `sort` in a create or update body is refused with `400 READONLY_FIELD` before the Bitrix24 call; the field stays visible in the field map together with a description of the reason, so it can be read on the spot. Ordering by it works as before: `?sort=sort&order=asc` and `order=desc` give a different order. Filtering by `sort` is still refused with `400 UNSUPPORTED_FILTER`.

**Impact on integrators**

Remove `sort` from product-section create and update bodies — otherwise the whole request now gets `400 READONLY_FIELD` instead of the former success. If your code read `sort` out of a response, it was never there: the value came back `undefined`. Change the order of sections in the Bitrix24 interface, and read the order by sorting the list on that field. There is no parallel support for the previous behaviour: the previous behaviour was that the value was silently lost, so there is nothing to keep.

### FIX-0806-16: the active field of a site is read-only now — activation goes through publishing

**Before**

`GET /v1/sites/fields` described `active` as an ordinary writable field, and a request carrying it went through: [POST /v1/sites](/docs/entities/sites/create) and [PATCH /v1/sites/:id](/docs/entities/sites/update) answered with a success. The value was dropped. Bitrix24 accepts `ACTIVE` neither in `landing.site.add` nor in `landing.site.update` — their contract does not declare the field, and a new site is always created inactive. A live round-trip on both verbs confirmed the loss: a create with `active: true` and an update to `active: true` both answered with a success while the flag stayed off.

**After**

`active` is marked `readonly`. Passing it in a create or update body is refused with `400 READONLY_FIELD` before the Bitrix24 call. The field stays in the `list`/`get` response and in the `/fields` directory — reading it is unchanged, filtering and grouping by it included.

A site is activated by publishing it in the Bitrix24 account interface.

**Impact on integrators**

If your code passed `active` in a site create or update body, drop it. The value was never stored anyway, but now the whole request is refused, so the rest of the body is not applied either — title, code, domain, description. There is no parallel support for the previous behaviour: the previous behaviour was that the value was silently lost, so there is nothing to keep.

### FIX-0806-17: base image registry unavailable during a galaxy app build is now transient, not fatal

**Before**

When the public image registry was unreachable at build time, `POST /v1/infra/servers/:id/deploy`
returned 502 with a raw Docker message and the app was marked broken — the retry had to be issued
by hand and the response carried no hint.

**After**

The response carries the `GALAXY_BASE_IMAGE_UNAVAILABLE` code, `retryable: true` and a `hint`
telling you to re-send the same request in 2-3 minutes. The slot is not marked broken, so the retry
lands on it. For agents the platform retries on its own. One-shot create-with-source
(`POST /v1/infra/servers` with `source`) no longer holds the HTTP response, so there the app is
still marked broken — but the error text names the cause and asks you to re-deploy.

### FIX-0806-18: issuance refusals now name a service problem instead of an access one

**Before**

When Bitrix24 refused key issuance or app installation, the answer was chosen from a stale local snapshot of the account's marketplace state. An account whose entitlement was in force could still receive an access-paywall answer with a purchase call to action — while nothing was actually missing on the account side.

**After**

While the account's marketplace entitlement is in force, an issuance refusal is returned as `502 CONNECTOR_REST_UNAVAILABLE` with a "try again / contact support" message and no purchase call to action. The response carries `error.details.reason` (the original refusal reason) and `error.details.retryable: true`. When the entitlement really is missing, the access answer is unchanged. Affects `POST /v1/apps` and the matching in-product routes for key issuance and app creation. Additionally, a "REST unavailable" refusal is now retried once automatically, which clears the race right after an entitlement is granted.

### FIX-0806-19: the platform now passes the application its port in the PORT variable

**Before**

The platform agreed with itself about the application port on three levels — public traffic forwarding, the image `EXPOSE`, and the healthcheck — but never told the application. An application written to the common cloud-platform convention (`listen(process.env.PORT)`) read an empty value, bound a random free port, and nothing listened on the expected one: the gateway served "application not found" even though [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy) reported success.

**After**

The platform passes the port number in the `PORT` environment variable — always equal to the request's `port` field (`3000` by default). On a dedicated virtual machine this is a separate platform file `.vibe-platform.env` that the systemd unit loads **after** your `.env`; your `.env` is neither read nor rewritten. In a galaxy application `PORT` arrives in the container environment at start. The response gained a `platform_env` step.

The `PORT` key is now reserved by the platform: if you pass your own `env.PORT` that differs from the `port` field, the platform overrides it and says so with a line in the response `warnings[]`. Passing a matching value is fine — there will be no warning.

**Impact on integrators**

In the normal case there is nothing to change: an application listening on `process.env.PORT` now works without an explicit `env.PORT`, and already running applications receive `PORT` on their next deploy. One exception is worth checking: if you kept something other than your own listen port in `env.PORT` (a database port, an upstream service port), rename that variable — `PORT` now belongs to the platform and your value no longer reaches the application. The deploy response carries a `warnings[]` entry when that happens. If you read the `.env` file directly instead of the process environment, read `process.env.PORT` — the platform value lives in a separate file. With your own systemd unit (`systemd: false`) the platform file is written but YOU load it — until you do, your `env.PORT` keeps winning, and the response warning says exactly that; [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy) describes how to load it.

### BC-0806-20: quota consumption is reported as percentages only

> Old format supported until: 06.02.2027

**Before**

[GET /v1/ai/quota](/docs/ai/consumption/quota) returned absolute consumption counters in `data.byModel[]` — `tokensIn`, `tokensOut` and `audioSeconds` — alongside the `pctOfLimit` share.

```json
{ "modelId": "bitrix/bitrixgpt-5.5", "calls": 1240, "tokensIn": 800000, "tokensOut": 350000, "audioSeconds": 0, "pctOfLimit": 1.2 }
```

**After**

The three fields are gone. Quota consumption — like the limit itself — is exposed only as a relative value: `pctOfLimit` (the share of the monthly limit consumed by the model) and `calls` (the call count).

```json
{ "modelId": "bitrix/bitrixgpt-5.5", "calls": 1240, "pctOfLimit": 1.2 }
```

**What integrators should do**

There is one authoritative figure for the account — `data.pctUsed`: it is computed from the charge ledger and accounts for the time-of-use discount. The `byModel[].pctOfLimit` breakdown shows WHERE the quota went and is computed by repricing the call journal at today's prices, so there is no need to add the per-model shares up and compare the sum with `pctUsed` — the two will differ. If you need token counts for your own accounting, take them from [GET /v1/ai/usage](/docs/ai/consumption/usage) or capture them at call time — the [POST /v1/chat/completions](/docs/ai/chat/completions) response still carries the `usage` block with `prompt_tokens` and `completion_tokens`.

### FIX-0806-21: an exhausted Cowork/Code quota is no longer served by a substitute model

**Before**

When the Cowork/Code quota ran out, a request from a desktop key was served by the reserve model with the request's `tools` and system prompt passed through unchanged. The model answered fluently and could report work it had not done. The response carried HTTP 200 and `X-Cowork-Fallback: true`.

**After**

One behaviour for every key: `tools`, `tool_choice` and `response_format` are stripped, and the model states that the limit is reached and when it resets. HTTP 200 when a reserve model is configured, otherwise 402 `cowork_quota_exhausted` as before. The `X-Cowork-Fallback: true` header and the `COWORK_QUOTA_FALLBACK` warning remain, but now mean "the limit was announced", not "the request was served by another model".

**Impact on integrators**

Do not expect `tool_calls` or a structured response on an exhausted quota: `response_format` is stripped, so you get prose, not JSON. Detect the state via the `X-Cowork-Fallback` header, the `COWORK_QUOTA_FALLBACK` warning, or the 402.
Separately, the monthly reset date on the free plan is fixed. `resetAt.month` (`GET /v1/cowork/me`), `windows.month.resetAt` and `subscription.currentPeriodEnd` (`GET /v1/cowork/state`) used to return the date stored on the subscription row, and on a free seat that date stopped moving once the period ended — so it arrived in the past and the countdown read "less than a minute" forever. All three now return the period the seat is actually in: the monthly counter resets on the first request after the period ends. In the 402 `cowork_quota_exhausted` body, the month window's `resetAt` is no longer `1970-01-01`.

**Impact on integrators**

If you cached a free seat's `currentPeriodEnd` as a fixed date, re-read it: on an overdue seat it moves forward.

### FIX-0806-22: server repair now reports why it failed and no longer leaves the agent stopped

**Before**

On failure `GET /v1/infra/servers/:id/repair-status` returned an `error` with no cause —
`SSH install failed (exit 255); serial fallback: Serial console install failed`. That text
could not distinguish a closed port from an unreachable machine or from a download that
never completed. The agent install also stopped the running service BEFORE downloading the
replacement: if the download failed (no outbound connectivity, unreachable download host),
the agent stayed stopped and the next repair attempt repeated the same sequence.

**After**

`error` now carries the cause: the SSH message for the regular path (`Connection refused`,
`Connection timed out` and so on), and a short excerpt of the console output for the
emergency-console path (for example `curl: (6) Could not resolve host: …`). The excerpt is
stripped of secrets and length-bounded. The install downloads the new agent first and only
then stops the service, and brings the agent back up if any later step fails.

### FIX-0806-23: key issuance now checks platform access

**Before**

`POST /v1/keys` and `POST /v1/apps` issued a new key to any authenticated account, including accounts whose access to the platform was closed. The key then worked — issuance never consulted the access check.

**After**

Access is verified before the key is issued. An account without access is refused with `INT_TARIFF_REQUIRED`, the same code it already receives on other surfaces.

Keys already issued keep working. Key rotation, automatic recovery and ownership transfer are unaffected: an account whose access lapsed must still be able to wind its own affairs down.

**Impact on integrators**

Handle the refusal on issuance the same way it is handled on the other surfaces: restore access and retry. Keys issued earlier need no changes.

### NEW-0806-24: a clear refusal when a personal key is left with only placement or entity

A personal key is backed by a Bitrix24 incoming webhook, and that surface does not store
the `placement` and `entity` scopes — they require an application context. Such a request
used to fail opaquely: key creation returned `502 DEVKEY_MINT_FAILED` advising the caller
to contact the account administrator, and a scope edit returned `502 DEVKEY_SCOPE_SYNC_FAILED`.

Both cases now answer `400 PERSONAL_KEY_WEBHOOK_SCOPES_INVALID` with a message that says
what to do: add at least one regular scope (for example `crm` or `user_brief`), or create
an OAuth application if you need placements.

The refusal fires only when dropping those two scopes leaves nothing Bitrix24 can bind to
the webhook. A mixed set (`placement` + `crm`) still succeeds.

**Affected endpoints:** [POST /v1/keys](/docs/keys-auth), [PATCH /v1/keys/{id}](/docs/keys-auth),
[POST /v1/keys/{id}/rotate](/docs/keys-auth)

Related response change: for a personal key, the `scopes` field of a create or update
response no longer echoes `placement` and `entity` — the webhook never carried them, and
the response used to promise a scope the key does not have. App keys and system keys are
unaffected.

### FIX-0806-25: a Galaxy Python build no longer fails on a version that exists

**Before**

When a `runtime: python311*` deploy could not reach the package index, `No matching distribution found for <package>` was all you got — for a version that exists and installs fine. The platform showed nothing next to that text, so it read as your mistake.

**After**

The default package index on this platform is the canonical PyPI, and that has not changed. Your own `--index-url` in `requirements.txt` or in the install command still wins over ours.

If the index still does not respond, `error.category` is now `INSTALL_REGISTRY_UNAVAILABLE` (was `GENERIC`), and `error.buildHint` — plus the field of the same name on `GET /v1/infra/servers/:id` — carries a readable reason instead of nothing. The value is additive: existing codes are unchanged. The failure is terminal — the platform does not retry it for you.
