# API changes: September 8, 2026

[← Changelog](/docs/changelog) · [September 2026](/docs/changelog/2026-09)

### FIX-0908-1: warehouses no longer substitute a different id, and refuse an unsupported filter out loud

**Before**

A warehouse id in the request path was read leniently: `GET /v1/warehouses/12.5` was not rejected but quietly became `12`, so a read, an update or a delete landed on a different, genuinely existing warehouse and looked like a success. `12abc`, `1e2`, `007`, an id with a space and an id beyond integer precision behaved the same way, and the same lenient reading applied to `productId` and `limit`. The `filter[]` parameter on the warehouse list, on warehouse stock and on the stock totals was not read at all: any condition — an invented field name as well as a real one — was silently ignored and the full list came back. In the API description seven warehouse operations and the aggregation operation declared success as their only outcome, and warehouse deletion was declared as a 200 response although it answered an empty 204.

**After**

A warehouse id, `productId` and `limit` are accepted only in canonical form — digits only, no sign, no leading zero, no fraction, no exponent, and within integer precision. Anything else is refused with 400 `INVALID_PARAMS` before Bitrix24 is called, so a typo in the id can no longer reach someone else's object. A `filter[]` passed to the three endpoints above is refused with 400 `UNSUPPORTED_FILTER` listing the keys received, instead of a full list that looked filtered. Requests with a canonical id and without `filter[]` work as before, and their successful 200 response is unchanged. The API description now carries the real refusal codes (400, 401, 403, 404, 422) for the warehouse operations and for aggregation, and warehouse deletion is declared as 204 — the response it was already sending.

### BC-0908-2: A value of the wrong type in a write field is refused instead of silently corrupting data

> Old format supported until: not provided

**Before**

A field declared as a number in the entity schema accepted any string: `{"amount":"one hundred"}`
on [POST /v1/deals](/docs/entities/deals/create) answered `201` while the amount was stored as
zero — Bitrix24 casts a non-numeric string to `0`. On update this erased an already-stored
amount: a `PATCH` with an unparsable value answered `200` and zeroed the field. Every other
numeric field in the registry behaved the same way (`sort` on catalogs and products), boolean
fields accepted any word, and the `color` string field on order statuses was silently truncated
at the database column width.

**After**

Such a value is refused with `400` and error code `INVALID_PARAMS` before any call reaches
Bitrix24: a numeric field accepts a JSON number or a numeric string using `.` as the decimal
separator; a boolean accepts `true`/`false` and the recognized string forms (`"yes"`/`"no"`,
`"y"`/`"n"`, `"1"`/`"0"`, `"true"`/`"false"`); a string field with a declared length limit
accepts a value within that limit. The `priority` and `status` fields on tasks accept only the
values listed in their enumeration.

**What integrators should do**

Send monetary amounts as a number or as a string using a dot: `1234.56`, not `"1234,56"` — a
comma decimal separator is refused with an explicit error rather than quietly reinterpreted.
Review places where values arrive from external systems as strings: such a request used to
answer with success while the data was lost, and now returns an error naming the field.

### FIX-0908-3: Paging through recent dialogs no longer loses or repeats records

**Before**

[GET /v1/chats/recent](/docs/chats/discovery/recent) forwarded the page size and offset to
Bitrix24 as they were, where they bound an internal table join rather than the dialog count. A
page could return fewer records than requested while promising another one; the last record of a
page arrived with its chat and last-message fields empty; and neighbouring windows overlapped, so
a `offset += size` walk returned some dialogs twice and missed others.

**After**

The page is read with headroom and sliced on the Vibecode side: a window holds exactly the number
of fully populated records requested, under-populated rows are not returned, and the has-more flag
is computed from the window actually served. A window past the end of the list comes back empty and
no longer promises another page. A request that names no page size now returns 50 records — the
size used to be Bitrix24's to choose and is now stated explicitly.

A caveat about deep windows: the overlap protection holds while offset plus page size stays at or
below 190 — the window together with the headroom for under-populated rows has to fit inside one
Bitrix24 page, and that page is 200 records. Past that the offset is forwarded to Bitrix24 and, on very long lists,
neighbouring windows may overlap again — a limitation of the method itself, not of the platform.

**What integrators should do**

Nothing: the requests are unchanged and the response is now honest. If your code de-duplicated
dialogs by hand, that workaround is no longer needed.

### BC-0908-4: Fields that Bitrix24 assigns itself are now declared read-only

> Old format supported until: not provided

**Before**

Five fields were declared writable and accepted with `200`/`201`, but Bitrix24 never stored
them — the value stayed empty, and the response gave no way to tell that apart from a
successful write: the origin identifiers on sales pipelines, the owner module on
[document templates](/docs/entities/doc-templates/update), and the problem flag with its
reason on [payments](/docs/entities/payments/update) when updating.

**After**

These fields are declared server-assigned: a write attempt is refused with `400` and error code
`READONLY_FIELD` before any call reaches Bitrix24, and the field description states plainly that
the platform sets the value. The payment problem flag and its reason are still accepted when
creating a payment — only the update path, where the value was lost, is closed. The pipeline
origin identifiers were not declared in the schema at all and were picked up as writable; they
are declared now and refused.

**What integrators should do**

Remove these fields from an update request body. The platform stamps the template module itself,
the pipeline origin identifiers are never persisted, and the payment problem flag should be set
when the payment is created.

### FIX-0908-5: search and aggregate answer with an error on a wrongly typed parameter instead of silent emptiness

**Before**

In the body of `POST /v1/{entity}/search`, the `limit` and `select` fields accepted a value of any type. A non-numeric `limit` (for example `"abc"`) produced `HTTP 200` with an empty record list and `meta.hasMore: true` at the same time — a client paging while the platform promises more went into an endless loop, receiving neither a record nor an error. A `select` value that was neither a string nor a list of strings turned into a field name such as `"999"` or `"[object Object]"`, travelled to Bitrix24 and came back as `HTTP 502` with the code `BITRIX_UNAVAILABLE` — the platform reported its own unavailability where the fault was in the client request.

In the body of `POST /v1/{entity}/aggregate`, the `groupBy` field was checked only when it arrived as a string or a list of strings. A number, a boolean or an object was dropped silently: the answer came back as `HTTP 200`, without the `groups` key and without a warning — the client asked for a breakdown, received the overall total, and had no way to notice.

The `meta.total` key of list answers carried a fabricated number on pages beyond the collection, growing together with the offset: on an account holding five storages, `GET /v1/storages?offset=100` answered `total: 100`, and `GET /v1/storages?offset=1000` answered `total: 1000`.

**After**

A wrongly typed `limit` is refused with `INVALID_LIMIT`, a wrongly typed `select` with `INVALID_SELECT_TYPE`, and a wrongly typed `groupBy` with `INVALID_PARAMS`; all three refusals arrive as `HTTP 400` and name the type that came in. A numeric `limit` behaves as before, including the string spelling of a number (`"50"`), while `null`, an empty string and an empty list still mean "parameter not supplied" and are not errors.

The `meta.total` key is withheld when the page is empty and the offset is above zero: nothing can vouch for a count there, and the description of the field already warns that the key may be absent at a non-zero offset. Wherever the page is non-empty or the offset is zero, `meta.total` arrives as before, and the response remains HTTP 200.

### FIX-0908-6: task favorites, comments and time entries answer honestly

**Before**

Adding a task to favorites and removing it from them answered `200` with `success: true` even for a task that does not exist: Bitrix24 confirms that action for any identifier while saving nothing. The `404 TASK_NOT_FOUND` promised by the API description never arrived.

The task comment list ignored `offset` on a request without a filter and with a sort by identifier — the same page came back at any value — and `meta` was counted over raw chat messages. On a task whose slice held only system notifications the answer was `data: []` together with `total: 1` and `hasMore: true`, so a `while (hasMore) offset += limit` walk never finished.

Addressing a checklist item or a time entry that does not exist answered `422 BITRIX_ERROR` carrying the internal Bitrix24 exception text (`TASKS_ERROR_EXCEPTION_#512; …; 512/TE/ITEM_NOT_FOUND_OR_NOT_ACCESSIBLE`), and on the time route that text spoke about a checklist.

The `from` and `to` parameters of `GET /v1/task-time` were not validated: `?from=notadate` silently returned the whole range with status `200`, as if no period had been requested.

**After**

Before adding to or removing from favorites the service checks that the task exists and is accessible to the key, and answers `404 TASK_NOT_FOUND` when it is not. On a task that exists the response remains `200`. Rate limiting and an authorization failure are not substituted by "not found".

The comment list honours `offset` on every read path, and `meta.hasMore` and `meta.total` are counted over comments rather than raw chat messages: a slice of only system notifications reads further, and `hasMore: false` with `total: 0` means there are no comments.

A missing checklist item and a missing time entry answer `404 NOT_FOUND` — without the internal Bitrix24 text and in terms of the requested resource. The code is declared in the API description for every method that addresses a single record.

An unparseable `from` or `to` answers `400 INVALID_PARAMS` before Bitrix24 is called; `YYYY-MM-DD` and ISO 8601 are accepted. An empty value still means no filter. The API description of `GET /v1/tasks/{taskId}/comments` now declares the `limit` and `offset` it actually reads.

### BC-0908-7: Lead conversion stamps backlinks and answers in the standard read format

> Old format supported until: not provided

**Before**

[POST /v1/leads/{id}/convert](/docs/entities/leads/convert) answered `200`, but the deal, contact
and company it created carried no link back to the lead they came from: filtering deals by lead
returned nothing, and a deal could not be traced to its source. The response itself came in raw
Bitrix24 shape — upper-case underscored keys and stringified numbers — the only such response
among the routes of this section: the platform read the created records through legacy methods
whose field names did not match the schema, so the values bypassed normalization.

**After**

Every created record gets a link to the source lead, and the lead itself gets links to the
contact and company that were created (only to those actually created). The response is read
through the same method as an ordinary read of a deal, contact or company, so its shape matches
[GET /v1/deals/{id}](/docs/entities/deals/get) and its neighbours — field names in the platform's
usual style, numbers as numbers.

**What integrators should do**

If your parsing of the conversion response was written for the raw Bitrix24 shape, switch it to
the ordinary read format used by every other route of this section. The backlinks only add data
and break nothing: filtering deals by lead now finds the created deal.

### BC-0908-8: a payment currency that disagrees with the order is no longer swapped silently

> Old format supported until: not provided

**Before**

[Creating a payment](/docs/entities/payments/create) accepted any `currency` with `201`, while
Bitrix24 stored the payment in the currency of its order — the requested one was dropped
without a word. The field description meanwhile promised a free choice with a pointer to
`GET /v1/currencies`, so the discrepancy only surfaced during reconciliation.

**After**

When `currency` is passed and does not match the currency of order `orderId`, the request is
refused with `409` and error code `CURRENCY_MISMATCH` before any call reaches Bitrix24; the
message names both currencies and the order. A matching currency is still accepted, so a
payment read and sent back whole keeps working. The same rule applies to
[batch](/docs/batch) payment creation. When the order currency cannot be read, the request is
let through rather than refused.

**What integrators should do**

Either omit `currency` entirely to inherit the order currency, or send exactly the one the
order carries: `GET /v1/orders/:id` shows it.

### NEW-0908-9: the standalone server auto-sleep policy is now visible through the API

[POST /v1/infra/servers](/docs/infra/servers/create) returns the saved `data.sleepAfterMinutes` for every successful create, reuse, and idempotency replay. This is a response field, not a new create parameter. A successful standalone deploy returns the same policy snapshot in `data.sleepAfterMinutes`, while SSE returns it as `sleepAfterMinutes` in the `done` event. For a regular standalone server that is not covered by agent/bot idle-sleep protection, a numeric timeout with no enabled recurring wake windows appends an entry starting with `AUTO-SLEEP POLICY:` to `warnings[]` with two choices: a wake window for a task that finishes before the timeout, or a deliberate always-on mode. A window only wakes the server and does not keep a background process running. The existing successful `slept: false`, `reason: "WAKE_IMMINENT"` outcome of [POST /v1/infra/servers/:id/sleep-now](/docs/infra/lifecycle/sleep-now) is now documented for an imminent wake window. The HTTP 200 response itself is unchanged.

### BC-0908-10: writing an amount to a smart-process type without product rows no longer answers with a false success

> Old format supported until: not provided

**Before**

`POST /v1/items/{entityTypeId}` and `PATCH /v1/items/{entityTypeId}/{id}` accepted `opportunity` and `isManualOpportunity` on any smart-process type and answered `201`/`200`. When the type has product rows disabled (`isLinkWithProductsEnabled: false`), Bitrix24 does not store those fields — the amount was silently lost while the caller saw success and moved on.

**After**

The platform compares what was requested against the re-read record — it already re-read it before responding, so no additional Bitrix24 calls were added. When the request EXPLICITLY asked for manual mode (`isManualOpportunity: true`) and did not get it, the answer is `422 AMOUNT_NOT_APPLIED`: `error.details.unappliedFields` lists the fields that were not applied, and `data` carries the actual state of the record that was already created or updated. An amount sent WITHOUT that flag is not checked: from the response it is indistinguishable from a legitimate recalculation from the product rows, which works as documented — so those requests answer exactly as before. The check does not touch deals, leads, invoices or quotes — those always have product rows.

### FIX-0908-11: the host from galaxyId is now available for reading

**Before**

The Galaxy application creation response already contained `galaxyId`, but [GET /v1/infra/servers](/docs/infra/servers/list) did not show that shared host, and [GET /v1/infra/servers/:id](/docs/infra/servers/get) answered `404 NOT_FOUND` to the same API key.

**After**

Both GET operations return a limited host projection with `access.via: "galaxy-reference"`. It contains only safe read fields. Host management and application operations still require a resource managed by the current key.

**Impact on integrators**

Requests require no changes. A client using the newly visible shared host should check `access.via`: `galaxy-reference` means read-only access.

### NEW-0908-12: key issuance now says whether the key got access to Bitrix24 data

`POST /v1/connect/token` now returns an optional `b24_credentials` field, and
`GET /v1/cowork/me` the same object as `b24Credentials`. It answers whether the issued key can
read Bitrix24 account data: `ready: true` — the rights work, `ready: false` — they do not, and
a `reason` from a closed set comes with it (`WEBHOOK_NOT_CONFIGURED`, `WEBHOOK_MINT_FAILED`,
`WEBHOOK_MINT_REFUSED_BY_PORTAL`, `INT_TARIFF_REQUIRED`, `VIBE_SCOPES_ONLY`), plus an
`upgradeUrl` for the reason that names a paid plan. The `401 TOKEN_MISSING` body already
carries the same set, so a client parses it with one branch of code.

Why it exists. A key is issued even when the account refuses to connect it to its own data:
the key still works for AI calls, while every request for account data answers
`401 TOKEN_MISSING`. Until now a client only found that out by hitting it, and could not tell
a missing right from a network failure. The state now arrives together with the key and can be
re-read at any time from `GET /v1/cowork/me`.

The field is additive: the other fields and the response status are unchanged, and requests
that ignore it behave exactly as before. It is absent where the question does not apply — for
example an app key, whose account tokens are stored separately.

### FIX-0908-13: workday history is returned while a day is still open

**Before**

`GET /v1/workday/records` answered `502 BITRIX_UNAVAILABLE` whenever the page contained a
record of a workday that was not finished yet. Such a record carries no end time, no duration
and no approval flag, and the endpoint required those fields to be filled in, rejecting the
whole page — including the closed days that arrived in the same response. In practice every
employee currently at work got an error.

**After**

The record of an unfinished day is returned as is, together with the rest of the page: the
HTTP 200 response is unchanged and the closing fields arrive exactly as Bitrix24 sent them.
The checks that keep a wrong answer from passing silently are untouched: the page is still
rejected when a record belongs to a different employee or carries no valid start time, which
is what `tzOffset` is derived from. No client action is required.

### NEW-0908-14: agents are available for Open Channel binding

Added `GET /v1/agents`: the method returns agents owned by the API-key owner and the `bitrixBotId` value accepted by `welcomeBotId` in an Open Channel configuration. `agentId` is not used for a Hermes agent; `queue` continues to contain human operator IDs.

### NEW-0908-15: department include in user records

The `include=department` parameter adds an `_included.departments` array to a user record with the full records of every department referenced by `departmentId`. A user without a department returns an empty array. The request requires the `user` and `department` scopes.

**Affected endpoints:** [GET /v1/users](/docs/entities/users/list), [GET /v1/users/:id](/docs/entities/users/get), [POST /v1/users/search](/docs/entities/users/search)

### NEW-0908-16: labels and descriptions for every basic field

In `GET /v1/{entity}/fields` responses, every declared basic field now has a localized label and description: Russian for Russian-language Bitrix24 accounts and English for international ones.

### NEW-0908-17: protection against writing over newer sources

`POST /v1/infra/servers/{id}/sources` accepts an optional `X-Parent-Version: v<N>` header —
"this is the version I edited". If the latest version has moved on by the time the write
lands, it is refused with `SOURCE_VERSION_CONFLICT` (409) and
`error.details.latestVersionId` names the current one (`null` when the server has no live
versions at all). A malformed header value is `400 INVALID_VERSION_ID`. Without the header the
endpoint behaves exactly as before.

"Current" here means the newest LIVE version — the same one that heads the version list and can
be downloaded, so the version named in the refusal can always be fetched and the write retried.
Deleted versions do not count, though their numbers are never reused. Deduplication is no
exception: an archive byte-identical to an existing version is refused with `409` too if the
head has moved on.

The guarantee runs in a single direction: your write will not land on top of a newer version.
The reverse half does not exist — sources saved from the browser come from the deploy
auto-save, which has no parent to declare, so a later browser deploy can still save over
yours. The refusal is decided in the same place the version is created, so two uploads
declaring the same parent cannot both win, and the refused archive does not stay in storage.

The `POST /v1/cowork/deploy-key` response gained two fields about the move onto the fresh key:
`repointTruncated` — not everything moved, the remainder goes on the next call, and
`applicationSlotBlocked` — an application card stayed on its previous key because the fresh
key's slot is taken by another card. Previously a partial outcome was visible only in the
platform's own journal.

**Affected endpoints:** `POST /v1/infra/servers/{id}/sources`, `POST /v1/cowork/deploy-key`

### NEW-0908-18: replace an application key in one call, and see its mask on the card

An application can be added to Cowork/Code from the catalog — created earlier, on another
machine, or before the Code tab existed. Such an application has no raw key and none can be
recovered: the platform stores only a hash. There was nothing to publish with.

The new `POST /v1/cowork/applications/{id}/key` brings an
application key into working order in one call. An empty personal-key slot gets a key issued,
an occupied one gets it replaced; `issued` (`minted` or `rotated`) says which happened. The raw
key is returned once, as it is on application creation. `Idempotency-Key` is required, and a
replay answers `rawApiKey: null` with `KEY_NOT_REPLAYABLE`. If the request failed AFTER a key was issued
(a 500 naming `orphanKeyId`), that Idempotency-Key is spent for good: repeating it answers
`IDEMPOTENCY_KEY_ALREADY_USED` rather than minting a second key. Read the card, then retry
with a NEW Idempotency-Key. One case apart: when the key was issued but the card could
not be read afterwards, the answer is still `201` with the secret, `application` arrives null,
and `warningCodes` carries `APPLICATION_CARD_UNAVAILABLE`. Read the card with
`GET /v1/applications/{id}` — there is no reason to lose the only copy of the secret over it.

The previous key is not revoked immediately: it gets a one-day expiry, so a publish already in
flight finishes. `previousKey.graceUntil` names the moment it stops authenticating. The request
body accepts `syncServerEnv: true` — the platform then swaps the key in the deployed server's
environment and restarts the application, and `envSync` reports the outcome: updated, no such
line in the file, server asleep, restart failed, and so on. Without it the application keeps
the old key and starts getting refusals a day later.

Refusals arrive as distinct codes rather than a generic `400`: `NOT_APPLICATION_OWNER` — the
application belongs to someone else; `KEY_ROTATE_OAUTH_APP_KEY`, `KEY_ROTATE_SYSTEM_KEY`,
`KEY_ROTATE_LIVE_OAUTH_GRANT`, `KEY_ROTATE_NOT_ACTIVE` — the key in the slot cannot be
replaced, the code says why; `KEY_ROTATE_KEY_VANISHED` — the key was removed while the request
was in flight; `KEY_LIMIT_REACHED` — the key quota is spent;
`APPLICATION_KEY_REPLACE_IN_PROGRESS` — a replacement for this application is already running.
One replacement per application at a time, whatever Idempotency-Key the second request carries:
otherwise both would issue a key and the slot would go to whichever finished last. A replacement
abandoned by a crashed request stops blocking after two minutes.

On an empty slot the issue goes through the platform-wide access gate, like every other
key-issuing door: an account without access gets a `402`. A rotation deliberately does not —
it carries the previous key's rights forward and creates none, and an owner whose access has
lapsed must still be able to wind their affairs down.

The application card gained a `key` block: `slot` (`auth` or `api` — which key is shown),
`present`, `prefix`, `suffix`, `status`, `expiresAt`, `lastUsedAt`, `rotatable` and
`rotateBlockedReason` (whether replacement works and why not), `affectsShownKey` (whether it
touches the very key shown). The mask is assembled client-side from `prefix` and `suffix`.
The `rotatable` flag reports what the platform ALREADY
knows, and neither side of it is a guarantee: `false` means "a refusal is known, and here is
its reason" (`rotateBlockedReason`, which covers access as well as key state), `true` means "no
known refusal". Driving the button off it is convenient — grey it out on `false` and show the
reason — but do not turn `false` into a dead end: the refusal may already be gone while the
card has yet to learn of it (on the lag and on the two gates outside the flag, see below).
Judge by the door's answer, not by the flag alone. The block is ALWAYS there; someone the application was merely shared with
receives empty fields (`present: false`) and `rotatable: false` with reason `NOT_MANAGER` —
which means "not for you", not "there is no key". On an EMPTY slot, where the call would issue a key, the flag also covers the issuing
gates: `ISSUANCE_BLOCKED` (the account has no platform access), `INFRA_DISABLED`
(infrastructure is switched off), `KEY_QUOTA_REACHED` (the key quota is spent) and
`READONLY_POLICY` (the account issues read-only keys only). About the CALLING key the field
speaks the door's own codes, and they repeat on EVERY card of the response: `INSUFFICIENT_SCOPE`
(the key does not carry the `vibe:cowork` scope) and `COWORK_HARNESS_KEY_FORBIDDEN` (the key was
issued for an external agent). Accuracy is one-sided by design: `false` on an empty slot is read
off the access state the platform already knows, so an account whose Bitrix24 commercial plan
changed seconds ago may still show `false` until that state refreshes. TWO Cowork/Code gates stay
OUTSIDE the field and can still refuse a call made on a `true`: the platform-wide kill switch
(503 `COWORK_FEATURE_DISABLED`) and the state of your Cowork/Code seat (403
`COWORK_NOT_ACTIVATED`, which `GET /v1/cowork/state` already reports). Neither is a fact about the
card itself, so neither is repeated per card — handle the door's refusal instead of reading `true`
as a guarantee.

**Affected endpoints:** `POST /v1/cowork/applications/{id}/key`,
[GET /v1/applications](/docs/applications), `GET /v1/applications/{id}`

### NEW-0908-19: read the sprint list, active sprint, and a sprint by ID

The Vibecode API adds [GET /v1/scrum/sprints](/docs/scrum/sprints/list) for reading visible sprints, [GET /v1/scrum/sprints/active](/docs/scrum/sprints/active) for reading a project's current sprint, and [GET /v1/scrum/sprints/:id](/docs/scrum/sprints/get) for reading one sprint by ID. The response contains its name, dates, status, and project link. If no sprint is active, the active-sprint endpoint returns `data: null`.

### BC-0908-20: a paused Cowork/Code seat no longer opens Bitrix24 past the account plan

> Old format supported until: not provided

**Before**

A Cowork/Code desktop key and an agent key opened the Bitrix24 REST API on an account whose plan does not carry REST access, regardless of whether the seat itself was in force. A paused seat kept that ability until its key was revoked — about a month.

**After**

The ability follows the state of the seat. The bypass applies only to a seat in the `ACTIVE` state, including a seat with a scheduled cancellation, until the end of its paid term. For a paused, cancelled or parked seat the key is served by the ordinary account rule: where the Bitrix24 plan already carries REST access nothing changes, and on a plan without it the calls to Bitrix24 get the same account refusal as any other application.

**What integrators should do**

Resume the Cowork/Code subscription, or move the account onto a paid Bitrix24 plan. The current state of the seat arrives in the `subscription.state` field of the [GET /v1/cowork/state](/docs/cowork/state) response.

### FIX-0908-22: the sources registry no longer advertises a door that answers 403 to the calling key

**Before**

[GET /v1/me/sources](/docs/source-storage/registry) derived `reachableViaApi` from ROW
reachability alone — "is the server alive, is the app not deleted" — and never looked at the KEY
that called. The doors its pointers lead to are key-scoped and reject an OAuth application key
whenever the server belongs to someone else.

In practice: the listing enumerates snapshot owners per USER, not per key, so calling with
your own application's key returned your servers created with PERSONAL keys carrying
`reachableViaApi: true` and a working-looking `listEndpoint` — while
[GET /v1/infra/servers/{id}/sources](/docs/source-storage/servers) answered that very key
`403 NOT_AUTHORIZED` at that very address.

The same lie appeared on application rows: the author of applications A and B, calling with the
key of B, saw the row of A with a working pointer whose door answers
`403 SOURCE_APP_ID_MISMATCH`.

**Now**

`reachableViaApi` answers one question: will the CALLING KEY be admitted through the drill-in. A
row whose door that key will not open carries `reachableViaApi: false` with `listEndpoint` and
`latestDownloadEndpoint` set to `null`. Both `kind` branches of the listing (`server` and
`legacy-app`) compute it with the same predicate the door itself uses.

The row itself does NOT disappear from the listing: a false promise was withdrawn, not visibility.
The owner still sees that snapshots exist and whose they are via `user.id`.

**What integrators should do**

Check `reachableViaApi` before following `listEndpoint` or `latestDownloadEndpoint`. Both fields
were already declared `string | null` and already arrived empty for an orphaned server and a
soft-deleted application, and `reachableViaApi: false` was already a documented value for those
same two cases — so a client that honoured the contract needs no change. If you get `false` where
you expected `true`, you called with an application key: repeat the call with a personal key
(your own or the server owner's) or with a Bitrix24 account administrator's key. The same holds for an
application row — an application key opens only its own application.

**What this does NOT change**

Access. No door started or stopped admitting anyone: the access rule is unchanged, only the
listing stopped promising access that was never there. Collapsing the doors of one server onto a
single ownership model is a separate breaking change and ships as its own entry — and so do the
publish and deploy refusal hints, because there the same address is withdrawn TOGETHER with the
`hint.requiredAction` field, which is a documented-field removal.
