# API changes: August 22, 2026

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

### NEW-0822-1: promo code on an occupied seat: the preview returns branches and redemption executes them

**Before**

`POST /v1/cowork/coupon/preview` answered only what the code grants, and on an occupied seat
redemption refused with "the seat is already paid" — with nothing the person could do about it.

**After**

The preview returns a `decision` field — what this code does to the seat and what may be offered:
`apply`, `extend` (same tier, the paid window moves), `choose` (branches to pick from, where
"apply now" carries `losesDays` — the whole days of paid term that burn) or `refuse` with a refusal
code. The chosen branch goes to `POST /v1/cowork/coupon/redeem` in the new optional field `action`
(`apply` | `extend` | `force` | `resume-and-apply`); omitting it keeps the previous behaviour.

The action is re-checked at redemption time: if the seat changed meanwhile, the answer is
`409 COUPON_ACTION_NOT_AVAILABLE` and the code is NOT spent. The correct reaction is to re-read the
preview and offer the branches it returns now. An unknown `action` value answers `400 INVALID_ACTION`
— such a body previously answered `400 INVALID_CODE`, pointing at the code field, which was not the problem.

### BC-0822-2: the create and rotate responses now match the documented key shape

> Old format supported until: not provided

**Before**

On some accounts `POST /v1/keys` and `POST /v1/keys/:id/rotate` returned the whole key
row with only the secret fields removed. Alongside the documented shape, the response
carried internal platform fields that appear neither in the key-shape reference nor in
`GET /v1/keys` and `GET /v1/keys/:id`: `preMigrationScopes`, `ownerActive`, `deletedAt`,
`isOAuthApp`, `appId`, `purpose`, `scopesAuthoritative`, `linkedServerId`,
`userAgentAutoModel`, `webhookScopesRepairedAt`, `tokenExpiresAt`.

**After**

Both methods return exactly the documented key shape — the same field set as
`GET /v1/keys/:id`, plus the one-time `rawKey`. The internal fields listed above are
gone from the response; none of them was documented or part of the contract.

If your integration read any of them, switch the source: key state is `status`, access
mode is `accessMode`, and the issuing channel is `issuedVia`. Documented fields, expiry
and error codes are unchanged.

### BC-0822-3: key rotation now requires the same Bitrix24 plan as creation

> Old format supported until: not provided

**Before**

On an account whose Bitrix24 plan does not grant access to the platform,
`POST /v1/keys/:id/rotate` issued a new key, while `POST /v1/keys` on the same account
answered with a plan-required refusal. Rotation stayed a way around that requirement.

**After**

Both methods answer the same way. On an account whose plan does not grant access,
rotation returns `403` with the same code as creation, `INT_TARIFF_REQUIRED`.

Only accounts without a qualifying plan are affected. Self-hosted accounts, accounts on
a qualifying plan and already-issued keys are unchanged: nothing is revoked or stopped,
and rotation resumes as soon as the account is on a qualifying plan.

### NEW-0822-4: a key now reports the channel it was issued through, and a refusal reports its exact cause

**Before**

`POST /v1/keys` and `POST /v1/keys/:id/rotate` did not report which channel issued the
key on the account. On accounts where only the platform module can create an inbound
webhook, both methods answered with an issuance error even though issuance on the
account itself worked.

**After**

The key shape carries an optional `issuedVia` field — the issuing channel; it arrives
in the responses of `POST /v1/keys`, `POST /v1/keys/:id/rotate`, `GET /v1/keys`,
`GET /v1/keys/:id` and `PATCH /v1/keys/:id`. A refusal body gained an optional
`error.reason` field with the exact cause, while `error.code` on existing refusals is
unchanged. Accounts where the platform module issues keys are now served by both
methods.

The create and rotate responses are now identical regardless of the issuing channel —
some channels used to return a narrower field set. The `message` field stays
human-readable and may change: branch on `error.code`, not on its text.

### FIX-0822-5: a rotation refused over scopes now reports the same code on every account

**Before**

`POST /v1/keys/:id/rotate` on a key left with only application-context scopes —
`placement`, `entity`, `userfieldtype` — answered differently depending on the channel the
account issues keys through. Where the platform module issues the key,
`400 PERSONAL_KEY_WEBHOOK_SCOPES_INVALID` arrived, while other accounts got a generic
`502 BITRIX_UNAVAILABLE` after a failed issuance attempt.

**After**

The scope set is checked before the account is contacted, so the refusal is the same
everywhere — `400 PERSONAL_KEY_WEBHOOK_SCOPES_INVALID`, the code creation and update
already return. Nothing changes on the client side: the refusal is more precise and
arrives earlier. To let rotation through, add a data scope to the previous key via
`PATCH /v1/keys/:id`.

### NEW-0822-6: platform requests to Bitrix24 now carry a signed origin proof

**Before**

Bitrix24 refused the call on plan grounds or for a missing Market subscription, and a platform request looked to it exactly like any other integration.

**After**

Every Bitrix24 request carries an `X-Vibecode-Origin` header — a short-lived signed token. The token names the origin: `cowork` for a paid Cowork/Code seat key, `app` for every other user key, `vibecode` for the platform's own service calls. An account with the Vibecode connector module installed admits Cowork/Code requests on that proof; other values grant nothing. Accounts without the module ignore the header; existing integrations are unaffected.

### FIX-0822-7: Reliable source downloads for dedicated VM deploys

**Before:** a dedicated virtual machine fetched `source.url` through its own network path. One orchestrator retry did not tolerate intermittent egress or NAT failures, and callers received raw agent networking text.

**After:** for servers enrolled in the staged rollout, the platform downloads the archive and streams it to the VM through the tunnel, with up to four attempts and backoff inside a shared five-minute budget. The compatible VM-download fallback and public [`/upload`](/docs/infra/deploy/upload) receive bounded retries, a safe outbound policy, and stable messages under the same rollout; before enrollment, the existing contract is preserved. `DEPLOY_FAILED` is unchanged; in the new mode a failed `download` step adds `causeCode`, `retryable`, and `attempts`. See the [deploy documentation](/docs/infra/deploy/deploy).

**Impact:** request payloads do not change. Automation can branch on the new fields instead of parsing message text; archives up to 500 MB are streamed and do not consume an inline-memory slot. For public `/upload`, the long phase in the new URL mode completes under an HTTP 200 keepalive response, so determine success or failure from JSON `success` and `error.code`; pre-dispatch checks and pre-enrollment behavior retain their previous HTTP statuses.

### BC-0822-8: rotating and bringing back platform-issued keys is closed

> Old format supported until: not provided

**Before**

[POST /v1/keys/:id/rotate](/docs/management-keys) rotated any key of its owner, and
[PATCH /v1/keys/:id](/docs/management-keys) moved any key back to `ACTIVE` and pushed its expiry
back — including the keys the platform issues on its own separate endpoints: a Cowork/Code desktop
key, an agent key and a project deploy key. In the `GET /v1/keys` listing such a key is barely
distinguishable from a personal one, so a "rotate or switch back on all my keys" script walked over
it along with the rest: rotation answered `201` with a fresh `rawKey`, switching back on answered
`200` — and the key value never changed there, so the previous one started working again.

**After**

Both operations on such a key answer `403`: rotation with the code `SYSTEM_KEY_ROTATE_FORBIDDEN`,
moving back to `ACTIVE` and extending the expiry with the code `SYSTEM_KEY_REACTIVATE_FORBIDDEN`.
The response text says where a key of that class is issued again. The key is left as it was in both
cases. Both operations bypassed the checks that guard the issuance of each such key: for keys
carrying `vibe:cowork` — Cowork/Code access, the account "third-party clients" policy and the
subscription state; for a project deploy key — three platform switches, the Cowork/Code key
requirement and an active subscription. Rotation also handed out a value with no expiry at all: on a
project deploy key that removed the seven-day lifetime, the only thing that limits a leaked key.
Revoking a key and shortening its expiry are still allowed: those stop a key rather than hand one
out. The former behaviour is gone at once, with no transition period.

**What integrators should do**

Exclude the keys the platform issues from bulk rotation and bulk switch-on: a Cowork/Code desktop
key, an agent key and a project deploy key. The first two are recognisable in the `GET /v1/keys`
response by the `vibe:cowork` scope; a project deploy key has no separate marker in the listing, so
treat a `403` with these codes as the final answer for that key and do not retry. A desktop key is
issued again by connecting the Cowork/Code desktop app, an agent key — by re-issuing the agent key
in the Vibecode dashboard, a project deploy key — by calling `POST /v1/cowork/deploy-key` again.
Other keys rotate and switch back on as before.

### NEW-0822-9: POST /v1/cowork/deploy-key is closed to external-agent keys

**Before**

The endpoint checked the `vibe:cowork` scope only.

**After**

On [POST /v1/cowork/deploy-key](/docs/cowork/deploy-key) a key issued for a third-party agent gets
`403 COWORK_HARNESS_KEY_FORBIDDEN`. Such a key lives inside
someone else's application, while minting a project deploy key revokes the previous one and moves the
owner's servers and applications onto the new key. Desktop and agent keys are unaffected.

### NEW-0822-10: Reactivating a subscription key checks the issuance gates

**Before**

A subscription-billed key went back to active through an ordinary `status` field change, and its
expiry was cleared with `null`. Only the permanent block was checked, so a revocation by the
Bitrix24 account administrator was undone in a single request — and the secret itself never changed.

**After**

[PATCH /v1/keys/:id](/docs/management-keys) answers `403` with the matching gate code when the
request moves a subscription key to `ACTIVE`, or pushes back or clears `expiresAt`, while Cowork
access, the Bitrix24 "third-party clients" policy or the subscription state is closed. Shortening
the expiry, revoking and editing the other fields work as before. Ordinary keys are unaffected.

### NEW-0822-11: Rotating a subscription key checks the issuance gates

**Before**

Rotation carried the key's scopes and purpose over verbatim, asking nothing.

**After**

[POST /v1/keys/:id/rotate](/docs/management-keys) answers `403` with the matching gate code when
rotating a subscription-billed key while Cowork access, the account's "third-party clients" policy or
the subscription state is closed. Ordinary keys rotate as before.

### NEW-0822-12: POST /v1/keys rejects subscription billing

**Before**

The `billing` field did not exist in the request body; the unknown key was silently dropped by the schema.

**After**

On [POST /v1/keys](/docs/management-keys) the value `billing: "subscription"` is rejected with
`BILLING_MODE_NOT_SUPPORTED`. Subscription-billed keys are
issued from the Vibecode dashboard only — that is where the access, Bitrix24 account policy and
subscription-state checks live, and the public route has none of them. `billing: "wallet"` and requests without the field
behave exactly as before.

### FIX-0822-13: The inactive-subscription refusal now points at the subscription page

**Before**

The `402` refusal carrying code `cowork_subscription_inactive` advised activating the
subscription by calling `POST /api/cowork/subscription`. That route works only inside a
signed-in web session and is unavailable to a client authenticating with an API key. A
third-party agent client shows the `message` field verbatim, so the advice looked actionable
while there was nothing to act on.

**After**

The same refusal on [POST /v1/chat/completions](/docs/ai/chat) names the state and the place
where it changes: the subscription is resumed in the Cowork/Code section of your Vibecode
account, and the key stays the same. The code, the status and the other body fields are unchanged.
