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

API changes: September 7, 2026

← Changelog · September 2026

FIX-0907-1: redeeming a coupon on your own Bitrix24 account is now refused with a dedicated code

Before

POST /v1/cowork/coupon/redeem looked only at the coupon, the campaign and the seat. It did not care who was redeeming or whose account it was: a platform employee holding the coupon-issuing right could mint a code and activate it on an account they stand behind — the plan was granted at the platform expense, and the response was a plain HTTP 200.

After

Redemption is refused with COUPON_SELF_PORTAL (HTTP 409) when the account is backed by a platform employee holding the coupon-issuing right — the redeemer themselves, or any account member, API key owner or server owner on it. A successful redemption is not affected: on an account with no such people the HTTP 200 response is unchanged, body included, and no integration work is required. The refusal does not consume an attempt against the code-guessing counter — the cause is the account's composition, not a wrong guess. The code is listed in the endpoint's response table in the documentation.

NEW-0907-2: self-description now says whether infrastructure is stopped for non-payment

The GET /v1/me and GET /v1/cowork/state responses carry a new infraState block: frozen tells whether servers, deploy and storage are stopped for non-payment, reason is the reason code (DEBT or null), and topupUrl is the dashboard top-up address or null.

The block answers a question that had no readable source before: what exactly is stopped for an account in debt. The strings inside the block are machine-readable, and the human-facing text is up to the client.

Note that what the field means depends on whether infrastructure-debt scoping is enabled for the account (the FIX in this same release). Until it is, a negative balance refuses V1 calls broadly, GET /v1/cowork/state included; only GET /v1/me stays readable in that mode, being exempt from the freeze. There infraState.frozen means "almost everything is stopped". Once it is enabled the field means exactly what it says: infrastructure is stopped while calls within the plan monthly quota keep working.

The block is additive, existing fields are unchanged.

FIX-0907-3: fixed pagination for business process activity and automation rule lists

Before

A request with offset=50 returned the same codes as a request with offset=0. The response remained successful with HTTP 200.

After

offset skips the specified number of codes in the full list. meta.total reports the full list size, while meta.hasMore reports whether codes remain after the current page. The same behavior applies to list sub-calls for these entities in the global batch request, including both total channels. The response still returns HTTP 200, and OAuth authorization requirements are unchanged.

Impact on integrators

No action is required. Integrations using offset now receive the requested page instead of a repeated first page; the response format and authorization requirements are unchanged.

Affected endpoints: GET /v1/bizproc-activities, GET /v1/bizproc-robots, POST /v1/batch.

FIX-0907-4: infrastructure debt no longer disables what is paid for separately

Before

A negative wallet balance returned 402 ACCOUNT_FROZEN on almost every V1 call — only self-description, the guide and the support conversation were exempt — including calls that never touch the wallet: proxying to your own Bitrix24, Cowork subscription state, the model list. Debt for a virtual machine disabled AI granted by the Bitrix24 plan.

After

The account freeze applies to what the wallet pays for: servers, storage, deploy, search and research on platform credentials, spending beyond the monthly quota, deploy-key issuance and application creation. A call within the plan monthly quota, a paid Cowork subscription period and REST proxying to your own Bitrix24 account go through whatever the wallet state is.

Note the behaviour sits behind the wallet-debt-scoped-to-infra feature flag; with the flag off the responses are unchanged.

FIX-0907-5: an oversized numeric CRM ID is rejected before Bitrix24 is called

Before

Numeric CRM IDs were checked for shape only: any digit string without leading zeros passed the guard. This affected regular record :id values in paths (/v1/deals/{id}, /v1/contacts/{id} and similar entities), as well as positive smart-process entityTypeId values and related CRM operations. An ID beyond the safe integer range reached Bitrix24, where it could be rounded to a different value and some crm.item methods answered it with a raw PHP error instead of a clear refusal.

After

Such IDs are rejected as invalid parameters before Bitrix24 is called. The check covers regular record IDs on path and batch surfaces, as well as smart-process entityTypeId values, dynamic parameters and related CRM operations. The boundary is the safe integer: 9007199254740991 is still accepted, while 9007199254740992 and anything longer is rejected. The ID 0 remains valid wherever it was already allowed, such as the main deal pipeline.

FIX-0907-6: lead conversion respects the selected deal pipeline

Before

POST /v1/leads/{id}/convert ignored the categoryId parameter, so the new deal was placed in the default pipeline.

After

When categoryId is provided, the new deal is created in the selected pipeline immediately. The value 0 still selects the default pipeline.

A non-negative integer string remains accepted for compatibility and is normalized to a number. Other value shapes remain ignored and leave the deal in the default pipeline; null and an omitted parameter are equivalent.

NEW-0907-7: Bitrix24 employee id of the app author in /v1/apps responses

Responses of the apps family now carry two new fields: authorBitrixUserId — the numeric Bitrix24 employee id of whoever created the app — and authorBitrixUserIdSource, telling where that id came from. authorBitrixUserId is the same identifier the id field of GET /v1/users returns, so it is the join key between the two responses: an app previously carried only authorId, a Vibecode platform user identifier, with nothing to match the creator against an employee card.

Values of authorBitrixUserIdSource: member — the id comes from the author's confirmed membership of this Bitrix24 account and is safe to link; snapshot — the id comes from a value captured when the app was created, which is best-effort and may point at a different employee than the app's current author; null — the id is unknown, and authorBitrixUserId is null as well. A registry that must not be wrong should link to an employee card only on member. On self-hosted accounts and on accounts with microservice credentials the snapshot value is never returned: there the captured value has no identity-confirmed origin. Confirmed membership still works on such accounts, so the id there is either member or empty.

The author's name is deliberately NOT returned by the apps responses, which narrows the original request — it asked for the name as well. Personal data does not travel in a response every account key can read, the key embedded in a deployed application's code included; the name is fetched by authorBitrixUserId from GET /v1/users, where the user permission gates it.

Existing requests keep working unchanged: the fields are additions, nothing was removed or renamed.

Affected endpoints: GET /v1/apps, GET /v1/apps/:id, POST /v1/apps, PATCH /v1/apps/:id, POST /v1/apps/:id/publish, POST /v1/apps/:id/unpublish, POST /v1/apps/:id/relink-oauth

BC-0907-8: companies field removed from the contacts API

Old format supported until: not provided

Before

The companies field was advertised by GET /v1/contacts/fields and accepted in select for contact read operations. Bitrix24 did not return a value for this field, so a successful response could omit the companies key.

After

The companies field is no longer advertised and is always removed from contact responses. Requesting it in select is rejected with 400 UNKNOWN_SELECT_FIELD.

What integrators should do

Do not request companies. Use the companyIds field for related company identifiers.

Affected endpoints: GET /v1/contacts, GET /v1/contacts/:id, POST /v1/contacts/search, POST /v1/contacts/aggregate, POST /v1/contacts/batch, GET /v1/contacts/fields.

FIX-0907-9: multipart upload replaces a large object under its existing key

Before

POST /v1/storage/objects/multipart/create returned 409 STORAGE_KEY_EXISTS when a live app object already occupied the same key. Content larger than 10 MB therefore could not be replaced without changing the key. For a personal key, the advice to use multipart led to the same failure.

After

For an existing app object, the request returns 200 and opens a replacement session with the existing objectId. Reads return the old content until POST /v1/storage/objects/multipart/complete publishes the new bytes; a successful response confirms the new version. A confirmed POST /v1/storage/objects/multipart/abort before publication preserves the old version. If complete returns 502 STORAGE_BUCKET_ERROR, publication may have happened: that response does not identify the current version, the session remains active, and object deletion stays blocked until a platform administrator resolves it. Object visibility does not change.

POST /v1/storage/objects/multipart/create returns 409 STORAGE_KEY_EXISTS for an existing live object owned by a personal key and does not open an upload session. Delete the object first, then start a new multipart upload under the same key. This operation is non-atomic: the object is unavailable between deletion and successful completion of the new upload.

Impact on integrations

App files larger than 10 MB can be updated by multipart upload under the existing key. Multipart create, complete, and abort may return the retryable 409 STORAGE_KEY_CONFLICT when another request is changing the same key or session state changed. For create and complete, retry the same operation. A 409 from abort during finalization requires retrying complete with the same parts; if complete returns 502 STORAGE_BUCKET_ERROR, do not infer the current version and contact a platform administrator. If abort returns 502 STORAGE_BUCKET_ERROR, keep retrying abort until session release is confirmed. DELETE /v1/storage/objects/{key} returns 409 STORAGE_MULTIPART_IN_PROGRESS while a multipart session is active: complete or abort the session first, accounting for the finalization case above.

FIX-0907-10: moving a deal/lead to a nonexistent stage now returns an error instead of a silent success

Before

POST /v1/deals/{id}/move, POST /v1/leads/{id}/move, PATCH /v1/deals/{id} and PATCH /v1/leads/{id} answered 200 success:true with the full deal/lead object even when the requested stageId (or a deal's categoryId) did not exist in the reference list — Bitrix24 silently ignored the value, the stage stayed unchanged, and the caller could not tell a real move from a rejected one without manually comparing the data.stageId field.

After

The response remains 200 when the requested stage or pipeline exists and is applied — behavior for valid values is unchanged. When the requested value was not applied (Bitrix24 accepted the call but the actual stage stayed the same), the response is 422 STAGE_NOT_APPLIED with details in error.message: what was requested and what is actually there after re-reading the record.

FIX-0907-11: The placement-bind note says trial, not demo

Before

The apps.bindPlacements capability note in GET /v1/me still called Bitrix24 trial access a demo, while the refusal it describes has always been named after a trial. FIX-0904-7 settled the latin terminology on the word trial everywhere else, so this string was the last live disagreement of its kind. The same term also survived in two documentation articles — the one on starting Bitrix24 trial access, and the plan name reported by GET /v1/me.

After

The note says trial, and the documentation uses the same term. Refusal codes, statuses and response fields are unchanged, and a successful response stays successful. The note is not localised and is served identically on every installation.

Impact on integrators

None for logic. A client matching this note by substring should re-check the comparison.

FIX-0907-12: the spec declares every server mode-switch response

Before

The public spec GET /v1/openapi.json declared only 200, 400, 403 and 404 for PATCH /v1/infra/servers/:id/mode. A client generated from the spec treated the remaining outcomes as impossible, although the endpoint returned them: the exhausted-balance refusal, the server-role refusal, a parallel-request conflict and two network-policy failures.

After

The spec gains the responses the endpoint does return: 401, 402 ACCOUNT_FROZEN and OPEN_MODE_REQUIRES_COMMERCIAL, 409 SERVER_NOT_RUNNING, AGENT_NOT_CONNECTED, MODE_SWITCH_SEALED_ROLE and CONFLICT, 502 IPTABLES_FAILED, PROVIDER_NOT_CONFIGURED, GATEWAY_UNREACHABLE and TUNNEL_NOT_FOUND, 503 SECURITY_GROUP_ATTACH_FAILED and a refusal whose code starts with GATEWAY_TIMEOUT. The 400 description gains the codes SAME_MODE, NO_SUBDOMAIN and MODE_SWITCH_STANDALONE_ONLY. The same codes now appear in the endpoint page's error table: before this change it named none of the three state refusals — server not running, agent not connected, server without a subdomain. The code PROVIDER_ERROR is removed from the endpoint page's error table: a mode switch does not return it. Status 429 is deliberately left undeclared — the endpoint carries no limiter of its own, and the platform-wide edge limit is documented on the limits page rather than declared per route. Endpoint behaviour is unchanged: the successful response remains HTTP 200 with the same body, no request needs changing, and the edit touches the declaration and the documentation only.

NEW-0907-13: `/v1/cowork/me` and `/v1/cowork/state` now name the Bitrix24 account the key is bound to

Both Cowork/Code self-info endpoints gained a new portal field:

JSON
{
  "portal": { "id": "8f3c…", "domain": "acme.bitrix24.com" },
  "tier": "PRO",
  "state": "ACTIVE"
}

The field answers which account the rest of the body is about. A Cowork/Code seat is defined on the pair «key owner + account», and a key is bound to a single account for good, so for a person with several accounts the app reports the tier and quota of one account while the browser shows another. Until now the domain arrived exactly once, when the code was exchanged for a key (POST /v1/connect/token), and could not be asked for again.

portal.id is always present — it is taken from the key itself. portal.domain is null only in the degenerate case where the account row is already gone. Every other field of both responses is unchanged, and requests written before the field existed keep working without edits.

BC-0907-14: a partial lead conversion failure now answers 422, not 200

Old format supported until: not provided

Before

POST /v1/leads/:id/convert creates the deal, contact and company as separate records one after another. When only some of them were created, the response still arrived with status 200: the success field was false, while error, code and details sat at the top level of the body, and error was a string rather than an object. A client branching on the HTTP status or reading error.code took a partially written CRM state for a success.

After

A partial failure answers 422 with code CONVERSION_FAILED in the usual error envelope: error is an object carrying code and message. The outcome of every requested operation moved into error.details — one key per deal, contact and company, each with a success flag and either the id of the created record or the Bitrix24 refusal text. The top-level error string and code are gone from the response. A lead that could not be read answers 404 with code ENTITY_NOT_FOUND in the same envelope. A successful conversion still answers 200 with a data field.

What to do

Branch on the HTTP status and on error.code, not on the success field and not on the top-level code; read per-record outcomes from error.details rather than from details. Records already created are still not rolled back and the lead keeps its status, so before repeating a call check what already landed in CRM: a repeat creates a second set of records. There is no support window for the former 200 response: it reported success where some CRM records had not been created, so keeping it would mean going on passing a failure off as a success.

Before

A JSON literal null as the request body crashed POST /v1/{entity}, PATCH /v1/{entity}/{id} and POST /v1/{entity}/batch with 500 INTERNAL_ERROR; that status was not declared for any of the three routes. A JSON array or a JSON string as the body of POST /v1/{entity} created a real record with empty or auto-generated fields and answered 201, as if the request had been valid. On PATCH /v1/{entity}/{id} a JSON string passed the empty-body check and answered 200 without actually changing anything. On POST /v1/{entity}/search an array body answered 400 INVALID_FILTER_SHAPE with a message that misnamed the type actually sent (reporting a function instead of an array); a bare scalar body (a number, a string, a boolean) was treated as "no filter" and answered 200 with the unfiltered record list.

After

A well-formed body (a JSON object — for /search, including an empty {}, which is the legitimate "no filter" request) is handled unchanged. The response still remains 201 on create and still remains 200 on update, batch and search — none of that changed. A body whose JSON root is not an object (null, an array, a string, a number, a boolean) now answers 400 on all four routes, and no record is created or modified: EMPTY_CREATE_BODY on create, EMPTY_UPDATE_BODY on update, INVALID_BATCH_ACTION on the per-entity batch route, INVALID_REQUEST on search. The search error message now names the type actually sent instead of an internal implementation artifact.

FIX-0907-16: an absent record answers 404 for warehouses, catalogs, order statuses, basket items and smart processes

Before

One and the same scenario — "no record with this id" — answered with different codes inside a single product area. GET /v1/order-statuses/{id} and GET /v1/basket-items/{id} returned 422 BITRIX_ERROR, while the neighbouring orders, invoices, payments and quotes returned 404 at the same step. GET /v1/catalogs/{id}, GET /v1/warehouses/{id} and the product-property pages behaved the same way, even though catalog products and prices already answered 404. A delete-confirmation check based on the response code silently failed on those paths.

Separately, on an account whose interface language is not Russian, PATCH and DELETE for /v1/smart-processes/{id} with a nonexistent entityTypeId answered 400 INTERNAL_ERROR — a code the DELETE contract does not even declare. On a Russian-language account the same call correctly answered 404 SMART_PROCESS_NOT_FOUND, and GET was correct in every language.

After

An absent record answers 404 on all of the paths above. GET, PATCH and DELETE for warehouses, catalogs, order statuses and basket items return 404 ENTITY_NOT_FOUND; the Bitrix24 text in the message field is unchanged, only the classification of the response is. The same 404 now arrives on the product-property and property-value pages.

PATCH, DELETE and POST /v1/smart-processes/batch answer 404 SMART_PROCESS_NOT_FOUND regardless of account language, exactly as GET has done for a long time. A client can rely on one existence code across the whole Vibecode API instead of keeping per-area and per-language exceptions.