
## Создать сервер

`POST /v1/infra/servers`

Создаёт приложение у облачного провайдера. Всегда в режиме Black Hole — iptables блокирует все входящие порты, приложение невидимо из интернета.

На портале есть две модели размещения, и от модели зависит, чего ждать после создания:

- **Отдельная виртуальная машина** (модель по умолчанию). Ответ возвращается сразу со статусом `provisioning`, провижининг занимает 1–3 минуты, после чего нужно опрашивать [`GET /v1/infra/servers/:id`](./get.md) до `status: "running"` и `blackholeStatus: "CONNECTED"`. Ответ **единственный раз** содержит SSH-данные `ssh.password` и `ssh.privateKey` — они будут нужны при переключении в режим OPEN, сохраните их сразу. Эта страница описывает данную модель в полях запроса, полях ответа и таблице ошибок ниже.
- **Galaxy-приложение** — контейнер на общем хосте. Если портал размещает приложения в галактиках, тот же `POST /v1/infra/servers` создаёт galaxy-приложение, а не виртуальную машину. Признак модели в ответе — поле `createdVia` равно `galaxy`. Раздел [«Galaxy-приложение»](#galaxy-приложение) ниже описывает два сценария запуска, полная модель — на странице [Galaxy-приложение](/docs/infra/galaxy).

## Galaxy-приложение

Если портал размещает приложения в галактиках, `POST /v1/infra/servers` создаёт **galaxy-приложение** — контейнер на общем хосте, а не отдельную виртуальную машину. Запрос и порядок вызовов те же, отличается жизненный цикл. Признак модели в ответе — поле `createdVia` равно `galaxy`. Полная модель, стоимость и отличия деплоя — на странице [Galaxy-приложение](/docs/infra/galaxy).

**Не ждите `CONNECTED` перед загрузкой кода.** Galaxy-приложение **никогда** само не доходит до `blackholeStatus: "CONNECTED"` — его контейнер собирается при загрузке кода. Если просто опрашивать статус, приложение останется в `provisioning`, а примерно через 20 минут платформа пометит его как `error` и запишет в поле `provisionError`, что код так и не был загружен. Поэтому код загружают сразу — одним из двух сценариев ниже.

### Сценарий 1 — прозрачный, рекомендуется

Один запрос. Передайте в `POST /v1/infra/servers` код приложения в поле `source` вместе с `runtime` и `start` (и при необходимости `install`, `env`, `port`). Сборка пойдёт в фоновом режиме. После ответа опрашивайте [`GET /v1/infra/servers/:id`](./get.md) до `status: "running"`. Отдельный вызов загрузки кода не нужен.

```bash
curl -X POST https://vibecode.bitrix24.tech/v1/infra/servers \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my-crm-app",
    "displayName": "Мой CRM-бот",
    "source": { "content": "<base64-архив>" },
    "runtime": "node20",
    "start": "node index.js",
    "install": "npm ci",
    "port": 3000
  }'
```

Поля `source`, `runtime`, `start`, `install`, `env`, `port`, `healthPath` совпадают по смыслу с телом [`POST /:id/deploy`](/docs/infra/deploy/deploy):

| Поле | Тип | Обяз. | Описание |
|------|-----|:-----:|----------|
| `source` | object | да (для этого сценария) | Источник кода. `source.content` — архив приложения в base64, до 96 МБ на тело запроса, то есть около 72 МБ самого архива: base64 тяжелее исходных байт примерно на треть. Тело сверх потолка отклоняется кодом `413 INLINE_SOURCE_TOO_LARGE`. Архив крупнее разворачивайте в два шага — сценарий 2 ниже |
| `runtime` | string | да (если передан `source`) | ID рантайма: `node20`, `python311`, `php83`, `static` и другие. Список — [`GET /v1/infra/runtimes`](/docs/infra/deploy/runtimes) |
| `start` | string | да (если передан `source`) | Команда запуска приложения, одной строкой. Перенос строки вернёт `400` |
| `install` | string | нет | Команда установки зависимостей, одной строкой |
| `env` | object | нет | Переменные окружения `{ "KEY": "value" }`. Подставляются в контейнер при запуске, см. ниже |
| `port` | number | нет | Порт, на котором приложение слушает |
| `healthPath` | string | нет | Путь, по которому платформа проверяет готовность приложения внутри контейнера. До 500 символов, начинается со `/`. По умолчанию `/`. На создании отдельной виртуальной машины игнорируется |

Других полей деплоя одношаговое создание не принимает: `preStart`, `systemd`, `serviceName`, `cleanDeploy`, `extractTo`, `hardening`, `preserveEnv`, `dataDirs`, `dataDirsRecursive` вернут `400 UNKNOWN_PARAM`, а полный список допустимых полей придёт в `details.validParams`. Эти поля относятся к деплою отдельной виртуальной машины и к galaxy-приложению не применяются — команды, которые нужно выполнить до старта приложения, задавайте в `install`.

Без полей `runtime` и `start` поле `source` вернёт `400` — оба поля обязательны. На портале без режима галактик `source` запрещён и вернётся `400 SOURCE_AT_CREATE_GALAXY_ONLY`. Тот же ответ придёт на бесплатном тарифе Битрикс24, пока у аккаунта нет галактики: новая на бесплатном тарифе не создаётся, поэтому разворачивайте в два шага — создание без `source`, затем [`POST /v1/infra/servers/:id/deploy`](/docs/infra/deploy/deploy).

### Сценарий 2 — в два шага

Создайте приложение **без** `source` — ответ придёт со статусом `provisioning` и подсказкой `next: "deploy"`. Сразу вызовите [`POST /v1/infra/servers/:id/deploy`](/docs/infra/deploy/deploy) с кодом в `source`, рантаймом и командой запуска. Загружайте код сразу, не дожидаясь `CONNECTED`.

```bash
# Шаг 1 — создать приложение без кода
curl -X POST https://vibecode.bitrix24.tech/v1/infra/servers \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "provider": "bitrix-cloud", "name": "my-crm-app", "plan": "bc-medium", "region": "ru-central1-a" }'

# Ответ содержит data.id, data.next = "deploy" и data.hint.
# Шаг 2 — сразу загрузить код, не дожидаясь CONNECTED
curl -X POST https://vibecode.bitrix24.tech/v1/infra/servers/SERVER_ID/deploy \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "source": { "content": "<base64-архив>" },
    "runtime": "node20",
    "start": "node index.js",
    "port": 3000
  }'
```

`provider` и `plan` обязательны по схеме для создания без `source`. `region` необязателен: без него платформа сама подставит регион по умолчанию для выбранного провайдера. Для galaxy-приложения значения информационны — приложение наследует провайдера, план и регион своего хоста.

Если присланное значение расходится с тем, что приложение реально получило, ответ несёт запись в `warnings[]` рядом с `data`: она называет разошедшиеся поля, показывает и присланное, и действующее значение и предупреждает, что повторная отправка ничего не изменит. Совпавшие поля не упоминаются, так что корректный вызов остаётся без предупреждения. Нужна машина, у которой провайдера, план и регион выбираете вы, — создавайте её с `placement` равным `dedicated`.

### Отдельная виртуальная машина вместо galaxy-приложения

Если портал размещает приложения в галактиках, а конкретному приложению нужен свой сервер — передайте поле `placement` равным `dedicated`. Пара `provider`, `plan` при этом обязательна, `region` по-прежнему необязателен. Поле `source` в таком запросе вернёт `400 SOURCE_AT_CREATE_GALAXY_ONLY`: отдельная виртуальная машина разворачивается в два шага, код загружают через [`POST /:id/deploy`](/docs/infra/deploy/deploy).

```bash
curl -X POST https://vibecode.bitrix24.tech/v1/infra/servers \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "bitrix-cloud",
    "name": "my-crm-app",
    "plan": "bc-small",
    "region": "ru-central1-b",
    "placement": "dedicated"
  }'
```

В ответе `kind` равен `STANDALONE`. Дальше действует порядок для отдельной виртуальной машины: дождаться `status: "running"` и `blackholeStatus: "CONNECTED"`, затем деплой.

### Диагностика и переменные окружения

- **Поле `provisionError`.** Если сборка или запуск упали, [`GET /v1/infra/servers/:id`](./get.md) вернёт в `data.provisionError` причину сбоя, а при ошибке сборки образа — ещё и хвост лога сборки Docker. Читайте это поле, чтобы понять причину.
- **Переменные `env` подставляются при запуске.** Значения из `env` передаются в контейнер во время запуска через `docker run --env`, а не вшиваются в образ. Вместе с ними платформа передаёт `PORT` (равен полю `port`, по умолчанию `3000`): ключ `PORT` зарезервирован, присланный свой перекрывается, и ответ несёт строку в `warnings[]` — см. [POST /v1/infra/servers/:id/deploy](../deploy/deploy.md). Поэтому ключи API и секреты из `env` не попадают в слои образа.

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

| Поле | Тип | Обяз. | Описание |
|------|-----|:-----:|----------|
| `provider` | string | да | ID провайдера из [`GET /v1/infra/providers`](../providers/list.md), например `bitrix-cloud`. 1–50 символов |
| `name` | string | да | Системное имя сервера — технический идентификатор. 2–63 символа, только строчные латинские буквы, цифры и `-`, первым символом — буква. Паттерн: `^[a-z][a-z0-9-]*$`. Используется в URL поддомена (`app-<hex>` генерируется отдельно), логах, audit-журнале. **Неизменяемый после создания.** Для отображаемого имени используйте `displayName` |
| `displayName` | string | нет | Отображаемое имя на любом языке (русский, китайский, эмодзи — что угодно). 2–100 символов, без управляющих байтов. Отображается в UI, уведомлениях о падении сервера, security-алертах, биллинг-строках, B24-каталоге. Если не передан — подставится `name`. Само `name` остаётся техническим идентификатором, неизменяемым, оно используется в URL поддомена и логах |
| `description` | string | нет | Описание приложения для карточки каталога Битрикс24, до 500 символов. Переносы строк и табуляции разрешены, прочие управляющие байты запрещены. Значение обрезается по краям. Если не передать — описание останется пустым, задать его позже можно через [`PATCH /v1/infra/servers/:id`](./update.md) |
| `plan` | string | да | ID тарифа из [`GET /v1/infra/providers/:providerId/plans`](../providers/plans.md), например `bc-small` |
| `region` | string | нет | ID региона из [`GET /v1/infra/providers/:providerId/regions`](../providers/regions.md), например `ru-central1-b`. Без него платформа подставит регион по умолчанию для провайдера |
| `image` | string | нет | ID образа ОС из [`GET /v1/infra/providers/:providerId/images`](../providers/images.md). Если не передан — платформа сама подставит актуальный образ провайдера. Если передаёте — берите актуальный из ответа: ID меняется при обновлении сборки |
| `sshPublicKey` | string | нет | Ваш SSH-публичный ключ (`ssh-rsa …`, `ssh-ed25519 …`, `ecdsa-sha2-nistp256/384/521 …`, security-keys). До 8192 символов. Если не передан — платформа сгенерирует приватный ключ и вернёт его один раз в `ssh.privateKey` |
| `placement` | string | нет | Модель размещения: `auto` (по умолчанию) или `dedicated`. На портале с режимом «Сначала галактика» (`galaxies-only`) значение `dedicated` создаёт отдельную виртуальную машину, а не galaxy-приложение — перенос приложения на собственный сервер. Проходит те же проверки, что и обычное создание сервера: политику `serverCreation` и квоту серверов на пользователя. При `auto` поведение прежнее (в режиме «Сначала галактика» создаётся galaxy-приложение) |
| `graduateFrom` | string | нет | ID вашего galaxy-приложения (`kind=GALAXY_APP`), которое платформа удалит сразу после того, как создаст для него выделенный сервер — чтобы старое приложение не осталось висеть в галактике. Ограничен владельцем: тот же ключ, тот же портал, `kind=GALAXY_APP`. Чужой идентификатор или идентификатор не galaxy-приложения вернёт `404`, ничего не удаляя. Имеет смысл только вместе с `placement: dedicated`. Заголовок `Idempotency-Key` с `graduateFrom` для выделенного сервера несовместим — см. [Идемпотентность](#идемпотентность) |

> **Примечание:** для модели отдельной виртуальной машины параметр `runtime` в `POST /v1/infra/servers` не принимается — передача вернёт `400 RUNTIME_PARAM_REMOVED`. Рантайм устанавливается на этапе деплоя. См. [POST /:id/deploy](/docs/infra/deploy/deploy). **Исключение — режим галактик:** при передаче поля `source` параметр `runtime` обязателен (см. раздел [«Galaxy-приложение»](#galaxy-приложение)), и `400 RUNTIME_PARAM_REMOVED` в этом случае не возвращается.

## Примеры

### curl — личный ключ

```bash
# name — только строчные латинские буквы, цифры и дефис, первый символ буква (^[a-z][a-z0-9-]*$)
curl -X POST https://vibecode.bitrix24.tech/v1/infra/servers \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "bitrix-cloud",
    "name": "my-crm-app",
    "displayName": "Мой CRM-бот",
    "plan": "bc-small",
    "region": "ru-central1-b",
    "image": "fd83esfomhq25p2ono90"
  }'
```

### curl — OAuth-приложение

```bash
# name — только строчные латинские буквы, цифры и дефис, первый символ буква (^[a-z][a-z0-9-]*$)
curl -X POST https://vibecode.bitrix24.tech/v1/infra/servers \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "bitrix-cloud",
    "name": "my-crm-app",
    "displayName": "Мой CRM-бот",
    "plan": "bc-small",
    "region": "ru-central1-b",
    "image": "fd83esfomhq25p2ono90"
  }'
```

### JavaScript — личный ключ

```javascript
// name — только строчные латинские буквы, цифры и дефис, первый символ буква (^[a-z][a-z0-9-]*$)
const res = await fetch('https://vibecode.bitrix24.tech/v1/infra/servers', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    provider: 'bitrix-cloud',
    name: 'my-crm-app',
    displayName: 'Мой CRM-бот',
    plan: 'bc-small',
    region: 'ru-central1-b',
    image: 'fd83esfomhq25p2ono90',
  }),
})
const { data } = await res.json()
console.log('Server ID:', data.id, 'Subdomain:', data.subdomain)

// Сохраняем SSH-креды на всякий случай — они возвращаются только один раз
if (data.ssh.privateKey) {
  await saveLocally(`${data.id}.key`, data.ssh.privateKey)
}
```

### JavaScript — OAuth-приложение

```javascript
// name — только строчные латинские буквы, цифры и дефис, первый символ буква (^[a-z][a-z0-9-]*$)
const res = await fetch('https://vibecode.bitrix24.tech/v1/infra/servers', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    provider: 'bitrix-cloud',
    name: 'my-crm-app',
    displayName: 'Мой CRM-бот',
    plan: 'bc-small',
    region: 'ru-central1-b',
    image: 'fd83esfomhq25p2ono90',
  }),
})
```

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

| Поле | Тип | Описание |
|------|-----|----------|
| `success` | boolean | Всегда `true` при успехе |
| `data.id` | string (UUID) | Уникальный идентификатор сервера |
| `data.status` | string | `"provisioning"` сразу после создания нового сервера — ждите `"running"` через 1–3 минуты. **В reuse-ответе (`reused: true`) это статус существующего сервера и он может быть любым**, в том числе сразу `"running"` — тогда ожидание `"running"` ничего не проверяет |
| `data.provider` | string | Эхо переданного `provider`. **Исключение — galaxy-приложение** (`createdVia: "galaxy"`): возвращается провайдер хоста-носителя, а не запрошенный |
| `data.name` | string | Системное имя сервера. Эхо переданного `name` при создании нового. **В reuse-ответе — имя существующего сервера**, а запрошенное вами лежит в `data.requestedName` |
| `data.kind` | string | Тип сервера, всегда: `STANDALONE` или `GALAXY_APP` |
| `data.galaxyId` | string | Только для galaxy-ветки — ID хоста-галактики. В standalone-ответе ключ отсутствует |
| `data.displayName` | string \| null | Отображаемое имя. Если при создании не передавали `displayName` — будет равно `name` |
| `data.description` | string \| null | Эхо переданного `description`. Ключ есть только в ответе для отдельной виртуальной машины. **Для galaxy-приложения**, где `createdVia` равно `galaxy`, ключа в ответе нет, хотя описание сохраняется — прочитайте его через [`GET /v1/infra/servers/:id`](./get.md) |
| `data.ip` | string \| null | Публичный IP. `null` сразу после создания, заполнится когда виртуальная машина запустится |
| `data.ssh.user` | string | Пользователь для SSH: `root` для новых серверов |
| `data.ssh.port` | number | Порт SSH: `22` |
| `data.ssh.password` | string \| null | **Одноразово!** Пароль root. Сохраняйте сразу — позже не вернётся. Нужен при переключении в OPEN-режим |
| `data.ssh.privateKey` | string \| null | **Одноразово!** Приватный ключ в формате OpenSSH (если `sshPublicKey` не передавали при создании). Сохраняйте сразу |
| `data.plan` | string | Эхо переданного `plan`. **Исключение — galaxy-приложение** (`createdVia: "galaxy"`): возвращается тариф хоста-носителя, а не запрошенный |
| `data.region` | string | Регион, в который сервер реально попал. Может отличаться от запрошенного при переключении на запасную зону (см. «Известные особенности»). **Для galaxy-приложения** — регион хоста-носителя |
| `data.image` | string | Эхо переданного `image` |
| `data.mode` | string | Всегда `"BLACKHOLE"` сразу после создания |
| `data.createdVia` | string | `"api"` для вызовов через v1 API, `"ui"` для вызовов из личного кабинета. **В reuse-ответе — значение существующего сервера**, поэтому на ответ v1-создания может прийти `"ui"` |
| `data.subdomain` | string | Субдомен для приложения, например `app-92fb1c34`. Используется в `appUrl` |
| `data.blackholeStatus` | string | Состояние туннеля агента. Сразу после создания — `"NONE"`, затем проходит через `WAITING` и завершается `CONNECTED` |
| `data.accessPolicy` | string | Политика доступа к приложению. По умолчанию `"OWNER_ONLY"` — только владелец ключа |
| `data.runtimeId` | string \| null | Всегда `null` при создании. Заполняется при деплое с `runtime` |
| `data.runtimeStatus` | string \| null | Устаревшее поле, оставлено для совместимости. Для серверов, созданных после 2026-04-25, всегда `null`. Готовность рантайма после деплоя определяет успех шага `runtime` в ответе [`POST /:id/deploy`](/docs/infra/deploy/deploy), а не это поле |
| `data.appUrl` | string \| null | HTTPS-адрес приложения: `https://{subdomain}.vibecode.bitrix24.tech` |
| `data.createdAt` | string (ISO 8601) | Момент создания |
| `warnings` | array\<string\> | Рядом с `data`, не внутри. Приходит, только если есть что сказать: например, `displayName` или `description` потеряли не-ASCII символы по дороге (типичный случай — Windows PowerShell без явной сериализации в UTF-8). Сервер при этом создаётся, а обе строки уезжают в карточку каталога Битрикс24 как есть |

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

Отдельная виртуальная машина:

```json
{
  "success": true,
  "data": {
    "id": "db008c84-91a5-4e15-b9d5-6c6aa2838448",
    "status": "provisioning",
    "provider": "bitrix-cloud",
    "name": "docs-test-temp",
    "kind": "STANDALONE",
    "ip": null,
    "ssh": {
      "user": "root",
      "port": 22,
      "password": "0FPIIR9EfO2OAeSSMK6bKA",
      "privateKey": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmU…\n-----END OPENSSH PRIVATE KEY-----\n"
    },
    "plan": "bc-agent",
    "region": "ru-central1-b",
    "image": "fd83esfomhq25p2ono90",
    "mode": "BLACKHOLE",
    "createdVia": "api",
    "subdomain": "app-92fb1c34",
    "blackholeStatus": "NONE",
    "accessPolicy": "OWNER_ONLY",
    "runtimeId": null,
    "runtimeStatus": null,
    "appUrl": "https://app-92fb1c34.vibecode.bitrix24.tech",
    "createdAt": "2026-04-22T10:50:11.477Z"
  }
}
```

Galaxy-приложение по сценарию 1 — `source` передан, сборка идёт в фоне. Полей `ssh` и `next` нет:

```json
{
  "success": true,
  "data": {
    "id": "7c2b1f08-3a4d-4e91-9b6c-2f5e8a1d0c33",
    "status": "provisioning",
    "provider": "bitrix-cloud",
    "name": "my-crm-app",
    "kind": "GALAXY_APP",
    "galaxyId": "<galaxy-host-id>",
    "displayName": "Мой CRM-бот",
    "ip": null,
    "ssh": null,
    "plan": "bc-medium",
    "region": "ru-central1-a",
    "image": "ubuntu-2404-lts",
    "mode": "BLACKHOLE",
    "createdVia": "galaxy",
    "subdomain": "app-7c2b1f08",
    "blackholeStatus": "NONE",
    "accessPolicy": "OWNER_ONLY",
    "runtimeId": null,
    "runtimeStatus": null,
    "appUrl": "https://app-7c2b1f08.vibecode.bitrix24.tech",
    "createdAt": "2026-04-22T10:50:11.477Z"
  }
}
```

Galaxy-приложение по сценарию 2 — `source` не передан, появляются `next` и `hint`:

```json
{
  "success": true,
  "data": {
    "id": "7c2b1f08-3a4d-4e91-9b6c-2f5e8a1d0c33",
    "status": "provisioning",
    "provider": "bitrix-cloud",
    "name": "my-crm-app",
    "kind": "GALAXY_APP",
    "galaxyId": "<galaxy-host-id>",
    "displayName": "Мой CRM-бот",
    "ip": null,
    "ssh": null,
    "plan": "bc-medium",
    "region": "ru-central1-a",
    "image": "ubuntu-2404-lts",
    "mode": "BLACKHOLE",
    "createdVia": "galaxy",
    "subdomain": "app-7c2b1f08",
    "blackholeStatus": "NONE",
    "accessPolicy": "OWNER_ONLY",
    "runtimeId": null,
    "runtimeStatus": null,
    "appUrl": "https://app-7c2b1f08.vibecode.bitrix24.tech",
    "createdAt": "2026-04-22T10:50:11.477Z",
    "next": "deploy",
    "hint": "This is a galaxy app — POST /v1/infra/servers/:id/deploy with source.content now; it will not reach \"running\" on its own."
  }
}
```

Если сборка galaxy-приложения упала, [`GET /v1/infra/servers/:id`](./get.md) вернёт причину в `data.provisionError`:

```json
{
  "success": true,
  "data": {
    "id": "7c2b1f08-3a4d-4e91-9b6c-2f5e8a1d0c33",
    "status": "error",
    "createdVia": "galaxy",
    "provisionError": "Docker build failed: npm ci exited with code 1"
  }
}
```

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

400 — нарушена валидация (имя начинается с заглавной):

```json
{
  "success": false,
  "error": {
    "code": "INVALID_REQUEST",
    "message": "name: Name must start with a letter and contain only lowercase letters, digits, and hyphens"
  }
}
```

## Ошибки

| HTTP | Код | Описание |
|------|-----|----------|
| 400 | `INVALID_REQUEST` | Нарушена валидация полей (неверный формат имени, отсутствует обязательное поле, неверный SSH-ключ и т. д.). Поле `message` содержит конкретную причину. Если на портале с размещением в галактике не передано ни `source`, ни тройка `provider` + `plan` + `region` — добавляется объект `error.hint` (см. ниже) |
| 400 | `SOURCE_AT_CREATE_GALAXY_ONLY` | Поле `source` передано на портале без режима галактик либо вместе с `placement: "dedicated"`. Загружайте код через [`POST /:id/deploy`](/docs/infra/deploy/deploy) после создания |
| 400 | `GALAXY_SOURCE_URL_NOT_ALLOWED` | Ссылка в `source.url` не прошла проверку до отправки на хост галактики. Причины: адрес ведёт не в [хранилище исходников](/docs/source-storage) платформы, значение не является абсолютным URL, схема не `http` и не `https`, адрес ведёт не в публичную сеть — на локальный, внутренний или служебный узел. Текст ответа называет сработавшее правило, а для стороннего адреса подсказывает замену — сохранить архив версией и выложить её вторым шагом через [выкладку кода](/docs/infra/deploy/deploy). Та же проверка стоит и на самой выкладке |
| 400 | `RUNTIME_PARAM_REMOVED` | Параметр `runtime` передан без `source`. Указывайте рантайм в [`POST /:id/deploy`](/docs/infra/deploy/deploy) либо передайте `runtime`, `start` и `source` вместе в этом же запросе. На портале с размещением в галактике добавляется объект `error.hint` (см. ниже) |
| 400 | `UNKNOWN_PARAM` | В теле есть неизвестное поле (например `deployMode` вместо `placement`). `details.unknownFields` перечисляет лишние поля, `details.suggestions` подсказывает правильное имя, `details.validParams` — полный список допустимых полей |
| 400 | `INVALID_PLAN` | Тариф из поля `plan` отсутствует в каталоге провайдера. Список — [`GET /v1/infra/providers/:providerId/plans`](../providers/plans.md) |
| 400 | `INVALID_REGION` | Присланный `region` отсутствует в каталоге провайдера. Список — [`GET /v1/infra/providers/:providerId/regions`](../providers/regions.md). Второй случай: `region` не передан, а у провайдера нет ни одного региона — тогда `message` называет провайдера, и подставлять значение по умолчанию платформе не из чего |
| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |
| 401 | `INVALID_API_KEY` | Неверный или просроченный API-ключ |
| 402 | `COMMERCIAL_PLAN_REQUIRED` | Бесплатный тариф Битрикс24 и пробный период недоступны (пробный уже использован). См. [раздел «Тариф и доступ»](/docs/infra#тариф-и-доступ) |
| 402 | `TRIAL_EXPIRED` | Trial использован и завершился |
| 402 | `TRIAL_PORTAL_LIMIT` | Превышена квота серверов портала на trial |
| 402 | `TRIAL_USER_LIMIT` | Превышена квота серверов на пользователя на trial |
| 402 | `PLAN_NOT_ALLOWED_ON_TRIAL` | На trial разрешён только тариф `bc-micro` |
| 402 | `ACCOUNT_FROZEN` | Баланс Вайбкод заморожен, нужно пополнить |
| 403 | `INFRA_NOT_PERMITTED` | Инфраструктура отключена на платформе или на портале |
| 403 | `SERVER_CREATION_DISABLED` | Создание серверов запрещено политикой портала |
| 403 | `MAX_SERVERS_REACHED` | Превышен лимит серверов на API-ключ. Удалите ненужные через [`DELETE`](./delete.md) или создавайте новый API-ключ |
| 403 | `WRITE_BLOCKED_READONLY_KEY` | У ключа режим «только чтение», а создание сервера — операция записи. Переключите ключ на чтение и запись, порядок — [Режим доступа](/docs/keys-auth/access-mode) |
| 403 | `INFRA_SCOPE_REQUIRED` | У ключа нет скоупа `vibe:infra` — управление инфраструктурой недоступно. Добавьте скоуп или используйте ключ с правами на инфраструктуру |
| 403 | `INFRA_FORBIDDEN_FOR_COWORK_KEY` | Вызов сделан ключом Cowork/Code — такой ключ работает только с данными, изменяющие операции ему закрыты. Что делать — [Проектный ключ для деплоя](/docs/cowork/deploy-key) |
| 404 | `NO_CREDENTIALS` | Провайдер `provider` не сконфигурирован на платформе |
| 409 | `GALAXY_FULL` | Портал размещает приложения в галактиках, и места под ещё одно приложение нет. Прежде чем отказать, платформа пробует разместить приложение на другой галактике, поэтому этот ответ означает, что места действительно нет. Удалите ненужное galaxy-приложение либо запросите отдельную машину — поле `placement` со значением `dedicated` |
| 413 | `INLINE_SOURCE_TOO_LARGE` | Тело со встроенным `source.content` больше 96 МБ, то есть около 72 МБ самого архива. Решение принимается по заголовку `Content-Length` до чтения тела, поэтому отвергнутые байты никуда не отправляются. Отказ детерминированный — то же тело повторной отправкой не пройдёт. В `error.hint` приходят четыре строки — `reason`, `recovery`, `recoveryAction` и `note`: рецепт здесь — создать сервер без `source`, сохранить архив версией на полученном идентификаторе и развернуть её по `{"source": {"versionId": "vN"}}`, потому что все адреса версионного пути строятся по идентификатору сервера, которого до создания ещё нет |
| 429 | `RATE_LIMITED` | Превышен общий лимит запросов платформы |
| 429 | `DEPLOY_BACKEND_BUSY` | Запрос нёс архив в `source.content`, а на бэкенде уже идёт предельное число таких же тяжёлых запросов (общий счётчик с [`POST /:id/deploy`](/docs/infra/deploy/deploy) и [`POST /:id/upload`](/docs/infra/deploy/upload)). В ответе заголовок `Retry-After: 30` — повторите через полминуты. Чтобы не зависеть от очереди, создайте сервер без `source`, а код загрузите отдельным запросом со ссылкой `{source: {url: ...}}` — у этого пути ограничения на одновременность нет |
| 502 | `PROVIDER_ERROR` | Облачный провайдер вернул ошибку при создании виртуальной машины. Запись сервера помечается как удалённая, квота не расходуется — можно сразу повторить попытку. Поле `message` содержит локализованное описание сбоя и код инцидента — сырой ответ провайдера в тело не попадает, он остаётся в логах платформы. Назовите код инцидента поддержке |
| 503 | `POOL_EXHAUSTED` | Сервис временно перегружен — исчерпан пул соединений с базой данных. В ответе `retryAfter` и заголовок `Retry-After`, повторите через несколько секунд |

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

### Подсказка `error.hint`

На портале с размещением в галактике ошибки `INVALID_REQUEST` и `RUNTIME_PARAM_REMOVED` дополняются объектом `error.hint`. Он называет причину отказа и даёт готовое тело запроса. Условие у двух кодов разное. У `INVALID_REQUEST` подсказка приходит, когда в теле нет ни `source`, ни полной тройки `provider` + `plan` + `region`. У `RUNTIME_PARAM_REMOVED` — когда передан `runtime` без `source`, причём тройка при этом может быть заполнена полностью. На портале с отдельными виртуальными машинами подсказки нет ни в одном из случаев.

Не путайте с `data.hint` из успешного ответа: там строка про следующий шаг после создания приложения по сценарию 2, здесь — объект внутри `error`.

| Поле | Тип | Описание |
|------|-----|----------|
| `error.hint.reason` | string | Почему запрос отклонён |
| `error.hint.recovery` | string | Что изменить в запросе, чтобы он прошёл |
| `error.hint.example` | object | Готовое тело запроса, которое можно взять за основу |

```json
{
  "success": false,
  "error": {
    "code": "INVALID_REQUEST",
    "message": "provider: Required; plan: Required",
    "hint": {
      "reason": "This portal places new apps on shared galaxy hosts, and the request lacked `source` and the full provider/plan/region tuple.",
      "recovery": "RECOMMENDED: create-and-deploy in ONE call — POST /v1/infra/servers { name, source: { content }, runtime, start }; OMIT provider/plan/region. Two-step also works: pass provider/plan/region (informational for a galaxy — values from GET /v1/infra/providers catalogs) to create an empty slot, then POST /v1/infra/servers/:id/deploy with the source. See GET /v1/me -> deployment.galaxyApp.checklist. For a deliberate dedicated standalone VM pass placement: \"dedicated\" together with provider/plan/region.",
      "example": {
        "name": "<slug>",
        "source": { "content": "<base64 gzip-tar of the app>" },
        "runtime": "node20",
        "start": "node server.js",
        "port": 3000
      }
    }
  }
}
```

Если в теле передан `placement: "dedicated"`, подсказка другая: она предлагает добавить `provider`, `plan` и `region`, сохранив выделенный сервер, а не переводить приложение в контейнер галактики.

## Переиспользование сервера приложения

Ключ вызова принадлежит приложению из раздела «Приложения», и приложению можно привязать ровно один сервер. Если сервер уже привязан и работает, `POST /v1/infra/servers` **не создаёт второй**, а возвращает `201` с уже существующим сервером. Имя, которое вы передали в `name`, в этом решении не участвует — повтор платформа определяет по вызывающему ключу, а не по имени.

**В reuse-ответе (`reused: true`) все поля `data` описывают существующий привязанный сервер, а не ваш запрос** — `status`, `provider`, `plan`, `region`, `image`, `createdVia`, `createdAt` относятся к нему. Статус может быть любым, поэтому «дождаться `running`» **не является** проверкой того, что сервер ваш. Проверка — это `reused` и пара `name` ⊕ `requestedName`.

Ответ на такой запрос всегда несёт `data.reused: true` и рядом с `data` — массив `warnings` минимум с одной записью. Сравните `data.name` с `data.requestedName`: они различаются, если сервер приложения был создан раньше и под другим именем.

| Поле | Тип | Когда |
|------|-----|-------|
| `data.reused` | boolean | Всегда на таком ответе. Новый сервер не создан |
| `data.reusedReason` | string | `APPLICATION_ALREADY_HAS_SERVER` |
| `data.requestedName` | string | Эхо вашего `name` — всегда, даже если совпадает с именем существующего сервера |
| `data.sourceIgnored` | boolean | Переданный `source` **не** развёрнут и отброшен |
| `data.deploying` | boolean | Переданный `source` собирается на контейнер приложения прямо сейчас |
| `data.metaIgnored` | boolean | Переданные `displayName`/`description` **не** применены |
| `warnings` | array | Рядом с `data`, не внутри. Минимум одна запись |

**Что делать перед деплоем.** Запросите [`GET /v1/infra/servers/:id`](./get.md) и посмотрите, что на сервере уже развёрнуто. Деплой **заменяет** работающий код, он не добавляет к нему. Если это не тот сервер, который вы имели в виду, остановитесь и спросите пользователя, с каким приложением должен работать этот ключ.

**Про переданный `source`.** Одношаговое создание разворачивает исходники в переиспользуемое galaxy-приложение только тогда, когда контейнера ещё нет (ни разу не деплоилось, либо прошлая сборка упала) — в ответе будет `deploying: true`, опрашивайте `GET /v1/infra/servers/:id` до `running`. Во всех остальных случаях, включая переиспользованную отдельную виртуальную машину, архив **отбрасывается** и в ответе появляется `sourceIgnored: true` — разверните его явно через [`POST /v1/infra/servers/:id/deploy`](/docs/infra/deploy/deploy), когда убедитесь, что сервер тот.

**Про переданные `displayName` и `description`.** Переиспользование никогда не переименовывает сервер, который отдаёт: у него остаются собственные имя и описание, а переданные вами значения отбрасываются — в ответе это видно по `data.metaIgnored: true` и отдельному предупреждению. Переименуйте осознанно через [`PATCH /v1/infra/servers/:id`](./update.md), убедившись, что сервер тот.

**Про `data.next`.** В reuse-ответе поле приходит **только** тогда, когда на возвращённом сервере нечего перезаписывать — это galaxy-приложение без контейнера, прикреплённое к своему galaxy-хосту. Если контейнера нет, но приложение откреплено от хоста, деплоить некуда — `next` тоже не придёт. Если сервер может выполнять код, `next` в ответе **отсутствует** намеренно — отсутствие рекомендации и есть сигнал: сначала разберитесь, тот ли это сервер. Не трактуйте отсутствие `next` как ошибку.

**Как получить для приложения другой сервер.** Отдельного вызова, меняющего привязку, нет. Если нужна отдельная виртуальная машина и привязку трогать не надо — создайте её с `placement: "dedicated"`: переиспользование не сработает, но и к приложению новый сервер привязан не будет. Если нужно именно сменить привязанный сервер — удалите текущий через [`DELETE /v1/infra/servers/:id`](./delete.md), и следующий `POST /v1/infra/servers` тем же ключом снимет устаревшую привязку и привяжет вновь созданный сервер. Удаление необратимо и уничтожает всё, что развёрнуто на сервере, — сначала убедитесь по [`GET /v1/infra/servers/:id`](./get.md), что это не чужая работа.

Создание с `placement: "dedicated"` переиспользование не затрагивает.

## Идемпотентность

Передайте необязательный заголовок `Idempotency-Key`, чтобы безопасно повторять создание **отдельного** сервера (standalone). Если ответ на первый запрос потерялся (обрыв сети, таймаут балансировщика), повтор с тем же ключом не создаст второй сервер — вернётся тот же самый сервер, что и в первый раз, со статусом `201` и заголовком ответа `Idempotent-Replayed: true`.

- Ключ — строка 1–255 символов из набора `[A-Za-z0-9_.:-]`. Область действия — ваш API-ключ.
- **При повторе одноразовые SSH-данные не выдаются повторно.** В теле ответа `ssh.privateKey` и `ssh.password` будут `null`, а поле `note` пояснит это. Сохраняйте данные из ответа первого создания.
- **Заголовок работает только для standalone-серверов.** На порталах с размещением в галактике корректный ключ игнорируется без ошибки, и защита от повторного создания на этот путь **не распространяется**.
- Заголовок несовместим с `graduateFrom` для выделенного сервера — вернётся `400 IDEMPOTENCY_UNSUPPORTED_WITH_GRADUATION`.
- **Ключ не защищает от повторной сборки на переиспользованном сервере.** Ответ с `reused: true` ключом не помечается, поэтому повтор запроса с `source` на переиспользованное galaxy-приложение, у которого ещё нет контейнера, запустит сборку заново. Сначала опросите [`GET /v1/infra/servers/:id`](./get.md).

| Код | `code` | Когда |
|-----|--------|-------|
| 400 | `INVALID_IDEMPOTENCY_KEY` | Ключ не проходит валидацию (длина или недопустимые символы) |
| 400 | `IDEMPOTENCY_UNSUPPORTED_WITH_GRADUATION` | Ключ вместе с `graduateFrom` для выделенного сервера |
| 409 | `IDEMPOTENCY_KEY_ALREADY_USED` | Ключ уже использован для сервера, который затем был удалён — возьмите новый ключ |
| 409 | `IDEMPOTENCY_CONCURRENT_RETRY` | Параллельный запрос с тем же ключом ещё выполняется — повторите чуть позже |

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

- **Примеры корректных и некорректных имён** по паттерну `^[a-z][a-z0-9-]*$`:
  - Правильно: `my-app`, `bot-1`, `crm-dashboard`.
  - Неправильно: `My-App` (заглавная), `bot_1` (подчёркивание), `my.app` (точка), `кириллица`.
- **`sshPublicKey` vs автогенерация.** Если передаёте свой публичный ключ — платформа не генерирует приватный, и `ssh.privateKey` в ответе будет `null`. Пароль в `ssh.password` всё равно возвращается.
- **Когда `region` в ответе отличается от запрошенного.** В запрошенной зоне могли кончиться IP-адреса (`Address space exhausted`) — тогда платформа автоматически пробует следующие зоны в порядке из [`GET /v1/infra/providers/:providerId/regions`](../providers/regions.md), и фиксирует это в аудит-логе событием `SERVER_ZONE_FALLBACK`.
- **Таймаут провижининга — 15 минут.** Если `status` держится в `provisioning` дольше, платформа переводит сервер в `error` и заполняет `provisionError` и `provisionErrorCode`. Дальше остаётся вызвать [`DELETE`](./delete.md) и создать новый.
- **Новая машина засыпает через 60 минут простоя.** Таймаут ставится при создании, задавать его отдельно не нужно. Собственные исходящие запросы приложения простой не прерывают, поэтому приложение с постоянным опросом внешнего API через час останавливается вместе с машиной. Текущее значение приходит в [`GET /v1/infra/servers/:id`](./get.md). Что считается активностью, допустимые значения таймаута и отключение авто-сна значением `null` — [Настроить авто-сон](/docs/infra/lifecycle/sleep).
- **Рантайм устанавливается на этапе деплоя.** Параметр `runtime` без `source` в `POST /v1/infra/servers` не принимается (вернёт `400 RUNTIME_PARAM_REMOVED`). Указывайте `runtime` в теле [`POST /:id/deploy`](/docs/infra/deploy/deploy) — он установится между извлечением архива и запуском приложения. Вместе с `source` рантайм принимается и обязателен: это одношаговое создание galaxy-приложения, где `runtime` и `start` идут в том же запросе.
- **Кириллица в `displayName` и `description` из Windows PowerShell.** Отправленная без явной сериализации в UTF-8, она сохраняется знаками вопроса (`?`): байты теряются на стороне клиента, до отправки запроса. Готовый вызов с `UTF8.GetBytes` — [Windows / PowerShell и UTF-8](/docs/infra#windows-powershell-и-utf-8).

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

- [Провайдеры и каталоги](/docs/infra/providers)
- [Получить сервер](./get.md)
- [Список серверов](./list.md)
- [Обновить имя и описание](./update.md)
- [Удалить сервер](./delete.md)
- [Настроить авто-сон](/docs/infra/lifecycle/sleep)
- [Deploy API](/docs/infra/deploy)
- [Полный деплой приложения](/docs/infra/deploy/deploy)
- [Список рантаймов](/docs/infra/deploy/runtimes)
- [Тариф и доступ](/docs/infra#тариф-и-доступ)
- [Режим доступа](/docs/keys-auth/access-mode)
