# API changes: September 2, 2026

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

### BC-0902-1: Documents expose the PDF transformation status

> Old format supported until: not provided

**Before**

The `isTransformationError`, `transformationErrorCode`, `transformationErrorMessage`, `transformationCancelReason`, and `pullTag` fields were not declared in the documents contract. If Bitrix24 returned them through [GET /v1/documents/:id](/docs/entities/documents/get), the Vibecode API passed their values without normalization. The same fields in [POST /v1/documents](/docs/entities/documents/create) and [PATCH /v1/documents/:id](/docs/entities/documents/update) bodies were not rejected by the local read-only field validation.

**After**

The five fields are declared as nullable response-only fields. `isTransformationError` is returned as `boolean | null`, and the other four fields are returned as `string | null`. Empty strings in the string fields become `null`. A write request containing any of these fields returns `400 READONLY_FIELD`.

**What integrators should do**

Do not send these five fields in document create or update bodies, including when resubmitting a previously received object. Accept `null` when reading the fields and use `pullTag` to subscribe to transformation status updates.

### BC-0902-2: The mode-switch address in the access-mode refusal is no longer fixed

> Old format supported until: not provided

**Before**

The `WRITE_BLOCKED_READONLY_KEY` access-mode refusal always returned `details.switchUrl` as
`"/keys"`, and the machine schema pinned that value with `enum: ["/keys"]`. The keys section lists
personal keys only, so for the two other kinds the path was a dead end: neither an application
auth key nor a management key is ever listed there, by construction — neither for the owner
nor for the Bitrix24 account administrator. The holder was given the address of a page that
carries no switch for their key.

**After**

`details.switchUrl` points at the page that switches the mode for that particular key: the
keys section `"/keys"` for a personal key, the Applications page `"/applications"` for an
application auth key, the management keys section `"/management-keys"` for a management key.
The message text names the same address as the field — they are built together and cannot
disagree. The value enumeration is gone from the schema: this is a UI
path, not a protocol constant, and pinning a literal made the contract unsatisfiable once the
page moved. The field is still always present and still required — the value changed, not its
presence.

**What integrators should do**

Read `switchUrl` from the response instead of hardcoding `"/keys"`. Clients that generate
types from the schema no longer receive the literal type `'/keys'` — regenerate your types,
otherwise value validation will fail.

### FIX-0902-3: a stream that never started ends with a retryable error for models from every provider

**Before**

Stream setup for `bitrix/*` models already made a single attempt on one shared budget, while for models from every other provider the platform silently repeated the request and waited for another full budget. The client received nothing meanwhile — up to two full budgets of silence — and then saw a generic provider failure that does not tell a stalled stream setup apart from any other unclassified error.

```
data: { "error": { "code": "ai_provider_unavailable", "type": "server_error", "retryable": true, "retryAfter": 6 } }
data: [DONE]
```

**After**

Stream setup makes a single attempt on one shared budget for models from EVERY provider. If the headers do not arrive within it, the wait no longer doubles and the stream ends with the same `stream_idle_timeout` event as a stream that went silent mid-response — naming the stream itself as the thing that stalled.

```
data: { "error": { "code": "stream_idle_timeout", "type": "server_error", "retryable": true, "retryAfter": 7 } }
data: [DONE]
```

The wait no longer doubles invisibly and the reason is named precisely — now across the whole model set, not only `bitrix/*`.

### NEW-0902-4: booking resources lookup in V1

[GET /v1/booking-resources](/docs/entities/booking-resources) is now available — it returns the booking resources available to the key and closes the gap in the first-booking flow: [POST /v1/bookings](/docs/entities/bookings/create) requires a non-empty `resourceIds`, and there was no way to learn valid identifiers through V1 alone.

The `booking` scope is required. A key in read-only mode works — the method is a read. The `typeId` and `searchQuery` filters are optional and may be combined, and any other parameter is rejected with `400 INVALID_PARAMS`.

The catalogue is collected in full, so there are no pagination parameters: `meta.total` equals the length of `data`, and `meta.hasMore` is always `false`. Each record carries exactly four fields — `id`, `name`, `typeId` and `isMain`. The `description` field is withheld: it is free-form text from the Bitrix24 account and is not needed to pick a resource.

Completeness is guaranteed for a catalogue that did not change during the request — Bitrix24 provides no snapshot of the collection. An account with more than 500 resources gets `502 BITRIX_RESULT_TOO_LARGE` instead of a truncated list; the same two filters narrow the lookup.

### NEW-0902-5: money-in export marks revoked orders and returns the recognizable amount

In the per-payment export `GET /v1/platform/revenue/money-in/payments` every row now carries a `revokedAt` field — the package revocation date in UTC ISO-8601 format, or `null` for a live order. A revoked order's row does not disappear from the export and its amounts do not change: the money did arrive on the `paymentDate`, and cash-in still reconciles with the bank statement.

In the aggregate `GET /v1/platform/revenue/money-in` two nested tiers now travel alongside the previous sums. The `revoked` tier is the revoked part of the same values (`ordersCount`, `grossCashRub`, `vatRub`, `netRubExclVat`, `vibesCredited`), and the `recognized` tier is the same values minus the revoked part, that is the recognizable amount. Top-level fields do not change their values and remain the cash-in for the period, so existing integrations keep working with no changes.

A revocation arrives as a separate event and may happen after the period is closed, so on a repeated export a previously exported row may turn out to have `revokedAt` filled in. The set of rows stays the same, and the `orderId` key is stable.

### NEW-0902-6: device-flow accepts a device identifier

`POST /v1/connect/device/authorize` accepts an optional `device_id` — a stable identifier of the client installation (up to 128 characters; letters, digits, `_.:-`). It takes effect for the Cowork/Code application: when the same device connects again, the previous key is revoked instead of sitting in the list next to the new one. Other clients may send the parameter, but it does not yet affect how their keys are issued.

The parameter is optional, and behaviour without it is unchanged — every connection issues its own key and earlier ones stay active. Several distinct devices for one person keep working as before: their identifiers differ, so their keys never displace each other. A value that does not match the format is ignored — the connection still succeeds, only the replacement is lost.

### NEW-0902-7: new INT_BOX_PARTNER_REQUIRED refusal code for self-hosted accounts on the international installation

On the international installation platform access for a self-hosted account can require a
confirmed partner (NFR) licence. A self-hosted account without that confirmation receives
HTTP 402 with the new code `INT_BOX_PARTNER_REQUIRED` — on server, agent and managed-bot
creation, and on key issuance. The refusal body is shaped like its neighbours in this family
(`userMessage`, `alternatives`, `hint`), but `details.requiredTariffs` is empty: moving to
another Bitrix24 plan does not clear this refusal, and the mark is read when the connector
module registers. The same code appears in the `reason` of the `capabilities.servers.create` slot of
`GET /v1/me`, and the documentation address in its `alternatives[].url`. The requirement is off by default and is switched on by
a platform administrator; while it is off, no account sees this code and responses stay
unchanged.

### NEW-0902-8: stable account identifier in the key self-description

[GET /v1/me](/docs/keys-auth/me) returns a new field, `portalId` — the stable identifier of the Bitrix24 account the key is bound to. It arrives in the same request, next to the existing `portal` and `owner`, and is present for `vibe_api_` and `vibe_app_` keys.

Unlike the domain in the `portal` field, this identifier survives a rename or a move of the account. Use it whenever local data is split per account: an account key built from the domain stops matching the previous one after a move, and the data of the same account ends up in a new empty store.

In a successful response the field is never empty: a key with no account bound never reaches this response, it gets `401 NO_PORTAL`. Management keys `vibe_live_` do not get the new field — they are not bound to a single account and still return the list of accounts in `portals`.

### NEW-0902-9: server creation answers 409 `REISSUE_IN_PROGRESS` while the application authorization key is being re-issued

`POST /v1/infra/servers` has a new refusal code `REISSUE_IN_PROGRESS` with status `409`. The Vibecode platform answers with it when the application card owner is re-issuing its authorization key at that moment: no server is created, because it would land on a key that is being revoked in the same second.

The refusal is transient — repeat the request once the re-issue finishes. The successful response and every other refusal code are unchanged, and requests that worked before keep working.

### FIX-0902-10: Open Channels dashboard methods are now available in OpenAPI

**Before**

Six public Open Channels statistics methods were described on documentation pages but were absent from `/v1/openapi.json` and the generated API cards.

**After**

All six methods are available in OpenAPI and API cards with the `imopenlines` scope, parameters, and response schemas. Runtime behavior is unchanged: a successful response remains HTTP 200, and the existing error statuses and schemas are preserved.

### NEW-0902-11: support code in an unrecognized key issuance refusal

When Bitrix24 refuses to issue a key for a reason the platform does not
recognize, the response now additionally carries `error.details.incidentCode` —
a six-character support code. The platform writes the same code to the account
journal next to the refusal breakdown, so quoting it is enough for support to
find the entry.

The field is optional and added to the existing response body: the HTTP status,
`error.code` and `error.details.reason` are unchanged, and clients need to do
nothing.

### NEW-0902-12: the Performance Review section is available through /v1/performan/review

Six endpoints are now available under the new `performan` scope. Reads: `GET /v1/performan/review/campaigns`, `GET /v1/performan/review/self-reviews`, `GET /v1/performan/review/peer-reviews`, `GET /v1/performan/review/manager/questions`, `GET /v1/performan/review/manager/reviews`. Every list but campaigns requires the `campaignId` parameter; manager cards also accept an optional `revieweeUserId` filter. The response is `{ "success": true, "data": [...], "meta": { "nextCursor": ... } }`. Pagination is cursor based: `limit` (1..200, 50 by default) and `afterCursorId`, taken from `meta.nextCursor.id` of the previous page. A `limit` outside that range answers `400`. These methods have no `offset` parameter and no total-count field.

Every list is scoped to the calling user: their campaigns, their cards and the manager relations where they are the reviewer. An empty response means "nothing for this user", not "nothing on the account".

Write: `POST /v1/performan/review/manager/answers` with a body of `{ "relationId": 4, "answers": [...], "isAutosave": true, "expectedStateHash": "..." }`. The `isAutosave` field defaults to `true`, which saves a draft; pass `false` to finalise the review, which moves the card to the `completed` status and cannot be undone. The `expectedStateHash` value is returned only by a write, never by a read method, so the first call goes without it. When the state changed since it was read, the answer is `409` with the `PERFORMAN_STATE_CONFLICT` code: re-read the card and repeat the request with the fresh value. A relation that belongs to somebody else and a relation that does not exist both answer `403`. A read-only key gets `403 WRITE_BLOCKED_READONLY_KEY` on the write.

The section is only available on accounts where the Performance Review module is installed and the surface is enabled for them: elsewhere all six addresses answer `404` and are absent from that account's `GET /v1/openapi.json`. The `performan` scope is withheld from the default set for the same reason.

A Cowork key gets the scope reactively: if it has not been granted yet, the first request to any of the addresses answers `409 PERFORMAN_SCOPE_JUST_GRANTED` and grants the right on the spot — retry the same request and it will succeed. A read-only key does not get the right this way: the reactive grant is a write, so such a key is refused with `403` and its owner adds the scope in the dashboard instead.

A choice question cannot be answered through the API: no read method exposes option ids, and the question list returns a question without its options.

[GET /v1/openapi.json](/docs/api-reference) now carries a rate limit per caller address. The precise cap is returned in the `x-ratelimit-limit` response header — read it there instead of hard-coding a value. Exceeding it answers `429 RATE_LIMITED`. The cap is generous and normal reading does not reach it: the spec is served with an `ETag`, so a discovery client or an SDK generator gets a `304` instead of the body on a repeat fetch.

The spec body depends on the request's key: with a key of an account the performan surface is enabled for, it carries the performan paths and the `performan` scope in the catalogue; without a key it does not. The response is therefore marked `Vary: Authorization, X-Api-Key` (the key is read from either header), and with such a key it is served as `Cache-Control: private` rather than `public`. Do not reuse one stored spec file across different keys.

### FIX-0902-13: the Bitrix24 plan refusal on key issuance and app installation now states its cause

**Before**

When Bitrix24 refused issuance with `FEATURE_NOT_AVAILABLE_ON_CURRENT_PLAN` (the Bitrix24 plan does not include Vibecode), the answer carried nothing actionable on ANY issuance endpoint — both where a key is issued ([POST /v1/keys](/docs/management-keys) → `502 CONNECTOR_KEY_ISSUE_FAILED`) and where an app is installed ([POST /v1/apps](/docs/apps/create) → `502 CONNECTOR_APP_INSTALL_FAILED`). The body held no human-readable text and the cause stayed in an internal field only, so clients read the refusal as a transient platform failure and retried. Retrying never helps: the gate lifts only when the Bitrix24 plan changes.

```
HTTP 502
{ "success": false, "error": { "code": "CONNECTOR_KEY_ISSUE_FAILED", "message": "connector refused to issue the key" } }
```

**After**

The refusal is now resolved against the account region. Where access is on sale, the answer is `402` with a plan paywall code and human-readable text: `INT_VIBE_PLUS_REQUIRED`, carrying `details.requiredTariffs: ["vibe+"]` and a `details.upgradeUrl` pointing at the plan page inside the Bitrix24 account itself. Where there is nothing to offer (a self-hosted portal, an account already on a paid plan, an unrecognised region), the answer stays `502` but gains its own code `CONNECTOR_PLAN_REQUIRED` and a non-empty `userMessage` instead of an empty body.

```
HTTP 402
{ "success": false, "error": { "code": "INT_VIBE_PLUS_REQUIRED", "message": "This action requires a Vibe+ plan on your Bitrix24 account.", "details": { "requiredTariffs": ["vibe+"], "upgradeUrl": "https://example.bitrix24.com/online/?feature_promoter=limit_why_pay_tariff_vibe" } } }
```

**What integrators should do**

Successful responses are unchanged. A client that branches on the error code now receives a terminal class instead of a transient one and can stop retrying: none of the new codes clears on a retry. A client that only looked at the status keeps working — the operation ended in an error before and still does.

### FIX-0902-14: a refusal on the consent page returns the user to the partner application

**Before**

The user approved access, hit a key issuance refusal — no subscription, an unsupported region, the key limit used up — and stayed on the consent page. Nothing arrived at `redirect_uri`: the partner learned neither of the attempt nor of its reason.

**After**

The refusal now carries a return address: the page shows a "Back to the app" button, and `redirect_uri` receives `error=access_denied` (`temporarily_unavailable` when Bitrix24 is down), the machine-readable reason in `error_description` and the original `state`. If the registered return address carried a `code` parameter of its own, a refusal strips it — a refusal never arrives mixed with the marks of success. Refusals before the confirmation behave the same way — an expired consent link and a broken CSRF token, including on the "Decline" button.
