
# Infrastructure

Create and manage virtual servers for deploying Bitrix24 applications. Each server is invisible from the internet by default (Black Hole mode) — the application is reachable only through the HTTPS subdomain `app-{id}.vibecode.bitrix24.com`. Server management and deployment go through the REST API without SSH.

**Scope:** `vibe:infra` · **Base URL:** `https://vibecode.bitrix24.com/v1` · **Authorization:** `X-Api-Key` header

## Documentation sections

- [Providers and catalogs](/docs/infra/providers) — list of providers, plans, regions, and OS images (4 endpoints).
- [How much infrastructure costs](/docs/infra/pricing) — what a server costs in Vibe credits, how to stop charges and where to see spending.
- [Servers](/docs/infra/servers) — create, list, get details, update name and description, delete, server activity feed (8 endpoints).
- [Lifecycle](/docs/infra/lifecycle) — start, stop, sleep, wake, run mode, tunnel repair, provisioning status (10 endpoints).
- [Scheduled wake](/docs/infra/wake-schedules) — recurring wake windows for a sleeping server on a cron schedule (4 endpoints).
- [Work schedule library](/docs/infra/work-schedules) — schedules for servers in the schedule run mode: presets and custom ones, shared across the Bitrix24 account, and the schedule marked as the default for new machines (6 endpoints).
- [Access and modes](/docs/infra/access) — access policy, user/department list, SSH credentials, BLACKHOLE↔OPEN mode (7 endpoints).
- [Custom domain](/docs/infra/domain) — attaching your own domain to an application on a server: DNS records, verification, detaching (4 endpoints).
- [Deploy API](/docs/infra/deploy) — run commands, upload files, logs, deploy the application, operation outcome and operation list, operation outcome from the server feed, port, metrics, lock, runtimes (11 endpoints).
- [Access tokens](/docs/infra/access-tokens) — short-lived tokens for e2e checks and shareable links (4 endpoints; the section is enabled on the platform side).
- [What the app receives](/docs/infra/app-runtime) — Gateway injection of `X-Vibe-Authorization: Bearer`, reading identity via `/v1/me`, handler skeletons in Node/Python/Go.
- [Moving an app to your own server](/docs/infra/move-to-own-server) — what to change in the code and the address when moving from the dashboard to your own server, and how to move back.
- [Bitrix24 event subscriptions](/docs/infra/event-subscriptions) — delivery of Bitrix24 events (`ONTASKADD` and similar) to the app through the tunnel, without polling (3 endpoints).
- [Activity and automation rule callback delivery](/docs/infra/bizproc-callbacks) — a business process activity or automation rule handler on a Black Hole subdomain: queued calls, sleeping server wake-up, registration with a single request.
- [Galaxy app](/docs/infra/galaxy) — the "container in a shared galaxy" placement model: how to tell it from a regular server (`kind`), the build-on-upload lifecycle, per-galaxy pricing.
- [Galaxy app sleep and wake](/docs/infra/galaxy-sleep) — what stops while the app sleeps, which fields report the real state and how to bring the app up.
- [Application database](/docs/infra/database) — the SQLite database of a Galaxy application: schema, rows, SQL queries, database backups and restoring from a backup (13 endpoints).
- [Server access recovery](/docs/infra/server-access-recovery) — the server is running but a new key cannot see it: an empty list, `404 NOT_FOUND`, changing the managing key.

## What to know up front

1. **The application port is always 3000.** The Black Hole tunnel proxies exactly this port; you don't need to change it. The server is an isolated environment: `:3000` inside the virtual machine has no relation to the ports on your local machine.
2. **Deploy API is BLACKHOLE-only.** All of `/deploy`, `/exec`, `/upload`, `/logs` require servers in `BLACKHOLE` mode with the tunnel agent in the `CONNECTED` status. For OPEN servers they return an error.
3. **`/deploy` and `/exec` return JSON by default.** This is safe for AI agents and MCP clients — no extra query parameters are needed. If you genuinely need a streaming response (live deploy-step output in the UI), pass `?stream=true` — then you get SSE (Server-Sent Events). The documentation used to claim the opposite (SSE by default, `?stream=false` for JSON) — that is outdated and no longer matches the API behavior.
4. **`accessPolicy` is a security setting.** Changing the policy from `OWNER_ONLY` to `PORTAL`/`AUTHENTICATED`/`PUBLIC` opens the application to other users. **Never change `accessPolicy` without explicit user confirmation.**
5. **The server is a clean Ubuntu 24.04 with root access enabled.** The virtual machine is created from a stock Ubuntu image with no preinstalled software (except the tunnel agent). The agent runs as the `root` user — no `sudo` is needed in `preStart`, `install`, or `/exec` commands. Outbound internet is unrestricted: `apt-get`, `curl`, `wget`, `pip` work directly. Inbound traffic is blocked except the tunnel connection. **The app itself, however, does not run as `root` — it runs under a dedicated unprivileged account.** This applies to the app process only. Deploy commands and `/exec` still run with administrator privileges. Details and how to opt out — [Deploying an app](/docs/infra/deploy/deploy).
6. **The Bitrix24 plan plays a dual role.** First, the Bitrix24 REST API itself is available only on commercial Bitrix24 account plans — without one, none of the following work: applications, the `/v1/deals` proxy, bots, or any other call proxied into Bitrix24. Second, on top of that, creating servers and deploying require a qualifying Bitrix24 plan: a commercial plan grants full access, a trial plan grants limited access, and on a free plan the request is declined with `INT_TARIFF_REQUIRED`. On `.com` full platform access is sold as a plan of the **Vibe+** line, and the refusal text names it — read `error.details.requiredTariffs` for the plans that clear the refusal today. This is an AI Beta; terms may change at any time. When access ends, the platform can put the account's servers to sleep and block waking them. AI Router works independently of the Bitrix24 plan — it does not proxy into REST and is available even on free plans (BYOK is free; platform models are billed against the Vibecode balance). Details are in the "Plan and access" section below.
7. **User authorization inside the application.** On every request **through the Gateway** — that is, when the application is opened in a browser — the Gateway injects six headers prefixed with `X-Vibe-`: `Request-Id` always, plus `User-Id`, `User-Name`, `User-Role`, `Portal-Id`, `Authorization` (`Bearer vibe_session_<…>`) for an authenticated request. The browser never sees or stores the `Authorization` token — it lives only between the Gateway and the app server. For a quick user identifier the `X-Vibe-User-Id` header is enough; the full context (scopes, `capabilities`, plan, application info) comes from a single `GET /v1/me` call with server-side caching. The user ID in the `/v1/me` response is `data.currentUser.bitrixUserId`; the Bitrix24 account domain is `data.portal`. The [application external API](/docs/applications/external-api) takes a different path and carries no identity headers. The full header table, the BFF pattern, and handler skeletons in Node/Python/Go — [What the app receives](/docs/infra/app-runtime).
8. **A new machine goes to sleep after 60 minutes of inactivity.** A separate virtual machine is created with a 60-minute idle timeout. The exception is a Bitrix24 account where an administrator has marked a schedule for new machines: the machine is created with that schedule and runs during its windows, see [Set the default schedule](/docs/infra/work-schedules/set-default). Idleness is measured by inbound requests **to** the application — requests to its HTTPS subdomain. Requests the application sends outbound do not reset the timer, so an application built around continuously polling an external API stops together with its machine after an hour. The allowed timeout values and how to disable auto-sleep with `null` — [Configure auto-sleep](/docs/infra/lifecycle/sleep).
9. **Creating servers requires a user session for `vibe_app_` keys.** `POST /v1/infra/servers` is gated by a plan check that needs to know exactly who is creating the server. For `vibe_api_` the user context already lives in the key itself; for `vibe_app_` an `Authorization: Bearer <session>` header is required — without it the response is `401 UNAUTHENTICATED` with an `error.hint` pointing at the OAuth flow. Reads and the Deploy API on already-existing servers don't require a session — the "Endpoint authorization" table below collects all the rules in one place.
10. **Something broke — start with the server activity feed.** [`GET /v1/infra/servers/:id/activity`](/docs/infra/servers/activity) shows undelivered Bitrix24 account events, bot delivery problems, failed deploys, wake-ups and status changes over the last 30 days.

## Quick start

Three calls — create a server and run the application.

### curl — personal key

```bash
export VIBE_KEY="YOUR_API_KEY"

# 1. Create a server (automatically in Black Hole mode)
curl -X POST https://vibecode.bitrix24.com/v1/infra/servers \
  -H "X-Api-Key: $VIBE_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "bitrix-cloud",
    "name": "my-app",
    "plan": "bc-small",
    "region": "bc-eu-central",
    "image": "ubuntu-2404-lts"
  }'

# 2. Separate virtual machine — wait until ready:
#    status=running AND blackholeStatus=CONNECTED.
#    A Galaxy app (kind=GALAXY_APP in the step 1 response) never reaches
#    that state — go straight to step 3, see the note below.
curl -H "X-Api-Key: $VIBE_KEY" \
  https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID

# 3. Deploy the application (JSON is the default).
#    The example below is for a separate virtual machine: the code comes
#    from an external URL. A Galaxy app accepts an inline archive only —
#    "source": { "content": "<base64>" }, see the note below.
#    X-Skip-Source-Snapshot: deploy from an external URL while source
#    storage is enabled, otherwise 409 SNAPSHOT_REQUIRED (see below).
curl -X POST "https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/deploy" \
  -H "X-Api-Key: $VIBE_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Skip-Source-Snapshot: deploy from external URL" \
  -d '{
    "source": { "url": "https://github.com/user/app/archive/main.tar.gz" },
    "runtime": "node24",
    "install": "cd /opt/app && npm install --production",
    "start": "cd /opt/app && node server.js",
    "port": 3000
  }'
```

The application is reachable at `https://app-{id}.vibecode.bitrix24.com` — the `appUrl` field in the `/deploy` response.

**Deploying from an external URL and source storage.** When [source storage](/docs/source-storage) is enabled for your Bitrix24 account, a deploy from an external address — not from Vibecode storage — returns `409 SNAPSHOT_REQUIRED` so that the application's version history is not lost. The `X-Skip-Source-Snapshot: <reason>` header lets the deploy proceed without saving a snapshot. To keep a snapshot, first upload the archive via `POST /v1/apps/:id/sources`, then deploy it via `{ "source": { "versionId": "vN" } }`. More details — [Source storage](/docs/source-storage).

### curl — OAuth application

```bash
# Same thing, just add the Authorization: Bearer header with the session token
curl -X POST https://vibecode.bitrix24.com/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-app", "plan": "bc-small", "region": "bc-eu-central", "image": "ubuntu-2404-lts" }'
```

> **Galaxy app — a different contract.** If your Bitrix24 account places apps in galaxies, the same `POST /v1/infra/servers` creates a Galaxy app — a container on a shared host. In the create response its `kind` is `GALAXY_APP` and `createdVia` is `galaxy`. Such an app never reaches `blackholeStatus: "CONNECTED"`: its container is built by uploading the code, so step 2 does not apply to it and the code is uploaded **right after creation**. For such an app the code source is an inline base64 archive in the `source.content` field. `source.url` is available where the platform has enabled link deploys for you (otherwise `400 GALAXY_DEPLOY_CONTENT_ONLY`) and may point only at the platform [source storage](/docs/source-storage) (otherwise `400 GALAXY_SOURCE_URL_NOT_ALLOWED`). `source.versionId` is not accepted at creation — deploy a saved version as a second step, via `POST /v1/infra/servers/:id/deploy`. The `runtime` and `start` fields are required. If you need a separate virtual machine, pass the `placement` field set to `dedicated` in the create body. The full model, lifecycle, cost, and deploy differences — [Galaxy app](./infra/galaxy).

## Full example

A realistic JavaScript scenario — create a server, wait until ready, deploy, get the application URL.

```javascript
const VIBE_KEY = process.env.VIBE_KEY
const BASE = 'https://vibecode.bitrix24.com/v1'

async function api(method, path, body = null, extraHeaders = {}) {
  const opts = { method, headers: { 'X-Api-Key': VIBE_KEY, ...extraHeaders } }
  if (body) {
    opts.headers['Content-Type'] = 'application/json'
    opts.body = JSON.stringify(body)
  }
  const res = await fetch(`${BASE}${path}`, opts)
  if (!res.ok) throw new Error(`${method} ${path} → ${res.status}`)
  return res.json()
}

// 1. Pick provider, plan, region, image
const { data: plans } = await api('GET', '/infra/providers/bitrix-cloud/plans')
const { data: regions } = await api('GET', '/infra/providers/bitrix-cloud/regions')
const { data: images } = await api('GET', '/infra/providers/bitrix-cloud/images')

const plan = plans.find(p => p.id === 'bc-small')
const region = regions.find(r => r.id === 'bc-eu-central')
const image = images[0]

// 2. Create a server (always in Black Hole)
const { data: server } = await api('POST', '/infra/servers', {
  provider: 'bitrix-cloud',
  name: 'my-crm-bot',
  plan: plan.id,
  region: region.id,
  image: image.id,
})
console.log(`Server created: ${server.id}, subdomain: ${server.subdomain}`)

// 3. Separate virtual machine: wait for running and CONNECTED.
//    A Galaxy app (kind === 'GALAXY_APP') never reaches that state —
//    the step is skipped for it, the code is uploaded right away.
let info = server
if (server.kind === 'STANDALONE') {
  while (info.status !== 'running' || info.blackholeStatus !== 'CONNECTED') {
    await new Promise(r => setTimeout(r, 10000)) // 10 seconds between polls
    const res = await api('GET', `/infra/servers/${server.id}`)
    info = res.data
    console.log(`status=${info.status}, blackhole=${info.blackholeStatus}`)
  }
}

// 4. Deploy the application (JSON is the default).
//    The code source below is an external URL — that is the variant for a
//    separate virtual machine. For a Galaxy app (kind === 'GALAXY_APP') the
//    only source is inline: source: { content: '<base64>' }.
//    The X-Skip-Source-Snapshot header is needed when deploying from an
//    external URL while source storage is enabled — otherwise 409 SNAPSHOT_REQUIRED.
const deploy = await api('POST', `/infra/servers/${server.id}/deploy`, {
  source: { url: 'https://github.com/user/app/archive/main.tar.gz' },
  runtime: 'node24',
  install: 'cd /opt/app && npm install --production',
  preStart: 'cd /opt/app && npx prisma migrate deploy',
  start: 'cd /opt/app && node server.js',
  port: 3000,
  env: { NODE_ENV: 'production' },
}, { 'X-Skip-Source-Snapshot': 'deploy from external URL' })

console.log(`Application is live: ${deploy.data.appUrl}`)

// 5. Change the idle timeout if the default 60 minutes does not fit.
//    Allowed values are 15, 30, 60, 240 and null, which disables
//    auto-sleep. An application that polls continuously needs null:
//    outbound requests do not reset the idle timer.
await api('PATCH', `/infra/servers/${server.id}/sleep`, { sleepAfterMinutes: 240 })
```

## Endpoint reference

Endpoint reference for the section. Links lead to pages with parameters, examples, and error codes.

**Providers and catalogs:**

| Method | Path | Description |
|-------|------|----------|
| GET | [`/v1/infra/providers`](/docs/infra/providers/list) | List of cloud providers |
| GET | [`/v1/infra/providers/:providerId/plans`](/docs/infra/providers/plans) | Provider plans |
| GET | [`/v1/infra/providers/:providerId/regions`](/docs/infra/providers/regions) | Provider regions |
| GET | [`/v1/infra/providers/:providerId/images`](/docs/infra/providers/images) | OS images |

**Servers:**

| Method | Path | Description |
|-------|------|----------|
| POST | [`/v1/infra/servers`](/docs/infra/servers/create) | Create a server (always Black Hole) |
| GET | [`/v1/infra/servers`](/docs/infra/servers/list) | List of your servers |
| GET | [`/v1/infra/servers/:id`](/docs/infra/servers/get) | Server details |
| GET | [`/v1/infra/servers/:id/activity`](/docs/infra/servers/activity) | Server activity feed |
| PATCH | [`/v1/infra/servers/:id`](/docs/infra/servers/update) | Update the name and description |
| DELETE | [`/v1/infra/servers/:id`](/docs/infra/servers/delete) | Delete a server |

**Lifecycle:**

| Method | Path | Description |
|-------|------|----------|
| POST | [`/v1/infra/servers/:id/start`](/docs/infra/lifecycle/start) | Start a stopped/sleeping server |
| POST | [`/v1/infra/servers/:id/stop`](/docs/infra/lifecycle/stop) | Stop a running server |
| POST | [`/v1/infra/servers/:id/reboot`](/docs/infra/lifecycle/reboot) | Reboot the server |
| POST | [`/v1/infra/servers/:id/wake`](/docs/infra/lifecycle/wake) | Wake a sleeping server (async or blocking) |
| POST | [`/v1/infra/servers/:id/sleep-now`](/docs/infra/lifecycle/sleep-now) | Immediately put a BLACKHOLE server to sleep |
| PATCH | [`/v1/infra/servers/:id/sleep`](/docs/infra/lifecycle/sleep) | Configure auto-sleep |
| PATCH | [`/v1/infra/servers/:id/run-mode`](/docs/infra/lifecycle/run-mode) | Set the run mode: sleeps when idle, runs on a schedule or runs around the clock |
| POST | [`/v1/infra/servers/:id/refresh`](/docs/infra/lifecycle/refresh) | Request status and IP from the provider |
| POST | [`/v1/infra/servers/:id/repair`](/docs/infra/lifecycle/repair) | Restore the tunnel via serial console |
| GET | [`/v1/infra/servers/:id/repair-status`](/docs/infra/lifecycle/repair-status) | Tunnel restore progress |

**Scheduled wake:**

| Method | Path | Description |
|-------|------|----------|
| GET | [`/v1/infra/servers/:id/wake-schedules`](/docs/infra/wake-schedules/list) | List wake windows and their firing history |
| POST | [`/v1/infra/servers/:id/wake-schedules`](/docs/infra/wake-schedules/create) | Create a wake window |
| PATCH | [`/v1/infra/servers/:id/wake-schedules/:scheduleId`](/docs/infra/wake-schedules/update) | Update a wake window |
| DELETE | [`/v1/infra/servers/:id/wake-schedules/:scheduleId`](/docs/infra/wake-schedules/delete) | Delete a wake window |

**Custom domain:**

| Method | Path | Description |
|--------|------|-------------|
| POST | [`/v1/infra/servers/:id/domain`](/docs/infra/domain/attach) | Attach a custom domain |
| GET | [`/v1/infra/servers/:id/domain`](/docs/infra/domain/get) | Get the domain and its DNS records |
| POST | [`/v1/infra/servers/:id/domain/verify`](/docs/infra/domain/verify) | Verify the DNS records and activate the domain |
| DELETE | [`/v1/infra/servers/:id/domain`](/docs/infra/domain/detach) | Detach the domain |

**Work schedule library:**

| Method | Path | Description |
|-------|------|----------|
| GET | [`/v1/work-schedules`](/docs/infra/work-schedules/list) | List the schedules of the Bitrix24 account |
| GET | [`/v1/work-schedules/:id`](/docs/infra/work-schedules/get) | Get a schedule |
| POST | [`/v1/work-schedules`](/docs/infra/work-schedules/create) | Create a custom schedule |
| PATCH | [`/v1/work-schedules/:id`](/docs/infra/work-schedules/update) | Update a schedule |
| DELETE | [`/v1/work-schedules/:id`](/docs/infra/work-schedules/delete) | Delete a schedule |
| PUT | [`/v1/work-schedules/:id/default`](/docs/infra/work-schedules/set-default) | Mark the default schedule for new machines |

**Access and modes:**

| Method | Path | Description |
|-------|------|----------|
| GET | [`/v1/infra/servers/:id/ssh`](/docs/infra/access/ssh) | SSH credentials (OPEN only) |
| PATCH | [`/v1/infra/servers/:id/mode`](/docs/infra/access/mode) | Return a server to BLACKHOLE. Switching to OPEN is done in the Vibecode dashboard |
| PATCH | [`/v1/infra/servers/:id/access-policy`](/docs/infra/access/access-policy) | Application access policy |
| GET | [`/v1/infra/servers/:id/access`](/docs/infra/access/access-list) | List of users and departments with access |
| POST | [`/v1/infra/servers/:id/access`](/docs/infra/access/access-add) | Add a user or department |
| DELETE | [`/v1/infra/servers/:id/access/:accessId`](/docs/infra/access/access-delete) | Remove an access entry |
| GET | [`/v1/infra/servers/:id/b24-users`](/docs/infra/access/b24-users) | Search Bitrix24 account users |

**Deploy API:**

| Method | Path | Description |
|-------|------|----------|
| POST | [`/v1/infra/servers/:id/exec`](/docs/infra/deploy/exec) | Run a command (SSE or JSON) |
| POST | [`/v1/infra/servers/:id/upload`](/docs/infra/deploy/upload) | Upload a file (base64 or by URL) |
| GET | [`/v1/infra/servers/:id/logs`](/docs/infra/deploy/logs) | Service logs (`journalctl` utility) |
| POST | [`/v1/infra/servers/:id/deploy`](/docs/infra/deploy/deploy) | Full application deploy |
| GET | [`/v1/infra/operations/:operationId`](/docs/infra/deploy/operation-status) | Deploy or command outcome by operation ID |
| GET | [`/v1/infra/servers/:id/operations`](/docs/infra/deploy/operations) | Recent deploy and command operations started by the current key |
| GET | [`/v1/infra/servers/:id/operations/:operationId`](/docs/infra/deploy/server-operation) | Outcome of an operation from the server feed, by the "server, identifier" pair |
| PATCH | [`/v1/infra/servers/:id/port`](/docs/infra/deploy/port) | Set the application port |
| GET | [`/v1/infra/servers/:id/metrics`](/docs/infra/deploy/metrics) | Tunnel activity metrics |
| DELETE | [`/v1/infra/servers/:id/lock`](/docs/infra/deploy/lock) | Release a stuck operation lock |
| GET | [`/v1/infra/runtimes`](/docs/infra/deploy/runtimes) | List of available runtimes |

**Access tokens:**

| Method | Path | Description |
|-------|------|----------|
| POST | [`/v1/infra/servers/:id/access-tokens`](/docs/infra/access-tokens/create) | Issue an access token (`api-bearer` or `share-url`) |
| POST | [`/v1/infra/servers/:id/access-tokens/:tokenId/refresh`](/docs/infra/access-tokens/refresh) | Issue a fresh JWT for the same `api-bearer` token |
| GET | [`/v1/infra/servers/:id/access-tokens`](/docs/infra/access-tokens/list) | List of server tokens |
| DELETE | [`/v1/infra/servers/:id/access-tokens/:tokenId`](/docs/infra/access-tokens/delete) | Revoke a token |

**Bitrix24 event subscriptions:**

| Method | Path | Description |
|-------|------|----------|
| POST | [`/v1/infra/servers/:id/event-subscriptions`](/docs/infra/event-subscriptions) | Subscribe the server to a Bitrix24 event (`event.bind` under the OAuth application) |
| GET | [`/v1/infra/servers/:id/event-subscriptions`](/docs/infra/event-subscriptions) | List subscriptions + recent deliveries |
| DELETE | [`/v1/infra/servers/:id/event-subscriptions/:subId`](/docs/infra/event-subscriptions) | Remove a subscription |

## Endpoint authorization

All infra endpoints require the `X-Api-Key` header. For `vibe_app_` keys (bound to an OAuth application) some POST operations additionally require `Authorization: Bearer <session>` — without it the response is `401 UNAUTHENTICATED` with an `error.hint`. For `vibe_api_` keys the user context already lives in the key itself, so a separate session is not needed.

| Endpoint | `X-Api-Key` | `Authorization: Bearer` for `vibe_app_` | When Bearer is required |
|----------|:-----------:|:--------------------------------------:|----------------------|
| `GET /v1/infra/providers/*` | yes | no | — |
| `GET /v1/infra/servers`, `GET /v1/infra/servers/:id` | yes | no | — |
| `GET /v1/infra/servers/:id/logs`, `/metrics`, `/access`, `/b24-users`, `/ssh` | yes | no | — |
| `GET /v1/infra/runtimes` | yes | no | — |
| `GET /v1/infra/servers/:id/activity`, `GET /v1/infra/servers/:id/operations/:operationId` | yes | no | — |
| `POST /v1/infra/servers` (create a server) | yes | **yes** | Plan gate: the platform needs to know exactly who is creating the server. |
| `POST /v1/infra/servers/:id/deploy`, `/exec`, `/upload` | yes | no | The `vibe:infra` scope on the key is enough. |
| `POST /v1/infra/servers/:id/start`, `/stop`, `/reboot`, `/wake`, `/sleep-now`, `/refresh`, `/repair`, `PATCH /sleep` | yes | no | — |
| `PATCH /v1/infra/servers/:id/run-mode`, `POST/PATCH/DELETE /v1/work-schedules` | yes | no | Until run modes are enabled for the Bitrix24 account, the response is `400 RUN_MODE_UNAVAILABLE`. |
| `GET /v1/work-schedules`, `GET /v1/work-schedules/:id` | yes | no | Reading is available even before run modes are enabled. The library is closed to a Cowork key entirely, reads included. |
| `PUT /v1/work-schedules/:id/default` | yes | no | Only a key whose owner is an administrator of the Bitrix24 account: a `vibe_app_` key gets `403 ADMIN_ONLY` even with a session. Until run modes are enabled for the Bitrix24 account, the response is `400 RUN_MODE_UNAVAILABLE`. |
| `GET /v1/infra/servers/:id/wake-schedules` | yes | no | — |
| `POST /v1/infra/servers/:id/wake-schedules`, `PATCH .../wake-schedules/:scheduleId` | yes | no | Requires scheduled wake to be enabled for the Bitrix24 account; otherwise `403 WAKE_SCHEDULE_DISABLED`. If the feature is enabled for the account and the server is a Galaxy app, the response is `403 WAKE_SCHEDULE_GALAXY_DISABLED`: for those apps the feature is enabled separately from standalone servers. |
| `DELETE /v1/infra/servers/:id/wake-schedules/:scheduleId` | yes | no | Deleting a window is not restricted by those conditions — a window can be removed even after scheduled wake has been disabled for the Bitrix24 account or for Galaxy apps. |
| `PATCH /v1/infra/servers/:id/mode`, `/access-policy`, `/port` | yes | no | — |
| `POST /v1/infra/servers/:id/access`, `DELETE /v1/infra/servers/:id/access/:accessId` | yes | no | — |
| `POST/GET /v1/infra/servers/:id/access-tokens`, `DELETE .../:tokenId`, `POST .../:tokenId/refresh` | yes | no | The access-tokens section must be enabled by the platform; otherwise all four endpoints return `503 FEATURE_DISABLED`. The pre-call check is `data.capabilities.servers.preview` in [`GET /v1/me`](/docs/keys-auth/me). |
| `POST/GET/DELETE /v1/infra/servers/:id/event-subscriptions` | yes | no | The server must be bound to an OAuth application with an `application_token`; otherwise `400 NOT_OAUTH_APP`. |
| `PATCH /v1/infra/servers/:id` (name and description) | yes | no | — |
| `DELETE /v1/infra/servers/:id` | yes | no | — |
| `DELETE /v1/infra/servers/:id/lock` | yes | no | — |

Quick check before calling: `GET /v1/me` → `data.capabilities.servers.create.available`. For `vibe_app_` without a session it returns `false` with `reason: "SESSION_REQUIRED"` and a hint in `userMessage` — the model immediately sees that it needs to go through the OAuth flow rather than hitting a `401` on `POST /v1/infra/servers` itself.

## Limits

| Limit | Value |
|-------|----------|
| Servers per API key | 100. Only separate virtual machines created with this key are counted — Galaxy apps and the galaxy host machines themselves are outside the limit. To see your current limit and how much of it is used, call `GET /v1/me`: `data.infra.limits.max` and `data.infra.limits.used` |
| Deploy API operations per minute per API-key-and-server pair | 10 |
| Concurrent `exec`/`deploy` per server | 1 |
| `exec` timeout | 1–600 seconds (default 300) |
| Body size with inline base64 (`upload content`, `deploy source.content`, `source` on server creation) | 96 MB of body, about 72 MB of archive. Over the cap — `413 INLINE_SOURCE_TOO_LARGE` |
| File size via `source.url` / `upload url` | 500 MB |
| Archive size of a saved version (`source.versionId`) | 500 MB |
| Multipart archive size in `deploy` | 500 MB as long as the archive streams into storage. Otherwise about 72 MB — [conditions](/docs/infra/deploy/deploy) |
| `/ssh` request rate | up to 10 per minute |

The rate figures in the table are platform-wide totals. The current limit for your key is returned in the `x-ratelimit-limit` header. The platform rate limit is shared across all V1 endpoints — [see the "Limits and optimization" section](/docs/optimization).

## Server statuses

| Status | Description |
|--------|----------|
| `provisioning` | The virtual machine is being created at the provider (1–3 minutes) |
| `running` | The virtual machine is running, IP assigned. For the tunnel you also need `blackholeStatus: CONNECTED` |
| `sleeping` | Stopped by the sleep timer or manually. Wakes on a `/deploy`/`/start`/`/wake` call, and a request to the HTTPS subdomain wakes it under the [automatic wake conditions](/docs/infra/lifecycle/wake) |
| `error` | The server is not in a working state: the virtual machine was deleted at the provider, the agent hasn't connected for a long time, the `externalId` is missing |
| `deleted` | The server is deleted (soft-delete). Cannot be restored |

The `blackholeStatus` field describes the agent tunnel state independently of `status`:

| Value | Description |
|----------|----------|
| `NONE` | The agent has not connected yet. For a machine created from scratch — until its first connection. The first create response carries `NONE`; on a retry with the same `Idempotency-Key` it is the current value. A pre-provisioned machine may become `CONNECTED` by the very first `GET` |
| `CONNECTED` | The tunnel is active, the Deploy API is available |
| `DISCONNECTED` | The agent was connected, but the connection is now lost — try [`/repair`](/docs/infra/lifecycle/repair) |

The `kind` field distinguishes the placement model: `STANDALONE` — a separate virtual machine, `GALAXY_APP` — a Galaxy app (a container in a galaxy), `GALAXY` — the galaxy itself (the host machine that carries them, created by the platform rather than by the user). A Galaxy app's `blackholeStatus` stays `NONE` until the code is uploaded — it does not connect on its own. More details — [Galaxy app](/docs/infra/galaxy).

The `runtimeStatus` field is deprecated and kept for compatibility. For servers created after 2026-04-25 (when the `runtime` parameter was removed from [`POST /v1/infra/servers`](/docs/infra/servers/create)) it always returns `null`. The runtime is now installed at the [`POST /:id/deploy`](/docs/infra/deploy/deploy) stage, and the readiness signal is the success of the `runtime` step itself in the deploy response.

## Plan and access

Access to Vibecode infrastructure is gated at two levels.

**Level 1 — Bitrix24 REST API.** Bitrix24 itself enables the REST API only on commercial Bitrix24 account plans. This is a Bitrix24 restriction, not a Vibecode one: on free plans Bitrix24 simply does not return REST responses. So without a commercial Bitrix24 plan the following do not work:

- Creating and publishing applications — the app is registered in the Bitrix24 account via REST.
- REST proxy: `/v1/deals`, `/v1/contacts`, `/v1/batch`, `/v1/bots`, `/v1/tasks` and all other entities.
- Bots, chats, tasks — everything proxied into Bitrix24.

**Level 2 — Vibecode infrastructure.** On top of the first condition, creating servers and deploying require a qualifying Bitrix24 plan — a commercial plan grants full access, a trial plan grants limited access, and on a free plan the request is declined with `INT_TARIFF_REQUIRED` (this is an AI Beta; terms may change at any time). On `.com` that refusal names a plan of the **Vibe+** line — full platform access is sold as a Vibe+ plan. For accounts where access is narrowed to the Vibe+ plan line, infrastructure and key issuance require a Vibe+ plan, and an ordinary commercial plan is declined with `INT_VIBE_PLUS_REQUIRED`. The operations gated at this level:

- `POST /v1/infra/servers` — creating a server.
- `POST /v1/infra/servers/:id/deploy` — deploying the application.
- Creating agents and managed bots (they provision servers under the hood).

**Waking a separate virtual machine does not run the plan check at call time.** When access ends, the platform itself puts the account's servers to sleep and blocks waking them — [`POST /v1/infra/servers/:id/wake`](/docs/infra/lifecycle/wake) then returns `403 SERVER_WAKE_BLOCKED`. Accounts narrowed to the Vibe+ plan line are not put to sleep this way. A Vibe+ trial machine is handled separately: once the trial ends, the platform freezes it, and its wake is refused with `403 VIBE_DEMO_EXPIRED` until the account is on a paid Vibe+ plan. A Galaxy app wakes through the galaxy host, and while access is closed the refusal comes with status `402` and the access-gate code `INT_TARIFF_REQUIRED` or `INT_VIBE_PLUS_REQUIRED`.

**What works on any Bitrix24 plan, including free:**

- AI Router — `POST /v1/chat/completions`, `POST /v1/audio/transcriptions`, `GET /v1/models`. It does not proxy into Bitrix24 and goes directly to LLM providers. Free with BYOK keys; platform models are billed against the Vibecode balance.
- Basic platform endpoints: `GET /v1/me`, `GET /v1/feedback`, `GET /v1/guide` — so an AI agent can orient itself.

**Check before calling:** `GET /v1/me` → the `capabilities.servers.create.available` field. If it is `false`, the `capabilities.servers.create.userMessage` field contains a localized explanation for the user.

**Force refresh after a plan upgrade:** `GET /v1/me?refresh=tariff` — skips the cache (one hour by default) and re-requests the plan from Bitrix24.

**Access to servers:** the single signal is `capabilities.servers.create` in the `GET /v1/me` response (see above). If access is denied, `POST /v1/infra/servers` returns `402` with the access-gate code (see "Error codes" below). If the Bitrix24 plan could not be read, the request is refused with `403` instead (see "Error codes" below). Access is governed by your Bitrix24 plan — see Level 2 above.

**Response headers** of the infra endpoints:

| Header | Value |
|-----------|----------|
| `X-Tariff-Checked-At` | ISO timestamp of the last SUCCESSFUL plan reconciliation with Bitrix24, cached for up to 1 hour. A failed attempt does not set the header: its absence means "no reliable reconciliation" |
| `X-Tariff-Is-Commercial` | `"true"` or `"false"` |

The access-gate error codes are listed in the "Error codes" section below.

## Windows / PowerShell and UTF-8

Non-ASCII characters (accented letters such as ö, ü, é, or characters from any non-Latin script) in the server `displayName` and `description` may turn into question marks (`?`) if the request is sent from Windows PowerShell without explicit UTF-8 serialization. This is not a display problem on the platform side — the bytes are lost on the client side, before the HTTP request is even sent.

**Cause.** By default, PowerShell re-encodes the string from the `-Body` parameter of `Invoke-WebRequest` and `Invoke-RestMethod` into the system's legacy (non-UTF-8) code page — for example `windows-1252` on Western installs — and any character outside that page is lost before the request is assembled. The `Content-Type: charset=utf-8` header does not help here — by the time it applies, the original bytes are already lost.

**Solution.** Pass the request body as a UTF-8 byte array.

```powershell
# 1. Console output encoding — does not affect how the body is encoded
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8

# 2. Build the JSON and convert it to a UTF-8 byte array
$body = @{
  displayName = 'Kundenübersicht'
  description = 'The bot sends notifications about deals'
} | ConvertTo-Json -Compress

$bytes = [System.Text.Encoding]::UTF8.GetBytes($body)

# 3. Pass a byte array to -Body, not a string, and set the encoding in Content-Type
Invoke-WebRequest `
  -Uri 'https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID' `
  -Method PATCH `
  -Headers @{
    'X-Api-Key'    = 'YOUR_API_KEY'
    'Content-Type' = 'application/json; charset=utf-8'
  } `
  -Body $bytes
```

**Common mistakes:**

- Saving the `.ps1` with a UTF-8 BOM — older PowerShell versions may fail to parse the script itself.
- Passing the string `$body` to `-Body` instead of the byte array `$bytes` — the string is re-encoded through the system code page.
- Relying only on `Content-Type: application/json; charset=utf-8` without `UTF8.GetBytes` — this header does not restore the lost bytes; it only declares the body encoding to the server.

The same serialization is needed everywhere you pass a display name and a description: [creating a server](/docs/infra/servers/create), [updating the name and description](/docs/infra/servers/update) and [deploying an application](/docs/infra/deploy/deploy) — from there these values go into the application card in the Bitrix24 catalog.

**Node.js (`fetch`) and Python (`requests`)** encode the body in UTF-8 themselves; no extra steps are required. The problem is specific to PowerShell.

## Error codes

### Infrastructure errors

| Code | HTTP | Description |
|-----|------|----------|
| `NOT_FOUND` | 404 | Server not found, or it belongs to another API key while you are not on its development team |
| `SERVER_ROLE_FORBIDDEN` | 403 | You are on this server's development team, but the operation goes beyond your role. `error.hint` carries `yourRole`, `requiredRole`, the denied action and the list of calls open to you in `allowedHere`. Role breakdown — [List servers](/docs/infra/servers/list) |
| `INVALID_REQUEST` | 400 | Validation error (invalid name, plan, region, image) |
| `INFRA_NOT_PERMITTED` | 403 | Infrastructure disabled on the platform or on the portal |
| `SERVER_CREATION_DISABLED` | 403 | Server creation forbidden by portal policy |
| `MAX_SERVERS_REACHED` | 403 | The per-API-key server limit is exceeded |
| `NO_CREDENTIALS` | 404 | The provider is not configured on the platform |
| `SERVER_NOT_READY` | 409 | The server is still being created — the operation is not available yet |
| `CONFLICT` | 409 | The server is in a status from which the action cannot be performed (e.g. `start` on a running one) |
| `PROVIDER_ERROR` | 502 | The cloud provider returned an error |
| `VM_MISSING` | 422 | The record has no `externalId` and the cloud review sees no machine under this server's cloud name — the virtual machine was not created at the provider or was deleted externally. Replace the server: delete it and create a new one. `error.hint` names the endpoint of each step only if that step is available to this request, and for an unavailable step it gives the reason for the refusal and what to change |
| `VM_POINTER_LOST` | 422 | The record has no `externalId`, but the cloud review does see a machine under this server's cloud name: the link is lost while the machine itself is most likely running and billed. Do NOT delete the server and create a new one — that would leave the machine running with no record at all. The server is not moved to `error`; contact support to have the link restored |
| `PORT_RESTRICTED` | 400 | Port 1–1023 (system ports forbidden). Allowed: `0` (auto-detect) and `1024–65535` |
| `BLACKHOLE_ONLY` | 400 | The endpoint works only for BLACKHOLE servers (applies to `/sleep-now`, `/sleep`, `/metrics`) |
| `OPEN_MODE_DISABLED` | 403 | A request to switch to OPEN while the mode is disabled platform-wide |
| `DIRECT_NETWORK_CHANGE_REQUIRES_HUMAN` | 403 | A request to switch to OPEN through the API: a person performs the switch in the dashboard |
| `SAME_MODE` | 400 | The server is already in the requested mode |
| `REPAIR_BLOCKED` | 409 | Tunnel repair is blocked: the server is asleep or in error and has a wake block (`preventWake=true`) |

### Deploy API errors

| Code | HTTP | Description |
|-----|------|----------|
| `SERVER_NOT_READY` | 409 | The server is not ready for the operation: it is not running, the tunnel is not connected, or the server is listed as connected but the Gateway has no live tunnel. The response carries a `hint` field with the reason and the next step. The platform attempts to restore the tunnel itself. If that does not succeed, wake the server or call [`/repair`](/docs/infra/lifecycle/repair) and repeat the request |
| `EXEC_BUSY` | 409 | Another operation is already running on the server. Retry at the `Retry-After` interval; if that does not help, call the guarded [`POST /v1/infra/servers/:id/unstick`](/docs/infra/servers/unstick), which returns `409` for a live operation unless `force` is passed. The unconditional [`/lock`](/docs/infra/deploy/lock) is the last step, and only once the server is independently confirmed idle. In a galaxy this releases the platform lock only: if the server stays busy, the host's shared exec channel is occupied — retry at the `Retry-After` interval, and contact support if the refusals keep coming |
| `COMMAND_TOO_LONG` | 400 | The `/exec` command is longer than 10,000 characters. On a standalone server, send large payloads and scripts via [`/upload`](/docs/infra/deploy/upload). For a Galaxy host, `/upload` returns `GALAXY_HOST_NOT_A_DEPLOY_TARGET`. For a Galaxy app, it returns `GALAXY_APP_USE_GALAXY_ROUTE`. Include app files in the deploy archive through the `source` field of `POST /v1/infra/servers/:id/deploy` |
| `EXEC_TIMEOUT` | 200 | Execution timeout exceeded. The failure arrives in the response body |
| `EXEC_FAILED` | 200 | Command execution error on the agent. The failure arrives in the response body |
| `EXEC_NO_EXIT` | 200 | The `/exec` stream ended without sending an exit status: the command's outcome on the server is unknown. The failure arrives in the response body, the output collected so far in `data`. Standalone virtual machines (`kind: "STANDALONE"`) only |
| `UPLOAD_PATH_DENIED` | 403 | Forbidden upload path |
| `DEPLOY_FAILED` | 200 | One of the deploy steps failed — `error.step` names which one. The failure arrives in the response body |
| `DEPLOY_TIMEOUT` | 200 | The Gateway stopped waiting for a deploy step, so the operation outcome is unknown. Read the operation list and perform the read-only checks from `error.hint` first; do not repeat the deploy blindly |
| `DEPLOY_CONNECTION_TERMINATED` | 200 | The connection to the server dropped mid-deploy, so the operation outcome is unknown. Read the operation list and perform the read-only checks from `error.hint` first; do not repeat the deploy blindly |
| `DEPLOY_TUNNEL_STALE` | 200 | The Gateway lost its live tunnel during the deploy, so the operation outcome is unknown. Read the operation list and perform the read-only checks from `error.hint` first; do not call `/repair` or repeat the deploy blindly |
| `VALIDATION_ERROR` | 400 | Malformed Deploy API request body |

For [`/exec`](/docs/infra/deploy/exec) and [`/deploy`](/docs/infra/deploy/deploy) on a separate virtual machine (`kind: "STANDALONE"`), the connection is held open for the whole run, so the `200` status is sent before the work starts. A failure during execution arrives in the response body — the marker is `success: false`, not the HTTP status. Check `success`, otherwise a failed command will be mistaken for a successful one. For a Galaxy app (`kind: "GALAXY_APP"`) the same error arrives with the status `502` — the `200` HTTP statuses in the table above apply to a separate virtual machine. The exception is `EXEC_BUSY`: a busy shared host exec channel arrives as `409` with a `Retry-After` header, because it means "busy, retry" rather than a failure.

In streaming mode (`?stream=true`) a failure arrives as an SSE `error` event with `code` and `message` — this is how both `/exec` and `/deploy` report a failure on an exception or a transport drop. For a deploy, a failure of an individual **step** arrives differently — as a `step` event with `status: "error"` and the step name. There is no `success` field in the stream.

### Access-gate and billing errors

| Code | HTTP | Description |
|-----|------|----------|
| `INFRA_SCOPE_REQUIRED` | 403 | The key does not have the `vibe:infra` scope. Returned when creating a server and on wake schedules |
| `INFRA_FORBIDDEN_FOR_COWORK_KEY` | 403 | The call was made with a Cowork key — it is data-plane only and does not reach the control plane. It arrives on EVERY method of this section except reads (`GET`). `error.details.requiredAction` carries the exact fix, and `error.details.deployableKeys` lists the owner's ordinary keys that can deploy, up to the five most recent. The special-purpose project key never appears there, so an empty list does not mean no suitable key can be issued. To issue a suitable one, see [Project key for deploy](./cowork/deploy-key.md) |
| `WRITE_BLOCKED_READONLY_KEY` | 403 | The key is in read-only mode. It arrives on ANY write in this family that is not on the exception list — creating a server, driving its lifecycle (stop, start, delete, reboot, sleep, wake and wake schedules) and any other non-GET. **Important:** that list lives in code. The full set arrives in the [GET /v1/me](/docs/keys-auth/me) response as `writeRestriction.exceptions` and is printed, broken down by group, on the [Access mode](/docs/keys-auth/access-mode) page — check it there, it is not repeated here. In broad strokes it holds delivery to a server the key already owns, creating an application in `READONLY` mode (`POST /v1/apps`) and two emergency exits that are not writes — revoking the key itself and releasing a stuck lock (`DELETE /v1/infra/servers/{id}/lock` — it releases the backend lock UNCONDITIONALLY and is therefore a second step, allowed only after the server has been independently confirmed idle). The `recoveryAction` field of an `EXEC_BUSY` response points not to the lock release but to `POST /v1/infra/servers/{id}/unstick`. The permitted delivery has two consequences beyond the server: deploying to a sleeping server wakes the machine and starts spending the balance, and it creates and updates the application card in the Bitrix24 account catalog, which every employee sees; the icon changes the picture on that card. How to change the mode — [Access mode](/docs/keys-auth/access-mode) |
| `INT_TARIFF_REQUIRED` | 402 | The portal is on a free Bitrix24 plan — creating servers and deploying require a commercial plan (a trial plan grants limited access). On wake, only a Galaxy app gets this code |
| `INT_VIBE_PLUS_REQUIRED` | 402 | Account access is narrowed to a paid Vibe+ plan, and an ordinary commercial Bitrix24 plan is no longer enough. An account on a trial plan keeps its trial access and does not receive this code |
| `PORTAL_TARIFF_UNREADABLE` | 403 | The account plan could not be read, so the response does not name the plan that is missing: `error.details.requiredTariffs` is empty and the button points to support. Buying a plan does not clear this refusal. Returned when creating a server |
| `COMMERCIAL_PLAN_REQUIRED` | 402 | The platform could not find the record of the Bitrix24 account the key is bound to, so it cannot check access. Returned when creating a server |
| `TRIAL_PORTAL_LIMIT` | 402 | The per-portal server limit for the trial period is exceeded (1 server per portal) |
| `PLAN_NOT_ALLOWED_ON_TRIAL` | 402 | The requested plan is not available during the trial period. `error.details.allowedPlans` lists the plans the trial gate is configured for — the set varies, so read it instead of hardcoding it. `error.details.requestedPlan`, when present, echoes the rejected plan |
| `ACCOUNT_FROZEN` | 402 | The Vibecode balance is frozen. A top-up is required |
| `BILLING_EXHAUSTED` | 402 | The Vibecode balance is exhausted. Waking and deploying are blocked |
| `VIBE_DEMO_EXPIRED` | 403 | The Vibe+ trial has ended: a trial machine is frozen and refuses start, wake, reboot and repair until the account is on a paid Vibe+ plan |
| `SERVER_WAKE_BLOCKED` | 403 | Waking is blocked (not due to billing: expired platform access, an administrative block, security violations) |

### System errors

| Code | HTTP | Description |
|-----|------|----------|
| `MISSING_API_KEY` | 401 | The `X-Api-Key` header was not provided |
| `INVALID_API_KEY` | 401 | The key is not recognized — no such key exists on the platform |
| `RATE_LIMITED` | 429 | The request rate limit was exceeded. The response carries the `Retry-After` header |
| `INTERNAL_ERROR` | 500 | Internal server error |

For the full reference for common errors, see [Errors](/docs/errors).

## App icon

The app icon (SVG) appears in the Bitrix24 catalog and as the browser-tab favicon. The format, requirements and order (favicon `<link>` before deploy, `POST /v1/infra/servers/:id/icon` upload after) live on a dedicated page — [App icon](/docs/infra/app-icon).

## Recipes

- [Load a database dump onto a server](/docs/recipes/db-dump-restore) — upload a dump and restore the database in the background via `exec`.
- [A fast release cycle](/docs/infra/deploy/fast-cycle) — shorten the edit cycle without running every step.

## See also

- [API overview](/docs/entity-api)
- [Limits and optimization](/docs/optimization)
- [Keys and authorization (`/v1/me`)](/docs/keys-auth)
- [What the app receives](/docs/infra/app-runtime)
