# API changes: August 19, 2026

[← Changelog](/docs/changelog) · [August 2026](/docs/changelog/2026-08)

### 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](/docs/bots/messages/send) 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](/docs/keys-auth/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](/docs/apps/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](/docs/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](/docs/apps/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](/docs/source-storage).

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

**Before**

[POST /v1/apps/:id/publish](/docs/apps/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](/docs/source-storage)), 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](/docs/apps/unpublish) answered `200` and cleared `placements` whether or not the platform had removed the bindings on the Bitrix24 account. [POST /v1/apps/{id}/publish](/docs/apps/publish) and [PATCH /v1/apps/{id}](/docs/apps/update) with a changed placement set answered `200` even when the placements being dropped could not be removed. [POST /v1/placements/unbind](/docs/apps/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](/docs/apps/publish) and [PATCH /v1/apps/{id}](/docs/apps/update) 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](/docs/apps/publish), [POST /v1/apps/{id}/unpublish](/docs/apps/unpublish), [PATCH /v1/apps/{id}](/docs/apps/update), [POST /v1/placements/unbind](/docs/apps/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.
