# API changes: September 22, 2026

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

### 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](/docs/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](/docs/batch)
and the single [GET /v1/{entity}](/docs/entity-api) and
[POST /v1/{entity}/search](/docs/entity-api) 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**

1. Drop `offset` from sub-calls to `telephony-lines`: an offset is unavailable for that
   entity, and the `UNSUPPORTED_OFFSET` response says so directly. Narrow the selection with a
   filter instead.
2. If your code relied on a page longer than requested — pass the number you need in `limit`
   explicitly, up to 5000 records.
3. Page by the `hasMore` of the sub-call result rather than by arithmetic over `total`.

### 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](/docs/ai/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](/docs/management-keys),
[DELETE /v1/keys/{id}](/docs/management-keys),
[POST /v1/cowork/applications/{id}/key](/docs/cowork/applications-create),
[POST /v1/infra/servers](/docs/infra/servers/create),
[POST /v1/infra/servers/{id}/access-tokens](/docs/infra/access-tokens/create),
[POST /v1/bots/{botId}/transfer](/docs/bots/management/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/constructor` returned a Bitrix24 error instead of the
  `400 UNKNOWN_ENTITY` refusal;
- `GET /v1/requisite-links?filter[constructor]=…` and `?sort=constructor` sent a garbage
  field name to Bitrix24 instead of `400 UNKNOWN_FILTER_FIELD` / `400 UNKNOWN_SORT_FIELD`;
- `GET /v1/openline-configs?filter[constructor]=…` and `?sort=constructor` sent a garbage
  field name instead of the expected `CONSTRUCTOR`; this endpoint has no refusal for an
  unknown field and never had one;
- `POST`/`PATCH /v1/userfields/:entity` sent such a field to Bitrix24 under a substituted
  name instead of passing it through; the same for the inner keys of `list` items;
- `POST /v1/users/invite` used 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](/docs/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.
