For AI agents: markdown of this page — /docs-content-en/changelog/2026-07-23.md documentation index — /llms.txt

API changes: July 23, 2026

← Changelog · July 2026

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 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, POST /v1/infra/servers/:id/deploy, GET /v1/infra/servers/:id/sources, GET /v1/infra/servers/:id/sources/:versionId/download — the sources contract lives on the 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 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 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 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 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 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 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 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.

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.