# API changes: July 27, 2026

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

### NEW-0727-1: app favicon in one line — /_gw/icon

The browser-tab favicon is now set with a single static line that carries no server id:

```html
<link rel="icon" href="/_gw/icon">
```

`/_gw/icon` is a platform-served path on the app's own origin; it always returns the currently uploaded icon. One upload via [POST /v1/infra/servers/:id/icon](/docs/infra/app-icon) drives both the Bitrix24 catalog card and the favicon: re-upload the icon and the favicon refreshes on its own (~5 minutes), no rebuild needed. You no longer need to ship your own static icon file in the app. The line is compatible with one-shot app creation (no id required).

Additionally, the [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy) response and the one-shot create [POST /v1/infra/servers](/docs/infra/servers) response now return a `warnings[]` entry when no icon has been uploaded yet — with the exact upload endpoint (the icon is a separate request, since the id only exists after create). The same `warnings[]` still flags a missing `displayName`/`description`. A successful deploy that left the app icon-less or unnamed is no longer silently treated as finished.

### NEW-0727-2: source-at-create: SOURCE_AT_CREATE_GALAXY_ONLY now carries a hint with a path to a galaxy

A [POST /v1/infra/servers](/docs/infra/servers/create) refusal with `source` that could not be placed in a galaxy (a `both`-mode account with no open galaxy host, or a standalone-only account) now additionally carries `error.hint` — an actionable path: how to get a galaxy host and/or how to deploy to a dedicated server in two steps. The error code and message are unchanged.

### FIX-0727-3: galaxy apps: PATCH /sleep and PATCH /port now return 400 — manage them from the Galaxies page

**Before**

For an app hosted in a galaxy (`GALAXY_APP`), [PATCH /v1/infra/servers/:id/sleep](/docs/infra/lifecycle/sleep) returned `200` and wrote `sleepAfterMinutes`, and [PATCH /v1/infra/servers/:id/port](/docs/infra/deploy/port) returned 404/409 — contradicting the documented `/v1/me` (`deployment.galaxyApp`) contract, where no V1 lifecycle action applies to a galaxy app.

**After**

Both calls for a galaxy app return `400` with `error.code = "GALAXY_APP_USE_GALAXY_ROUTE"` and change nothing. Configure the app's auto-sleep via the galaxy route; a galaxy app's port is host-pinned and not settable. Standalone-server behavior for `/sleep` and `/port` is unchanged.

### FIX-0727-4: galaxy app recovers after its host tunnel drops

**Before**

When a galaxy host's secure tunnel dropped (the server still `RUNNING`, but the connection lost), deploying and running commands for a galaxy app returned `502 GALAXY_HOST_UNREACHABLE`, and a logs request returned an empty response with a hint. The host stayed unreachable until a manual repair: retrying the same request hit the same error indefinitely.

**After**

The platform now restores the host's tunnel in the background without holding up the response. Retrying the same request succeeds as soon as the host reconnects (usually within a minute). Parallel deploys/commands against one shared host do not start a duplicate recovery.

**Impact**

The error code and response shape are unchanged — `502 GALAXY_HOST_UNREACHABLE` (for deploy and exec) is still marked retryable, and logs still return an empty list with a hint. What changed is that a retry now succeeds instead of failing forever. Keep retrying on that code and on the empty logs response with your usual policy.

**Affected endpoints:** [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy), [POST /v1/infra/servers/:id/exec](/docs/infra/deploy/exec), [GET /v1/infra/servers/:id/logs](/docs/infra/deploy/logs)

### NEW-0727-5: warning when a deploy changelog has nowhere to be published

The [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy) response now carries an entry in `warnings[]` when the request included a `changelog` but the deploy produced no new source version. A release note is bound to a version, so without one the text is stored nowhere and never reaches the app's channel feed in the Bitrix24 messenger.

The reason is visible in the `source` field of the same response: source storage is disabled (`feature-disabled-platform` or `feature-disabled-portal`), the save failed (`save-failed`), or the uploaded bytes matched the previous version. Such a deploy used to answer with a plain success, leaving no way to learn the note was lost. The warning is delivered both in JSON mode and in the `done` event when `?stream=true` is used.

### FIX-0727-6: bizproc-templates list without an explicit select now returns every field, including id

**Before**

`GET /v1/bizproc-templates` and `POST /v1/bizproc-templates/search` without an explicit `select` returned only the `documentType` field. Without `id` a client could not issue a follow-up `update`/`delete` — the list was useless without a second request carrying an explicit `select`.

**After**

Both calls without a `select` return the full declared field set (`id`, `moduleId`, `entity`, `documentType`, `autoExecute`, `name`, `description`, `modified`, `isModified`, `userId`). An explicit `select` works as before. The request format is unchanged.

### NEW-0727-7: uploading a file to a knowledge-base document now returns assetMarkdown right away

An upload via [POST /v1/note/documents/{documentId}/files](/docs/note/files/upload) used to return only `{ id }`, so getting the ready-to-embed block meant a second call to `GET /v1/note/documents/{documentId}/files/{id}` — or assembling the `[[image fileId=N]]` markup by hand.

The response now carries the whole file object — `id`, `documentId`, `name`, `size`, `mimeType`, `assetType`, `assetMarkdown` — the same shape GET returns. Upload the image, take `assetMarkdown` from the response, put it into the document body and call PATCH: no second request needed.

The change is additive: `id` stays in place with the same value, so a client reading only that field keeps working unchanged. If a particular Bitrix24 account returns an object without `assetMarkdown`, the platform invents nothing — the response carries exactly what Bitrix24 sent.

### FIX-0727-8: `ttlSeconds` bounds for access tokens in the machine-readable schema now match the behaviour

**Before**

The OpenAPI schema for [POST /v1/infra/servers/{id}/access-tokens](/docs/infra/access-tokens/create) advertised `ttlSeconds` between 60 seconds and 30 days. The platform, from day one, accepted 300 seconds up to 315,360,000 (ten years) and rejected anything outside that with `400 INVALID_TTL`. So a client — or a client library generated from the schema — that took the minimum straight from the schema hit a hard error on a value the schema itself offered, while the "unlimited" option from the interface looked unavailable through the API. The prose documentation was correct all along; only the machine-readable schema disagreed.

**After**

The schema reads its bounds and default from the very constants the request is validated against, so the two can no longer drift: minimum 300, maximum 315,360,000, default 86,400.

**What this means for integrators**

The endpoint's behaviour did not change — only what the machine-readable schema says about it. If you generate a client from the OpenAPI document and it validates `ttlSeconds` on your side, regenerate it: the old client would reject valid values above 30 days and allow values below 300 seconds that the platform always refused.

### NEW-0727-9: A placement app can auto-resize its iframe

An app embedded in a placement can now report its content height to the platform, which grows the iframe to fit it — previously the height was fixed by Bitrix24 and tall content was clipped. The app posts a message to its parent window: `window.parent.postMessage({ type: 'vibe:resize', height: <pixels> }, '*')`. The accepted type is `vibe:resize` or `vibe:setHeight` with a numeric `height` field; `targetOrigin` must be `'*'` — the browser checks it against the window's immediate parent. Recompute the height when the content changes, for example with a `ResizeObserver`. A full description and recommendations are in the "iframe auto-height" section of the app runtime guide.

The feature is activated per Bitrix24 account on the platform side; if the resize does not take effect yet, it is not active for your account.

### FIX-0727-10: list offset now counts records, not pages

**Before**

Bitrix24 returns lists in pages of 50 and reads the offset as a page ordinal, not a record count. We forwarded `offset` verbatim, so it was silently floored to a multiple of 50: `offset=0`, `offset=7` and `offset=49` all returned the same first page — no error, no warning. Walking a selection in steps smaller than 50 records looped on the first page, while a step of exactly 50 worked and made the parameter look healthy.

**After**

`offset` counts records on the generic list endpoints: `GET /v1/{entity}`, `POST /v1/{entity}/search`, `POST /v1/batch` (action `list`) and `GET /v1/{entity}/{id}/activities`. `offset=7` starts at the 8th record. Offset and `limit` are independent: `?limit=2&offset=51` returns exactly two records starting at the 52nd. Bitrix24 still pages by 50 — Vibecode fetches the page covering the requested position and trims the head; the cost is at most one extra Bitrix24 page per request.

Two consequences are fixed alongside. `meta.hasMore` accounts for the offset: previously, for entities whose list Bitrix24 returns under a named key — deals, contacts, companies, leads, tasks, orders, products, invoices and others, two dozen in all — it compared only the page length against the total and stayed `true` on the last page whenever `offset` was non-zero. `GET /v1/{entity}/{id}/activities` now honours `limit`: the underlying Bitrix24 method accepts no size limit, so the whole page used to come back regardless of the requested value.

When the requested position lands past what Bitrix24 returned for the fetched page — because it filters after paginating — the response carries `meta.warnings` with code `OFFSET_BEYOND_FETCHED_PAGE`; this used to surface as an unexplained empty list.

For deep paging over large selections a keyset cursor (`filter[>id]` with `order[id]=asc`) is still more reliable than an offset: it is depth-independent and stable under concurrent writes.

**Impact on integrators**

Nothing to change: an offset that is a multiple of 50 behaves exactly as before, and code that already used it that way keeps working untouched.

Two things are worth checking. First, `GET /v1/{entity}/{id}/activities` with an explicit `limit`: it used to return the whole page (up to 50 records) regardless of the value, and now returns exactly what was asked for. If your code relied on getting more than it requested in one call, raise the `limit` or page through the selection. Second, walking a selection in steps smaller than 50: that used to loop on the first page and now moves forward, so a loop that leaned on an extra counter-based exit will start returning new records.

Residual exceptions where the offset is NOT row-exact. First, four generic-layer entities whose Bitrix24 method rounds the offset down to a page boundary and whose selection cannot be widened beyond the requested size without risking lost records: `calendar-events`, `calendar-sections`, `telephony-lines`, `workgroups`. The offset there is unchanged. Second, individual endpoints with their own handlers, which were out of scope for this change: `/v1/warehouses`, `/v1/bookings`, `/v1/posts`, `/v1/requisite-links`, `/v1/lists`, `/v1/timeline-logs` and the stage history under `/v1/crm-extras`. Their behaviour is unchanged.

The four generic-layer entities above keep their previous offset behaviour, but their `meta.hasMore` is now more accurate: on the last page with a non-zero offset it could previously say "no more" while records remained.

### FIX-0727-11: a closed feedback ticket no longer reports itself as "in progress"

**Before**

A ticket caught by probe-campaign detection reported `status: NEW` to its author regardless of what had actually happened to it. The list filter runs on the real status, so a closed ticket both passed the "Solved" tab filter and rendered an "in progress" badge — the same ticket contradicting itself. Affected [GET /v1/feedback](/docs/feedback) and `GET /v1/feedback/{id}`.

**After**

Only the state the mask exists for is masked: the auto-archive. A closed ticket returns `RESOLVED`, a ticket in progress returns its real status, and an auto-archived one still arrives as `NEW`. Keys with feedback access (management, `vibe:feedback` scope) see the real status as before.

**What this means for integrators**

A client that read `status` and expected `NEW` for such a ticket now gets its actual status — that is the fix. In addition, on a detection-flagged ticket that is already closed, a withdrawal (`PATCH {"status":"WITHDRAWN"}`) and an author reply are now rejected with `409 FEEDBACK_CLOSED`, like on any closed ticket; they used to be accepted because the gate consulted the masked status.

### FIX-0727-12: Connect keys: stored scopes are authoritative — vibe:ai / vibe:search are no longer added automatically

**Before**

A key issued via Vibecode Connect automatically gained the platform scopes `vibe:ai` and `vibe:search` on every request, even when they were neither requested nor consented. Such a key could reach the AI endpoints (`/v1/chat/completions`, `/v1/ai/*`) and the Search endpoint (`/v1/search`), and the spend was charged to the billing account the Bitrix24 account is bound to.

**After**

The scopes stored on a key are now authoritative — the platform no longer widens them automatically. A key issued via Connect reaches the AI and Search endpoints only when the corresponding scope is actually present on the key; otherwise the response is `403`. Rotating a key preserves this property. Keys created in the dashboard are unchanged.

### FIX-0727-13: Apps: a derived key and app-scope sync grant no more than the calling key holds

**Before**

Calling `POST /v1/apps` with an authoritative key (issued via Vibecode Connect, or derived from one) minted the app's paired key with the full set of default platform scopes (`vibe:infra`, `vibe:ai`, `vibe:search`, `vibe:storage`) — even when the calling key held none of them. Likewise `PATCH /v1/apps/:id` could write a `vibe:*` scope into the app declaration that the key does not hold. In both cases the derived key gained the ability to reach AI and Search, charged to the billing account the Bitrix24 account is bound to.

**After**

A derived key receives exactly the platform scopes the calling key actually holds. When an authoritative key lacks a scope, a request carrying it in the body returns `403` `SCOPE_GRANT_REQUIRES_CONSENT`, with the unconsented scopes listed in `error.details.unconsented`. Scopes the key does hold (for example a consented `vibe:storage`) pass through as before. The derived key inherits the caller's expiry. App-scope synchronization to paired keys no longer adds `vibe:*` to an authoritative key — scope narrowing still applies. Keys created in the dashboard are unchanged: they still receive the default platform scopes.

### FIX-0727-14: Bitrix24 call timeout is now configurable, the internal retry after a timeout is removed

**Before**

The platform always aborted an HTTP call to Bitrix24 at the 15-second mark, and for read methods issued one internal retry after the abort — up to ~30 seconds before the `503 BITRIX_TIMEOUT` response. The retry also started a second concurrent execution of the same call on Bitrix24: dropping the connection does not stop Bitrix24 from processing the request.

**After**

- The time limit of a single Bitrix24 call is now platform-configurable (default stays 15 seconds). On accounts where Bitrix24 responds slowly the platform can raise the limit — requests that previously ended in a stable `503 BITRIX_TIMEOUT` now live to the real answer and return data.
- The internal retry after a timeout is removed for all methods. `503 BITRIX_TIMEOUT` on reads arrives roughly twice as fast (~15 seconds instead of ~30), and the request is no longer executed on Bitrix24 twice. `429` (rate limit) retries are untouched.
- The error text `Bitrix24 did not respond within 15s` now carries the actual limit (e.g. `within 60s`) — do not rely on the constant in the text.
