For AI agents: markdown of this page — /docs-content-en/changelog/2026-09-22.md documentation index — /llms.txt
API changes: September 22, 2026
BC-0922-1: single-entity batch paginates: a sub-call offset is finally read
Old format supported until: not provided
Before
A list sub-call of POST /v1/{entity}/batch ignored offset: the parameter was never read
out of params, never reached Bitrix24 and was never applied as a client-side window. Two
sub-calls differing only in the offset came back HTTP 200 with the same records, so paging
through a single-entity batch was impossible — while the global POST /v1/batch
and the single GET /v1/{entity} and
POST /v1/{entity}/search all read it. For entities whose
Bitrix24 method supports no navigation that sub-call also answered HTTP 200 — the first
portion of records at any offset, that is, silently not the window the client asked for.
After
The sub-call reads offset and answers with an honest [offset, offset + limit) window,
through the same resolution the global door uses: the offset is aligned to the Bitrix24 page,
the remainder is trimmed on our side, and for entities whose method applies no navigation
(statuses, deal-categories, bizproc-activities, bizproc-robots) — as well as those
returning the whole set in one response — the window is cut over the complete collection. A
sub-call result now carries hasMore next to total, and a degenerate window arrives as an
OFFSET_BEYOND_FETCHED_PAGE warning in the meta of that same sub-call.
There is one breaking part: an entity whose Bitrix24 method cannot honour an offset (today
that is telephony-lines) answers a sub-call with an offset above zero with an
UNSUPPORTED_OFFSET error instead of the former HTTP 200 and first portion of records. The
refusal is per sub-call — the neighbours in the batch still run, and the batch request itself
still answers HTTP 200. Along with the offset the sub-call now honours limit as well: a page
Bitrix24 returned longer than asked is cut to limit (50 by default), as on every other door.
Such a sub-call used to hand back everything Bitrix24 sent — with windows that would mean
pages of different offsets overlap and a client reads the same records twice.
What integrators should do
- Drop
offsetfrom sub-calls totelephony-lines: an offset is unavailable for that entity, and theUNSUPPORTED_OFFSETresponse says so directly. Narrow the selection with a filter instead. - If your code relied on a page longer than requested — pass the number you need in
limitexplicitly, up to 5000 records. - Page by the
hasMoreof the sub-call result rather than by arithmetic overtotal.
NEW-0922-2: two new refusal codes for agent runtime keys
An agent runtime key now receives two refusal codes that did not exist before. While an agent turn is waiting for user confirmation, POST /v1/chat/completions answers 409 with the code agent_turn_awaiting_confirmation: no model calls are made for that turn, so waiting for a human answer no longer consumes the limit. The code clears as soon as the user confirms or cancels the operation, and also once the waiting period expires.
A DELETE call to V1 from the same key that matches the previous one by method and address within a 30 second window answers 409 with the code AGENT_WRITE_ALREADY_EXECUTED. Such a call never reaches the Bitrix24 account, while error.firstStatus carries the response code of the first delete and error.firstExecutedAt carries its time. A failed first delete does not lock the repeat: a retry goes through as usual. Create and update are not deduplicated.
Keys that do not belong to an agent runtime are touched by neither code, and previously successful responses are unchanged.
NEW-0922-3: result delivery for a long-running solution method
A public route POST /v1/solution-calls/:callId/result is available: the solution posts the result of an asynchronous contract call (solution.invoke with mode: async) there. The address, a single-use reply token and the deadline reach the solution together with the call itself in the X-Vibe-Reply-Url, X-Vibe-Reply-Token and X-Vibe-Reply-Deadline headers; the platform sends the call with Prefer: respond-async, so the solution either answers 202 and posts the result here or answers 200 with the same body right away. Authentication is Authorization: Bearer vcr_…; no platform API key is accepted on this route. The body is {"outcome":"completed","result":{…}} or {"outcome":"failed","error":{"code","message","problem"}}; the answer is 200 with data.state. Errors: 401 REPLY_TOKEN_INVALID (the token matches no open call; whether the callId exists is not disclosed), 409 SOLUTION_CALL_CLOSED (the call is already closed, error.state carries its state), 422 RESULT_INVALID (body over 512 KiB, not JSON of the expected form, or result off the schema of the contract snapshot the call was accepted under). A 422 closes the call as failed — retrying with the same body is pointless. The platform delivers the accepted outcome to the Bitrix24 account itself. Existing calls and answers are unchanged: before this release mode: async answered FEATURE_DISABLED, and on Bitrix24 accounts where the capability is not enabled the answer stays the same.
FIX-0922-4: a Bitrix24 «parameter not supplied» refusal answers 400, not 422
Before
When Bitrix24 refused a call before the method ran — because the call did not carry a parameter the method declares as required — Bitrix24 answered Could not find value for parameter {…} and the Vibecode platform passed that out as 422 BITRIX_ERROR, that is, as a business error of the Bitrix24 account. The name of the missing parameter lived in the message text alone, there was no hint, and repeating the same request returned the same refusal — on the requisite list (GET /v1/requisites) 92 calls repeated that way across five accounts in three weeks.
After
The same refusal arrives as 400 INVALID_PARAMS — the code the other parameter rejections from Bitrix24 already use — and error.hint names the parameter whose value was missing and says that repeating the request unchanged will not help. The answer to any request Bitrix24 accepts is unchanged.
Impact on integrators
A client that told this case apart by 422 will now see 400 INVALID_PARAMS. The answer still carries the Bitrix24 text in error.message, and the parameter name is now also in error.hint. Refusals raised by the platform's own checks before it calls the account keep answering 400 MISSING_REQUIRED_PARAMS.
FIX-0922-5: Bitrix24 account address change: the call answers 409 `PORTAL_ADDRESS_CHANGED` instead of 500
Before
After a Bitrix24 account address change, the webhook behind an issued key stayed bound to the
previous address. The platform refused such a call before sending it — a secret is never sent to
an address the account no longer lives at — but the refusal was unnamed: every proxied request
answered 500 INTERNAL_ERROR with no code and no hint, and inside a batch envelope the sub-call
fell into the generic CALL_FAILED bucket. The response told the client neither the cause nor
the way to recover.
After
The refusal names both the cause and the action: 409 with code PORTAL_ADDRESS_CHANGED, and the
hint points at reconnecting the key — the Reconnect button in the Keys section; the key string and
any bots linked to it are preserved. The same code with the same hint travels in the batch
envelope sub-error. The refusal carries no retry deadline on purpose: retrying does not help until
the key webhook is re-issued.
FIX-0922-6: deleting an app releases the authorization key held by its card
Before
DELETE /v1/apps/{id} removed the app and revoked its paired keys, yet the application card
still counted the authorization key as issued. Issuing a new one was impossible — the issue
endpoint answered 409 APPLICATION_HAS_AUTH_KEY and the re-issue endpoint answered
409 APPLICATION_NO_AUTH_KEY. For the same reason POST /v1/cowork/applications/{id}/key
returned warningCodes containing APPLICATION_HAS_AUTH_KEY for an application that no
longer had an authorization key.
After
The delete now releases that link as well: afterwards the application counts as having no
authorization key, the false APPLICATION_HAS_AUTH_KEY warning for it is gone, and issuing a new
key succeeds. The
successful response of the delete itself is unchanged — it is still HTTP 204.
FIX-0922-7: the OpenAPI aggregation schema now describes fields the way the API accepts them
This corrects the DESCRIPTION published by GET /v1/openapi.json; the behaviour of POST /v1/{entity}/aggregate is unchanged and its responses are the same.
Before
The aggregate[].field property was published as a closed list of "*" plus the entity's grouping fields. Grouping fields and the fields sum/avg/min/max can be computed over are different sets, so a client validating the request body against the spec withheld calls the API executes: aggregation over a number-typed field of the entity outside the grouping list (on deals that is probability, taxValue, receivedAmount and others) and over a user field of the account typed integer, double or money. At the same time the spec admitted pairs the API always answers 400 INVALID_PARAMS to: count over a named field, and a numeric function over "*" or an empty name. The groupBy property was published as the same closed list and rejected the user fields the API accepts.
After
aggregate[].field is described together with function: count is computed over "*", while sum/avg/min/max take the name of a numeric field — either declared by the entity or a user field of the account. The entity's number-typed fields are listed in examples as a hint rather than a constraint: the set of user fields depends on the account and cannot be published in a shared spec, and the current list is served by GET /v1/{entity}/fields. The pairs the API always refused are now refused by the schema as well — including aggregation over a field the entity declares with a non-numeric type: such a name is rejected by the API regardless of what the account has configured. groupBy admits user fields and rejects names colliding with the response keys (count, aggregates, meta, groups), which the API never accepted. The operation's prose description no longer passes the grouping list off as the list of aggregation fields: it now names the two roles separately.
FIX-0922-8: default per-user limits raised — keys, servers, bots
Before
On a Bitrix24 account where these limits were never set by hand, the per-user defaults were 10 API keys, 5 servers and 5 bots.
After
The defaults are raised to 100 keys, 50 servers and 50 bots per user. This affects only Bitrix24 accounts that never set their own values; an account with its own configuration keeps it unchanged. Existing requests keep working — there is simply more headroom.
BC-0922-9: Retrying an unfinished key rotation requires recovery
Old format supported until: not provided
Before
After an incomplete key-replacement rollback, a request with a new Idempotency-Key
could answer 201 for an orphan candidate although revoking the source key's tokens
had not been confirmed.
After
Another rotation of either the source or replacement key answers
409 KEY_ROTATION_RECOVERY_REQUIRED. Replacing an application key or assigning
such a key to a server answers 409 APPLICATION_KEY_RECOVERY_REQUIRED.
A new Idempotency-Key does not bypass the refusal.
Minting an access token during an unfinished rotation also answers
409 KEY_ROTATION_RECOVERY_REQUIRED; if the server has already changed its key,
it answers 403 SERVER_KEY_MISMATCH, without a new token.
Transferring a bot with a source or target key in unfinished rotation answers
400 TARGET_KEY_INVALID with error.reason: recovery_required, without changing ownership.
A concurrent transfer for the same Bitrix24 account answers the existing
409 BOT_TRANSFER_CONFLICT instead of an opaque 500, without changing ownership.
Affected endpoints: POST /v1/keys/{id}/rotate, DELETE /v1/keys/{id}, POST /v1/cowork/applications/{id}/key, POST /v1/infra/servers, POST /v1/infra/servers/{id}/access-tokens, POST /v1/bots/{botId}/transfer.
Unconfirmed revocation answers 500 KEY_ROTATE_REVOKE_FAILED without a raw secret.
The candidate is named in error.details.orphanKeyId, and the rollback remainder
in error.details.strandedSlots. A nonempty remainder requires support.
An absent or empty remainder does not permit retry by itself.
An unreferenced candidate can only be removed through the normal DELETE endpoint
after the API confirms that no other resource still references it.
Impact on integrators
Do not retry with a new idempotency key. Complete recovery through support or verify normal deletion of the empty candidate, then re-read the current key and server state before a new request. The previous behavior is no longer supported.
FIX-0922-10: JavaScript service names no longer replace a parameter value
Before
Names every JavaScript object carries — constructor, toString, valueOf,
hasOwnProperty — counted as found when a value was checked against a list of allowed ones.
The value turned out to be a service function, and the behaviour then differed per endpoint:
GET /v1/userfields/constructorreturned a Bitrix24 error instead of the400 UNKNOWN_ENTITYrefusal;GET /v1/requisite-links?filter[constructor]=…and?sort=constructorsent a garbage field name to Bitrix24 instead of400 UNKNOWN_FILTER_FIELD/400 UNKNOWN_SORT_FIELD;GET /v1/openline-configs?filter[constructor]=…and?sort=constructorsent a garbage field name instead of the expectedCONSTRUCTOR; this endpoint has no refusal for an unknown field and never had one;POST/PATCH /v1/userfields/:entitysent such a field to Bitrix24 under a substituted name instead of passing it through; the same for the inner keys oflistitems;POST /v1/users/inviteused the same substituted name instead of passing it through;model: "constructor"in AI requests replaced the model name with a service value.
After
Allowed values are checked against own keys, so service names are not found among them. Where an endpoint refuses an unknown value, that refusal now comes before the Bitrix24 call; where there is no refusal, the field name is passed through as is.
NEW-0922-11: Cowork/Code desktop ticket for phone access
The new POST /v1/cowork/relay-ticket method issues the Cowork/Code desktop app a short-lived signed ticket that the phone-access relay requires before it lets the desktop in. The body carries pubkey, the desktop's Ed25519 public key (32 bytes, unpadded base64url). A 200 response contains ticket (an EdDSA JWT valid for 5 minutes) and expiresAt. The method is available only to a desktop app key with the vibe:cowork scope and only while the Cowork/Code seat is active.
Refusal codes: 403 INSUFFICIENT_SCOPE (no vibe:cowork scope), 403 COWORK_DESKTOP_KEY_REQUIRED (not a desktop app key), 404 COWORK_NOT_ACTIVATED (no seat), 403 COWORK_SEAT_INACTIVE (the seat is not active), 400 INVALID_PUBKEY (malformed key), 429 (at most 30 tickets per hour per person), 503 COWORK_FEATURE_DISABLED and 503 COWORK_RELAY_NOT_CONFIGURED (ticket issuance is temporarily unavailable).
FIX-0922-12: the owner-only policy now behaves the same on every entry point
Before
Under the OWNER_ONLY policy, a user listed in the access list could open the application
through the placement inside Bitrix24, yet was refused on the direct link. The documentation
states that access-list entries do not apply under OWNER_ONLY: the «restore privacy»
scenario — switch the server back to OWNER_ONLY and leave the entries in place — therefore
did not close access for everyone.
After
Every path now applies one rule: under OWNER_ONLY only the owner opens the application, and
access-list entries do not apply, exactly as documented. To open access to specific people
again, switch the policy to NAMED_USERS — the entries are already stored and take effect
immediately.
Integrator impact
If your scenario relied on a listed user entering through the placement under OWNER_ONLY,
switch the server to NAMED_USERS: the set of people stays the same.
FIX-0922-13: a wake schedule can be set for an app inside a galaxy again
Before
An app inside a galaxy — an AI agent included — could not be given a schedule window:
POST /v1/infra/servers/{id}/wake-schedules answered 400 ALWAYS_ON_CONFLICT, as if the owner
had paid to keep the server online around the clock. Nobody made that choice: the app inherits
its plan from the galaxy, and a galaxy only accepts non-interruptible plans, so the
«runs 24/7» condition was met on its own. Combined with idle sleep being unavailable to an
agent, the owner had no way to stop the work overnight — while being billed around the clock.
After
A plan inherited from the galaxy no longer counts as choosing the around-the-clock mode: an app
inside a galaxy takes schedule windows the usual way. A standalone server whose owner did pick a
non-interruptible plan and turned sleep off behaves as before — 400 ALWAYS_ON_CONFLICT.
Integrator impact
No action required. A request that used to be rejected for an app inside a galaxy now creates a
window, and GET /v1/infra/servers/{id}/logs on a sleeping app no longer reports the window as
refused.
NEW-0922-14: three more presets in the work-schedule library
GET /v1/work-schedules returns three additional presets — weekdays-9-19, weekdays-9-20 and
weekdays-8-19 (weekdays, Monday to Friday). There used to be three presets, and when an account's
working week did not match any of them, the only option was a custom schedule via
POST /v1/work-schedules — even if the difference was a single hour.
The new presets are created in the account on the first read of the library, just like the earlier
ones, and they are just as read-only: a preset carries canEdit: false, and an edit attempt answers
WORK_SCHEDULE_PRESET_READONLY. The platform will not assign them to any machine on its own — they
only show up in the list.
The earlier keys and their windows are unchanged, and machines already assigned kept their schedules.
The set of presets may keep growing, so treat an unknown presetKey as a preset you have no local
label for rather than an error. Identify presets by presetKey, not by name: the name depends on
the language of the key.