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

API changes: September 8, 2026

← Changelog · September 2026

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 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 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, and the problem flag with its reason on payments 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.

Old format supported until: not provided

Before

POST /v1/leads/{id}/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} 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 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 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 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 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 did not show that shared host, and GET /v1/infra/servers/:id 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, GET /v1/users/:id, POST /v1/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, 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 for reading visible sprints, GET /v1/scrum/sprints/active for reading a project's current sprint, and GET /v1/scrum/sprints/:id 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 response.

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

Before

GET /v1/me/sources 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 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.