# API changes: August 18, 2026

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

### BC-0818-1: application and source-management contract clarified

> Old format supported until: not provided

**Before**

OpenAPI did not describe the exact success responses, metadata limits, and some error codes for the [application source-storage operations](/docs/source-storage). One application's OAuth key could mutate another application and read or mutate the same author's server sources when the server belonged to a different key. The note limit counted UTF-16 code units, so some valid Unicode strings were rejected. An intermediary cache could retain a temporary download URL.

**After**

The eight-operation contract now describes the actual response schemas, limits, error codes, and access matrix. An OAuth application key can now directly manage only its own application and access sources of a server owned by that same key. When publishing its own application, an explicit `sourceServerId` may select the same author's server under a personal key, but not a server owned by another OAuth application. The author's personal keys and account-administrator keys retain access. V1 application-operation audit records store the key owner's Vibe UUID rather than the numeric Bitrix24 user ID. The note limit is counted in Unicode code points. Application and server temporary-link responses are marked `Cache-Control: private, no-store`, and the URL, including its storage path, is explicitly identified as a short-lived bearer credential.

No transition window is provided because an OAuth application key's former access to another key's resources was an access-control defect.

### FIX-0818-2: `?refresh=tariff` on a self-hosted portal re-checks its access state at the source

**Before**

`GET /v1/me?refresh=tariff` on a self-hosted portal re-checked the plan but returned the portal's access state from the previous snapshot. A change made right before the call stayed invisible until a background check picked it up.

**After**

The request re-checks that state at the source as well, and the response is built from fresh data. A change made right before the call is visible immediately.

### NEW-0818-3: workgroup roster with member roles

A new endpoint `GET /v1/workgroups/:groupId/users` returns the members of a workgroup together with their role in it. Previously the `/v1/workgroups` facade exposed only the owner and the member count, so an application that had to tell a head, a moderator and an ordinary member apart could not do so.

Every row carries `userId` and `role`. The `role` field holds the Bitrix24 letter verbatim: `A` owner, `E` moderator, `K` member. An unrecognised letter is passed through rather than mapped onto a known one or dropped, so branch on the values you know and treat anything else as no permission.

The operation takes no pagination parameters and returns the whole roster, so `meta.total` always equals the number of rows in `data`. An empty roster is an ordinary answer, and the owner is not necessarily among the rows — read the owner identifier from `GET /v1/workgroups/:id`. A `404` means the workgroup does not exist **or** is not visible to the Bitrix24 identity the key acts as, because Bitrix24 refuses both cases identically.

The endpoint returns membership data, not an authorization decision: what it shows depends on the identity behind the key, and Bitrix24 account administrator status is a separate fact served by `GET /v1/users/me`. Nested resources are not addressable through `POST /v1/batch`, so the operation is called on its own path only. Requires the `sonet_group` scope.

### FIX-0818-4: deals: seven fields from GET /v1/deals/fields are now accepted in a filter

**Before**

`GET /v1/deals/fields` listed fields the filter would not accept. A
`POST /v1/deals/search` carrying `filter[leadId]`, `filter[quoteId]`,
`filter[taxValue]`, `filter[originId]`, `filter[originatorId]`,
`filter[additionalInfo]` or `filter[lastActivityBy]` was rejected with
`400 UNKNOWN_FILTER_FIELD` before Bitrix24 was called at all, even
though Bitrix24 does accept those fields in a filter. A report selecting deals by
their lead could not be built.

**After**

All seven fields are declared in the deals schema and are accepted in `filter` —
on `GET /v1/deals`, on `POST /v1/deals/search` and on `POST /v1/deals/aggregate`.
They were already accepted in `select`, but with an `UNKNOWN_SELECT_FIELD`
warning; that warning is gone. Sorting by them already worked before this change
and is unaffected. Field names in responses are unchanged. The set of `groupBy`
axes is untouched.

Two notes on values. `leadId`, `quoteId`, `originId`, `originatorId` and
`additionalInfo` are now marked `nullable` in `GET /v1/deals/fields` and in
OpenAPI: they return `null` on a deal that was not created from a lead, from a
quote or by import. They were absent from the specification before, so a client
generated from it must be ready for `null`. And on the three string fields
(`originId`, `originatorId`, `additionalInfo`) an empty string from Bitrix24 is
normalised to `null`, exactly as it already is on every other string field of the
API. Across a 300-deal sample no empty string occurred at all — Bitrix24 returns
`null` on these fields — so no change on real data is expected; but if your code
compares such a field with `""`, compare it against an empty value instead.

Six fields — `utmSource`, `utmMedium`, `utmCampaign`, `utmContent`, `utmTerm`
and `contacts` — stay rejected in filter and sort, because Bitrix24 does not
support them there. The rejection is deliberate: accepting them would produce a
silently wrong selection. The reason is now visible up front, in the
`description` of each field in `GET /v1/deals/fields`. To filter by contact, use
`contactId` (the primary contact) or `contactIds` (any linked contact).

On leads, quotes and smart-process items, the five UTM fields remain available
in responses but no longer pass the local filter or sort guard: Bitrix24 does
not accept them in `crm.item.list`. Instead of a Bitrix24 `422`, the request now
receives `400 UNKNOWN_FILTER_FIELD` or `400 UNKNOWN_SORT_FIELD` before Bitrix24
is called. Other operations on these fields are unchanged.

### FIX-0818-5: empty workday method results are returned as null

**Before**

When Bitrix24 returned an empty successful result, the `data` field could contain a service envelope with `result`, `total`, and `next` fields instead of the result value.

**After**

[POST /v1/workday/open](/docs/workday/open), [POST /v1/workday/close](/docs/workday/close), [POST /v1/workday/pause](/docs/workday/pause), [GET /v1/workday/status](/docs/workday/status), [GET /v1/workday/settings](/docs/workday/settings), and [GET /v1/workday/schedule](/docs/workday/schedule) return `data: null` when the successful result is empty.

**Impact on integrations**

Clients no longer need to extract an empty value from the Bitrix24 service response envelope.

### NEW-0818-6: the field reference for orders and order statuses now returns names and explanations

**Before**

`GET /v1/orders/fields` and `GET /v1/order-statuses/fields` answered with nothing but a type and a read-only flag per field. There was no human-readable name and no explanation for any of the 54 fields, so a client had only the field name to go on. Generating a form or a typed model from such a schema was not possible: a name like `recountFlag` or `empStatusId` explains nothing on its own.

**After**

Every field of both entities now carries a `label` (short name) and a `description` (explanation) in the language of the segment. The explanation states what the type cannot: why `price` is accepted on create only (Bitrix24 recalculates the amount from the basket items), that `requisiteLink` arrives as an empty array when the link is unset, that `clients`, `payments`, `basketItems` and `propertyValues` are returned by the order card only, and that Bitrix24 requires the `type` field on every status update. The response grew; the set of fields and their types did not change, so existing requests keep working.

### NEW-0818-7: machine issuance of promo codes: three V1 methods and two new integration-key scopes

Vibecode now exposes three V1 methods for an integration that hands out promo codes on its own: `POST /v1/platform/coupons/issue` issues a batch of codes inside an existing campaign, `GET /v1/platform/coupons/campaigns` lists the campaigns available for issuance, and `GET /v1/platform/coupons/campaigns/{slug}` reads one campaign by its code. Authorization is a platform integration key in the `Authorization: Bearer` header, with the `coupons:issue` and `coupons:read` scopes respectively; a platform administrator grants them when the key is issued.

The key issues codes but neither creates campaigns nor moves their ceiling: both stay with a platform administrator, otherwise the campaign ceiling would stop being a ceiling. A campaign is addressed by the same code (`slug`) that prefixes every promo code it issued, so the integrator and support share one identifier. The `remainingToIssue` field answers how many codes may still be issued; `null` means the campaign has no ceiling.

The `Idempotency-Key` header is required. Codes are returned once and are not stored on the platform side — only their hashes are — so there is nothing to replay: a request with an already used key gets `409 IDEMPOTENCY_KEY_ALREADY_USED` with a reference to the issued batch, not a second set of codes. Issuing into a draft campaign is refused with `409 COUPON_CAMPAIGN_IN_DRAFT`: codes handed out before the campaign is activated would be refused at redemption and cannot be reissued. The remaining refusals: `409 COUPON_CAMPAIGN_NOT_ISSUABLE` — the campaign is finished or archived, `409 COUPON_CAMPAIGN_CAP_REACHED` — the batch does not fit under the ceiling, `403 INSUFFICIENT_SCOPE` — the key lacks the required scope, `404 CAMPAIGN_NOT_FOUND` — no campaign carries this code.

Redeeming a promo code through a machine method is not part of this release: a code is redeemed by a person in the dashboard, under their own session and on their own account.

### NEW-0818-8: promo codes for Cowork/Code tiers and the refusal codes of redemption

Vibecode now has promo codes. A partner gets a code from the organiser and redeems it in the Cowork/Code section of the dashboard: the granted tier switches on for their seat for a term the platform pays for, with no debit from the account balance. Campaigns, issuing a batch of codes and revoking unissued codes are run by the platform administrator; redemption is a user action in the dashboard and has no public API method.

A refused redemption answers with a code in the `code` field, and most causes collapse into a single `COUPON_INVALID` on purpose: "not found", "revoked", "already redeemed", "expired", "the campaign is over" and the campaign limits are indistinguishable from one another, so a refusal never confirms that a live code exists. Only the causes a person can act on stay distinguishable: `COUPON_TOO_MANY_ATTEMPTS` — too many attempts in a row; `COUPON_PORTAL_ACCESS_GATED` — the account has no Cowork/Code access yet; `COUPON_SEAT_PAUSED`, `COUPON_SEAT_CANCELLED` and `COUPON_SEAT_CANCELLATION_SCHEDULED` — the state of the seat prevents the grant; `COUPON_TIER_DOWNGRADE_BLOCKED`, `COUPON_SEAT_ALREADY_ON_TIER`, `COUPON_SEAT_ALREADY_PAID_LONGER` and `COUPON_SEAT_IS_PAID` — the current tier is already no lower than the granted one, or is paid further ahead. `CONCURRENT_REDEMPTION` stands apart: several codes of one campaign were redeemed at the same moment, and the attempt only needs repeating.

A promo code never downgrades the current tier and is never applied on top of an already paid seat — in both cases it stays with its holder and is redeemed later. A redeemed promo code cannot be revoked: revocation applies only to a code nobody has used yet.

### NEW-0818-9: promo code in the Cowork/Code app: check a code and redeem it with a desktop key

The Cowork/Code app now accepts a promo code itself instead of sending the person to the dashboard. Two methods have appeared: `POST /v1/cowork/coupon/preview` shows what a code grants (tier, term, campaign name) without changing anything, and `POST /v1/cowork/coupon/redeem` redeems it and turns on the granted tier for a term the Vibecode platform pays for.

Both methods require a Cowork/Code desktop key with the `vibe:cowork` scope. An agent key carrying the same scope gets `403 COWORK_DESKTOP_KEY_REQUIRED`: redemption is irreversible and one-shot, and there is no human behind such a key to make the decision.

The check answers `200` and reports `valid: false` with reason `COUPON_INVALID` when the code does not work. One reason covers every "code does not work" case, so the method never hints to someone guessing codes how a non-existent code differs from a revoked one. Valid here means "the code is live and the campaign is open": the seat's own conditions (already paid, already on this tier) are verified at redemption and may refuse after a successful check.

Redemption returns the granted tier, the term and an `accessGranted` field. `false` means the tier was granted but the Bitrix24 account administrator has not opened access to Cowork/Code yet — show that separately, otherwise the person sees success and runs into a closed door.

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

### NEW-0818-10: applications catalog in the API: list and card

The Vibecode API now exposes an applications family: [GET /v1/applications](/docs/applications/list) returns the list and [GET /v1/applications/:id](/docs/applications/get) a single card. Rows are scoped to the key OWNER rather than to the calling key, so applications created in the Vibecode dashboard are included too — their servers belong to other keys of the same person and never appear in `GET /v1/infra/servers`.

The `scope` parameter accepts `mine`, `shared` (applications of other people that you can access — both those shared with you personally and those open to the whole Bitrix24 account) and `feed` (the default), alongside the `page` and `limit` paging parameters. The card is also available to someone the application was shared with, not only to its owner; the viewer's relationship to the application arrives in the `viewerState` field.

The list response carries `truncated` next to `total`. In the `feed` scope the ordering is computed by the platform, so the selection has an upper bound: when `truncated` is `true`, `total` is that bound rather than the full number of applications, and there are no pages beyond it. Deriving a page count as `total / limit` is only valid while `truncated` is `false`; when it is `true`, reach for the `mine` and `shared` scopes, which have no such bound.

Every card carries two extra blocks. `sources` reports whether saved source versions exist and returns the latest one in the very form the version download accepts, so no upfront call for the version list is needed. `activeOperation` reports an operation in flight: its kind, step and start time; the value `unknown` means the operation did start but its outcome is not known. Both blocks are filled in only for whoever manages the application: a viewer the application was merely shared with receives an empty `sources` (`hasVersions: false`, both fields `null`) and `activeOperation: null`. The response shape does not change, so beware of the wrong conclusion: an empty `sources` on someone else's application means "this data is not disclosed to you", not "there are no versions".

One caveat about `activeOperation`: `null` means "no operation with a stored record", not "nothing is happening to this application". The field covers deployment, repair, server plan changes and moving a container between galaxies — other actions are not journalled by the platform and never surface here.

Where to open an application is no longer yours to work out: the card returns a ready `openUrl` and `openTarget` pair. `openTarget` currently has a single value — `app`, meaning the application's own address opens; both fields are `null` when there is nothing to open. The value set is closed and may grow, so treat an unfamiliar value as "nothing to open here" rather than as an error.

**Important:** for an application embedded into Bitrix24 both fields arrive `null` — the platform does not yet know the address such an application opens at inside the account. Its own address is deliberately NOT substituted into `openUrl`: that address leads to the gateway sign-in page rather than into the application, so following it would look successful without being so.

The `isEmbedded` field tells those two states apart — "embedded, opens inside Bitrix24" versus "not published yet". Both arrive with an empty open pair, yet the text a user should see differs. Do NOT infer embedding from the presence of a server: an embedded application with no server of its own is a normal state (the embedding is done, the code has not been deployed yet), and the flag does not depend on the server at all. The field is disclosed both to the owner and to someone the application was shared with.

The server summary gained a `reachable` flag — `true` when the server both runs and answers over the network. It is separate from `status` because those are different facts: a server can be up while the network tunnel to it is not, and by `status` alone such an application looks healthy. A client cannot check this from outside, so the platform computes the flag.

The flag already implies the `RUNNING` state: it never arrives true for another server state, so there is no need to conjoin it with `status`. **For an application inside a galaxy (`server.kind: "GALAXY_APP"`) the second half of the flag is taken from the galaxy HOST, not from the container itself** — the host holds the connectivity, the container has no tunnel of its own by design. Hence a consequence worth knowing up front: a freshly created container that has not reached `RUNNING` yet (it only does so after its first source upload) arrives with `reachable: false` even on a fully healthy host. That means "the container is not up yet", not "the host is unreachable" — the flag alone cannot tell the two apart, `status` can.

The server summary gained a `lastDeployedAt` field — when the application was last deployed successfully. It is the only signal in this section that an application is actually lived in: `updatedAt` only moves when the card is edited, and `sources.latestSavedAt` means "code saved" and reaches only whoever manages the application. The stamp is written by the deployment itself at the moment it records success, so a failed deployment never moves it. Important: the field carries no history: for servers created before it existed it arrives `null` until their next deployment — we did not reconstruct the history, because the available source covers only one of the three deployment paths and a date would appear for some server kinds while missing for others.

Two more things worth knowing up front. The application `id` from this section and the `id` from `GET /v1/apps` are different values of different entities: an application card here, an application registration on the Bitrix24 account there. And the same thing by meaning arrives under different names: `name` here, `title` there. Do not carry one over into the other — the fields are not synchronized.

The list ordering is now stated explicitly so a page walk is reproducible. In the `feed` scope: pinned by you → your own → other people's, and within a group by `updatedAt` newest first, ties broken by `id`. In `mine` and `shared`: by `createdAt` newest first, ties broken by `id`. That secondary key is not a formality — applications created in a batch carry identical timestamps, and without it two pages of one walk could overlap or skip an application. Important: `updatedAt` only moves when the card itself is edited — a deployment does not touch it, so "freshness" in the feed means "when the card was last changed", not "when the application was last deployed".
