# API changes: July 23, 2026

[← Changelog](/docs/changelog) · [July 2026](/docs/changelog/2026-07)

### NEW-0723-1: app blueprints library — spec via API key

Ready-made specs for popular apps are now available via a key: `GET /v1/app/blueprints/:slug?locale=ru|en` returns the raw spec markdown (`Content-Type: text/markdown`). The endpoint requires `Authorization: Bearer <key>`; an unknown or hidden blueprint returns `404 BLUEPRINT_NOT_FOUND`. The copy-paste "AI prompt" shown when creating a key carries the spec link — the AI agent fetches the spec with the same key. The former anonymous path `/api/public/blueprints/:slug.md` has been removed.

### NEW-0723-2: sources of a deleted server: access, cleanup, and an honest answer for purged bytes

Sources outlive their server — a long-standing platform guarantee — but the API gave you no way to reach them: the whole `/v1/infra/servers/:id/sources*` surface answered `404` for a deleted server, so the owner could neither list their versions, nor untag them, nor delete them. For a version tagged `published` or `manual` that was a dead end: untagging is the only sanctioned way past `409 PROTECTED_BY_TAG`, and untagging was exactly what you could not do.

Read and cleanup verbs now work on a deleted server: version list, version metadata, download, `tag`, `PATCH`, `DELETE` and `cleanup`. Saving a new version (`POST /sources`) still answers `404` — a dead server accepts no new deposits.

So that a deleted server can be found at all, [GET /v1/infra/servers](/docs/infra/servers/list) accepts `?includeDeleted=true`. The default listing is unchanged. Every row now carries a `deletedAt` field (`null` for live servers).

Separately: a version whose bytes were already purged from storage now answers `410` with code `SOURCE_VERSION_BYTES_PURGED` instead of `404`. The difference matters — `404` claimed the version did not exist, when in fact its record is alive and the recovery is different: re-upload the archive rather than look for it elsewhere. The code arrives on download and on deploy by `{"source": {"versionId": "vN"}}`.

**Affected endpoints:** [GET /v1/infra/servers](/docs/infra/servers/list), [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy), `GET /v1/infra/servers/:id/sources`, `GET /v1/infra/servers/:id/sources/:versionId/download` — the sources contract lives on the [Source storage](/docs/source-storage) page

### FIX-0723-3: galaxy app deploy: a mid-build tunnel drop is no longer masked as "host unreachable"

**Before**

If the host tunnel blipped during the build and no healthy container of this deploy resulted (the build was interrupted, the container never came up, the exec channel was busy, or the app crashed), the deploy (`POST /v1/infra/servers/:id/deploy`) returned `502 GALAXY_HOST_UNREACHABLE` advising "retry once it reconnects". The host was often reachable — the advice was misleading, and the caller never saw the real cause (for example, their own app failing to start).

**After**

When the host is reachable after the blip but the deploy did not bring the app up, the deploy returns a new code `502 GALAXY_DEPLOY_INTERRUPTED` — "the host is reachable, but the deploy was interrupted before the app started: re-send the same deploy; if the app repeatedly fails to start, fix it first (the start command, port, dependencies, environment variables, or memory limit)". It stays retryable — the slot is intact, no need to delete and recreate it. The `GALAXY_HOST_UNREACHABLE` code now stays only for a genuinely unreachable host (no probe reached it). If the app truly crash-loops, that is reliably surfaced on the retry by the normal liveness check (code `GALAXY_APP_START_FAILED`).

### NEW-0723-4: deploy step timeout error now carries a recovery hint

The `DEPLOY_TIMEOUT` error from [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy) now carries a structured `error.hint` object (`reason`, `recovery`, `recoveryAction`) tied to the timed-out step (`error.step`). For user commands (`install`, `preStart`) the hint explains that the command did not finish within its time budget and advises making it non-interactive and self-exiting, and launching long-running services from the `start` command or detached (`docker compose up -d`). For the runtime install step (`runtime` — a platform step, not a user command) and other service steps it points at a possible tunnel stall and `POST /v1/infra/servers/:id/repair`. The field is additive: the existing `error.code`, `error.message` and `error.step` are unchanged, no integration change is required; the hint arrives in both JSON mode and the SSE `error` event.

### FIX-0723-5: a failed deploy step now shows the real cause, not a benign warning

**Before**

When a deploy step failed, `data.steps[].stderr` (and hence `error.message`) could carry only a benign one-stream warning, losing the real cause of the failure.

**After**

Both streams are returned together, labelled `stderr:` and `stdout:`; the real cause is no longer hidden. The response shape and field name are unchanged.

### FIX-0723-6: list sorting by a non-unique field no longer drops records on the second page

**Before**

A list or search sorted by a non-unique field (for example the date field `begindate`), over more than 50 records, could silently return fewer records than exist: at the page boundary some records sharing the same sort-field value were lost. The response was a 200 with no incompleteness signal. Affected the CRM smart-process-backed entities — deals, leads, contacts, companies, quotes, invoices and smart-process items `/v1/items/{entityTypeId}` — on `GET /v1/{entity}`, `POST /v1/{entity}/search`, inside `POST /v1/batch` sub-calls and per-entity `POST /v1/{entity}/batch`. The same instability class affected numeric aggregation (`POST /v1/{entity}/aggregate` with `sum`/`avg`/`min`/`max`/`groupBy`): the record fetch for the aggregate ran with no order, so over more than 50 records some rows could be lost and skew the result.

**After**

A secondary `id` key is appended to the sort, making the order fully deterministic, so paginated reads no longer drop or duplicate records regardless of the sort field. Your sort stays the primary key; records with an equal sort-field value are ordered by ascending `id`. No request changes are needed.

### FIX-0723-7: reopening a ticket via a comment no longer leaves the resolution stamp

**Before**

A team comment through `POST /v1/feedback/:id/comments` that moved a ticket from `RESOLVED` or `WITHDRAWN` back into an active status (`NEW`, `REVIEWING`, `AWAITING_USER`, `NEEDS_REVIEW`) did not reset `resolvedAt` and `resolvedBy`. They lingered from the prior close, so on reads (`GET /v1/feedback/:id`, `GET /v1/feedback`) a reopened ticket looked both active and resolved.

**After**

Such a comment clears `resolvedAt` and `resolvedBy` — on reads an active ticket no longer carries a resolution date. `resolution` is left as is: it mirrors the comment body. A comment that sets `RESOLVED` still stamps the fields; moving to `ARCHIVED` and a plain transition between active statuses leave the stamp untouched. This aligns the behaviour with the clear already applied on `PATCH /v1/feedback/:id`.

### FIX-0723-8: list filters on statuses / departments / storages / currencies / products are no longer silently ignored

**Before**

`GET /v1/statuses`, `/v1/departments`, `/v1/storages`, `/v1/currencies`, `/v1/products` run on legacy Bitrix24 methods (`crm.status.list`, `department.get`, `disk.storage.getlist`, `crm.currency.list`, `crm.product.list`) that silently ignore non-filterable keys and operators. An unknown or unsupported filter field — e.g. `filter[system]` on statuses, `filter[module]` on storages, or `filter[price]` on products — as well as operators `$gt` / `$contains` / `$ne` returned `200` with the whole table. The client received the full set instead of the expected subset — a silent failure with wrong data.

**After**

For these entities the filter is validated before the Bitrix24 call: only fields the method actually filters on (live-verified) are allowed. Any other field, operator, or empty set returns `400 UNSUPPORTED_FILTER` listing the filterable fields. Allowed fields per entity: `statuses` — id, entityId, statusId, name, sort, semantics, categoryId; `departments` — id, name, parentId, headId; `storages` — id, name, code, entityType, entityId; `products` — id, name, code, xmlId, active, sectionId, sort. `crm.currency.list` filters on nothing — any `filter` on `/v1/currencies` returns `400` with a hint to filter client-side.

### BC-0723-9: POST /v1/apps no longer returns the prefix and suffix fields in the create response

> Old format supported until: 21.07.2026

**Before**

The POST /v1/apps create response carried two `vibe_app_` values — a short prefix and the full rawKey. The short prefix was mistaken for the key, and a request using it returned 401.

**After**

The create response carries one `vibe_app_` value — the working rawKey. The prefix and suffix fields remain on GET /v1/apps and GET /v1/apps/:id for masked-key display.

**What integrators should do**

Use the rawKey field from the create response as X-Api-Key. If you need the masked prefix, read it from GET /v1/apps or GET /v1/apps/:id instead of the create response.

### FIX-0723-10: POST /v1/batch now persists phone and email on lead and contact create and update

**Before**

Through the shared POST /v1/batch the phone and email (multifield) values were dropped on a lead or contact create or update. The call returned success but the field was not saved. The same payload through the single POST /v1/leads or PATCH /v1/contacts/:id and through POST /v1/{entity}/batch saved correctly.

**After**

The shared POST /v1/batch serializes multifields the same way the single calls do. Phone and email persist on create and update.

### FIX-0723-11: Search and research no longer return 402 INSUFFICIENT_BALANCE on a funded account

**Before**

`POST /v1/search` and `POST /v1/research` with the platform-managed engine (`bitrix-search`) on a personal key could return `402 INSUFFICIENT_BALANCE` even with enough Vibe balance on the billing account. The pre-flight balance check looked the billing account up by the key owner, while the billing account is now one per Bitrix24 account — so it was not found.

**After**

The balance check and charge always resolve the billing account by the Bitrix24 account. With a positive balance the request runs and is charged correctly; `402` is returned only on a genuine shortfall. The request and response shape are unchanged.

### FIX-0723-12: POST /v1/apps reports the plan requirement clearly on the cloud-shared path

**Before**

Creating an app via the cloud-shared key-issuance path (the unified cloud↔box scheme, rolled out per account cohort) on an account without the required Bitrix24 plan made [POST /v1/apps](/docs/apps/create) return an opaque `502 CONNECTOR_APP_INSTALL_FAILED` with no cause.

**After**

A plan-access denial is now classified up front: before calling the connector, `POST /v1/apps` checks the account's access state and, when it's missing, returns `403 INT_TARIFF_REQUIRED` right away, with a readable message. Other cloud-shared issuance failures (module not installed, forbidden by the account administrator, other errors) are classified as before.

**Impact on integrators**

No action required, successful calls are unchanged. If you handled `502 CONNECTOR_APP_INSTALL_FAILED` on app creation, also handle `403 INT_TARIFF_REQUIRED` and prompt the user to upgrade their Bitrix24 plan.

### FIX-0723-13: starting a server no longer errors when the machine is already running

**Before**

[POST /v1/infra/servers/:id/start](/docs/infra/lifecycle/start) for a server in the `error` state
asked the cloud to start the machine and surfaced any rejection as `502 PROVIDER_ERROR`. When the
machine was already running — for example, brought back by automatic recovery after preemption —
the cloud rejected the call with "instance already in RUNNING state", and the endpoint returned an
error for an operation that had in fact succeeded. Clients saw a `502` and could not tell it apart
from a genuine failure.

**After**

That rejection is now treated as an idempotent success: when the machine is already running or in a
transitional state, the call returns `200` and the server moves to `provisioning`, exactly as on a
normal start. Genuine failures — insufficient permissions, exhausted quota, machine not found — still
return `502 PROVIDER_ERROR`.

This is the same idempotency criterion the agent start endpoint and the internal server wake path
already applied.

### BC-0723-14: the /v1/me deployment.standalone.requiredFields.create shape is now an object + documents the name slug

> Old format supported until: 22.01.2027

**Before**

In the `GET /v1/me` response, the per-kind sub-block `deployment.standalone.requiredFields.create` was an array `["provider", "name", "plan", "region"]` — the format of `name` was not stated; the sibling `deployment.galaxyApp.requiredFields.create` said only "required" for `name`. A name with non-Latin or uppercase characters was rejected by [POST /v1/infra/servers](/docs/infra/servers/create) with `400 INVALID_REQUEST`, but self-discovery never surfaced that constraint.

**After**

`deployment.standalone.requiredFields.create` is now an object (like its sibling `deployment.galaxyApp.requiredFields.create`), and in both sub-blocks `name` carries its format: a lowercase-Latin slug matching `^[a-z][a-z0-9-]*$`, 2–63 characters long. Put a human-readable label in the optional `displayName` field.

**What integrators should do**

The flat `deployment.requiredFields["POST /v1/infra/servers"]` (the array `["provider","name","plan","region"]`) is unchanged — if you read it, no action is needed and the set of required fields is the same. If your code parsed the per-kind sub-block `deployment.standalone.requiredFields.create` as an array (`.forEach` / `.includes("name")` / `.length` / `[0]`), switch to reading it as an object: the keys are field names (`provider`/`name`/`plan`/`region`) and the values are their descriptions.

### NEW-0723-15: GET /v1/contacts/fields now returns label and description for every field

The [GET /v1/contacts/fields](/docs/entities/contacts/fields) response now carries human-readable `label` and `description` for all 28 static contact fields. Previously the base fields (`name`, `lastName`, `typeId` and others) came back with only `type` and `readonly`, with no explanation of their meaning. Labels come in English. You can read a field's semantics programmatically from the response instead of cross-referencing the static documentation. In addition, `GET /v1/openapi.json` exposes these labels and descriptions (in English) as `title` and `description` annotations in the `Contact` and `ContactInput` schemas.

### NEW-0723-16: smart-processes: relations and linkedUserFields fields in the input schema

The `relations` (parent/child CRM entity links, e.g. linking a smart process to deals) and `linkedUserFields` fields are now declared in the input schema and shown in `GET /v1/smart-processes/fields`. Pass them in `POST /v1/smart-processes` and `PATCH /v1/smart-processes/:entityTypeId` to link a smart process to other CRM entities and surface it in user fields. Filtering and sorting by these fields are not supported — they are nested write structures, not query fields.

### FIX-0723-17: bizproc-activities and bizproc-robots: documentType type in /fields corrected to array

**Before**

`GET /v1/bizproc-activities/fields` and `GET /v1/bizproc-robots/fields` reported `documentType` as type `object`, while the field is a three-element array (`[moduleId, entity, documentType]`), as already declared for `bizproc-templates`.

**After**

The `documentType` type in `/fields` is now `array` across all three entities — consistent with the real contract.

### FIX-0723-18: PATCH /v1/bizproc-templates returns a numeric id

**Before**

[PATCH /v1/bizproc-templates/:id](/docs/entities/bizproc-templates/update) returned `data.id` as a string (`"1215"`), while `POST` returns a number (`1215`). A client comparing the id from the create response with the update response saw a false mismatch.

**After**

The `PATCH` response returns `data.id` as a number (`1215`) — the same as `POST`.

### FIX-0723-19: userfields: label type in the create schema corrected to string

**Before**

The OpenAPI schema for `POST /v1/userfields/{entity}` declared `label` as an `object`. B24 `crm.<entity>.userfield.add` accepts `LABEL` as a string only, so an SDK generated from the spec (where `label` was an object) sent the wrong type and failed. The OpenAPI spec is a public contract — clients generate SDKs from it, and anyone whose type was "object" had a broken client.

**After**

`label` in the create schema is declared as `string` (the account's default-language label). Multilingual labels are set via `editFormLabel` / `listColumnLabel` / `listFilterLabel` (PATCH after create). No runtime change — only the generated spec was corrected.

### FIX-0723-20: sleep-now on a galaxy app now returns 400 — manage it from the Galaxies page

**Before**

`POST /v1/infra/servers/:id/sleep-now` on a galaxy-hosted app (`GALAXY_APP`) put the container to sleep and returned `200`. This diverged from the session route, which already rejected such an app, and could desync the container state from its host.

**After**

The same call on a galaxy app returns `400` with `error.code = "GALAXY_APP_USE_GALAXY_ROUTE"` and changes no state: the container stays `RUNNING`. Manage the app's lifecycle through the galaxy routes instead. Standalone servers keep their existing sleep-now behavior.

### FIX-0723-21: a task comment is no longer served under a foreign task

**Before**

On legacy accounts `GET /v1/tasks/:taskId/comments/:id` returned `200` with the comment even when the comment did not belong to task `:taskId`: the same comment was served under any task, and the response `taskId` was a plain echo of the path.

**After**

The comment is checked against the task in the path before it is returned. If the comment does not belong to `:taskId`, the endpoint responds `404` with code `NOT_FOUND` and message "Comment not found". The response `taskId` now matches the real parent task. Fetching a comment through its real task keeps working unchanged.

### FIX-0723-22: company type is a single field typeId, not companyType

**Before**

The "company type" field name differed across layers. [POST /v1/companies](/docs/entities/companies/create) with a `companyType` field silently ignored the type — the company was created with the default type; the only way to set it was the `typeId` field. Reads (`GET`, search) always returned the type in the `typeId` field. The filter, however, accepted `companyType` but not `typeId`.

**After**

Company type is a single `typeId` field across all operations: create and update, read and search, filter (`filter[typeId]`) and grouping (`groupBy: typeId`). The values are unchanged — `CUSTOMER`, `SUPPLIER`, `COMPETITOR` (list: `GET /v1/statuses?filter[entityId]=COMPANY_TYPE`). The field is now described in [GET /v1/companies/fields](/docs/entities/companies/fields).

**Impact on integrators**

Set the type with the `typeId` field. Reads are unchanged — the type was always returned in `typeId`. On create and update `companyType` is no longer documented (it never persisted a value). In filters and grouping `typeId` now works, while `companyType` returns `400` (`UNKNOWN_FILTER_FIELD` in filters, `INVALID_AGGREGATION_FIELD` in grouping) — replace the name with `typeId`.
