# API changes: August 3, 2026

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

### FIX-0803-1: reopening an archived ticket now clears the resolution stamp too

**Before**

Reopening a ticket from `ARCHIVED` into an active status (`NEW`/`REVIEWING`/`AWAITING_USER`/`NEEDS_REVIEW`) — via `PATCH /v1/feedback/:id` or `POST /v1/feedback/:id/comments` — did not reset `resolvedAt`/`resolvedBy` when the ticket had been resolved and then archived. On reads (`GET /v1/feedback/:id`) such a ticket looked both active and resolved. The clear only fired for `RESOLVED`/`WITHDRAWN` sources.

**After**

`ARCHIVED` joins `RESOLVED`/`WITHDRAWN`: reopening out of any closed status into an active one clears `resolvedAt`/`resolvedBy`. On the `PATCH` path `resolution` is cleared too (including the archive reason) — an active ticket carries no resolution; an explicit `resolution` in the same request still wins. On the comment path `resolution` equals the comment body. Moving **into** `ARCHIVED` still preserves the stamp (archiving keeps resolution history).

### FIX-0803-2: missing attachment bytes now answer 404 instead of a truncated response

`GET /v1/feedback/{id}/attachments/{attId}/file` and `.../thumb` now confirm the bytes exist before any header is sent. When the attachment record is present but its bytes are not in storage (after manual cleanup or a cascading delete), the answer is a plain `404 NOT_FOUND`.

**Before**

The response opened as `200` and then broke off mid-body: the client received a truncated image or an empty stream under an already-sent success status, indistinguishable from a slow network.

**After**

`404 { "success": false, "error": { "code": "NOT_FOUND", "message": "Not found" } }` — the same code these routes already return for someone else's or a deleted attachment. Clients that already handle `404` here need no changes.

The change accompanies moving attachment files into object storage: serving an attachment no longer depends on which machine accepted the upload. Response shapes and route paths are unchanged.

### FIX-0803-3: server creation now says plainly when it returned the application's existing server

When the calling key belongs to an application whose server slot is already filled, [POST /v1/infra/servers](/docs/infra/servers/create) returns that server instead of creating a new one. That was already the behaviour, but the response gave you almost nothing to notice it by: the only signal was an undocumented `reused` field, and the `name` in the response belonged to the existing server rather than the one you asked for.

Such a response now carries the full disclosure: `data.reusedReason` with the value `APPLICATION_ALREADY_HAS_SERVER`, `data.requestedName` echoing the `name` you sent (always, even when it equals the existing name), and a `warnings` array next to `data` with at least one entry naming the existing server and stating that deploying replaces the code currently running on it. The `reused` and `deploying` fields are now declared in the schema and in the documentation.

The second silent loss is disclosed the same way: a reuse does **not** apply the `displayName` and `description` you sent — the server keeps its own name and description. The response now says so through `data.metaIgnored` and a dedicated warning; rename the server deliberately with [PATCH /v1/infra/servers/:id](/docs/infra/servers/update).

When the request carried a `source` that cannot be built onto the reused server (the galaxy application already has a live container, or the server is a dedicated virtual machine), the archive is discarded — and the response now says so through `data.sourceIgnored` and a dedicated warning. It used to be discarded silently.

**On a reuse response `data.next` now arrives only when there is nothing to overwrite.** It used to arrive on every two-step reuse, including one that returned a server already running someone's code — so the machine-readable "next step: deploy" contradicted the hint in the same body. When the server may be running code the field is deliberately absent: confirm the server is the right one first. A missing `next` is not an error.

Two existing fields changed meaning, though no client code has to change: on a reuse response `data.hint` was rewritten wholesale — instead of "deploy here" it now opens with `REUSED — no new server was created` and explains the risk; and the schema description of `data.next` was corrected, having previously named an empty galaxy slot as the only reason the field appears when it also arrives on a reuse response. No existing field or code changed and no client action is required. Do read `reused` before calling [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy): a deploy replaces whatever is already running on that server.

### NEW-0803-4: dictionary of catalog list-property values

A new entity `catalog-product-property-enums` exposes all possible options of a trade-catalog list property: `GET /v1/catalog-product-property-enums`, `GET /v1/catalog-product-property-enums/:id`, `POST /v1/catalog-product-property-enums/search`, and `GET /v1/catalog-product-property-enums/fields`. The entity is read-only: write operations and aggregation are not registered and answer `404`, and `data.batch` comes back as an empty array.

Previously a product's list-property value was available only as an option identifier: the `/v1/products` family returns an object with a numeric `value` in `PROPERTY_<N>`, and `GET /v1/products/fields` describes the property by name alone — there was nothing to expand the identifier into text with. The option list is now requested directly: `GET /v1/catalog-product-property-enums?filter[propertyId]=166&limit=1000` returns identifier-and-text pairs from which a `String(id)` → `value` map is built.

The `filter[propertyId]` filter is required: the dictionary is read one property at a time, and a request without it is rejected with `400 MISSING_REQUIRED_FILTER` before Bitrix24 is called — on the list and search endpoints. The check looks at the presence of the key and does not extend to batch sub-calls. Only properties with `propertyType: "L"` have an enumeration — for a property of any other type the response is an empty list, not an error. Pagination is ordinary: `limit` + `offset`, with the end of the selection signalled by `meta.hasMore`. The ceiling is 5000 records per call. The `catalog` scope is required.

Existing pages were clarified in the same change. `GET /v1/catalog-products/:id` now shows a captured response carrying a list property and explains the `value` / `valueEnum` / `valueId` triple, plus the shape a `listType: "C"` property returns — a bare `"Y"`/`"N"` scalar, that is the checkbox state rather than an option id. `GET /v1/products/:id` explains that an empty property value means either an unfilled property or one served by the catalog family alone, and how to tell the two apart with a single cross-call. `GET /v1/products/fields` states outright that a `PROPERTY_<N>` descriptor carries the name only, and where to go for the values. The error reference now lists the real set of entities that require a filter.

### FIX-0803-5: Support tickets work while the account is balance-frozen

**Before**

On a balance-frozen account every `/v1/feedback` route returned `402 ACCOUNT_FROZEN` — you could not report a problem from within the product exactly when it mattered most.

**After**

The whole conversation stays reachable by key: `POST /v1/feedback` (create), `GET /v1/feedback` (list), `GET /v1/feedback/{id}` (open a ticket) and `POST /v1/feedback/{id}/comments` (reply to the team). All four only read and write your own data. Attachment uploads (`POST /v1/feedback/attachments`), attachment downloads and `PATCH /v1/feedback/{id}` remain behind the freeze gate, as does every other `/v1` endpoint.

### FIX-0803-6: include now returns the full related record, not just link metadata

**Before**

With `?include=<relation>` the nested object carried only the link metadata — without the related record's `id` or fields.

**After**

`include` returns the full related record (`id` + fields), as the contract describes — a separate follow-up request for the related entity is no longer needed.

### FIX-0803-7: rolling-out Open Channels dashboard methods return METHOD_NOT_YET_AVAILABLE

**Before**

On an account where the update had not yet arrived, the Open Channels dashboard methods returned a raw `422 BITRIX_ERROR` — indistinguishable from a real integration error.

**After**

That response is recognised and returned as `422 METHOD_NOT_YET_AVAILABLE` with the release version — a clear signal that the method is not yet available on this Bitrix24 account, not an integration failure. The answer stays the same under regular polling: such calls no longer count toward error-loop protection, so a clear `422` is not replaced by `429 ERROR_LOOP_DETECTED` (for methods that are not rolling out, the protection works as before).

### FIX-0803-8: source-storage writes return precise error codes instead of a generic 500

**Before**

Client-side source-storage write failures were masked behind a generic `500 SOURCE_STORAGE_ERROR`, giving no actionable signal.

**After**

The cause is now distinguishable: insufficient balance returns `402 BILLING_INSUFFICIENT`; a transient storage access-key issuance failure returns `503 STORAGE_STS_UNAVAILABLE` (safe to retry). The source-storage error table lists both.

### FIX-0803-9: a value list in a statuses filter is rejected with a clean 400, not a 500

**Before**

`GET /v1/statuses` with a value list in the filter (`{field: {$in: [...]}}` or an array) was forwarded to Bitrix24, and the dictionary method answered differently per field: on `id` and `name` an internal error that reached the client as `502 BITRIX_UNAVAILABLE`, on `entityId`, `statusId`, `semantics` and `sort` a "value must be a string" error, and on `categoryId` a success response carrying another pipeline's records.

**After**

A value list is rejected before the Bitrix24 call with `400 UNSUPPORTED_FILTER` on every filter field — the dictionary method supports it on none of them. A single exact value is accepted (`{field: value}`); request several values in separate calls or via `POST /v1/batch`.

### FIX-0803-10: /v1/tasks/:taskId/time accepts a key with the task scope

**Before**

The task time-tracking endpoint returned `403 INSUFFICIENT_SCOPE` to a key holding the `task` scope — only `tasks` worked, even though the two are aliases of one permission.

**After**

`task` and `tasks` are treated as aliases (as on every other task endpoint) — a key holding either one is accepted.

### NEW-0803-11: the one-shot galaxy app create now accepts `healthPath`

The body of [POST /v1/infra/servers](/docs/infra/servers/create) with a `source` field now accepts the optional `healthPath` — the path used to check the app's readiness inside its container. Validation matches [POST /v1/infra/servers/{id}/deploy](/docs/infra/deploy/deploy): a string of up to 500 characters starting with `/`. The default is `/`.

Previously `healthPath` was declared only in the deploy body, so the one-shot call the platform itself recommends in `GET /v1/me` answered `400 UNKNOWN_PARAM` — while that same description called `healthPath` honored on the galaxy path. The field is now accepted exactly where the recommendation promises it; on a standalone create it is ignored.

**Impact on integrators**

Nothing to change — the field is optional. If you previously had to split the call into two steps just for `healthPath`, one call is now enough.

### FIX-0803-12: `/start` and `/wake` on a galaxy app now explain why they do not apply

**Before**

[POST /v1/infra/servers/{id}/start](/docs/infra/lifecycle/start) and [POST /v1/infra/servers/{id}/wake](/docs/infra/lifecycle/wake) checked the status before the server kind, so a galaxy app outside the allowed statuses (say `RUNNING`, `ERROR` or `STOPPED`) got the standalone-server text: "Server is RUNNING; /start requires one of SLEEPING, ERROR, PROVISIONING". Technically true and useless: those statuses would not have helped either, and nothing said that a container app has no cloud VM at all.

**After**

The kind check now comes first, mirroring `/reboot`. For a galaxy app in such a status `message` names the reason (this is a galaxy app, it has no cloud VM) and `userMessage` names the operations that do work: `POST /v1/infra/servers/{id}/deploy` (valid in any of these statuses) and `POST /v1/infra/servers/{id}/reboot` (only for a running or errored app). The error code is unchanged — still `SERVER_WRONG_STATE` (422) with `currentState` and `availableActions`.

**Impact on integrators**

Nothing to change: the HTTP status and code are the same, only the text changed. The response for statuses INSIDE the allowed list is byte-identical — in particular `/start` and `/wake` on a sleeping galaxy app still answer the documented `VM_MISSING`.

### FIX-0803-13: a galaxy app failure now names its actual cause in `provisionError`

**Before**

When an app built, started and then died, `provisionError` carried only the generic verdict: "the app did not stay running, check the logs" or, when the out-of-memory flag fired, "likely OOM at the galaxy memory limit". The real cause — say `TypeError: webidl.util.markAsUncloneable is not a function` from an incompatible runtime version — sat in `buildLog`, while the server list surfaces `provisionError` only. So the short text could not tell memory apart from code, and the "move to a standalone server" advice pointed the wrong way.

**After**

The same wording now gains a line from the container logs: `… Actual cause from the container logs: <line>`. The line is picked from the tail the platform captures on failure: first a typed exception or error code (`TypeError: …`, `EADDRINUSE`, `FATAL ERROR: … heap out of memory`), then the known build causes, then the last error-ish line. The tail goes through the same scrubbing as `buildLog` — internal host paths are replaced with `<build-context>`. When the tail holds nothing useful the text stays exactly as before. The memory wording is preserved and gains the cause: the `oom` flag sometimes fires where memory was not involved, and then the quoted line is the only truth the reader gets.

**Impact on integrators**

Nothing to change. The previous substrings are preserved, so a client matching on them keeps working; only the appended tail is new. The full log is still available in `buildLog` (`GET /v1/infra/servers/{id}`).

### FIX-0803-14: `/reboot` on a sleeping galaxy app now kicks the host repair and says so

**Before**

A sleeping galaxy app whose host had lost its tunnel had no way back. The documented way to wake an app is a deploy, and a deploy against an unreachable host fails. The host tunnel repair was already kicked from the app reboot, but the kick sat BEHIND the status check that rejects a sleeping app — so it was never reached.

**After**

Before the same `422 SERVER_WRONG_STATE` refusal the platform kicks a background host tunnel repair and, when a repair actually started, adds an optional `hint` object with `reason`, `recovery` (which call to retry) and `retryAfterSeconds` (a floor for the wait, not a promise). When no repair started — the kill switch is off, the tunnel is in fact alive, a repair is already running, or the machine is blocked from waking — `hint` is absent: claiming a repair that did not start would be a lie the client acts on. The same behaviour was added to the dashboard reboot.

**Impact on integrators**

Nothing to change: the code and HTTP status are the same and `hint` is additive. A client that reads `hint` only needs to retry `POST /v1/infra/servers/{id}/deploy` after the named delay.

### FIX-0803-15: a zip source archive no longer fails at extraction on a galaxy app

**Before**

The platform detects the archive format from its leading bytes and advertises `.zip` as supported, but on the galaxy path (`POST /v1/infra/servers` with `source`, and `POST /v1/infra/servers/{id}/deploy` for a galaxy app) the archive was handed over for extraction without preparing the host. When the extractor was missing there, the deploy failed with text like `exec: "unzip": executable file not found in $PATH` — which said neither what to do, nor that the same archive as `.tar.gz` would have worked.

**After**

Before uploading a zip archive the platform installs the extractor on the host (the same step the standalone server path already ran). The step is idempotent: with the extractor already present it does nothing and costs no time on later deploys. If the install fails, the archive is not uploaded at all and the deploy ends with an honest reason plus the suggestion to re-send the same source as `.tar.gz`; the text is available in `buildLog` and in `provisionError`.

**Impact on integrators**

Nothing to change. Deploys with `.tar.gz` take the previous path unchanged.

### NEW-0803-16: Source versions accept up to 500 MB, and a deploy from our own link now links to the version on any key

**Before**

An archive could be stored as a version only up to 200 MB, while the same archive was allowed inline in a deploy body up to 500 MB. A large project had exactly one way to ship — entirely inside the request body.

Separately: a `{"source": {"url": "…"}}` deploy using a link obtained from [GET /v1/infra/servers/:id/sources/:versionId/download](/docs/source-storage) was not linked to the version when the server belongs to a personal `vibe_api_*` key. The response carried `data.source.autoSaved: false` with `skippedReason: "external-url-or-toggles-off"`, and the version kept `linkedDeployId` and `deployStatus` empty.

**After**

The source-version cap is 500 MB on both intake endpoints: [POST /v1/infra/servers/:id/sources](/docs/source-storage) and [POST /v1/apps/:id/sources](/docs/source-storage). The body is still read as a stream, so archive size does not affect intake speed. A `Content-Length` above the cap is rejected with `413` before the body is read. The value is published as `capabilities.apps.sourceStorage.limits.maxBlobBytes` in `GET /v1/me` — now `524288000`.

A deploy from our own link is linked to the version regardless of the owner key type: the response carries `data.source.autoSaved: true` and `savedVersionId`, and the version gets `linkedDeployId` and `deployStatus` filled in.

**Affected endpoints:** [POST /v1/infra/servers/:id/sources](/docs/source-storage), [POST /v1/apps/:id/sources](/docs/source-storage), [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy), `GET /v1/me`

### FIX-0803-17: revoking access kills every key of the connection, not just the latest

**Before**

When a user went through consent again for the same application, the platform issued a new key but the previous one kept working. Revoking access killed only the key from the latest authorization — earlier keys still reached the API, while the user believed access was closed.

**After**

Revoking access kills every key the user issued to the application for that Bitrix24 account, including keys from earlier authorizations. A key that used to survive revocation now answers `401 KEY_INACTIVE`. Keys issued by other employees of the same account are not affected. On the partner side the usual `401 KEY_INACTIVE` handling is enough — prompt the user to authorize again.
