# API changes: August 25, 2026

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

### FIX-0825-1: an application card now gets its server on galaxy accounts too

**Before**

An application created without a server and then deployed on a galaxy account stayed in the Applications section with no server: `GET /v1/applications/{id}` returned `server: null` indefinitely. Linking the container to an already existing card depended on a setting that is off by default, so the card and the container lived apart.

**After**

The container is linked to the already existing card regardless of that setting, and `GET /v1/applications/{id}` returns the server right after the deploy. The setting still governs only the creation of a NEW card for an app created on a shared host — its behaviour is unchanged.

### BC-0825-2: a read-only key no longer writes on platform endpoints

> Old format supported until: not provided

**Before**

The read-only key mode filtered Bitrix24 calls but did not cover platform V1 endpoints.
A read-only key could still manage servers (`DELETE /v1/infra/servers/{id}`,
`POST /v1/infra/servers/{id}/stop|start|reboot|wake`), deploy and run commands on a server,
write to storage (`POST /v1/storage/objects`), submit feedback (`POST /v1/feedback`), manage
search and AI credentials, and call endpoints that spend credits.

**After**

Any write on a platform V1 endpoint made with a read-only key returns
`403 WRITE_BLOCKED_READONLY_KEY` before the operation runs. Endpoints that proxy a call to
Bitrix24 are unchanged: there the decision comes from the per-method classifier, so reads
that carry a request body (`POST /v1/deals/search`, a read-only `POST /v1/batch`, and
similar) are not affected.

There are two exceptions. `POST /v1/apps` — a read-only key can still create an application
and its paired key IN read-only mode, but cannot issue a read-write key.
`DELETE /v1/infra/servers/{id}/lock` — releasing a stuck lock does not change server state
and stays available: the `recoveryAction` field of an `EXEC_BUSY` response points to it.

If your application needs these operations, switch the key to read-write mode on the
`/keys` page. You can check the current mode and whether server creation is available via
[GET /v1/me](/docs/keys-auth/me): for a read-only key the response now carries a
`writeRestriction` field — the refusal code, the scope it applies to, and the address of the
access-mode page. It exists because the response names writing endpoints in some thirty
places (storage, feedback, source-storage and cowork hints); those are addresses, not
permissions, and the field says so outright. Per-operation availability stays in
`capabilities`.

### FIX-0825-3: /v1/me no longer reports app creation as unavailable to a read-only key

In the [GET /v1/me](/docs/keys-auth/me) response the `capabilities.apps.create` slot came back with `available: false` and the reason `WRITE_BLOCKED_READONLY_KEY` under a read-only key. That was wrong: [POST /v1/apps](/docs/apps/create) refuses such a key only when it asks for an app and paired key in read+write mode, while an app in READ-ONLY mode is created by the same key and answers `201`.

The slot is now reported as available, with the restriction spelled out in its `note` field. A client that branches on `capabilities` — which is what `/v1/me` exists for — no longer skips an operation that works.

### BC-0825-4: a Cowork desktop key can no longer change OAuth application registrations

> Old format supported until: not provided

**Before**

A key carrying the system `vibe:cowork` grant could register an OAuth application on the Bitrix24 account (`POST /v1/apps`), edit it (`PATCH /v1/apps/{id}`), delete it (`DELETE /v1/apps/{id}`) and re-point it at other credentials (`POST /v1/apps/{id}/relink-oauth`). The same key was already barred from infrastructure, the source depot and catalog publishing, so the restriction was incomplete.

**After**

All four operations answer `403 INFRA_FORBIDDEN_FOR_COWORK_KEY`, with `details.deployableKeys` listing the owner's usable keys. On `POST /v1/apps` the refusal comes before the body is checked, so an invalid body also gets this code rather than `VALIDATION_ERROR`. Reads of the family (`GET /v1/apps`, `GET /v1/apps/{id}`) are unchanged.

**What to do**

Run these operations under an ordinary application key: the `vibe:cowork` grant is platform-minted and meant for data only. Candidate keys arrive in `details.deployableKeys` of the same response — name, prefix and trailing characters, never the secret. There is no support window for the old behaviour: the `vibe:cowork` grant is not user-grantable, never appears in the key dialogs, and its only holder does not call these operations.

### BC-0825-5: re-uploading a file now replaces its content instead of failing with 500

> Old format supported until: not provided

**Before**

Uploading to a logical `key` that was already taken returned `500` with code `P2002` from
`POST /v1/storage/objects/upload`. The bytes in storage had already been overwritten, while the
object `sizeBytes` and `sha256` still described the previous version — so the size shown in listings
and billing did not match the actual file.

**After**

Re-uploading to the same `key` replaces the content: `200`, the same `object.id` (previously issued
links keep working), and `sizeBytes`, `sha256`, `contentType` and the new `contentUpdatedAt` field
match the new bytes.

This applies to an **app-bound key**. With a personal developer key that has no app binding,
re-uploading still creates a new object with its own `id` — unchanged behaviour, fixed separately.

When the object cannot be replaced in place, a `409` with a meaningful code is returned instead of
`500`: `STORAGE_KEY_DELETED` (the object is deleted and holds the name until the purge),
`STORAGE_MULTIPART_IN_PROGRESS` (a multipart upload is in progress),
`STORAGE_UPLOAD_PENDING` (the key holds an unfinished presigned reservation),
`STORAGE_KEY_OWNED_ELSEWHERE` (the name belongs to another object of this app),
`STORAGE_KEY_CONFLICT` (the object changed while the upload was in flight — retry).

**What you need to do**

1. The `visibility` field cannot change visibility while replacing: omit it or pass the current value.
   Any other value returns `400 STORAGE_VISIBILITY_MISMATCH`, and the current value is named in the
   message. Such a request used to return `500` while still replacing the bytes — so a client that
   ignored the error was in fact publishing updates and will stop doing so after this change.
2. A presigned URL (`POST /v1/storage/objects`) is no longer minted for an existing object:
   `409 STORAGE_KEY_EXISTS`. Its Content-Type is unsigned, so content can only be replaced by a
   direct upload. An unfinished reservation of your own is reused instead — `200` with the same
   `object.id` and a fresh URL; such a URL cannot change the reservation visibility, so a differing
   value returns `400 STORAGE_VISIBILITY_MISMATCH`.
3. A multipart upload (`POST /v1/storage/objects/multipart/create`) on a taken key also answers `409`
   instead of `500`: `STORAGE_KEY_EXISTS`, `STORAGE_KEY_DELETED`, `STORAGE_MULTIPART_IN_PROGRESS`,
   `STORAGE_UPLOAD_PENDING` or `STORAGE_KEY_CONFLICT` (a concurrent request created the object). Replacing an object through a multipart upload is not supported — use
   another key.
4. Responses now carry `object.contentUpdatedAt` — when the content last became current. It is `null`
   for objects written before this change.

### BC-0825-6: the binding and author fields of a timeline comment no longer look editable

> Old format supported until: not provided

**Before**

`GET /v1/timelines/fields` returned `entityType`, `entityId` and `authorId` as ordinary writable fields. `PATCH /v1/timelines/{id}` with any of them answered `200` and `success: true`, yet the value did not change — reading the record back showed the previous one. Meanwhile `id` and `createdAt` in the same output were honestly refused with `400 READONLY_FIELD`, so the field reference told the truth only selectively. An integrator or an AI agent concluded it had changed the author or moved the comment to another record, while nothing had changed at all.

**After**

The three fields are marked immutable in the field reference, and an attempt to write them is refused with `400 READONLY_FIELD` instead of a silent success. The reasons differ per field, and the reference now distinguishes them:

- `entityType` and `entityId` — available on create only: a comment is bound to its record at the moment it is added, and it cannot be moved to another one;
- `authorId` — read-only: Bitrix24 derives the author from the credentials the call is made with, so the value cannot be set on update or on create.

`comment` stays writable — it is the only field the Bitrix24 update accepts.

**What integrators should do**

Drop `entityType`, `entityId` and `authorId` from the body of `PATCH /v1/timelines/{id}` — they were never applied, and now a request carrying them returns `400 READONLY_FIELD`. Check **creation** separately: `POST /v1/timelines` carrying `authorId` also moves from "`201`, value ignored" to `400 READONLY_FIELD`, because the author cannot be set on create either. `entityType` and `entityId` stay required and accepted on create. If your code treated the successful answer as proof that the author had changed or the comment had moved, that expectation was already unmet before this change: the value stayed as it was. The comment text still updates through a normal `PATCH` with the `comment` field. Set the binding at creation time through `POST /v1/timelines`; the author cannot be changed — make the call under the account you need.

No support window for the previous behaviour is provided, deliberately: the previous behaviour was the defect — it silently discarded the value that was sent, and keeping it for a period would mean prolonging a silent data loss.

### BC-0825-7: deploy base version is required when the top saved source version belongs to somebody else too

> Old format supported until: not provided

**Before**

[POST /v1/infra/servers/{id}/deploy](/docs/infra/deploy/deploy) required `baseVersionId` only on a server that currently had a live development team. If the owner added a teammate, the teammate deployed their own version, and the owner later removed them from the team, the requirement disappeared along with the last team member — and the next deploy silently overwrote the saved work.

**After**

`baseVersionId` is also required when the top version saved in the server's source depot was saved by somebody other than the caller deploying now — regardless of whether the server currently has a team. The error is unchanged: `409 BASE_VERSION_REQUIRED` naming the current version.

**What integrations must do**

Send `baseVersionId` on every deploy. A client that already does needs no changes. A client that relied on "no team means no label needed" will get `409 BASE_VERSION_REQUIRED` on a server holding somebody else's saved version — including after an application ownership transfer, where the top version was saved by the previous owner. Handle it the way the team case is already handled: read the current version number from the error body and repeat the deploy declaring it as the base. The old behaviour is not kept for any period: it is exactly what caused other people's work to be lost.

### NEW-0825-8: a server's development-team member now sees its application in the catalog

A member of a server's development team now sees the application bound to that server in [GET /v1/applications](/docs/applications/list) (arriving with `viewerState: "shared"`) and can read its card via [GET /v1/applications/{id}](/docs/applications/get) — previously the card answered `403 FORBIDDEN`, because access was checked only by ownership and by the server's access policy, without considering team membership.

### FIX-0825-9: include is advertised only for entities with relations

**Before**

OpenAPI and the MCP reference advertised the `include` parameter for list, get, and search on every entity. For entities without available relations, a request with this parameter returned `400 INVALID_INCLUDE` with an empty list of available relations.

**After**

OpenAPI advertises `include` only for entities with available relations, while the MCP reference directs clients to check the capability through `discover` or `get_fields` first. Runtime validation and the `400 INVALID_INCLUDE` response for an unsupported `include` are unchanged.

**Impact on integrations**

Client generators no longer receive an unsupported `include` from OpenAPI, while MCP agents get explicit guidance to check the capability first. The common optional MCP key remains available. Existing valid requests continue to work unchanged, while previously unsupported requests receive the same `400 INVALID_INCLUDE` response.

### BC-0825-10: Inline archive cap on the deploy body narrowed to 96 MB

> Old format supported until: not provided

**Before**

[POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy) with code in inline `source.content`, [POST /v1/infra/servers/:id/upload](/docs/infra/deploy/upload) with inline `content`, and [POST /v1/infra/servers](/docs/infra/servers/create) with the `source` field at creation accepted a body up to 500 MB. A body over the cap was refused with `PAYLOAD_TOO_LARGE`.

**After**

The body of these three requests is capped at 96 MB. The unit is the HTTP body itself, not the archive: `source.content` / `content` is base64, which runs about a third larger than the raw bytes, so a 96 MB body corresponds to roughly a 72 MB archive. A body over the cap is refused with `413 INLINE_SOURCE_TOO_LARGE` — a new code, replacing the former `PAYLOAD_TOO_LARGE` on these three endpoints. The refusal is decided from the `Content-Length` header before the body is read, so it is deterministic — re-sending the same body fails identically. It saves no traffic: the whole body is uploaded first, and the refusal arrives once the upload has finished. The error envelope carries `error.hint` with `reason`, `recovery`, `recoveryAction` and `note` fields — a ready recovery recipe.

The multipart (`multipart/form-data`) form of [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy) falls under the same cap conditionally. The file part streams into storage — and keeps its former 500 MB archive limit — only when three conditions hold at once: the caller reaches the server as its direct owner, not through another application's card or a management key; the server is either not a galaxy app, or both link-based-deploy settings for galaxy apps are turned on for the caller; and the source storage feature is enabled, both platform-wide and for that account. If even one condition fails, streaming does not kick in and the form accepts the archive the same buffered way as the base64 fields above — under the same 72 MB archive cap, with the same `413 INLINE_SOURCE_TOO_LARGE` code and `error.hint`.

The former `PAYLOAD_TOO_LARGE` code has not gone away: it still applies at the edge nginx layer on `/v1/` (500 MB cap) and on every other platform route with its own limit — nothing was renamed.

Unchanged:

- `source.url` — still accepted up to 500 MB; the platform downloads the archive from the link itself;
- `source.versionId` — deploying an already-saved version, up to a 500 MB archive;
- the source-version save cap on [POST /v1/infra/servers/:id/sources](/docs/source-storage/servers) and `POST /v1/apps/:id/sources` — 500 MB, untouched.

**What integrators should do**

Send an archive larger than 72 MB through the versioned path in two calls — it works the same way for a personal key (`vibe_api_*`) and for an OAuth application key, and for the server's direct owner these two calls are all it takes:

```
POST /v1/infra/servers/:id/sources   # raw archive bytes, Content-Type: application/gzip, --data-binary
POST /v1/infra/servers/:id/deploy    # {"source":{"versionId":"vN"}}
```

If, on the other hand, the server is reached through an application card or a management key rather than by its direct owner, those two calls take a third one: `{versionId}` may answer `SOURCE_VERSION_REQUIRES_APP` or `SOURCE_VERSION_NOT_FOUND` — in that case fetch a signed link at [GET /v1/infra/servers/:id/sources/vN/download](/docs/source-storage/servers) and deploy from it instead: `{"source":{"url":"<link>"}}`. This same path is also the way around the multipart form's conditional cap — a versioned deploy never buffers the whole archive, so its 500 MB limit is unconditional.

The cap was narrowed at the observable-behavior level: a body of several hundred megabytes in base64 form (and a multipart archive for which at least one of the conditions above did not hold) had to be held in memory whole for the duration of the request, and such a request could abort without a response, taking concurrent requests on the same process down with it.

### NEW-0825-11: `GET /v1/me` now names the archive equivalent of the inline cap as its own field

**Before**

The `deployment.limits` block carried the inline body cap in a single `uploadInlineMax` field. Its unit is the HTTP body, while `source.content` and `content` travel as base64, so the archive size that fits into that body was left for the client to work out.

**After**

A sibling field `uploadInlineMaxArchive` was added — the same cap expressed as an ARCHIVE size: three quarters of `uploadInlineMax`, because base64 runs about a third larger than the raw bytes. The field is added to the response and removes nothing from it: `uploadInlineMax` stays where it was and means what it meant, so a client that ignores the new field needs no changes. Both values arrive as strings with a unit — for example `96MB` and `72MB`.

**What integrators should do**

Nothing is required. If you were converting the body cap into an archive size yourself, read `uploadInlineMaxArchive` instead — it is rendered from the same constant as the `413 INLINE_SOURCE_TOO_LARGE` refusal, so it cannot drift from the actual behaviour.

### BC-0825-12: POST /v1/apps now honours the Bitrix24 account policy for who may create apps

> Old format supported until: not provided

**Before**

The public route only checked the legacy list-based mode. An account whose Bitrix24 admin had
limited app creation to admins, or disabled it entirely, still let any personal API key create
an app — while the same action in the dashboard answered `403`.

**After**

[POST /v1/apps](/docs/apps/create) answers `403` with code `APP_CREATION_RESTRICTED` when the
account policy does not allow the caller to create apps. The list-based mode behaves as before:
a member on the list creates the app, everyone else gets `403`. On accounts where creation is
open to all, nothing changes.

**What integrators should do**

A key issued on a restricted account will start receiving `403` `APP_CREATION_RESTRICTED` where
it previously got `201`. Ask the Bitrix24 admin to grant the app-creation right, or create apps
with a key that already holds it.

### FIX-0825-13: Node.js 20 runtime setup no longer repeats completed steps

**Before**

When redeploying a Node.js 20 runtime through [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy), the platform reinstalled Node.js and `pm2`. An unavailable npm registry could consume the entire step budget and finish without a precise cause.

**After**

The platform skips installation when Node.js 20 and `pm2` are already available. If `pm2` is absent, its installation has time and retry limits, and an error or timeout stops deployment with an explicit diagnostic.

**Impact on integrators**

Repeat deployments finish faster and require no integrator changes. A `pm2` installation error is now immediately visible as the cause of an unsuccessful deployment.

### FIX-0825-14: placement bind answers 400 for an over-long iconName instead of a reasonless 502

**Before**

`POST /v1/placements/bind` accepted an `options.iconName` of up to 255 characters. Bitrix24 refuses anything longer than 50, so the request travelled to the account and came back as `502 BITRIX_UNAVAILABLE` carrying "Failed to register placement on Bitrix24 via dev key". Which field was at fault could not be told from the answer.

**After**

The schema bound now matches the Bitrix24 bound: a value longer than 50 characters is refused up front, and the `400` names the `options.iconName` field. Values that bound before keep binding — anything longer than 50 was never accepted by the account, and the default `fa-cube` icon sits well inside the bound.
