# API changes: July 10, 2026

[← Changelog](/docs/changelog) · [July 2026](/docs/changelog/2026-07)

### NEW-0710-1: wake-schedule window management (wake-schedules)

New CRUD contract on Black Hole servers: `GET`/`POST /v1/infra/servers/:id/wake-schedules`, `PATCH`/`DELETE /v1/infra/servers/:id/wake-schedules/:scheduleId`. Lets you declare one or more recurring wake windows (`cronExpr` + a required IANA `timezone`, optional `label`/`lead`/`enabled`) — the platform wakes a sleeping server ahead of the window, and the app's own in-VM cron runs the task from there.

Rolling out gradually and not yet available on every Bitrix24 account — until enabled on a given account, the call returns `403` with code `WAKE_SCHEDULE_DISABLED`. Available only for BLACKHOLE-mode servers (otherwise `400 BLACKHOLE_ONLY`) and not yet supported for galaxies (`400 GALAXY_NOT_SUPPORTED` on both the host and a nested app — coming later). The minimum cadence between occurrences and the per-server window cap (50) are platform-configured; violating either returns `400 CADENCE_TOO_LOW` and `403 WAKE_SCHEDULE_LIMIT` respectively. The create/update response additionally carries a `tzWarning` field — a heads-up that the VM's in-VM timezone may have drifted from the window's timezone if the server hasn't been redeployed since. **`PATCH` replaces the whole window (PUT semantics): omitted optional fields reset to their defaults — `enabled`→`true`, `label`/`lead`→null.** Send the full window object on update.

Affected endpoints: `GET|POST /v1/infra/servers/:id/wake-schedules`, `PATCH|DELETE /v1/infra/servers/:id/wake-schedules/:scheduleId`.

### NEW-0710-2: machine-readable tz-warning code in wake-schedules responses (`tzWarningCode`)

The wake-schedule create/update response (`POST`/`PATCH /v1/infra/servers/:id/wake-schedules[/:scheduleId]`) now additionally carries a `tzWarningCode` field alongside the existing text `tzWarning` — `"SINGLE_ZONE"` / `"MULTI_ZONE"` / `null` (when the server ends up with zero enabled windows after the mutation). It's a machine-readable companion to the same advisory, letting clients localize the message themselves instead of rendering the raw English `tzWarning` text. The field is additive — `tzWarning` is unchanged and stays for backward compatibility.

Affected endpoints: `POST|PATCH /v1/infra/servers/:id/wake-schedules[/:scheduleId]`.

### FIX-0710-3: wake-schedules: schedules rejected on always-on servers

`POST`/`PATCH /v1/infra/servers/:id/wake-schedules` now reject creating or updating a wake window on an always-on (24/7) server — the call returns `400` with code `ALWAYS_ON_CONFLICT`. Such a server never auto-sleeps, so a wake schedule would silently break the paid always-online guarantee. Ordinary sleeping Black Hole servers and preemptible agents/bots are unaffected. Turn off always-on to declare wake windows.

Affected endpoints: `POST /v1/infra/servers/:id/wake-schedules`, `PATCH /v1/infra/servers/:id/wake-schedules/:scheduleId`.

### FIX-0710-4: wake-schedules can now be declared on nested galaxy apps

**Before**

`POST`/`PATCH /v1/infra/servers/:id/wake-schedules` returned `400 GALAXY_NOT_SUPPORTED` for any galaxy-family server — both the host itself and nested apps (`kind=GALAXY_APP`).

**After**

Nested galaxy apps (`kind=GALAXY_APP`) are now accepted — you can declare a wake window on a specific app, and the platform wakes it (and the host, if needed) ahead of the window. The galaxy host itself still returns `400 GALAXY_NOT_SUPPORTED` — schedule the individual apps, not the host.

Affected endpoints: `POST|GET /v1/infra/servers/:id/wake-schedules`, `PATCH|DELETE /v1/infra/servers/:id/wake-schedules/:scheduleId`.

### FIX-0710-5: sleep-now declines to sleep when a scheduled wake is imminent

**Before**

[POST /v1/infra/servers/:id/sleep-now](/docs/infra/lifecycle/sleep-now) always put the server to sleep immediately, even when the next scheduled wake was a minute away — the server woke straight back up.

**After**

When the server has an enabled wake schedule and the next wake falls within the guard margin, the call does not sleep the server and responds `200` with `{ "success": true, "data": { "slept": false, "reason": "WAKE_IMMINENT" } }`. Otherwise the server sleeps and the scheduler wakes it at the next window.

**Impact on integrators**

The `data` block with `slept: false` arrives only when the call declines to sleep the server — check for its presence if you rely on the server always sleeping after this call. On a successful sleep the response is just `success: true`. Existing calls to servers without a schedule are unchanged.

### FIX-0710-6: non-streaming chat/completions and embeddings no longer abort at 360 seconds

**Before**

A non-streaming (`stream:false`) `POST /v1/chat/completions` (and `POST /v1/embeddings`) request whose generation took longer than ~2 minutes reliably aborted at ~360 seconds with `{"code":"ai_provider_unavailable","message":"This operation was aborted"}` — regardless of the client's own timeout. The doomed generation also burned triple the compute.

**After**

Such a request now runs within a single ~850-second budget (one attempt for the whole budget, no compute tripling). If generation still does not finish in budget, a meaningful `503` with code `ai_provider_timeout` is returned, with a localized `userMessage` and a hint (reduce the request size or use streaming mode `stream:true`) — without a `Retry-After` header (the timeout is not transient). A client disconnect now immediately cancels the provider-side generation. Streaming mode (`stream:true`) was never affected by this limit.

### FIX-0710-7: requisite-presets/:presetId/fields and requisite-links: list sorting, filtering, and parameter validation

**Before**

[GET /v1/requisite-presets/:presetId/fields](/docs/entities/requisite-presets) ignored `sort`/`order` and any `filter[...]` — it always returned the full list in Bitrix24 order. [GET /v1/requisite-links](/docs/entities/requisite-links) and [POST /v1/requisite-links/search](/docs/entities/requisite-links) accepted only plain equality; comparison operators (`$gte`, `>`, etc.), `sort`/`order`, and unknown fields were silently dropped, so the whole table came back.

**After**

Both lists now honour `sort`/`order` (including the `order[field]=asc|desc` form) and `filter`. requisite-links supports comparison operators (`$gt`/`$gte`/`$lt`/`$lte`, `$in`/`$nin`, `>=`/`>` prefixes). An unknown filter or sort field now returns `400` (`UNKNOWN_FILTER_FIELD` / `UNKNOWN_SORT_FIELD`), a top-level logical operator (`$or`/`$and`) returns `400 INVALID_FILTER_OPERATOR`, and filtering by `entityId` without `entityTypeId` returns `400 MISSING_ENTITY_TYPE_ID` instead of a raw "Access denied".

**Impact on integrators**

Requests on documented fields keep working and now actually sort/filter. If you relied on an unknown parameter being silently ignored, it now returns `400` — drop the typo or use a field from the response.

### FIX-0710-8: Bank details sorting by id honors the direction

**Before**

Listing bank details ([GET /v1/bank-details](/docs/entities/bank-details/list)) sorted by `id` descending (`?sort=-id`) returned records in ascending order — the sort direction was silently ignored.

**After**

`?sort=-id` (and `?sort=id`) sorts by the identifier in the requested direction.

**Integrator impact**

No code change required — requests that relied on sorting by `id` now return the expected order.

### NEW-0710-9: GET /v1/quotes/fields — ~26 quote fields declared with human-readable labels

The quotes entity schema now declares ~26 fields that Bitrix24 returned but that were undeclared: `quoteNumber`, `updatedBy`, `lastActivityBy`, `lastActivityTime`, `content`, `terms`, `leadId`, `storageTypeId`, `storageElementIds`, `personTypeId`, `webformId`, `lastCommunicationTime`, `contactIds`, `contacts`, `locationId`, `taxValue`, `actualDate`, `mycompanyId`, `utmSource`/`utmMedium`/`utmCampaign`/`utmContent`/`utmTerm`, `lastCommunicationCallTime`/`lastCommunicationEmailTime`/`lastCommunicationImolTime`/`lastCommunicationWebformTime`. They now appear in [GET /v1/quotes/fields](/docs/entities/quotes/fields) with readable labels (instead of service names like `STORAGE_TYPE_ID`/`UTM_SOURCE`), and their values are type-coerced (numbers, ISO dates) in list/get responses. Labels were also added to the previously label-less `stageId`, `opened`, `closed`.

### FIX-0710-10: a PATCH with no writable field is rejected with an explicit error

**Before**

`PATCH /v1/{entity}/:id` with an empty body — or a body carrying no recognized writable field (for example because of a typo in a field name) — reached Bitrix24, which silently ignored the request and returned success. The wrapper then replied `200` with the unchanged object, so the client believed the edit had applied while nothing actually changed — a silent-drift risk, especially for AI agents. This affected all three update surfaces: single `PATCH /v1/{entity}/:id`, per-entity `POST /v1/{entity}/batch` and global `POST /v1/batch`.

**After**

An update with an empty body returns `400 EMPTY_UPDATE_BODY` (a per-item error in batch calls) on all three surfaces, before Bitrix24 is called. For `/v1/catalog-products/:id` a strict check is added: a `PATCH` that carries no recognized writable field returns `400 NO_RECOGNIZED_UPDATE_FIELDS`. A meaningful update always carries at least one field — send a recognized field (custom properties `propertyNNN` and `UF_*` fields are accepted too). Entities that already validated their fields behave as before.

Catalog products additionally expose in `GET /v1/catalog-products/fields`, and now accept for explicit `select`, filter and sort, the fields from the product-update contract — `code`, `xmlId`, `sort`, `vatId`, `height`, `length`, `width`, `previewText`, `detailText` and others (filtering by them previously returned `400 UNKNOWN_FILTER_FIELD`). Filter and sort reliably by the indexable fields (`code`, `xmlId`, `sort`, `vatId`, dimensions); for the full-text ones (`previewText`, `detailText`) Bitrix24 may ignore the filter. List responses without an explicit `select` are unchanged.

### FIX-0710-11: leads, companies, quotes — /fields date keys are now createdTime and updatedTime

**Before**

`GET /v1/leads/fields`, `GET /v1/companies/fields` and `GET /v1/quotes/fields` advertised `createdAt` and `updatedAt`, while list/get/search responses always returned `createdTime` and `updatedTime` — the value was never readable under the `createdAt`/`updatedAt` name. Filter and sort accepted the `createdAt`/`updatedAt` names.

**After**

These entities' `/fields` advertise the real keys `createdTime` and `updatedTime` — like contacts, invoices and items. The keys in the response body are the same (`createdTime`/`updatedTime` were always returned), but the value is now normalized to ISO-8601 in UTC (`2026-04-15T07:00:00.000Z`) — previously it arrived in the raw Bitrix24 form with the account offset (`2026-04-15T08:00:00+01:00`). Same instant, only the representation changes.

**Impact on integrators**

Read dates from `createdTime` and `updatedTime` (the keys did not change). If you compare the date string byte-for-byte or cache by it, account for the offset → `Z` shift (same instant). In filter and sort use `createdTime`/`updatedTime`; the former `createdAt`/`updatedAt` now return `400 UNKNOWN_FILTER_FIELD` (filter) and `400 UNKNOWN_SORT_FIELD` (sort). On write, `createdAt`/`updatedAt` are no longer rejected as read-only — they are ignored as unknown fields (like contacts/invoices/items); the creation/update timestamp still cannot be set.

### FIX-0710-12: sleep settings: flipping to always-on is now rejected while wake windows are active

`PATCH /v1/infra/servers/:id/sleep` now rejects setting `sleepAfterMinutes: null` (always-on, 24/7) on a server that still has enabled wake-schedule windows — the call returns `400` with code `ALWAYS_ON_CONFLICT`. This is the reverse direction of an existing gate: creating a wake window on an always-on server was already rejected, and now the opposite transition is rejected symmetrically — otherwise the server would keep sleeping on schedule, silently breaking the paid always-online guarantee. Delete or disable the wake windows first, or keep a sleep timeout instead of "Never", to turn always-on on.

Affected endpoints: `PATCH /v1/infra/servers/:id/sleep`.

### NEW-0710-13: placement.bind on a self-hosted Bitrix24 returns a clear SESSION_REQUIRES_ADMIN for a non-administrator

On a self-hosted (BOX) Bitrix24, binding a placement via the developer-key path requires the key's user to be an account administrator. Previously a non-administrator request returned an opaque `502 BITRIX_UNAVAILABLE`.

Now [POST /v1/placements/bind](/docs/keys-auth) recognises the access-denied signal from Bitrix24 and returns `403 SESSION_REQUIRES_ADMIN` with a hint: bind from an account-administrator account, or ask an administrator to grant those rights. The requirement is visible up front in [GET /v1/me](/docs/keys-auth) — the `placements.bindPrerequisite` block for self-hosted accounts now includes the `SESSION_REQUIRES_ADMIN` code.

### FIX-0710-14: GET /v1/me capabilities now reflect read-only (READONLY) mode

**Before**

For a READONLY-mode key, [GET /v1/me](/docs/keys-auth) returned `capabilities.managedBots.create`, `agents.create`, `servers.create` and `apps.*` as `available: true`, even though every write is blocked with `403 WRITE_BLOCKED_READONLY_KEY`. An agent saw "can create" and hit the rejection.

**After**

For a READONLY key these write capabilities are returned as `available: false` with `reason: "WRITE_BLOCKED_READONLY_KEY"` and a hint to switch the key to read+write. Read-only capabilities and the AI Router are unchanged. For READWRITE keys the response is unchanged.

### FIX-0710-15: the feedback author can reply to their own ticket again without the vibe:feedback scope

**Before**

[POST /v1/feedback/:id/comments](/docs/keys-auth) rejected the ticket author with `403 FEEDBACK_SCOPE_REQUIRED` when the key lacked the `vibe:feedback` scope — even though the author branch was documented. The author could not reply to their own `AWAITING_USER` ticket, and it stalled.

**After**

The author check now runs before the scope gate: the author replying with the same key that created the ticket reaches the author branch (`authorType=USER`, ball-court rule `AWAITING_USER → NEEDS_REVIEW`) even without the `vibe:feedback` scope. A key that is neither the author nor scoped still gets `403 FEEDBACK_SCOPE_REQUIRED`.

### NEW-0710-16: AI quota pacing: pacing field in the response and 429 ai_pacing_limited error

The [GET /v1/ai/quota](/docs/ai/consumption/quota) response gained a `pacing` field — the state of monthly AI quota pacing (peak smoothing via a daily and a weekly limit layered on top of the overall monthly limit; enabled by the account administrator from the `/ai` dashboard page). `null` when pacing is off at the platform level or not configured for the account; otherwise an object `{ mode, active, day, week }`: `mode` is the enforcement behavior on a tripped limit (`wallet`/`block`/`ignore`), `active` signals whether tripping a window will reject a call right now (`false` in observation mode, and always `false` in `ignore` mode — windows are tracked as informational only, `429` is never returned), `day` and `week` are each `{ pctUsed, resetAt }` as a percentage of that window's OWN limit. The response is still cached for 30 seconds, so pacing state can lag by that long.

When the daily or weekly limit trips, calls to [POST /v1/chat/completions](/docs/ai/chat/completions), [POST /v1/embeddings](/docs/ai/embeddings), and [POST /v1/audio/transcriptions](/docs/ai/audio/transcriptions) can now return `429` with the body `{ success: false, error: { code: "ai_pacing_limited", type: "rate_limit_exceeded", message, reason, overageDenied, resetAt, retryAfter } }` and a `Retry-After` header. `reason` names the tripped window (`day_window` or `week_window`); `overageDenied` names why paid overage was refused (`wallet_empty`, `breaker`, `wallet_off`), or is `null` under the hard-block mode. Retrying before `Retry-After`/`resetAt` will not help — the quota is not any more available in the meantime. Pacing is off by default — the platform enables it.

### FIX-0710-17: GET /v1/pages, /v1/sites and POST /v1/{pages,sites}/search now honour offset

**Before**

Listing pages or sites with an offset (`GET /v1/pages?offset=50`, `POST /v1/pages/search` with `offset`) returned `422 BITRIX_ERROR "Unknown parameter: start"`. The first page (no `offset`) worked.

**After**

`offset` for `pages` and `sites` is handled correctly: the requested window `[offset, offset+limit)` is returned, with `meta.total` and `meta.hasMore` computed from the row count. No client change is needed — calls without `offset` behave as before.

### NEW-0710-18: /fields of documents, catalogs, prices, telephony lines, basket items and leads gain richer metadata

`GET /:entity/fields` of several entities now carries more complete schema metadata. Documents ([GET /v1/documents/fields](/docs/entities/documents/fields)), catalogs ([GET /v1/catalogs/fields](/docs/entities/catalogs/fields)), catalog prices ([GET /v1/catalog-prices/fields](/docs/entities/catalog-prices/fields)) and telephony lines now expose a human-readable `label` and `description` per field, returned in English.

For basket items ([GET /v1/basket-items/fields](/docs/entities/basket-items/fields)) the fields `weight`, `vatRate`, `measureCode`, `measureName`, `dimensions`, `productXmlId`, `catalogXmlId` are marked with a `nullable` flag — they may come back empty. For leads ([GET /v1/leads/fields](/docs/entities/leads/fields)) the same flag marks `secondName`, `sourceDescription`, `comments`, and the previously-undeclared fields `originatorId`, `dateClosed`, `lastCommunicationTime` and the tags `utmSource`/`utmMedium`/`utmCampaign`/`utmContent`/`utmTerm` are now declared — filter and sort work on them, and `dateClosed` is normalized to ISO-8601.

For landing sites the group-by dimensions are declared, so [POST /v1/sites/aggregate](/docs/entities/sites/aggregate) with `groupBy` (`type`, `active`, `deleted`, `lang`, `tplId`, `domainId`, `createdById`, `modifiedById`) no longer answers `Available: .`. For documents aggregation is disabled (every numeric field is an identifier): [POST /v1/documents/aggregate](/docs/entities/documents/list) returns `404`.

Existing calls keep working unchanged — this is additional field metadata.

### NEW-0710-19: Idempotent server creation — Idempotency-Key header

[POST /v1/infra/servers](/docs/infra/servers/create) now accepts an optional `Idempotency-Key` header when creating a standalone server. A retry with the same key — for example after a lost response or a network drop — does not create a second server: it returns the same server the first request created, with status 201 and an `Idempotent-Replayed: true` response header. The key is a 1–255 character string from `[A-Za-z0-9_.:-]`; it is scoped to your API key.

On a replay the one-time SSH credentials (`ssh.privateKey` / `ssh.password`) are NOT re-exposed — they are `null` in the response body and a `note` field explains this. Keep the credentials from the first create response.

New error codes: `400 INVALID_IDEMPOTENCY_KEY` (the key fails validation), `400 IDEMPOTENCY_UNSUPPORTED_WITH_GRADUATION` (the key together with `graduateFrom` for a dedicated server is not supported), `409 IDEMPOTENCY_KEY_ALREADY_USED` (the key was already used for a server that has since been deleted), `409 IDEMPOTENCY_CONCURRENT_RETRY` (a concurrent request with the same key is still in progress — retry shortly).

The header applies to standalone servers only. On galaxy-placement accounts a well-formed key is ignored without an error, and the retry protection does not extend to that path.

Additionally: the create response now returns the canonical server name (suffixed when a name collision is resolved) instead of the name from the request — these previously diverged on a same-name collision.

### NEW-0710-20: localized message on OPEN-mode switch denials

Responses from [PATCH /v1/infra/servers/:id/mode](/docs/infra/access/mode) with codes `OPEN_MODE_DISABLED` (OPEN mode is disabled platform-wide) and `OPEN_MODE_NOT_ALLOWED` (OPEN mode is not allowed by portal policy) now additionally carry an `error.userMessage` field — a localized, human-readable message pointing to the [Deploy API](/docs/infra/deploy) as the supported replacement for direct SSH. The field is additive: `error.message` (English technical string), `error.code` and the HTTP status are unchanged. It matches the shape of the existing `error.userMessage` on the `OPEN_MODE_REQUIRES_COMMERCIAL` denial. The `userMessage` text depends on the key owner's locale.

### BC-0710-21: Open Channels config fields normalized to camelCase and described in /fields

> Old format supported until: 10.01.2027

**Before**

[GET /v1/openline-configs](/docs/openlines/config/list), [GET /v1/openline-configs/{id}](/docs/openlines/config/get) and [POST /v1/openline-configs/search](/docs/openlines/config/search) returned most configuration fields in the Bitrix24-native form — upper case with underscores (`CRM_CREATE`, `WELCOME_MESSAGE`, `QUEUE_TIME`, and others). The [GET /v1/openline-configs/fields](/docs/openlines/config/fields) reference described only 6 fields, so the rest were invisible to programmatic discovery.

**After**

All configuration fields are normalized to a single camelCase form (`crmCreate`, `welcomeMessage`, `queueTime`, and so on), and `/fields` describes the full field set with human `label` and `description` values. Filtering and sorting by the new camelCase names work. On write (`create`/`update`) both cases are still accepted — existing calls that send upper-case names in the body keep working.

**What integrators should do**

Read response fields by their camelCase names: `config.crmCreate` instead of `config.CRM_CREATE`. The mapping is a direct transliteration from upper case to camelCase (`WELCOME_BOT_ID` → `welcomeBotId`, `WORKTIME_TO` → `workTimeTo`, `LINE_NAME` → `name`). The full list of new names is in the `/fields` reference.

### NEW-0710-22: Added GET /v1/warehouses/fields — warehouse field schema

A new [GET /v1/warehouses/fields](/docs/entities/warehouses/list) endpoint returns the schema of the 19 warehouse fields: for each field — its `type`, a read-only flag (`readonly`), a human `label`, and a `description`. Warehouses are a custom route (no entity schema), so they previously lacked the field reference that auto-generated entities have. The response is `{ success: true, data: { fields: { … } } }`. Requires the `catalog` scope.

### FIX-0710-23: app publish recovers from a placement drift

**Before**

On publish ([POST /v1/apps/:id/publish](/docs/apps)) or a placement update ([PATCH /v1/apps/:id](/docs/apps)), if a placement was registered on the Bitrix24 side but missing from the app (drift after an unpublish), the bind failed with "Handler already binded" and the placement stayed out of sync.

**After**

On that error the platform sweeps the stale binding once and retries — the placement syncs automatically. Recovery fires only on the confirmed conflict, so a live placement is never stripped by mistake; placements that need non-portable OPTIONS (chat widgets, the background worker) are excluded from auto-recovery and still surface a warning.

### FIX-0710-24: storage: object sha256 is populated on direct upload

**Before**

The `sha256` field in the storage-object upload response was always `null` for user objects, even though the schema described it as "computed on upload".

**After**

For direct upload ([POST /v1/storage/objects/upload](/docs/storage), files up to 10 MB) `sha256` now carries the SHA-256 hash of the object content — usable for integrity checks and duplicate detection (identical content yields an identical hash). Presigned and multipart uploads send bytes straight to storage bypassing the platform, so `sha256` stays `null` there for now.

### FIX-0710-25: pacing.active in GET /v1/ai/quota no longer reports an active limit when the quota is zero

**Before**

With pacing enabled on a Bitrix24 account whose monthly quota is zero (a hard stop via the `monthlyVibes = 0` override, or usage not yet initialized), `data.pacing.active` returned `true` — even though a `429 ai_pacing_limited` rejection is structurally impossible in that state: requests are rejected by the monthly limit, not by a pacing window.

**After**

`data.pacing.active` returns `true` only when exceeding the day or week window can actually produce `429 ai_pacing_limited`. With a zero quota the field honestly reports `false`. The response shape is unchanged; clients that built backoff logic on `active` need no changes — the signal is simply more accurate.
