For AI agents: markdown of this page — /docs-content-en/changelog/2026-09-09.md documentation index — /llms.txt
API changes: September 9, 2026
BC-0909-1: Audio transcription retains subscription billing when Cowork/Code is disabled
Old format supported until: not provided
Before
When Cowork/Code was disabled, requests to POST /v1/audio/transcriptions using an already-issued vibe:cowork key bypassed subscription quota, even with an active subscription and an audio price configured for an available model. Exhausting subscription quota did not prevent a successful HTTP 200 response.
After
Disabling the product does not change billing: with an active subscription and a configured per-minute or per-call audio price, usage counts toward its quota, just like chat. When a quota window is exhausted, the request receives HTTP 402 cowork_quota_exhausted with a Retry-After header. Without an active subscription, the existing billing path is preserved without drawing on subscription quota.
Integration changes
Handle HTTP 402 cowork_quota_exhausted even while the product is disabled: wait for the quota window to reset according to Retry-After or upgrade the subscription tier. The key and request format do not need to change.
BC-0909-3: Universal lists: an absent list answers "not found", and a bad offset answers with an error
Old format supported until: not provided
Before
The same absent list answered differently depending on which address was called: the element
list, the field list, the field-type set, a single element, a single field and element files all
answered 422 with a Bitrix24 message — an "something went wrong" error rather than "no such
list". A read-then-create-if-missing flow broke on that.
Paging through those same lists accepted any offset. start=-5, start=abc, start=1.5 and
start=1e2 were not refused: the offset was either silently dropped or applied distorted (1e2
became 1, so the second record arrived instead of the hundredth). The response was 200, so a
mistake in the calling code stayed invisible — while the neighbouring parameters of the same
request, iblockTypeId and sort, are refused with a clear message.
After
An absent list answers 404 with error code LIST_NOT_FOUND on the six addresses where
Bitrix24 reports it with a machine code. An offset is accepted only as a non-negative integer in
plain notation; anything else is refused with 400 and error code INVALID_PARAMS, and the
message names the parameter that did not fit. Both start and offset are checked, even when
the precedence between them is won by the first.
What integrators should do
If your code treated a 422 from the list collections as "the list is gone", switch it to 404.
What is recognized is Bitrix24's machine code, not the message text, so the answer does not
depend on the account language. The code itself was measured on the international platform: if
your account reports an absent list differently, the recognition does not fire and the answer
stays as it was.
If the offset came from an external source and could arrive negative or fractional, an error now
arrives instead of a page: fix the source rather than the walk. One input deserves a separate
mention because it used to be understood CORRECTLY: ?start=+5 — a plus in a query string
decodes to a space, and the old parsing read that as 5. It is now refused. If your builder
encodes with a plus, drop it: ?start=5.
Separately, about writing parameters with square brackets. ?start[]=7 and
?iblockTypeId[]=lists used to pass: the platform joined such a list into a single value and
carried on. That is now refused — brackets mean a structure, and these parameters do not take
one. If your request builder adds brackets to every list by habit, send these three parameters
as a plain value: ?start=7, ?offset=7, ?iblockTypeId=lists. Bracket shapes with a name inside
(?start[x]=1) used to break the request with an internal error; now they get a clear
refusal instead.
One more parameter belongs to the same row — the returned field set: ?select[x]=1 used to
break the request with an internal error, and now the set is simply not applied and the full
field list arrives; empty entries such as ?select[]=&select[]=NAME no longer travel to the
account as an empty string.
The same applies to the request body when creating a section or an element:
"iblockSectionId": [5] used to be read as the number five and placed the record under
that parent; the value is now dropped and the record lands at the root. Send a number, or a
string holding one.
The infoblock type in the body narrowed the same way but answers differently:
"iblockTypeId": ["lists"] used to pass (the list was joined into a single value), and now
400 with error code INVALID_IBLOCK_TYPE arrives on the eight addresses that read this
parameter from the body at all: creating and updating a list, a field, a section and an
element. Deletions do not take it and are untouched. Send a string: "iblockTypeId": "lists".
What this does not change
Reading the sections still answers with a complaint about a wrong infoblock type, and reading the list itself still answers with a permission refusal when the list id is numeric. With a symbolic id that same address already answered "not found" before this change: there Bitrix24 returns an empty result rather than a refusal. Bitrix24 reports an absent list on those addresses in the same words it uses for a genuine permission refusal and a genuinely wrong type, so recognizing them as "not found" would hide real refusals.
BC-0909-4: phantom contacts field removed from companies, leads, and deals
Old format supported until: not provided
Before
Field schemas and company, lead, and deal responses could contain contacts, although the field could not be read reliably from Bitrix24. An explicit select: ["contacts"] was accepted with a 200 response.
After
contacts is no longer published in field schemas or returned in records. An explicit select: ["contacts"] without * or UF_* is rejected with 400 UNKNOWN_SELECT_FIELD. Requests for all fields through * or UF_* still succeed, but contacts is removed from the response.
What integrators should do
Remove contacts from explicit select lists. Use contactIds for company contact relations, and contactId plus contactIds for leads and deals.
Affected endpoints: GET /v1/companies/fields, GET /v1/leads/fields, GET /v1/deals/fields, read and write operations under companies, leads, and deals, POST /v1/batch, POST /v1/{entity}/batch.
FIX-0909-5: a self-hosted portal now issues a key that needs no webhook
Before
On a self-hosted portal whose owner has no developer key, issuing or rotating a key carrying
only vibe:* scopes (for example vibe:infra + vibe:storage) answered
400 BOX_NO_DEVELOPER_KEY, even though no incoming webhook is registered on the portal for
such a key at all. It affected POST /v1/keys, POST /v1/keys/{id}/rotate and the personal
key issued for a Cowork application.
After
A scope set with no Bitrix24 scope is checked before the self-hosted guard: the key is issued,
webhookUrl stays empty and the portal is not called. The 400 BOX_NO_DEVELOPER_KEY refusal
stays in force for sets carrying at least one Bitrix24 scope — such a key does get a webhook,
and nothing but the owner's developer key can remove it. Rotation adds one more condition: the
previous row must carry no webhook. A key with no Bitrix24 scope that still has one on record
answers with the same 400 — nothing can remove that webhook without the developer key, and a
successful answer would hide that the old access keeps working.
FIX-0909-6: replacing an application key no longer hands out an already-expired secret
POST /v1/cowork/applications/{id}/key copied the lifetime of the replaced key onto the new one
verbatim. When that lifetime had already ended, the owner received a secret that passed no request
at all, while the expired key it replaced was given another 24 hours and started working again.
Before
An application whose key expired three days ago. The response is 201 with issued: "rotated",
key.expiresAt holds the same past date, and previousKey.graceUntil points 24 hours ahead of the
replacement.
After
The response is still 201 with issued: "rotated". key.expiresAt is counted afresh from the
lifetime configured in the Bitrix24 account (the same value GET /v1/cowork/applications/defaults reports as
keyExpiresInDays), so the new key works. previousKey.graceUntil never exceeds the replaced key's
own expiry: on an expired key it stays in the past, and the 24-hour grace does not revive it. A
lifetime that is still running, and the absence of one, are carried over as before — a key with no
expiry stays without one, a live expiry is repeated verbatim.
The same correction to the grace period applies to POST /v1/keys/{id}/rotate: rotating a key whose
lifetime had ended no longer grants the replaced key another 24 hours of service.
FIX-0909-7: a public numeric address in baseUrl and proxyUrl is no longer refused as private
Before
POST /v1/ai/credentials and PATCH /v1/ai/credentials/{id} carrying a public numeric address in
baseUrl or proxyUrl — http://203.0.113.10:8080, say — answered 400 with code
BASE_URL_PRIVATE or PROXY_URL_PRIVATE. Storing such a credential was only possible by writing
the same address as a domain name.
After
That request now succeeds, exactly as it does for a domain name. A private numeric address still
answers 400 with the same codes: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16,
100.64.0.0/10, 127.0.0.0/8, 169.254.0.0/16, 0.0.0.0/8, and for IPv6 ::1, ::ffff:,
fc00::/7 and fe80::/10. For numeric addresses only, that list now also covers the reserved
ranges a numeric address could never get through before: 192.0.0.0/24, 198.18.0.0/15,
224.0.0.0/4, 240.0.0.0/4 (including 255.255.255.255), and for IPv6 fec0::/10, ff00::/8
and 100::/64. For domain names the set of refused ranges is unchanged: a name pointing into any
of those reserved ranges keeps working, and a request carrying a domain name still succeeds as
before. The exception is addresses that wrap IPv4 in an IPv6 form (::a.b.c.d, NAT64
64:ff9b::/96, 6to4 2002::/16): those are refused the same way whether they are written as
numbers or a domain name points at one.
FIX-0909-8: the spec declares the 429 refusal of endpoints that carry their own rate limit
Before
Twenty-eight operations carry their own rate limit and answer 429 RATE_LIMITED once it is spent, while the public GET /v1/openapi.json spec did not declare that response for them. A client generated from the spec treated 429 as an undescribed status and built no retry branch at all. Affected: POST /v1/batch, GET /v1/bots/:botId/events, GET /v1/chats/recent, POST /v1/chats/events/subscribe, GET /v1/connect/authorize, POST /v1/connect/token, POST /v1/connect/revoke, GET /v1/app/blueprints/:slug, GET /v1/me/sources, POST /v1/feedback/attachments, GET /v1/platform/coupons/campaigns, GET /v1/platform/coupons/campaigns/:slug, GET /v1/workday/records and fifteen infrastructure endpoints — POST /v1/infra/servers, GET /v1/infra/servers/:id/ssh, POST /v1/infra/servers/:id/reboot, PATCH /v1/infra/servers/:id/sleep, PATCH /v1/infra/servers/:id/port, POST /v1/infra/servers/:id/wake-schedules, PATCH /v1/infra/servers/:id/wake-schedules/:scheduleId, DELETE /v1/infra/servers/:id/wake-schedules/:scheduleId, POST /v1/infra/servers/:id/sources, POST /v1/infra/servers/:id/exec, POST /v1/infra/servers/:id/upload, POST /v1/infra/servers/:id/icon, GET /v1/infra/servers/:id/logs, POST /v1/infra/servers/:id/deploy and POST /v1/infra/servers/:id/unstick. Separately, POST /v1/platform/coupons/issue did declare its 429, but named it with the code RATE_LIMIT_EXCEEDED, which the platform never sends.
After
Every one of these operations declares its 429 in the spec, and coupon issuance now names the real code RATE_LIMITED. The description sends you to the x-ratelimit-limit response header for the value in force and to Retry-After for the retry delay. No figure is written into the spec, deliberately: the header is the only source that cannot drift from the configured value. For POST /v1/infra/servers, POST /v1/infra/servers/:id/upload and POST /v1/infra/servers/:id/deploy a second code is named on the same status — DEPLOY_BACKEND_BUSY, which arrives when every slot for an inline request body is taken, refuses that one body rather than the caller, and carries Retry-After: 30. General information about limits stays on the limits page.
Impact on integrators
Neither the limits nor the behaviour of the endpoints changed, and no request needs editing. A client generated from the spec before this change is worth regenerating — 429 handling then appears where the refusal used to arrive as an undescribed status. Check the code-matching branch on coupon issuance if it was written against the spec: the server answers RATE_LIMITED.
FIX-0909-9: an expired OAuth user token returns an authentication error
Before
When an application could not refresh an expired user token, the Bitrix24 expired_token rejection was returned by the V1 API as 422 BITRIX_ERROR, which incorrectly indicated invalid request data.
After
A single V1 API call returns 401 TOKEN_EXPIRED with a hint to re-open the application from the Bitrix24 menu. In a batch request, TOKEN_EXPIRED appears in the individual call's error while the overall HTTP response remains 200. See API errors.
FIX-0909-10: the token endpoint's rate-limit refusal is now machine-readable
Before
When the platform-edge rate limiter fired, POST /v1/connect/token returned an HTML page and set no Retry-After header. An application could neither parse the body nor learn how long to wait. For the device code sign-in this is the only rate-limit refusal a live poll actually reaches.
After
The same refusal now arrives with a body in RFC 6749 form — {"error":"slow_down","error_description":"..."} — and with a Retry-After header naming the minimum pause in seconds. The form matches the one the endpoint already declares for its other refusals, so an off-the-shelf OAuth library parses it with no extra work. The response status stays 429, so a rate-limit refusal is still distinguishable from the authorization states, which arrive with code 400. Existing calls keep working.
Important: this route has two rate limiters and their bodies differ. Only the platform-edge limiter answers in the RFC 6749 form described above. The endpoint's own limiter answers with the same 429 status but in the general API envelope — { "success": false, "error": { "code": "RATE_LIMITED", "message": "..." } } — so a branch of the shape «on 429, parse the body and compare error against slow_down» recognises only half the refusals and takes the other half for an unknown error. Tell the two apart by the X-RateLimit-Limit header: the platform-edge refusal does not carry it, the endpoint's own refusal does. The Retry-After value is a minimum pause, not a promise that the next request is accepted, so an application needs to grow its own pause as well. Both refusals and both bodies are described in Partner Connect.
FIX-0909-11: model reasoning for Cowork no longer appears in final text
Before
When Cowork did not send an explicit reasoning setting, a model could return internal working text as a regular part of the final answer.
After
POST /v1/chat/completions applies the model-declared default reasoning
setting to keys with the vibe:cowork scope. Reasoning stays in its separate channel, while distinct
final answer text is preserved. The response remains HTTP 200.
Impact on integrators
No client changes are required. Calls without the vibe:cowork scope retain their previous behavior.
BC-0909-12: box top-ups now check out at the licence's own till
Old format supported until: not provided
Before
A box licence issued outside the installation's default region was pointed at its single default till: no other box till existed, so the platform deliberately resolved such a licence to the default region. The purchase did not go through there anyway — the till rejects a licence key from another region — so the path ended in a dead end with no explanation.
After
Where a country's own box till is open, checkout goes to that country's site in its own currency — prices, currency and the checkout URL come from the licence region's block. While a country's catalogue has no box items, top-up for such clients is refused with a clear message instead of pointing them at a till that would reject the purchase anyway. Once the items appear, checkout switches on by itself — no release needed.
Additionally, on installations whose catalogue carries more than one currency: a payment whose currency differs from the order's is now held for review instead of silently closing the order. Where the catalogue has a single currency this does not apply.
GET /v1/cowork/subscription/preview returns the licence country's currency for such a licence.
What to do
Read the currency from the response instead of assuming the default one: currency on packages and at the response root may now be the licence country's, and null while top-up is unavailable there (topUpAvailable: false). Amounts are in that currency's minor units — converting them as if they were the default currency yields wrong numbers.
Handle the BOX_TOPUP_NOT_AVAILABLE refusal on POST /api/billing/topup-init and the unavailable flag in the catalogue: they are returned while the country's catalogue has no box items. Previously this case returned a checkout URL for the default till — the purchase did not go through there anyway, so it can no longer be treated as a working one.