
## Выдать или заменить ключ приложения

`POST /v1/cowork/applications/:id/key`

Выдаёт ключ приложению с пустым слотом или заменяет уже выданный и возвращает новый ключ ровно один раз. Так публикуют приложение, добавленное из каталога: сырой ключ не восстанавливается, а перевыпуск через управляющий ключ настольному клиенту недоступен.

**Скоуп:** `vibe:cowork` (ключ Cowork/Code) | **Базовый URL:** `https://vibecode.bitrix24.tech/v1` | **Авторизация:** `X-Api-Key`

Какая ветка сработала, называет поле `issued`: `minted` — слот был пуст и ключ выдан, `rotated` — ключ в слоте заменён. Заранее ветку не угадать, поэтому решает платформа. Вызывают эндпоинт двое. Владелец приложения получает ключ в слоте приложения. Участник команды разработки сервера приложения получает собственный ключ этого сервера — ключ владельца при этом не меняется и не показывается.

## Параметры

| Параметр | Тип | Обяз. | Описание |
|----------|-----|-------|----------|
| `id` (path) | string | да | Идентификатор приложения. Список: [`GET /v1/applications`](/docs/applications/list) |
| `Idempotency-Key` (header) | string | да | Строка от 1 до 255 символов из набора `[A-Za-z0-9_.:-]`. Область действия — вызывающий ключ. Как отвечает повтор — в «Известных особенностях» |

## Поля запроса (body)

| Поле | Тип | Обяз. | Описание |
|------|-----|-------|----------|
| `syncServerEnv` | boolean | нет | Заменить ключ в окружении сервера приложения и перезапустить приложение. По умолчанию `false`. Действует только на ветке `rotated`: при `minted` прежнего ключа в окружении нет. Итог приходит в поле `envSync` ответа |

Тело необязательно: запрос без тела и запрос с пустым объектом `{}` равнозначны. Схема строгая: лишнее поле отвечает `400 VALIDATION_ERROR`. Размер тела ограничен 8 КиБ.

## Примеры

Эндпоинт принимает только ключ Cowork/Code, поэтому примеров два. У личного ключа и ключа OAuth-приложения нет скоупа `vibe:cowork`, отказ по скоупу — `403 INSUFFICIENT_SCOPE`.

### curl — ключ Cowork/Code

```bash
curl -X POST https://vibecode.bitrix24.tech/v1/cowork/applications/APPLICATION_ID/key \
  -H "X-Api-Key: YOUR_COWORK_KEY" \
  -H "Idempotency-Key: app-key-2026-09-22-01" \
  -H "Content-Type: application/json" \
  -d '{}'
```

### JavaScript — ключ Cowork/Code

```javascript
const res = await fetch('https://vibecode.bitrix24.tech/v1/cowork/applications/APPLICATION_ID/key', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_COWORK_KEY',
    'Idempotency-Key': 'app-key-2026-09-22-01',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({}),
})

if (!res.ok) {
  const { error } = await res.json()
  throw new Error(`${res.status} ${error.code}`)
}

const { data } = await res.json()

// Сырой ключ приходит один раз — сохраните его сразу
if (data.rawApiKey === null) {
  // Это повтор запроса: ключ уже выдан, заново он не показывается
} else {
  saveKey(data.applicationId, data.rawApiKey)
}

// Прежний ключ работает до previousKey.graceUntil — доставьте новый ключ серверу раньше
if (data.previousKey?.graceUntil) {
  console.log('Прежний ключ перестанет работать:', data.previousKey.graceUntil)
}
```

## Поля ответа

| Поле | Тип | Описание |
|------|-----|----------|
| `success` | boolean | Всегда `true` при успехе |
| `data.application` | object \| null | Карточка приложения — та же форма, что отдаёт [`GET /v1/applications/:id`](/docs/applications/get). Её блок `key` уже показывает новый ключ. `null` только в одном случае: ключ выдан, а карточку после этого прочитать не удалось — тогда в `warningCodes` приходит `APPLICATION_CARD_UNAVAILABLE` |
| `data.applicationId` | string | Повторяет `application.id`, чтобы связать локальную запись с карточкой, не разбирая её |
| `data.issued` | string \| null | `minted` — слот был пуст и ключ выдан, `rotated` — ключ в слоте заменён. При повторе запроса — то, что сделал исходный запрос. `null` у замены, сделанной до того, как платформа начала записывать ветку |
| `data.rawApiKey` | string \| null | Сырой ключ. Возвращается ОДИН раз, восстановить нельзя. Формат — `vibe_api_`, затем 32 символа латиницы и цифр, затем `_` и 6 шестнадцатеричных символов. `null` при повторе запроса |
| `data.key` | object \| null | Новый ключ без секрета — `id`, `prefix`, `suffix`, `scopes`, `status`, `expiresAt`, `createdAt`. `null` при повторе запроса |
| `data.key.scopes` | array | Права нового ключа. Ключ владельца, выданный в пустой слот, несёт только `vibe:infra` и `vibe:storage`. Замена переносит права прежнего ключа |
| `data.key.expiresAt` | string \| null | Срок действия нового ключа, ISO 8601. `null` — ключ бессрочный. При замене срок наследуется от прежнего ключа, см. «Известные особенности» |
| `data.previousKey` | object \| null | Заменённый ключ — `id`, `graceUntil`, `status`. Приходит только владельцу на ветке `rotated`, в остальных случаях `null` |
| `data.previousKey.graceUntil` | string \| null | Момент, когда прежний ключ перестанет работать: через 24 часа после замены или раньше, если раньше наступает его собственный срок. `null`, когда прежний ключ был заблокирован, — отсрочки у него нет |
| `data.envSync` | object \| null | Итог замены ключа в окружении сервера: статус в поле `status`, значения — в таблице ниже. `null`, когда `syncServerEnv` не передан, на ветке `minted` и в ответе участнику команды разработки |
| `data.warnings` | array | Предупреждения текстом. Пустой, когда предупреждений нет |
| `data.warningCodes` | array | Коды предупреждений. `APPLICATION_HAS_AUTH_KEY` — у приложения есть и ключ авторизации: карточка продолжает показывать его, а вызов затронул личный ключ. `ENV_SYNC_NOT_APPLICABLE_FOR_COLLABORATOR` — ответ участнику команды разработки. `KEY_NOT_REPLAYABLE` — повтор запроса. `APPLICATION_CARD_UNAVAILABLE` — карточку прочитать не удалось |

Значения `envSync.status`. Поле `variable` называет затронутую переменную окружения, поле `reason` уточняет `skipped` и `recreate_failed`.

| Статус | Что произошло |
|--------|---------------|
| `updated` | Ключ заменён в окружении приложения, приложение перезапущено |
| `restart_failed` | Ключ в окружении заменён, перезапуск не удался — перезапустите приложение сами |
| `owner_restore_failed` | Ключ в окружении заменён, но файл окружения остался недоступен приложению. Перезапуск не поможет, пока приложению не вернут доступ к файлу |
| `not_found` | Прежний ключ в окружении не найден, окружение не тронуто. Впишите новый ключ сами |
| `skipped` | Замена не выполнялась: `reason: "no_server"` — у приложения нет сервера, `reason: "galaxy"` — доставка ключа в контейнеры для аккаунта выключена |
| `unreachable` | Сервер или контейнер спит либо недоступен, окружение не тронуто |
| `failed` | Платформа не смогла определить, куда доставить ключ, или доставка оборвалась |
| `recreated` | Контейнер пересоздан с новым ключом в окружении. На время перезапуска приложение обрывает открытые соединения — это плановый простой |
| `not_required` | В окружении контейнера нет ключа платформы, менять нечего |
| `baked_in_image` | Ключ зашит в образ контейнера, пересоздание его не обновит. Переопределите переменную при запуске или пересоберите образ из исходников |
| `recreate_failed` | Замена началась и не завершилась. Причина — в `reason`: `rolled_back`, `app_down`, `busy`, `read_failed` или `timeout` |
| `superseded` | Более поздняя замена ключа уже применилась к этому контейнеру |

## Пример ответа

Замена ключа в занятом слоте, `issued: "rotated"`:

```json
{
  "success": true,
  "data": {
    "application": {
      "id": "cmud90k3b0009cicg8yxl2rf8",
      "name": "Отчёт по сделкам",
      "description": null,
      "type": "PERSONAL",
      "iconUrl": null,
      "createdAt": "2026-09-22T22:31:23.063Z",
      "updatedAt": "2026-09-22T22:31:23.092Z",
      "viewerState": "owner",
      "pinned": false,
      "author": { "name": "Автор приложений" },
      "isEmbedded": false,
      "openUrl": null,
      "openTarget": null,
      "server": null,
      "sources": { "hasVersions": false, "latestVersionId": null, "latestSavedAt": null },
      "activeOperation": null,
      "key": {
        "slot": "api",
        "present": true,
        "prefix": "vibe_api_…",
        "suffix": "9529",
        "status": "ACTIVE",
        "expiresAt": null,
        "lastUsedAt": null,
        "rotatable": true,
        "rotateBlockedReason": null,
        "affectsShownKey": true
      }
    },
    "applicationId": "cmud90k3b0009cicg8yxl2rf8",
    "issued": "rotated",
    "rawApiKey": "vibe_api_…",
    "key": {
      "id": "b3d37d59-5b50-4e72-aa12-8a2b40a012f8",
      "prefix": "vibe_api_…",
      "suffix": "9529",
      "scopes": ["crm", "vibe:infra", "vibe:storage"],
      "status": "ACTIVE",
      "expiresAt": null,
      "createdAt": "2026-09-22T22:31:23.086Z"
    },
    "previousKey": {
      "id": "5d192322-3ced-438e-bc4f-0cbba01b9937",
      "graceUntil": "2026-09-23T22:31:23.087Z",
      "status": "ACTIVE"
    },
    "envSync": null,
    "warnings": [],
    "warningCodes": []
  }
}
```

Выдача ключа в пустой слот, `issued: "minted"`:

```json
{
  "success": true,
  "data": {
    "application": {
      "id": "cmud90jwg0001cicg4qgipzws",
      "name": "Дашборд продаж",
      "description": null,
      "type": "PERSONAL",
      "iconUrl": null,
      "createdAt": "2026-09-22T22:31:22.816Z",
      "updatedAt": "2026-09-22T22:31:22.889Z",
      "viewerState": "owner",
      "pinned": false,
      "author": { "name": "Автор приложений" },
      "isEmbedded": false,
      "openUrl": null,
      "openTarget": null,
      "server": null,
      "sources": { "hasVersions": false, "latestVersionId": null, "latestSavedAt": null },
      "activeOperation": null,
      "key": {
        "slot": "api",
        "present": true,
        "prefix": "vibe_api_…",
        "suffix": "f08a",
        "status": "ACTIVE",
        "expiresAt": null,
        "lastUsedAt": null,
        "rotatable": true,
        "rotateBlockedReason": null,
        "affectsShownKey": true
      }
    },
    "applicationId": "cmud90jwg0001cicg4qgipzws",
    "issued": "minted",
    "rawApiKey": "vibe_api_…",
    "key": {
      "id": "61a91657-2e88-4aeb-848f-1951327efd71",
      "prefix": "vibe_api_…",
      "suffix": "f08a",
      "scopes": ["vibe:infra", "vibe:storage"],
      "status": "ACTIVE",
      "expiresAt": null,
      "createdAt": "2026-09-22T22:31:22.879Z"
    },
    "previousKey": null,
    "envSync": null,
    "warnings": [],
    "warningCodes": []
  }
}
```

## Пример ответа при ошибке

403 — ключ в слоте приложения шире вызывающего ключа:

```json
{
  "success": false,
  "error": {
    "code": "KEY_ROTATE_WIDER_THAN_CALLER",
    "message": "The key in this application slot is wider than your key. Re-issue it from the web cabinet, where the owner sees what the new key will be allowed to do."
  }
}
```

## Ошибки

Выписка ключа идёт тем же каналом Битрикс24, что у [создания приложения](./applications-create.md), и его отказы переподнимаются дословно. У коробочного аккаунта выписка идёт своим каналом и приносит собственные коды, разобранные в [Ключах и авторизации](/docs/keys-auth). Форма ответа та же, ветвитесь по `error.code`.

| HTTP | Код | Описание |
|------|-----|----------|
| 400 | `IDEMPOTENCY_KEY_REQUIRED` | Заголовок `Idempotency-Key` не передан |
| 400 | `INVALID_IDEMPOTENCY_KEY` | Заголовок передан, но не подходит по длине или набору символов |
| 400 | `VALIDATION_ERROR` | Лишнее поле в теле или `syncServerEnv` не булево значение |
| 400 | `PORTAL_NOT_LINKED` | Аккаунт Битрикс24 не связан с Битрикс24 Нетворк, и выписать ключ нечем |
| 400 | `BOX_NO_DEVELOPER_KEY` | Коробочный аккаунт: у владельца приложения нет ключа разработчика, и выписать ключ нечем |
| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |
| 401 | `INVALID_API_KEY` | Ключ не опознан: такой строки на платформе нет |
| 401 | `KEY_INACTIVE` | Ключ выключен |
| 401 | `KEY_EXPIRED` | Срок действия вызывающего ключа истёк |
| 402 | `ACCOUNT_FROZEN` | Баланс аккаунта исчерпан — пополните счёт |
| 402 | `MARKETPLACE_REQUIRED` | Только выдача, а не замена: у аккаунта нет действующей подписки BitrixGPT + Маркетплейс. В странах, где доступ даёт тариф Битрикс24, приходит код своей страны — `KZ_PAID_ONLY`, `UZ_PAID_ONLY` или `BY_PAID_ONLY`. Замена эту проверку не проходит: она переносит права прежнего ключа и новых не выдаёт |
| 402 | `INT_TARIFF_REQUIRED` | Только выдача, а не замена: аккаунт получает доступ по тарифу Битрикс24, а тариф бесплатный |
| 403 | `INSUFFICIENT_SCOPE` | У ключа нет скоупа `vibe:cowork` |
| 403 | `COWORK_HARNESS_KEY_FORBIDDEN` | Вызов сделан ключом стороннего агента, выписанным на подписку. Такому ключу эндпоинт недоступен — см. [Свой агент на подписке](/docs/cowork/harness) |
| 403 | `COWORK_NOT_ACTIVATED` | Нет активной подписки Cowork/Code для пары сотрудник и аккаунт |
| 403 | `WRITE_BLOCKED_READONLY_KEY` | Вызывающий ключ в режиме «только чтение», а выдача и замена ключа — запись. `error.details` называет ключ и его режим |
| 403 | `NOT_APPLICATION_OWNER` | Вызывающий не владеет приложением и не входит в команду разработки его сервера |
| 403 | `KEY_ROTATE_WIDER_THAN_CALLER` | Ключ в слоте приложения шире вызывающего ключа. Замена здесь молча сузила бы приложение, которым могут пользоваться другие, поэтому такой ключ перевыпускают в кабинете |
| 403 | `KEY_POLICY_READONLY_REQUIRED` | Только ключ участника команды разработки: выдаваемый ключ шире режима, который аккаунт назначает новым ключам. Владельцу ключ в пустой слот выдаётся в режиме политики, и этот отказ ему не приходит |
| 403 | `INFRA_DISABLED_FOR_PORTAL` | Только выдача, а не замена: администратор портала выключил работу с серверами, а новый ключ несёт `vibe:infra` |
| 403 | `PORTAL_TARIFF_UNREADABLE` | Только выдача, а не замена: тариф аккаунта прочитать не удалось, поэтому какой тариф нужен, ответ не называет. Покупка тарифа отказ не снимает |
| 403 | `PORTAL_SUBSCRIPTION_UNREADABLE` | Только выдача, а не замена: состояние подписки аккаунта прочитать не удалось. Покупка отказ не снимает — приложение нужно переподключить под главным администратором аккаунта |
| 404 | `APPLICATION_NOT_FOUND` | Приложения с таким `id` на аккаунте нет или оно удалено |
| 409 | `KEY_ROTATE_OAUTH_APP_KEY` | В личном слоте лежит ключ авторизации. Его перевыпуск пересоздаёт OAuth-приложение на аккаунте и меняет `client_id`, поэтому делается в кабинете |
| 409 | `KEY_ROTATE_SYSTEM_KEY` | В слоте лежит ключ, который платформа выдаёт своим эндпоинтом. Кнопки перевыпуска в кабинете у такого ключа нет — обратитесь в поддержку |
| 409 | `KEY_ROTATE_LIVE_OAUTH_GRANT` | У ключа есть живое разрешение авторизации, и замена оставила бы его на отзываемом ключе. Сначала отзовите разрешение в кабинете |
| 409 | `KEY_ROTATE_NOT_ACTIVE` | Ключ в слоте отозван или его срок истёк. Верните ключ в работу или продлите срок в кабинете и повторите |
| 409 | `KEY_ROTATE_KEY_VANISHED` | Ключ удалили, пока шёл запрос. Повторите |
| 409 | `APPLICATION_KEY_RECOVERY_REQUIRED` | Ключ в слоте — незавершённая замена. Новый ключ идемпотентности запрет не снимает: нужно восстановление через поддержку |
| 409 | `APPLICATION_KEY_REPLACE_IN_PROGRESS` | На приложении уже идёт другая замена ключа либо она завершилась, пока этот запрос ждал очереди. Одна замена на приложение за раз, с любым ключом идемпотентности. Дождитесь её, перечитайте карточку и только потом просите снова. Замена, брошенная упавшим запросом, перестаёт мешать через две минуты |
| 409 | `KEY_LIMIT_REACHED` | Только выдача, а не замена: квота ключей исчерпана. `error.details` содержит `used`, `limit` и `requested`, а `scope: "portal"` означает портальный потолок платформы |
| 409 | `COLLABORATOR_KEY_REQUIRES_SERVER` | Участник команды разработки: у приложения нет живого сервера, а ключ участника привязан к серверу |
| 409 | `COLLABORATOR_MEMBERSHIP_BIND_CONFLICT` | Участник команды разработки: членство не удалось надёжно привязать к аккаунту, и ключ не выдан. Попросите владельца сервера добавить вас заново |
| 409 | `IDEMPOTENCY_KEY_BODY_MISMATCH` | Тот же `Idempotency-Key` пришёл с другим телом |
| 409 | `IDEMPOTENCY_KEY_ALREADY_USED` | Ключ идемпотентности израсходован, и повторить нечего: он отправлен для другого приложения, карточку удалили либо ключ под ним уже выдан, а запрос потом упал. Прочитайте карточку и повторите с НОВЫМ ключом идемпотентности |
| 409 | `IDEMPOTENCY_CONCURRENT_RETRY` | Параллельный запрос с тем же ключом идемпотентности ещё выполняется. Единственный из трёх отказов идемпотентности, где повтор осмыслен |
| 413 | `PAYLOAD_TOO_LARGE` | Тело больше 8 КиБ. Проверяется до разбора тела, поэтому ничего не резервируется и ключ не выписывается |
| 415 | `FST_ERR_CTP_INVALID_MEDIA_TYPE` | Тело прислано с типом содержимого, который маршрут не разбирает. Отправляйте `Content-Type: application/json` или запрос без тела |
| 429 | `RATE_LIMITED` | Превышен предел частоты на связку аккаунт и владелец ключа. Суммарный лимит платформы — 6 запросов в минуту. Действующее для вашего ключа значение приходит в заголовке `x-ratelimit-limit` — оно ниже суммарного, поскольку лимит делится между репликами |
| 429 | `QUOTA_EXCEEDED` | Исчерпана суточная бесплатная квота вызовов при нулевом балансе на предоплате |
| 500 | `APPLICATION_KEY_ATTACH_FAILED` | Ключ выдан в пустой слот, но привязать его к приложению не удалось. `error.details.orphanKeyId` называет этот ключ: отзовите его и повторите с НОВЫМ ключом идемпотентности |
| 500 | `APPLICATION_KEY_REPOINT_FAILED` | Новый ключ выписан, но карточка не дошла до проверенного конечного состояния. Ключ `error.details.orphanKeyId` остаётся под защитой восстановления, и повтор с другим ключом идемпотентности запрещён. Если на этот ключ ничего не указывает, удалите его штатным удалением ключа. Непустой `error.details.strandedSlots` перечисляет то, что ещё указывает на ключ, — тогда обратитесь в поддержку |
| 500 | `KEY_ROTATE_REVOKE_FAILED` | Замена заблокированного ключа: отзыв его живых токенов доступа не подтвердился. Сырого ключа в ответе нет, выписанный ключ остаётся под защитой восстановления, и повтор не разрешён, пока восстановление не завершено |
| 502 | `BITRIX_UNAVAILABLE` | Битрикс24 отказал или был недоступен во время выписки ключа. Прежний ключ остаётся как был: действующий продолжает работать, заблокированный — нет |
| 503 | `COWORK_FEATURE_DISABLED` | Cowork/Code отключён на уровне платформы |

Полный список общих ошибок API — [Ошибки](/docs/errors).

## Известные особенности

**Режим выданного ключа не шире вашего ключа.** На ключ, который получает владелец приложения, режим места Cowork/Code не влияет. В пустой слот ключ выдаётся в самом узком из режима, который аккаунт назначает новым ключам, и режима вызывающего ключа: политика «только чтение» выдачу не отбивает, а сужает. Замена даёт ключ не шире текущего режима заменяемого ключа и не шире вызывающего.

**Карточка заранее говорит, пройдёт ли вызов.** Блок `key` [карточки приложения](/docs/applications/get) несёт `rotatable` — пройдёт ли этот вызов для вас — и `rotateBlockedReason` с причиной отказа. `WIDER_THAN_CALLER` значит, что ключ в слоте шире вашего, `WRITE_BLOCKED_READONLY_KEY` — что ваш ключ не меняет данные. Незнакомую причину читайте как «заменить нельзя, причина неизвестна». Флаг отражает то, что платформа уже знает, поэтому `false` не тупик, а `true` не гарантия: отказ самого эндпоинта всё равно обрабатывайте.

**Без `syncServerEnv` новый ключ до сервера не доходит.** Приложение держит прежний ключ в своём окружении и начинает получать отказы в момент `previousKey.graceUntil` — это не всегда через 24 часа. Присылайте `true`, если у вас нет своего способа доставить ключ серверу.

**Срок нового ключа при замене наследуется.** Бессрочный ключ остаётся бессрочным, действующий срок переносится как есть. Срок, который уже прошёл, заменяется сроком по умолчанию аккаунта — тем же `keyExpiresInDays`, что отдают [параметры создания](./applications-defaults.md).

**Заблокированный ключ в слоте замене не мешает.** Его живые токены доступа гасятся и на новый ключ не переезжают.

**Участник команды разработки получает свой ключ.** Ключ привязан к серверу приложения и открывает только маршруты разработки этого сервера. Окружение сервера владельца такой вызов не меняет. Режим ключа участника — самый узкий из режима вызывающего ключа и режима места Cowork/Code, если вызывающий ключ принадлежит месту: у места в режиме «только чтение» ключ участника выдаётся в том же режиме, даже когда сам вызывающий ключ может изменять данные.

**Повтор с тем же ключом идемпотентности ключа не возвращает.** Повтор с тем же телом отвечает `201` с заголовком `Idempotent-Replayed: true`. Секрет хранится только в виде хеша, поэтому сохраняйте `rawApiKey` сразу, в том же обработчике ответа.

## Смотрите также

- [Создать приложение из Cowork/Code](./applications-create.md)
- [Параметры создания приложения](./applications-defaults.md)
- [Карточка приложения](/docs/applications/get)
- [Проектный ключ для деплоя](./deploy-key.md)
- [Ключи и авторизация](/docs/keys-auth)
- [Cowork/Code](/docs/cowork)
- [Ошибки](/docs/errors)
