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

API changes: August 19, 2026

← Changelog · August 2026

FIX-0819-1: bot message sending accepts top-level dialogId

Before

When sending a bot message through MCP, top-level dialogId was not included in the POST /v1/bots/:botId/messages request, while a request without a recipient returned a platform error.

After

Top-level dialogId is forwarded as the canonical recipient, while the previous body.dialogId remains a compatible fallback. A request without a non-empty dialogId string returns MISSING_PARAMS before sending the message.

FIX-0819-2: the `X-Tariff-Checked-At` header is sent only after a successful plan lookup

Before

The timestamp was written after ANY plan lookup attempt against Bitrix24, including a failed one: an account whose lookup timed out or was rejected for a key without the required scope still received a fresh X-Tariff-Checked-At. Headers gave no way to tell "the plan was read an hour ago" from "an hour ago we tried and failed".

After

The header is sent only when the last lookup actually read the plan. A failed attempt now sends no header at all, so its absence means "there is no reliable lookup", not "this account was never checked". The X-Tariff-Is-Commercial header is unchanged and arrives as before.

FIX-0819-3: clarified personal-key embedding and handled OAuth without state

Before

GET /v1/me incorrectly claimed that a personal key could not publish an embedded application at all. When Bitrix24 returned an OAuth code without state, /v1/bitrix-handler stayed on the intermediate page.

After

portalEmbedding describes the working POST /v1/apps → OAuth authorization → POST /v1/apps/:id/publish flow and separately warns that a personal key cannot call placements/bind directly and does not itself provide transparent auth. An OAuth callback without state now redirects to a controlled error page without forwarding the code.

FIX-0819-4: money paths now survive a race for the shared account balance

Before

Under a rare race of concurrent writes to an account's balance (a Postgres Serializable-transaction conflict, 40001), one of the competing money paths could fail outright and never complete: the payment provider's webhook credit, Bitrix24 events (bonus grant and revoke, package revoke), the real-time web-search charge, Cowork subscription renewal and tier change, and the daily crons (including vibe-package expiry) — every one of them wrote the balance with no retry and failed on the very first concurrent attempt.

After

Each of these paths now survives a serialization conflict: the platform automatically retries the specific money operation until it succeeds (a finite number of attempts, with jitter between them), so a rare overlap of two concurrent operations on the same balance no longer loses money or returns a failure to the caller.

Impact on integrators

No action required. The payment provider's webhook and Bitrix24 events are processed as normal even under contention with another operation on the same balance — this change requires no additional manual retries on the integrator's side.

BC-0819-5: the freshnessWindowMinutes field is no longer returned

Old format supported until: not provided

Before

GET /v1/me returned a freshnessWindowMinutes field set to 10 inside the capabilities.apps.sourceStorage block, and the 409 SNAPSHOT_REQUIRED refusal of POST /v1/apps/:id/publish carried the same field inside hint. The field named the window, in minutes, within which a saved source snapshot counted as usable for publication.

After

The field is present neither in the capability block nor in the refusal hint. It is returned only when publication checks snapshot age, and right now it does not — a snapshot of any age can be published. No explicit null takes its place: the field is simply absent. The rest of the capabilities.apps.sourceStorage block and the other hint fields are unchanged.

What integrators should do

Read freshnessWindowMinutes as an optional field: if your code requires it, coerces it to a number without checking, or drives a re-save timer from it, drop that dependency. Treat the missing field as "snapshot age is not checked". The field returns only if publication starts checking age again, and its value will be meaningful again at that point. The full hint schema — Source storage.

BC-0819-6: publication refuses when the files of the saved source version are gone

Old format supported until: not provided

Before

POST /v1/apps/:id/publish picked a saved source version by its record, without checking whether its files were still there. A version whose files had already been purged from storage or marked for deletion therefore qualified for publication: the request answered 200, the app moved to PUBLISHED, and downloading the published version afterwards answered 410 SOURCE_VERSION_BYTES_PURGED. Sources that did not exist ended up published.

After

Publication does not pick versions without files at all. If the app or the server has no other saved version, the answer is 409 SNAPSHOT_REQUIRED with hint.reason = app_snapshot_missing or server_snapshot_missing: no files means no snapshot. If such a version is passed as an explicit sourceVersionId, the answer is the same 409, and hint.lastSnapshot arrives as null. This change is unrelated to the snapshot-age check and applies at all times.

What integrators should do

Handle a 409 SNAPSHOT_REQUIRED from publication in the case where it previously could not occur — an app whose source files have been purged from storage or marked for deletion. There is one recovery path: save the archive again through POST /v1/apps/:id/sources or POST /v1/infra/servers/:id/sources and retry publication. Retention and file deletion are described in Source storage.

FIX-0819-7: app publication no longer refuses because the saved sources are old

Before

POST /v1/apps/:id/publish answered 409 SNAPSHOT_REQUIRED when the saved source snapshot was older than ten minutes, even if the code itself had not changed. The check measured the time since the last save rather than whether the sources matched, so the usual order — deploy, then authorize the app on the Bitrix24 account, then publish — ran into a refusal: the authorization step is done by a person and easily takes longer than the window. Getting past it meant saving the very same archive again through POST /v1/apps/:id/sources or POST /v1/infra/servers/:id/sources. The refusal hint carried hint.reason set to app_snapshot_stale or server_snapshot_stale.

After

The age of the saved sources no longer limits publication: a snapshot of any age is accepted. A 409 SNAPSHOT_REQUIRED refusal is left only when there is no usable snapshot at all, and then hint.reason holds app_snapshot_missing or server_snapshot_missing. The values app_snapshot_stale and server_snapshot_stale no longer appear in the response — they come back only if publication starts checking snapshot age again.

Impact on integrators

Nothing to change: publication has simply lost one of its reasons to refuse. Re-saving an unchanged archive before publishing is now redundant and can be dropped from the flow. If your code branches on hint.reason, the branches for app_snapshot_stale and server_snapshot_stale stop firing, yet stay valid: the set of values has not changed.

FIX-0819-8: Publishing an app no longer marks the source version as successfully deployed

Before

After a successful publish the platform wrote deploy status success onto the selected source version — regardless of how the deploy of that version ended, or whether there had been one at all. In the version list (GET /v1/infra/servers/:id/sources), in the manifest and in the sources registry, a version from a failed deploy looked successful after publishing, and a hand-saved version received a deploy status it never had.

After

Publishing writes only its own marker — a linkedDeployId of the form publish:<timestamp>. The deployStatus field keeps whatever the deploy made it: success, failed, or empty for versions saved by hand.

Impact on integrators

No action required. If you read deployStatus as "this version is published", that was never its meaning; publication is carried by the published entry in tags and by the publish: prefix in linkedDeployId.

BC-0819-9: promo code on a closed seat: the `COUPON_SEAT_CANCELLED` refusal is gone

Old format supported until: not provided

A promo code now works on a closed seat — that is how a departed customer comes back. Redemption turns on the granted tier from the moment it is applied, the seat becomes active again, and its closing mark is cleared.

As a result the COUPON_SEAT_CANCELLED refusal code disappeared: nothing produces it any more. If your code matches refusal reasons against a list, drop it — that branch is now unreachable.

One refusal on a closed seat remains, under a different code. A seat closed early while its paid term had not expired answers COUPON_SEAT_ALREADY_PAID_LONGER: redemption would overwrite the paid term with the gifted one, and the purchased months would vanish from the row a refund is computed from.

Affected endpoints: POST /v1/cowork/coupon/preview, POST /v1/cowork/coupon/redeem

FIX-0819-10: a promo code no longer eats the paid term of a downgraded seat

Before

When a platform admin downgraded a seat off a prepaid plan, the row kept its paid term (an end date in the future and the price of the purchased month) while no charge was scheduled any more. Redeeming a promo code on such a seat went through: POST /v1/cowork/coupon/redeem answered with success, and the granted gift rewrote the end of the paid term to the length of the gift. The purchased months disappeared from the row the refund is computed from.

After

Such a redemption is refused with COUPON_SEAT_PAID_TERM_ACTIVE, new in the refusal set of POST /v1/cowork/coupon/redeem. The paid term on the seat is left alone, the promo code stays with the person and is redeemed once the term ends. The other refusals of the set and their conditions are unchanged.

BC-0819-11: an unbind the platform could not confirm no longer looks like success

Old format supported until: not provided

Before

POST /v1/apps/{id}/unpublish answered 200 and cleared placements whether or not the platform had removed the bindings on the Bitrix24 account. POST /v1/apps/{id}/publish and PATCH /v1/apps/{id} with a changed placement set answered 200 even when the placements being dropped could not be removed. POST /v1/placements/unbind removed the placement from the application list without waiting for the account to confirm it.

After

Unpublish still answers 200 and moves the application to UNPUBLISHED, but placements now carries the codes it could not remove and warnings explains each one. Publish and PATCH answer 502 with code PLACEMENT_UNBIND_FAILED and the code list in error.placements when removal is unconfirmed; the application is not published and the catalog metadata is not saved. A single-placement unbind answers 502 with code BITRIX_UNAVAILABLE and keeps the placement in the application list.

Separately: POST /v1/apps/{id}/publish and PATCH /v1/apps/{id} started answering 503 with code NETWORK_DEVKEY_REQUIRED when the account is switched to developer-key transport and the application author holds no such key. Neither endpoint used to return that code.

What integrators should do

The capability is rolling out gradually and is enabled per account: before it is enabled on your account all four endpoints behave as before, an unconfirmed removal still looks like success, and the codes PLACEMENT_UNBIND_FAILED and NETWORK_DEVKEY_REQUIRED do not occur at all. The code BITRIX_UNAVAILABLE on unbinding a single placement existed before the rollout too — only its condition changes: once enabled, an unconfirmed removal answers with the same code. Once enabled, read placements in the unpublish response: an empty list means everything was removed. On 502 with code PLACEMENT_UNBIND_FAILED, retry — the listed placements are still on the account. If your flow treated 200 as proof of removal, switch the check to placements being empty. On 503 with code NETWORK_DEVKEY_REQUIRED a retry will not help: ask the application author to reconnect the account.

Affected endpoints: POST /v1/apps/{id}/publish, POST /v1/apps/{id}/unpublish, PATCH /v1/apps/{id}, POST /v1/placements/unbind

NEW-0819-12: a self-hosted portal can start the Marketplace demo

Before

POST /v1/portals/{id}/activate-market-trial and POST /v1/cowork/activate-market-trial always refused a self-hosted portal: 409 TRIAL_ACTIVATION_UNAVAILABLE, with /v1/cowork/state reporting not_cloud as the reason. The Marketplace demo was available to cloud portals only.

After

A self-hosted portal starts the demo through the same endpoints. Eligibility is decided by Bitrix24 from the account licence, so the refusal reason is now more precise: demo_used when the demo cannot be granted, and not_supported while eligibility has not been read yet. Response shapes and error codes are unchanged.

The capability is rolled out gradually and is off by default.

BC-0819-13: request body parsing for POST /v1/cowork/deploy-key

Old format supported until: not provided

Before

A request with the Content-Type: application/json header and an empty body answered 400. A request with the Content-Type: text/plain header and a non-empty body was accepted.

After

An empty body is accepted with any header: the endpoint does not read the body. A non-empty body of an unknown type answers 415.

What to do

Drop Content-Type: text/plain from the call or send no body at all. The former behaviour is removed at deploy time, there is no support window.

NEW-0819-14: revoking a Cowork/Code device key with the key itself

Before

A device key could only be revoked from the Vibecode dashboard: both sign-out endpoints take session auth, while an application only holds a key. There was nothing for the app to call.

After

DELETE /v1/cowork/key is available. It revokes the presented key — no identifier is passed, so another key cannot be revoked. It requires the vibe:cowork scope and a Cowork/Code desktop-class key, otherwise 403 COWORK_DESKTOP_KEY_REQUIRED; without the scope, 403 INSUFFICIENT_SCOPE. Calling it again with the same secret answers 401 KEY_INACTIVE. The rate is 5 requests per minute per key, over the limit 429 RATE_LIMITED.

The endpoint works on a zero balance and past the daily quota as well: an emergency exit is never locked.