# API changes: August 1, 2026

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

### FIX-0801-1: AI endpoint response bodies no longer carry platform-internal fields

**Before**

Responses of `POST /v1/chat/completions` on Bitrix24 models returned a platform infrastructure identifier in `system_fingerprint`. Alongside the declared fields, other undocumented internal fields were returned as well — both in the response envelope itself and inside `choices` and `choices[].message`. Streaming responses and `POST /v1/embeddings` were not observed to carry those fields, but they had no check in place either.

**After**

`system_fingerprint` now carries the neutral value `vibecode`. When the upstream sends no fingerprint, the field is absent from the response, as before. Internal fields have been removed from the response envelope and from the `choices` objects; inside `choices[].message` the `provider_specific_fields` container has been removed. In an in-stream error event the `error` object keeps its `message`, `type`, `param`, `code`, `retryAfter` and `retryable` fields; internal fields next to them have been removed.

The declared contract is unchanged: `id`, `object`, `created`, `model`, `choices`, `usage` for chat and `object`, `data`, `model`, `usage` for embeddings arrive exactly as before — including provider extensions inside `usage`, reasoning fields inside `choices[].message`, and tool calls. The in-stream error event is still delivered and still carries `error`. No client changes are required.

The change affects the response body of Bitrix24 models only. In an in-stream error event `error.message` is now normalised to the public model name, and if the upstream put something other than a string in `message` the field is not returned at all. The text of ordinary platform errors (`4xx`, `5xx`) is unchanged.

### FIX-0801-2: galaxy app deploy no longer reports an interruption after an update that actually succeeded

**Before**

If the connection to the host dropped mid-way through [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy), the platform re-checked the app state once and, when that check went unanswered, returned `502 GALAXY_DEPLOY_INTERRUPTED`. In the common case the update was still in progress and completed successfully — seconds later the app answered `health`, served the new version and applied the new environment variables. Telling such a spurious failure apart from a real one required a manual check, and repeating the deploy redeployed the very same version.

**After**

After a drop the platform re-checks the app state for a bounded period instead of once, and answers `success` when the new version came up, exactly as if no drop had happened. `502 GALAXY_DEPLOY_INTERRUPTED` is now returned only when no confirmation appears within that period. The wait is sized against the window the platform must answer within, so worst-case request duration does not grow.

The body of that error additionally carries `error.retryable: true`, so a client can tell it apart from a permanent failure without parsing prose. The existing fields (`error.code`, `error.message`, `error.hint`) are unchanged.

### BC-0801-3: publication checks application authorization before snapshot freshness

> Old format supported until: 30.01.2027

**Before**

[POST /v1/apps/:id/publish](/docs/apps/publish) checked source-snapshot freshness first and application authorization only afterwards. For a caller without authorization the response depended on an unrelated condition: a stale snapshot gave `409 SNAPSHOT_REQUIRED`, a fresh one gave `400 NO_USER_TOKEN`. The alternation read as "the token check passes sometimes", although authorization was absent in both cases.

**After**

Authorization presence is checked before the freshness gate. An application without authorization gets `400 NO_USER_TOKEN` immediately, whatever the snapshot state. The sequence is now monotone: you clear authorization first, and only the snapshot requirement remains.

The change affects the cases that previously answered with a different code: **no authorization AND the snapshot is stale or missing** — previously `409 SNAPSHOT_REQUIRED`, now `400 NO_USER_TOKEN`; **no authorization AND the resolved catalog title exceeds the limit** — previously `400 TITLE_TOO_LONG_FOR_CATALOG`, now `400 NO_USER_TOKEN` (same status, different code). If authorization exists but the token could not be renewed, the response is still `400` and still arrives after the freshness gate. Publication on a self-hosted account through a developer key needs no application authorization and is not affected by this check.

**What integrators should do**

If your handler reacted only to `409 SNAPSHOT_REQUIRED` and re-saved sources in a loop, add a branch for `400 NO_USER_TOKEN` — there you need to authorize the application, not save the sources again. The `error.hint` field in that response describes the action.

### NEW-0801-4: hints in publication responses: hint on NO_USER_TOKEN and presentedAt on SNAPSHOT_REQUIRED

The `400 NO_USER_TOKEN` response of [POST /v1/apps/:id/publish](/docs/apps/publish) now carries an `error.hint` object with `requiredAction` (what exactly to do to authorize the application), `docsUrl` and `oauthDocsUrl`. The object shape matches the `error.hint` of `409 SNAPSHOT_REQUIRED` on the same endpoint.

The `409 SNAPSHOT_REQUIRED` response gained `error.hint.lastSnapshot.presentedAt` — when the version was last presented by a save. That is what `ageMinutes` is counted from, so the pair of fields shows why the version is considered stale. The neighbouring `timestamp` field still means the version creation time.

Both fields are additive: existing calls work unchanged.

### FIX-0801-5: re-saving the same sources unblocks publication

**Before**

The freshness check before [POST /v1/apps/:id/publish](/docs/apps/publish) counted its window from the version creation time. Re-saving the same bytes returned `HTTP 201` with `deduplicated: true` but created no new version and did not move that creation time, so publish kept answering `409 SNAPSHOT_REQUIRED`. A caller whose sources had not changed entered a `publish` → `409` → `POST /v1/apps/:id/sources` → `publish` → `409` loop that never converged: the only way to refresh the snapshot was to change the archive contents.

**After**

A save marks the version as presented again, and the freshness window is counted from that mark. A deduplicated save unblocks publication on a par with a real one. The version creation time (`data.timestamp`) does not change, so the stored file name and the version's place in the retention policy stay the same.

**Impact on integrators**

Nothing to change. The recipe from the `409` hint — save the sources and retry — now works even when the sources have not changed.

### BC-0801-6: a broken exec no longer answers with success and exitCode -1

> Old format supported until: 01.02.2027

**Before**

When the [POST /v1/infra/servers/:id/exec](/docs/infra/deploy/exec) stream ended without sending an exit status, the response arrived as `success: true` with `exitCode: -1`. The command's outcome on the server is unknown in that case, so such a response was not a success. In streaming mode (`?stream=true`) the stream simply closed in silence.

**After**

On a standalone virtual machine (`kind: "STANDALONE"`) that response now arrives as `success: false` with the code `EXEC_NO_EXIT` and a `hint` object pointing at a server-state check. `data` carries the output collected up to the break (`stdout`, `stderr`); the `exitCode`, `duration` and `truncated` fields are absent — their values are unknown, and filling them with zeroes would assert something the platform does not know. In streaming mode an `error` event with the same code arrives. On a galaxy app this case still arrives as before.

**What integrators should do**

Handle `EXEC_NO_EXIT` alongside the other error codes. If your code read `data.exitCode` without checking `success`, it will now get `undefined` instead of `-1` — branch on `success`. The command can be repeated if it is idempotent; if it is not, inspect the server state first via [GET /v1/infra/servers/:id/logs](/docs/infra/deploy/logs).

### FIX-0801-7: the JSON response body of /exec and /deploy now starts with an opening brace

**Before**

In JSON mode (without `?stream=true`), [POST /v1/infra/servers/:id/exec](/docs/infra/deploy/exec) and [POST /v1/infra/servers/:id/deploy](/docs/infra/deploy/deploy) hold the connection by sending spaces every 15 seconds. Those spaces went **before** the JSON document, so for a command longer than 15 seconds the response body started with spaces. Clients that validate the format strictly refused to parse it — while the command itself had completed successfully on the server.

**After**

The keepalive spaces now go **inside** the already-opened JSON object, so the body starts with `{` from its very first byte. The set of response fields is unchanged.

**Impact on integrators**

Clients that parsed the response with an ordinary JSON parser will notice nothing — both body shapes are valid. Clients that stripped leading spaces by hand no longer need to.

### FIX-0801-8: datetimes without a timezone no longer drift when the caller's zone differs from the webhook owner's

**Before**

A datetime value without an explicit offset — e.g. a deadline of 2026-07-15T13:00:00 — reached Bitrix24 verbatim and was read in the timezone of the account's webhook owner, not the caller's. An integration in Berlin that wrote 13:00 had 10:00 UTC stored instead of 11:00 UTC: one hour off in summer, two in winter. The value looked plausible, so corrupted payment dates, deadlines and meetings went unnoticed.

**After**

A client can declare its own timezone with the X-Vibe-Timezone header (an IANA name such as Europe/Berlin; a browser reads its own from Intl.DateTimeFormat().resolvedOptions().timeZone). Datetimes without an offset are stamped with that zone's offset as it was in force on the value's own date — daylight-saving transitions are honoured per value. This applies to fields that genuinely keep a time of day: Bitrix24 stores several fields it calls datetime as plain dates, where an offset would move the stored day, so those are left alone. Values that already carry Z or an offset are never rewritten. Without the header (or with an unrecognised zone) behaviour is unchanged — existing integrations are unaffected.

The header affects writes only. Bitrix24 discards a timezone suffix inside a filter, so the platform strips it and filter values are always read in the account's zone. With the header set, writing "2026-07-15T13:00:00" and then filtering on that same literal will not match: send the instant you want compared, or use a range wide enough to cover the offset.

### FIX-0801-9: bot deletion is idempotent: "bot already gone" now succeeds instead of returning 502

**Before**

[DELETE /v1/bots/:botId](/docs/bots/management/delete) answered `502 BOT_DELETE_PARTIAL` and kept the Vibecode database record on ANY Bitrix24 error — including when Bitrix24 reported that it no longer had such a bot. The desired end state was already reached, yet the call counted as failed and the record stayed in `GET /v1/bots` forever: a plain delete could never remove it, only `?force=true` did.

**After**

When Bitrix24 answers that the bot does not exist, the deletion counts as successful: the record is removed and the `200` response carries a new `data.alreadyAbsentOnB24: true` field. In addition, when Bitrix24 rejects the unregister for another reason, Vibecode checks once whether the bot is still on the account: if it is already gone the call succeeds, if the removal could not be confirmed the previous `502 BOT_DELETE_PARTIAL` with a preserved record is returned. With `?force=true`, a "bot does not exist" answer now also carries `alreadyAbsentOnB24: true` instead of `forced: true` — nothing is orphaned on the Bitrix24 side in that case. The `502` body gained an `error.incidentCode` field — a six-character code to quote to support.

**Impact on integrators**

No changes required. The "deleted a bot, got an error, the bot stayed in the list" scenario no longer happens; `?force=true` remains only for accounts that are unreachable for good. If your code branches on `data.forced`, note that the "bot already gone on Bitrix24" case now returns `data.alreadyAbsentOnB24` instead.

### FIX-0801-10: Bitrix24 error messages no longer carry HTML markup

**Before**

Validation text received from Bitrix24 was forwarded into `error.message` with a trailing `<br>` tag attached. A client rendering the message as text — which is what a JSON contract implies — showed the literal tag to the user after every error, in their own language. The same applied to `error.validation[].message`.

**After**

The markup is removed at the response boundary: `<br>` in any spelling becomes a newline. A newline rather than a space, because Bitrix24 uses that tag to join errors for different fields, and the boundary between them is worth keeping. Localisation is unchanged: the text stays in the language of the account and is not translated. Angle brackets inside user data (an email address quoted in the error text, or a comparison sign) are left alone — what is removed is the line-break tag, not markup in general. The same applies to `POST /v1/batch` responses, where an error arrives per sub-call. If you were stripping `<br>` on your side, that handling can go.

Two technical bounds were also introduced: a message longer than 8192 characters is truncated, and at most 100 entries of `error.validation` are returned. Both exist because the text and the number of fields come from the account and are otherwise unlimited; neither triggers on real responses.

Coverage: `error.message` and `error.validation[]` in every V1 error envelope, `POST /v1/batch` and `POST /v1/{entity}/batch` responses, `meta.pageErrorSample` during auto-pagination, and `POST /v1/bots`. In `POST /v1/chats/messages/bulk` the per-sub-call error fields are now normalised to the common `{code, message}` shape — previously the account's error object was forwarded as-is, with its own `error`/`error_description` keys.

Separately: count phrases in French and Portuguese e-mails now follow CLDR, where zero takes the singular form. And the English hint about the Universal Lists module no longer carries a Russian module name on international accounts.

### NEW-0801-11: CALL_CARD placement — panel inside the call card

**Before**

`GET /v1/placements/available` omitted `CALL_CARD`, and `POST /v1/placements/bind`
answered `VALIDATION_ERROR` for that code, even though Bitrix24 supports it.

**After**

`CALL_CARD` is listed and accepted for binding. The application needs the
`telephony` scope: Bitrix24 offers this placement only to applications holding it,
otherwise the bind is refused with "Placement not found". The same scope is now
required for the neighbouring `TELEPHONY_ANALYTICS_MENU` — it used to be listed
without the scope, so binding it failed silently on the Bitrix24 side.

### FIX-0801-12: four silent refusals: activities, warehouses, files, user fields

**Before**

Four requests answered with 200 and returned something other than what was asked for.

Activities: the `providerParams` and `settings` fields are declared as `object`, but an empty value arrived as an empty array. The field type depended on the content, so a client generated from the schema broke while deserializing exactly those records where the value was empty.

Warehouses: [GET /v1/warehouses](/docs/entities/warehouses/list) and [GET /v1/warehouses/:id/stock](/docs/entities/warehouses/stock) returned the first page for any offset that was not a multiple of 50 — `offset=1`, `offset=2`, and `offset=3` all returned the same records, while `hasMore` reported that more existed. Paging by offset looped on page one.

Files and folders: a filter on a field Bitrix24 cannot filter by (`createdBy`, `size`, `updatedBy`) was silently dropped, and the whole folder came back instead of the selected records. A non-existent field name behaved the same way.

User fields: `DELETE /v1/userfields/:entity/:id` and `DELETE /v1/items/:entityTypeId/userfields/:id` with a `Content-Type: application/json` header and an empty body answered with the `FST_ERR_CTP_EMPTY_JSON_BODY` error — the request never reached the handler. Without the header the same request worked.

**After**

Activities: an empty `providerParams` or `settings` arrives as an empty object, so the declared type is always correct. Non-empty values are unchanged.

Warehouses: the offset is row-exact — `offset=1&limit=3` returns the second, third, and fourth records. When the requested window is not covered by a single Bitrix24 page, `data` arrives empty and `meta.warnings` carries the `OFFSET_BEYOND_FETCHED_PAGE` code.

Files and folders: a filter on an unsupported field is refused with `400 UNSUPPORTED_FILTER` listing the fields you can filter by: `id`, `name`, `code`, `storageId`, `type`, `folderId` (`parentId` for folders), `deletedType`, `createdAt`, `updatedAt`, `deletedAt`. Tree navigation is untouched: the parent folder stays on the allowed list, so both forms — the `?folderId=` parameter and the `?filter[folderId]=` filter — work as before.

User fields: deletion is accepted both with and without the `Content-Type: application/json` header. Malformed JSON is still refused, with the `INVALID_JSON_BODY` code.

**Impact on integrators**

No action required. Three caveats if your code relied on the previous behaviour: an `Array.isArray` check no longer distinguishes an empty activity value from a populated one (count the keys instead); paging warehouses by offset now genuinely moves row by row rather than page by page; and a files or folders filter on a field outside the list above now returns an error instead of the whole folder.

### NEW-0801-13: task service fields now carry names, descriptions and a dictionary of accepted values

[GET /v1/tasks/fields](/docs/entities/tasks/fields) now returns a human-readable `label` and `description` for twenty Bitrix24 service fields that used to carry the field name itself instead of a name: `NOT_VIEWED`, `DURATION_TYPE`, `GUID`, `CHAT_ID`, `CHECKLIST`, `FAVORITE`, `IS_MUTED`, `IS_PINNED`, `IS_PINNED_IN_GROUP`, `ALLOW_CHANGE_DEADLINE`, `ALLOW_TIME_TRACKING`, `NEW_COMMENTS_COUNT`, `SERVICE_COMMENTS_COUNT`, `FORUM_ID`, `FORUM_TOPIC_ID`, `EXCHANGE_ID`, `EXCHANGE_MODIFIED`, `OUTLOOK_VERSION`, `SITE_ID`, `XML_ID`.

Alongside that, the field schema gained two keys Bitrix24 has been sending all along while we dropped them: `values` — the dictionary of accepted values as an array of `[{ "value": "Y", "label": "Yes" }]`, and `default` — the value the Bitrix24 account substitutes when the field is not passed. They arrive for every field where the account returns them, not only for the ones listed above — the `Y`/`N` dictionary is now visible on `MULTITASK`, `TASK_CONTROL`, `SUBORDINATE`, `ADD_IN_REPORT`, `REPLICATE` as well. Captions inside `values` are produced by the account itself and follow its localisation, so a field with bare codes may have no captions — `DURATION_TYPE` arrives as codes only: `secs`, `mins`, `hours`, `days`, `weeks`, `monts`, `years` (the `monts` typo comes from Bitrix24, that is the spelling the account accepts).

`values` is a separate key; it does not replace `items`: `items` still returns the raw enumeration directory on fields of type `enumeration` as `[{ "ID": "1", "VALUE": "First" }]`. The guarantee is per key — each always has its own shape, so read the one you need by name. On today's Bitrix24 accounts a field carries only one of the two, but both being present is not forbidden.

Existing calls keep working unchanged: the new keys are additive, and neither the set of fields nor their types changed. Five fields — `FAVORITE`, `IS_MUTED`, `IS_PINNED`, `NEW_COMMENTS_COUNT` and `NOT_VIEWED` — describe how the user the API key acts as relates to the task rather than a property of the task itself: a different key on the same Bitrix24 account will see different values.
