# API changes: September 1, 2026

[← Changelog](/docs/changelog) · [September 2026](/docs/changelog/2026-09)

### FIX-0901-1: a permission refusal on the sprint board arrives as 403, not as a platform error

**Before**

A key whose user has no access to the scrum project got `422 BITRIX_ERROR` for the sprint board columns — the same class the Vibecode platform uses to report a Bitrix24 business error. The code gave no way to tell "no rights" from "something is temporarily off on the account", so the request was retried even though the state is stable and a retry changes nothing.

```
HTTP 422
{ "success": false, "error": { "code": "BITRIX_ERROR", "message": "Access denied", "b24Code": "0" } }
```

**After**

`GET /v1/scrum/sprints/{sprintId}/stages` and `POST /v1/scrum/sprints/{sprintId}/stages` answer a permission refusal with `403 BITRIX_ACCESS_DENIED` — the code the common error reference already describes. It reads as "do not retry, grant the user access to the scrum project instead".

```
HTTP 403
{ "success": false, "error": { "code": "BITRIX_ACCESS_DENIED", "message": "Access denied" } }
```

Successful responses are unchanged: the column list still arrives with HTTP 200 and a created column with HTTP 201. The answer for a missing sprint is unchanged too — it stays `404 ENTITY_NOT_FOUND`.

### BC-0901-2: an empty employee photo is refused on update

> Old format supported until: not provided

**Before**

On employee UPDATE, an empty or whitespace-only string in `personalPhoto` reached Bitrix24 as a command to remove the current photo. The single `PATCH /v1/users/:id` returned `200 success:true` even though the photo was removed.

**After**

Such values are refused with `INVALID_PARAMS` before the Bitrix24 call. The single `PATCH /v1/users/:id` returns `400 success:false`; batch routes keep their existing error envelopes. The global `POST /v1/batch` also refuses `personalPhoto: null`: its sub-call encoder turns `null` into an empty query-string value, the same remove-the-photo command. On `PATCH /v1/users/:id` and in the per-entity batch, `null` gets no new refusal. To leave the photo unchanged, omit `personalPhoto`. On CREATE in `POST /v1/users`, an empty string does not get the new refusal; the working inline pair and separate employee-invite validation are unchanged.

### FIX-0901-3: rate limit descriptions now name the replica share, not just the total cap

**Before**

The descriptions of [POST /v1/feedback](/docs/feedback/submit) in the `GET /v1/guide` directory, in the `feedback.limits` block of the [GET /v1/me](/docs/keys-auth) response and in the `GET /v1/openapi.json` schema named the limit as a number — "5 requests per minute". Requests are served by several backend replicas and each one holds its own share of the limit, so the client received less than promised and the `X-RateLimit-Limit` header disagreed with the description. The same mismatch was present on the help pages for submitting a ticket and uploading an attachment, and in the schema for five operations of the Cowork section.

**After**

The descriptions name the number and explain next to it that the cap is divided across the replicas, while the value actually in force arrives in the `X-RateLimit-Limit` header. For the Cowork operations in the schema, the `429` refusal description is aligned with the form already used on the help pages. A single shared `429` row on the feedback page is split in two — creating a ticket and uploading an attachment carry different counters.

**Impact on integrators**

The limit values did not change and the behaviour is the same as before. A client that relied on the number from the description was refused earlier than expected — the source of the value in force is now named explicitly: read `X-RateLimit-Limit` from the response.

### BC-0901-4: task comment updates reject read-only fields

> Old format supported until: not provided

**Before**

On a legacy card,
[PATCH /v1/tasks/:taskId/comments/:id](/docs/entities/task-comments/update)
could return HTTP 200 when a client sent a comment object from GET with an
updated `message`, forwarding read-only fields to Bitrix24. An item in
[POST /v1/tasks/:taskId/comments/batch](/docs/entities/task-comments/comments-batch)
could similarly finish with `success:true`.

**After**

A single PATCH containing a read-only field returns HTTP 400
`READONLY_FIELD` before Bitrix24 is called. For a successful legacy-card
comment update containing only `{message}`, the response remains HTTP 200; the
existing `410 GONE` remains unchanged on the new card.

In the custom batch, the top-level response remains HTTP 200 with
`success:true`; the rejected item gets `success:false` and
`error:READONLY_FIELD`, is not sent to Bitrix24, and does not stop valid sibling
items.

**What integrators should do**

For a single PATCH, build a new body containing only `{message}`; do not send
response fields such as `id`, `taskId`, `authorId`, `createdAt`, or their aliases
back to the endpoint. In a custom batch item, keep lowercase `id` as the comment
selector and send only `message` beside it; remove an additional `ID` and all
other read-only fields. Clients that already send only documented writable
fields need no changes.

### FIX-0901-5: the 401 TOKEN_MISSING error now names the reason on every V1 route

**Before**

The reason a personal key had no Bitrix24 webhook was named only on entity routes
(`/v1/deals`, `/v1/tasks` and so on) and in [POST /v1/batch](/docs/batch). Every other
family — chats, lists, mail, scrum, notes, Open Channels, bots, userfields and the rest —
answered with the flat text "API key has no tokens configured." and no `error.details`
field. The same broken key got an explanation on `/v1/deals` and a text that implied
nothing on `/v1/chats`.

**After**

The `401 TOKEN_MISSING` body is built the same way on every V1 route: a personal key gets
the reason in `error.details.reason` plus the text naming the required action, an app key
gets a pointer to the missing OAuth step. The response remains HTTP 401 and the set of
codes is unchanged.

**Impact on integrators**

Read `error.details.reason` — that is the contract, not `error.message`. The message text
on these routes has changed, so code comparing it with the literal "API key has no tokens
configured." stops matching; that parsing was unreliable before as well. The reasons are
listed in the `GET /v1/me` response under `b24Credentials`.

### FIX-0901-6: direct upload can be repeated after object deletion

**Before**

After a successful [DELETE /v1/storage/objects/{key}](/docs/storage/objects/delete), a new [direct POST /v1/storage/objects/upload](/docs/storage/upload/direct) with the same key returned `409 STORAGE_KEY_DELETED` until the object was physically purged 30 days later.

**After**

For an app object in the `COMPLETED` state without a multipart session, direct upload returns HTTP 200, preserves the same `id` and `visibility`, and writes new content. A deleted public link with that `id` becomes available again with the new bytes. This does not restore the previous content.

Path B remains disabled and returns `503 STORAGE_PRESIGNED_UPLOAD_DISABLED` without a URL. Path C, deleted `PENDING` objects, and objects with a multipart session continue to return `409 STORAGE_KEY_DELETED`. DELETE itself and subsequent reads without a new eligible direct upload continue to return `410 STORAGE_OBJECT_DELETED` during the retention period.

**Impact on integrations**

No request changes are required. After deleting an eligible object, send the new bytes with a regular direct upload using the same key.

### FIX-0901-7: a transient image-registry outage no longer marks a galaxy app broken

**Before**

`POST /v1/infra/servers` with a `source` field that hit `502 GALAXY_BASE_IMAGE_UNAVAILABLE` — or ran the host out of disk — flipped the slot to `status=error`, even though the same refusal on `POST /v1/infra/servers/:id/deploy` left the status untouched and the error text promised the slot, its container and its `/data` volume were intact. Re-sending the deploy never cleared that `error`: `GET` kept reporting a broken slot, and the app stopped holding its host awake.

**After**

Both transient refusals leave the slot at `status=provisioning`; the cause stays readable in `provisionError` of `GET /v1/infra/servers/:id`. Re-sending the same deploy to a slot broken by such a cause clears `error` at the start of the attempt. An abandoned slot is still failed by the platform after ~20 minutes, but it keeps its real cause instead of the generic `DEPLOY_INCOMPLETE`. The `error.hint` copy, the `/v1/me` checklist and the docs no longer promise a "2–3 minutes" window: the registry outage can last an hour.

### BC-0901-8: Regular chat-bot blocks key deletion

> Old format supported until: not provided

**Before**

Deleting an API key could delete its linked regular chat-bot together with the token required for normal unregistration on Bitrix24.

**After**

Deleting a key with a regular chat-bot, including a disabled Bot Platform row, returns `409 KEY_HAS_LINKED_AGENT`. Take numeric `bitrixBotId` from `details.bots` in the `409` response, then unregister the bot through `DELETE /v1/bots/:botId` or transfer it to another key.

### FIX-0901-9: file uploads and AI requests now answer 429 under memory pressure

**Before**

`POST /v1/files/upload` (up to 70 MB), `POST /v1/note/documents/{id}/files` (up to 40 MB),
`POST /v1/chat/completions` (up to 30 MB) and `POST /v1/audio/transcriptions` (audio up to 25 MB)
accepted any number of concurrent large bodies. Several parallel uploads at the ceiling could drop
requests without a response — other requests in flight at that moment included.

One more observable difference: a request with a missing or invalid key on these routes could get
`400` (unparsed JSON), `413` (size) or `415` (content type) — an answer about the body before the
answer about the key.

**After**

Concurrent bodies over 1 MiB are bounded by their total size. A request over the bound gets `429`
with code `LARGE_BODY_BACKEND_BUSY` and a `Retry-After: 5` header, emitted before any Bitrix24
call, so it carries no side effects — and identically whether or not `Content-Length` is
declared:

```json
{
  "success": false,
  "error": {
    "code": "LARGE_BODY_BACKEND_BUSY",
    "message": "Too many large request bodies are being processed. Retry in a few seconds.",
    "retryAfter": 5
  }
}
```

On the OpenAI-compatible `POST /v1/chat/completions`, `POST /v1/ai/chat/completions` and
`POST /v1/audio/transcriptions` (+ `POST /v1/ai/audio/transcriptions`) the envelope matches the
rest of that surface — no `success`, lowercase code:

```json
{
  "error": {
    "message": "Too many large request bodies are being processed. Retry in a few seconds.",
    "type": "rate_limit_exceeded",
    "code": "large_body_backend_busy",
    "retryAfter": 5
  }
}
```

A request with a missing or invalid key now gets `401` on all of these routes — the key is checked
before the body is read. Successful requests and bodies under 1 MiB are unaffected; a retry after
5 seconds succeeds.

**Important:** on transcriptions the bound is counted from the declared `Content-Length`. An upload without one
(`Transfer-Encoding: chunked`) is not counted against the shared total and is bounded only by its
own 25 MB per-file ceiling.

### FIX-0901-10: creating a Hermes agent no longer fails because of the startup payload size

**Before**

Creating a Hermes agent failed right after the server step. The virtual machine was never created,
the agent stayed in the error state with an incident code, and the server field on the card stayed
empty. Retrying produced the same result. The cause was the cloud startup payload growing past the
size limit the cloud allows for such payloads.

**After**

Internal comments are no longer shipped to the cloud together with the install script, so the
startup payload fits well within the limit again and the virtual machine is created. The install
script itself and every other delivery path for it are unchanged. In addition, a size check now
runs before the cloud is called: should the payload ever grow past the limit again, no cloud
request is sent at all and Vibecode returns the usual provider error with an incident code.

### NEW-0901-11: the business-process editor is available through the API

A new `/v1/workflow-designer/*` section works with templates of the new business-process editor in the editor's own language: blocks, connections, a catalog of block types, graph validation, drafts and publication. It is not the same as `/v1/bizproc-templates`, which reads and writes template rows with the graph as an opaque blob.

The `bizprocdesigner` scope is required — a separate one from `bizproc`. Add it to the key and re-issue the key.

Writing takes two steps. `POST /v1/workflow-designer/templates/{id}/draft` saves a **draft**: the running business process keeps executing its published version, so a draft can be shown to a person, rewritten or thrown away. `POST /v1/workflow-designer/templates/{id}/publish` makes the draft the working version — irreversibly, and it deletes **every** draft of the template, other people's included. Publication requires the `draftFingerprint` from `GET /v1/workflow-designer/templates/{id}`, so that exactly the graph you read and showed the person is the one published; a draft someone rewrote in the meantime is refused instead of quietly replacing it.

Start with `GET /v1/workflow-designer/capabilities`: it answers whether the editor is available to this Bitrix24 account and this key, and names the reason when it is not (`DESIGNER_NOT_SUPPORTED` — Bitrix24 is not updated to a version that serves the editor through the API, or the module is absent, `DESIGNER_UNAVAILABLE` — switched off or not in the plan, `SCOPE_DENIED` — the key lacks the scope, `DESIGNER_REQUIRES_WRITE_KEY` — the key is read-only and the operation writes, `DESIGNER_PROBE_FAILED` — the check did not complete and the editor state is unknown). These states cannot be told apart from a refusal of the methods themselves, which is why the check is a call of its own.

A read-only key is served by this section: the graph, the block catalog, the document fields, the instructions from Bitrix24 and `validate` all read. `DESIGNER_REQUIRES_WRITE_KEY` arrives only on the three writing operations — creating a template, saving a draft and publishing one.

**Affected endpoints:** `GET /v1/workflow-designer/capabilities`, `POST /v1/workflow-designer/templates`, `GET /v1/workflow-designer/templates/{id}`, `GET /v1/workflow-designer/templates/{id}/brief`, `GET /v1/workflow-designer/templates/{id}/blocks`, `GET /v1/workflow-designer/templates/{id}/blocks/{blockId}`, `GET /v1/workflow-designer/templates/{id}/document-fields`, `POST /v1/workflow-designer/templates/{id}/validate`, `POST /v1/workflow-designer/templates/{id}/draft`, `POST /v1/workflow-designer/templates/{id}/publish`

### BC-0901-12: deleting a server now deletes the agent bound to it

> Old format supported until: not provided

**Before**

[DELETE /v1/infra/servers/:id](/docs/infra/servers/delete) deleted the server only. The agent that ran on it stayed alive on the Vibecode platform: its state still read as running, its bot stayed registered in the Bitrix24 account, its key stayed valid, and the agent pointed at a server that no longer existed.

**After**

The agent bound to the server is deleted together with it. The agent row is marked deleted, the bot is removed from the Bitrix24 account, the agent key is revoked, and the application card is removed from the Bitrix24 catalog. A successful delete still answers HTTP 200 with the body `{"success": true}`.

One new outcome appears. If another request was changing that same agent concurrently, the answer is `409 AGENT_DELETE_CONFLICT` in the usual rejection shape `{"success": false, "error": {"code": "...", "message": "..."}}`. In that case the server is not deleted either: it keeps running and keeps being billed, and the request has to be repeated.

**What integrators should do**

An integration that expected the agent to survive a server delete and be reused has to be reworked: create the agent again after creating the new server. Treat `AGENT_DELETE_CONFLICT` as retryable — the server is intact, so repeat the same request. Deleting a server with no agent and deleting a server of kind `GALAXY_APP` behave as before.
