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

API changes: August 1, 2026

← Changelog · August 2026

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, 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 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 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 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 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.

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 and POST /v1/infra/servers/:id/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 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 and GET /v1/warehouses/:id/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 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.