
# Galaxy app

A Galaxy app is a separate Black Hole application that lives in a container on a shared host — a galaxy — together with other such apps. Each app gets its own HTTPS subdomain and the same isolation as a separate virtual machine, but they all share one galaxy — and that is many times cheaper than keeping a separate server per app. If the Bitrix24 account administrator has enabled placement in galaxies, the same `POST /v1/infra/servers` call creates a Galaxy app instead of a virtual machine.

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

## Who chooses the placement model

The Bitrix24 account administrator chooses the placement mode in the Vibecode dashboard, on the **Galaxies** page, in the **App placement mode** section. A regular Bitrix24 account member sees the mode as read-only, and the API caller does not set it. The same `POST /v1/infra/servers` request creates either a separate virtual machine or a Galaxy app, depending on the placement mode. There is no "create a Galaxy app specifically" parameter in the request.

The placement mode is one of three:

- **Galaxy First** (`galaxies-only`) — new apps are placed densely inside galaxies. If an app outgrows its container, it is moved to a separate Black Hole server.
- **Hybrid** (`both`) — an app lands in a galaxy when there is room, otherwise it is deployed on a separate virtual machine.
- **Standalone only** (`standalone-only`) — galaxies are not used, and each app gets its own virtual machine.

What the caller should do about it:

- **Detect the model from the response; do not set it.** A Galaxy app is identified by `createdVia` equal to `galaxy` in the create response. The full list of markers is in the "How to identify a Galaxy app in the response" section below.
- **If you need a separate virtual machine, request it explicitly.** If your Bitrix24 account places apps in galaxies but a particular app needs a dedicated server, set the `placement` field to `dedicated` in the body of [`POST /v1/infra/servers`](./servers/create.md) — the platform will create a separate virtual machine instead of a Galaxy app. By default `placement` is `auto`, and your Bitrix24 account's placement mode applies.

During the trial period, no new galaxy is created. Before creating an app, read the `deployment` block from [`GET /v1/me`](/docs/keys-auth/me):

- When Galaxy mode is enabled but there is no host, the `galaxyApp` block is absent. The two-step path is available only while `capabilities.servers.create.available` is `true`: select an allowed plan ID from `capabilities.servers.create.limits.allowedPlans`, call [`POST /v1/infra/servers`](./servers/create.md) without `source`, then [`POST /v1/infra/servers/:id/deploy`](./deploy/deploy.md). When `available` is `false`, follow that slot's `userMessage` and `alternatives`.
- When Galaxy mode is disabled, `placementNote` is absent and a one-shot request carrying `source` returns `400 SOURCE_AT_CREATE_GALAXY_ONLY`. For a dedicated virtual machine, use the same conditional two-step path through `capabilities.servers.create.available` and select an allowed plan ID from `capabilities.servers.create.limits.allowedPlans`.
- When an existing host passes the preliminary stored-state check, the `galaxyApp` block is present and a one-shot create carrying `source` is worth trying. This is not a guarantee: only `POST /v1/infra/servers` checks live capacity.
- When an existing host is unsuitable, the `galaxyApp` block is absent. While `capabilities.servers.create.available` is `false`, `placementNote` rules out the two-step path: with trial enforcement active, the occupied slot returns `402 TRIAL_PORTAL_LIMIT`. To retry one-shot creation, wait for the host to recover, ask an administrator to repair it, or upgrade the plan. After removing the host, re-read `GET /v1/me` and use the two-step path advertised there.
- A commercial Bitrix24 plan lifts the trial restriction, but the create request still checks host suitability.

Readiness is determined differently for each model:

- **Separate virtual machine** — by `status: "running"` and `blackholeStatus: "CONNECTED"`.
- **Galaxy app** — only through a code upload. Waiting for `CONNECTED` does not work here (see "Lifecycle" below).

## Galaxy availability

Galaxies are enabled in two steps, and both are done on the Vibecode side: first the platform team turns them on for the platform, then the Bitrix24 account administrator turns them on for their own account. Apps are placed in galaxies only when both steps are done. The pre-call signal is the `deployment` block in the [`GET /v1/me`](/docs/keys-auth/me) response:

- `galaxyEnabled` — whether galaxies are open to the Bitrix24 account. A value of `false` means at least one of the two steps has not been done.
- `galaxyApp` — a nested block with the Galaxy app deploy contract. During trial access, it arrives only when galaxies are open, the placement mode is not "Standalone only", and the stored state shows a suitable host. The block is advisory: `POST /v1/infra/servers` checks actual host suitability.
- `primary` — the placement model the platform treats as primary for this Bitrix24 account: `galaxyApp` or `standalone`.

The `resolvedDeployMode` field shows the placement mode chosen in the dashboard and is always populated, regardless of availability. A value of `both` with `galaxyEnabled` equal to `false` means the mode is selected in the dashboard, but galaxies are not open for the Bitrix24 account — apps are deployed on separate virtual machines.

How to enable them:

1. **The platform team** turns galaxies on for the platform. Until that is done, the **Galaxies** toggle is not shown in the **Settings** section of the dashboard. You cannot enable them yourself — contact support.
2. **The Bitrix24 account administrator** turns galaxies on for their own account, with the **Galaxies** toggle in that same **Settings** section. A regular Bitrix24 account member does not change this setting.

Turning galaxies off blocks not only the placement of new apps but also the management of existing ones: while the toggle is off, mutating calls on a Galaxy app and on its galaxy return `403 GALAXY_DISABLED`. Reading, status refresh ([`POST /:id/refresh`](./lifecycle/refresh.md)) and deletion stay available at all times — leaving galaxies and cleaning up is possible even after they are turned off.

Cost and limits for a Galaxy app are counted differently from a separate virtual machine — see the "Cost" section below.

## How to identify a Galaxy app in the response

| Field | Where | Value |
|-------|-------|-------|
| `createdVia` | response of [`POST /v1/infra/servers`](./servers/create.md) | `galaxy` for a Galaxy app |
| `kind` | response of `POST /v1/infra/servers` and [`GET /v1/infra/servers/:id`](./servers/get.md) | `GALAXY_APP` — a Galaxy app (container), `GALAXY` — a galaxy (the carrier host), `STANDALONE` — a separate virtual machine |
| `galaxyId` | response of `POST /v1/infra/servers` and [`GET /v1/infra/servers/:id`](./servers/get.md) | ID of the galaxy the app runs in. `null` for other types |
| `appCount` | response of [`GET /v1/infra/servers/:id`](./servers/get.md) | For a galaxy (`GALAXY`) — the number of non-deleted apps in it. `null` for other types |

A Galaxy app has no SSH access of its own (`ssh` equals `null`), no public IP address and no external virtual-machine identifier — a container has none of these. The `region` and `plan` fields in the response are the region and plan of the carrier galaxy: the values passed at creation have no effect — the platform chooses placement itself.

## Lifecycle — build on code upload

A Galaxy app **never** reaches `blackholeStatus: "CONNECTED"` on its own. Its container and tunnel agent are created by the code upload itself — there is no separate provisioning step here, as there is for a virtual machine.

This leads to the main rule: **upload the code right after creation, without waiting for `CONNECTED`.** If you create a Galaxy app and just poll the status, it stays in `provisioning`, and after about 20 minutes the platform marks it as `error` and records in the `provisionError` field that the code was never uploaded.

Builds are limited to half of the galaxy host's memory. Run heavy compilation ahead of deployment or choose a larger galaxy plan.

## Creating and uploading code

Creation goes through the same [`POST /v1/infra/servers`](./servers/create.md) call. The code is uploaded in one of two scenarios.

- **Scenario 1 — single request.** Pass the code in the `source` field together with `runtime` and `start` right in `POST /v1/infra/servers`. The build runs in the background — no separate upload call is needed.
- **Scenario 2 — two steps.** Create the app without `source`, passing the required `provider` and `plan` (`region` is optional and defaults to the chosen provider's region), get `next: "deploy"` in the response, then upload the code via [`POST /v1/infra/servers/:id/deploy`](./deploy/deploy.md). For a Galaxy app the values are informational — see the [example in create.md](./servers/create.md).

Full request bodies, fields and examples for both scenarios are on the [Create a server](./servers/create.md) page.

Creating a Galaxy app via scenario 1 — `source` passed, build running in the background. The model is identified by `createdVia` equal to `galaxy`:

```json
{
  "success": true,
  "data": {
    "id": "7c2b1f08-3a4d-4e91-9b6c-2f5e8a1d0c33",
    "status": "provisioning",
    "name": "my-crm-app",
    "kind": "GALAXY_APP",
    "galaxyId": "<galaxy-host-id>",
    "ssh": null,
    "ip": null,
    "plan": "bc-medium",
    "region": "bc-eu-central",
    "mode": "BLACKHOLE",
    "createdVia": "galaxy",
    "subdomain": "app-7c2b1f08",
    "blackholeStatus": "NONE",
    "appUrl": "https://app-7c2b1f08.vibecode.bitrix24.com",
    "createdAt": "2026-04-22T10:50:11.477Z"
  }
}
```

## Deploy

Deploying a Galaxy app differs from deploying a virtual machine. The full contract is on the [Full application deploy](./deploy/deploy.md) page.

- The code source is the inline `source.content` (a base64 archive). The `source.url` and `source.versionId` variants are available where the platform has enabled link deploys for you. Otherwise they are rejected with `400 GALAXY_DEPLOY_CONTENT_ONLY`. With a link, the host downloads the archive itself, so a large archive never travels through the request body. The link must point to [source storage](/docs/source-storage) — an outside address is rejected with `400 GALAXY_SOURCE_URL_NOT_ALLOWED`, so a large archive is first saved as a version and then deployed by `source.versionId`.
- The `runtime` field is required. The `start` field is required for every runtime except `static`, and is ignored for `static`. Without `runtime` the request returns `400 GALAXY_DEPLOY_RUNTIME_REQUIRED`. The runtime list is at [`GET /v1/infra/runtimes`](./deploy/runtimes.md).
- You do not need to supply your own `Dockerfile` — the platform generates it from the `runtime`, `port` and `start` fields.
- The `env` variables are injected into the container at startup, not baked into the image, so secrets do not end up in the image layers. Alongside them the platform adds `PORT` — the app port. The `PORT` key is reserved — see [POST /v1/infra/servers/:id/deploy](./deploy/deploy.md) for details.
- The API does not return the current `env` set: the variables are absent from the [server card](./servers/get.md) and the [server list](./servers/list.md), and there is no dedicated call to read them. The app owner therefore keeps the full set on their side, for example in a secrets store, and sends it in full with every deploy they run. An owner deploy without user variables starts the container without the previous values and reports this in `warnings[]`. A deploy by a development team member keeps the variables set by the owner — see [Full application deploy](./deploy/deploy.md) for details.
- The container working directory is `/opt/app`, and the `/app` path is also a link to it. Store persistent data in the `/data` volume.

## Lifecycle management

A Galaxy app lives in a container on a shared host and has no cloud virtual machine of its own. Some lifecycle operations therefore work through the host, and some do not apply to the app at all. The full map is below — the individual operation pages keep only what is specific to each call.

| Operation | What happens to a Galaxy app |
|-----------|------------------------------|
| [`POST /:id/start`](./lifecycle/start.md), [`POST /:id/wake`](./lifecycle/wake.md) | Both calls bring the app up through the host: the platform starts the galaxy if needed, then starts the container. The response arrives immediately — track readiness by polling [`GET /v1/infra/servers/:id`](./servers/get.md), because `?wait=true` does not wait here. A wake block on the host stops both calls, and `/start` does not clear it: the refusal comes as `403 SERVER_WAKE_BLOCKED`, and when the cause is the Bitrix24 account plan, as `402` with `INT_TARIFF_REQUIRED` or `INT_VIBE_PLUS_REQUIRED`, depending on that plan. An app in `error` status is fixed by a new deploy, not by these calls |
| [`POST /:id/reboot`](./lifecycle/reboot.md) | Restarts the container on the host. The persistent `/data` volume is preserved |
| [`POST /:id/stop`](./lifecycle/stop.md) | Does not apply. A running app gets `422 VM_MISSING` — there is nothing to stop, because the container has no machine. An app that is not in `running` status is rejected earlier, with `422 SERVER_WRONG_STATE`. The app can be removed with [`DELETE /v1/infra/servers/:id`](./servers/delete.md) if deletion is available to this request, otherwise `error.message` gives the reason for the refusal and what to change |
| [`PATCH /:id/sleep`](./lifecycle/sleep.md) | Does not apply — `400 GALAXY_APP_USE_GALAXY_ROUTE`. The app sleeps together with its host, and its idle timer is not set through the Vibecode API |
| [`POST /:id/sleep-now`](./lifecycle/sleep-now.md) | Does not apply — `400 GALAXY_APP_USE_GALAXY_ROUTE` |
| [`PATCH /:id/port`](./deploy/port.md) | Does not apply — `400 GALAXY_APP_USE_GALAXY_ROUTE`. The container port is pinned to the host and is set by the `port` field when you [upload code](./deploy/deploy.md) |
| [`POST /:id/repair`](./lifecycle/repair.md) | Does not apply — `400 INVALID_INPUT`. Repair reinstalls the agent on a cloud virtual machine, and the container has none |
| [`POST /:id/refresh`](./lifecycle/refresh.md) | Returns the current status of the record. The cloud is not queried — there is nothing to query |
| Wake windows: [create](./wake-schedules/create.md), [update](./wake-schedules/update.md) | `403 WAKE_SCHEDULE_GALAXY_DISABLED` — the capability is not yet available for apps |
| [`DELETE /:id`](./servers/delete.md) | Works — the platform tears the container down inside the galaxy. Details are in the "Deletion" section below |

Three calls from the table — `PATCH /:id/sleep`, `POST /:id/sleep-now` and `PATCH /:id/port` — answer with the same `GALAXY_APP_USE_GALAXY_ROUTE` code. The server kind is checked before status and mode, so the app gets this code in any state, never `NOT_RUNNING` or `404`. Only the request-value checks run before the kind check: an invalid `sleepAfterMinutes` or a `port` outside `0–65535` returns `VALIDATION_ERROR` for a Galaxy app too, and a system port `1–1023` returns `PORT_RESTRICTED`.

## Commands, deploys and files — by the app id

The server list returns both the app (`kind: "GALAXY_APP"`) and the machine carrying it (`kind: "GALAXY"`), so a caller holds two different ids. Use the app id. The machine is shared: it also carries containers belonging to other keys of the same Bitrix24 account, so the platform refuses client operations addressed by its id.

| Operation | By the app ID (`kind: "GALAXY_APP"`) | By the machine ID (`kind: "GALAXY"`) |
|-----------|---------------------------------------|---------------------------------------|
| [`POST /:id/exec`](./deploy/exec.md) | Works — the command runs inside the app container. The `workdir` and `env` fields are not supported: `400 GALAXY_EXEC_NO_WORKDIR_ENV`. Prefix the command itself instead: `cd /opt/app; FOO=bar node script.js` | `403 GALAXY_HOST_EXEC_FORBIDDEN` |
| [`POST /:id/deploy`](./deploy/deploy.md) | Works — this is the main way to ship code, see the "Deploy" section above | `400 GALAXY_HOST_NOT_A_DEPLOY_TARGET` |
| [`POST /:id/upload`](./deploy/upload.md) | Does not apply: the container has no tunnel of its own, so the call fails the server readiness check. Put files into the deploy archive, or create them with a command through `/exec` | `400 GALAXY_HOST_NOT_A_DEPLOY_TARGET` |

Synchronous [`/exec`](./deploy/exec.md) returns at most 1 MiB (1,048,576 bytes) of output shared across `stdout` and `stderr`; any remaining output is discarded. This caps command output, not the size of the entire JSON response. Synchronous output of 24 MiB is not supported. When output is cut off, `truncated` is `true`. Older agents may omit this flag; for those agents, `truncated: false` does not guarantee complete output. There is no separate API for downloading large files from Galaxy.

A refusal against the machine id arrives before the platform does anything to the server and leaves no lock on it — repeating the same call is pointless. Take your app's row from the [server list](./servers/list.md) (`kind` equals `GALAXY_APP`) and address it by that id.

Log reading is not affected by this boundary: [`GET /:id/logs`](./deploy/logs.md) against the machine id still returns its system journal.

## Free disk space in a galaxy

The disk usage of a galaxy host machine is visible in your Vibecode account, on the galaxy card in the **Galaxies** section: how much is used out of the total, and when the measurement was taken. The platform does not probe a sleeping machine and shows the last known value — which is why the measurement time sits next to the figure itself. When little free space is left, the card shows a hint with two actions already available to you: expand the galaxy disk, or move the app to another galaxy.

This figure used to be read with a `df` command against the machine id — that call is now refused (`403 GALAXY_HOST_EXEC_FORBIDDEN`). Instead, a galaxy host row in [`GET /v1/infra/servers`](./servers/list.md) and in the [single-server response](./servers/get.md) carries four fields:

| Field | Meaning |
|---|---|
| `diskTotalMb` | disk size in mebibytes |
| `diskFreeMb` | free space in mebibytes |
| `diskState` | the verdict: `ok`, `warning`, `critical`, or `unknown` when nothing has been measured yet |
| `diskProbedAt` | measurement time in ISO-8601 |

The verdict is computed from the same threshold that decides whether an app build has enough room: `critical` means free space has dropped below that threshold and a build will be refused, `warning` means you are approaching it. So the warning arrives early, before the first refusal. For `STANDALONE` machines and for Galaxy apps, all four fields are `null`: the former are never probed, the latter have no disk of their own.

## Outbound IP

A Galaxy app has no static outbound IP address of its own. The app's requests to the outside — to external databases, payment gateways, third-party services — leave through the public IP of the carrier galaxy host, which all apps on that host share.

That address is dynamic. The platform chooses placement itself, and the outbound IP may change when the host is stopped and woken, on a scheduled infrastructure restart, and when the app is migrated to another galaxy. There is no separate endpoint that returns an app's outbound IP.

A dedicated virtual machine does have a public address of its own — it arrives in the `ip` field of [`GET /v1/infra/servers/:id`](./servers/get.md). That address is not reserved for the machine: on every wake the machine receives a new one, so after a few sleep cycles the `ip` field in the single-server response holds a different value. This applies to all plans.

The platform provides no permanent outbound address in any placement model. An IP allowlist on the side of an external service — a managed PostgreSQL, for instance — stops matching the actual address both for a Galaxy app and for a dedicated virtual machine.

## Cost

Cost is counted per galaxy, not per app. Several Galaxy apps in one galaxy share its cost — five apps on a `bc-small` plan cost roughly the same as one galaxy, not as five servers. Galaxy apps do not count toward the per-API-key server limit (`GET /v1/me` → `infra.limits.used`) — it counts only separate virtual machines.

## Logs

For a Galaxy app, [`GET /v1/infra/servers/:id/logs`](./deploy/logs.md) returns the output stream of the container itself (`stdout` and `stderr`), not the galaxy system journal. Reading logs does not wake a sleeping galaxy. If the galaxy is asleep or unreachable, the response is an empty `data.logs` array plus a diagnostic `data.hint` field. The `since` parameter for a Galaxy app accepts only a duration (`10m`, `2h`, `24h`) or a timestamp in RFC 3339 format (for example, `2026-04-22T10:50:11Z`).

## Backup and restore

Galaxy app backups are created and restored in the Vibecode dashboard. The Vibecode API has no such operations. If backups are enabled for the Bitrix24 account, the **Backups** item is in the app menu — on its card in the [Black Hole Servers](/black-hole) section and in the galaxy's app list on the **Galaxies** page. The app owner and the Bitrix24 account administrator can work with the app's backups. A backup stores the container image and the `/data` volume.

The backups table shows the date, status, size, and the **Created** column — how the backup was started and the email of the employee who started it.

The platform keeps the five newest completed backups of each app, and keeps each backup for no longer than 14 days after it completes. A scheduled check that runs every 30 minutes moves the extra backups and the backups older than 14 days to the **Expired** status. A sixth backup therefore displaces the oldest one not at the moment it is created, but at the next run of the check, within half an hour. The 14-day limit does not depend on the number of backups: even an app's only backup expires once it is older than 14 days. An app cannot be restored from a backup in the **Expired** status: its menu offers only **Delete**. If you still need an old backup, restore an app from it before it turns 14 days old and before you create a new backup beyond the five kept.

A restore never rolls the app back in place. Restoring from a backup always creates a new app with a new id and a new address, and the original app stays unchanged. The restored app can be deployed into any galaxy of the Bitrix24 account, including the original one, or onto a new dedicated server. Only the Bitrix24 account administrator can choose a new server. While the original app keeps running, the restored app with the same code and settings may duplicate its integrations.

The employee who started the restore becomes the owner of the restored app. The restored app has no managing API key, so [`GET /v1/infra/servers`](./servers/list.md) does not return it until a key is assigned in the Vibecode dashboard. The steps are on the [Server access recovery](./server-access-recovery.md) page.

## Deletion

A Galaxy app is removed with the same [`DELETE /v1/infra/servers/:id`](./servers/delete.md) call — the platform tears down its container in the galaxy and marks the record deleted. The platform wakes a sleeping galaxy itself before deletion. A galaxy with non-deleted apps is not deleted by this call — remove its apps first.

## Error codes

| HTTP | Code | Description |
|------|------|-------------|
| 400 | `GALAXY_APP_USE_GALAXY_ROUTE` | A Galaxy app received a call that belongs to a dedicated virtual machine — `PATCH /:id/sleep`, `POST /:id/sleep-now` or `PATCH /:id/port`. What does apply to an app is in the "Lifecycle management" section above |
| 403 | `GALAXY_DISABLED` | Galaxies are disabled for the portal — by the platform or by the portal administrator. Any mutating call on a Galaxy app and on its galaxy answers this way; reading, `POST /:id/refresh` and deletion stay available. How to enable them — see the "Galaxy availability" section above |
| 403 | `GALAXY_HOST_EXEC_FORBIDDEN` | A command was addressed to a galaxy host machine ID (`kind` equals `GALAXY`). Run it on the app, by its own ID — see the "Commands, deploys and files" section above |
| 400 | `GALAXY_HOST_NOT_A_DEPLOY_TARGET` | A deploy or a file upload was addressed to a galaxy host machine ID (`kind` equals `GALAXY`). The machine carries containers and is not a deploy target: address the app by its ID |
| 400 | `GALAXY_EXEC_NO_WORKDIR_ENV` | A command for a Galaxy app carried `workdir` or `env` — the container run does not accept them. Prefix the command itself instead: `cd /opt/app; FOO=bar node script.js` |
| 400 | `SOURCE_AT_CREATE_GALAXY_ONLY` | The `source` field was passed at creation on a portal without Galaxy mode, or together with `placement: "dedicated"`. Upload the code via [`POST /:id/deploy`](./deploy/deploy.md) after creation |
| 400 | `GALAXY_DEPLOY_CONTENT_ONLY` | Deploying a Galaxy app with `source.url` or `source.versionId` where link deploys are not enabled for you. Send the archive as the inline `source.content` |
| 400 | `GALAXY_DEPLOY_RUNTIME_REQUIRED` | Deploying a Galaxy app without `runtime`. Specify a runtime, for example `node24` |
| 502 | `GALAXY_BASE_IMAGE_UNAVAILABLE` | The build could not fetch its base image — the image registry was unreachable. This is transient: retry the same deploy every few minutes — see [Upload code](./deploy/deploy.md) |
| 502 | `GALAXY_LOW_DISK` | The host ran out of free disk space during the build. This is retryable: the app is not marked broken, the slot and its `/data` volume are intact, and the version already running keeps serving requests. Retry the deploy once space has been freed on the host — see [Upload code](./deploy/deploy.md). When the server is created with `source` in the same request, the app is not marked broken either |
| 409 | `GALAXY_UPLOAD_BUSY` | Another upload is using the source path. Retry the same deploy after at least 10 seconds; the body carries `retryable: true`, `retryAfter: 10` and `hint`, and the header is `Retry-After: 10`. The app status is preserved |
| 502 | `GALAXY_APP_BUILD_FAILED` | The container build failed. The tail of the build log arrives in the `buildLog` field, and the breakdown of the cause in `error.category` and `error.buildHint` — see [Upload code](./deploy/deploy.md) |
| 502 | `GALAXY_APP_START_FAILED` | The container built but crashed or went into a restart loop due to lack of memory right after start. The log tail arrives in the `buildLog` field, and the breakdown of the cause in `error.category` and `error.buildHint` — see [Upload code](./deploy/deploy.md) |
| 409 | `GALAXY_FULL` | There is no room for another Galaxy app. Before refusing, the platform tries to place the app on another galaxy, so this response means there really is no room left. Delete an unneeded Galaxy app, or ask for a dedicated machine — the `placement` field with the value `dedicated`, see [Create a server](./servers/create.md) |
| 409 | `GALAXY_HAS_APPS` | The server being deleted is a galaxy that still has non-deleted apps. The body contains `appCount`. Deleting a galaxy together with its apps is available in your Vibecode account |
| 409 | `GALAXY_HOST_WAKE_BLOCKED` | An app is being deleted on a galaxy that is blocked from waking — for example, billing is frozen. The refusal is terminal: retrying will not help until the cause is cleared. The cause arrives in `error.reason` — `BILLING_FROZEN`, `ACCESS_EXPIRED`, `STOPPED` or `UNKNOWN`. This response carries no `error.hint` |
| 502 | `GALAXY_DEPLOY_INTERRUPTED` | The deploy could not be confirmed. When the durable operation status is `unknown`, the response carries `retryable: false`: read the operation list, server state, and logs first; do not repeat the deploy blindly. Only an operation proven to have failed, whose hint permits the same request, carries `retryable: true`. Your app slot and the `/data` volume are preserved. `error.subcause` names the cause (`http_window_exhausted`, `exec_channel_busy`, `no_this_deploy_container`, `tail_unreached`, `source_fetch_interrupted`), and `error.repeated` with the value `true` requires stopping the retry loop and inspecting the app's own startup or contacting support, see [Upload code](./deploy/deploy.md) |
| 502 | `GALAXY_HOST_UNREACHABLE` | The galaxy is unreachable — its tunnel is not in `CONNECTED` status. For deploy, `retryable: false` means the durable outcome is `unknown`: read the operation list, server state, and logs first; do not repeat the deploy blindly. In other cases follow `error.hint`, see below. If waking is blocked, a terminal `409 GALAXY_HOST_WAKE_BLOCKED` arrives instead — it carries no `error.hint` |

The full reference of common API errors is on the [Errors](/docs/errors) page.

### The `error.hint` field of `GALAXY_HOST_UNREACHABLE`

A `502 GALAXY_HOST_UNREACHABLE` response carries an `error.hint` object with four string fields:

| Field | What it holds |
|-------|---------------|
| `reason` | Why the galaxy is unreachable right now — its virtual machine is waking up, or the agent is reconnecting the tunnel |
| `recovery` | What to do. The wording depends on where the error came from — see the three branches below |
| `recoveryAction` | The concrete action to retry. Also depends on the source of the error |
| `note` | A caveat: the galaxy status in the listing can lag behind the real tunnel state. If the state persists for more than 15 minutes, the galaxy really is unreachable |

The `reason` and `note` fields are always the same, except for a galaxy in the `ERROR` status that never connected (see below), while `recovery` and `recoveryAction` differ by the source of the error:

| Source | What the hint advises |
|--------|-----------------------|
| Code upload (`POST /v1/infra/servers/:id/deploy`) | With `retryable: false`, read the operation list, server state, and logs first; do not repeat the deploy blindly. Without `retryable: false`, the build was not dispatched: wait for the tunnel to recover and follow the hint. A sleeping galaxy is woken by the deploy itself |
| Command execution (`POST /v1/infra/servers/:id/exec`) | **Important:** if the galaxy is asleep, **retries will not wake it** — wake it first with a deploy or from the dashboard, and only then retry. If the galaxy was running, wait 1–2 minutes for the tunnel to reconnect |
| Deletion (`DELETE /v1/infra/servers/:id`) | Wait 1–2 minutes and retry the deletion. The server record is preserved until the teardown actually runs on the galaxy |

In all three cases **there is no need to delete and recreate the app**: the slot, its container and its `/data` volume are intact, and a fresh slot would land on the same galaxy and run into the same state.

**Exception — a galaxy in the `ERROR` status that never connected.** If the galaxy's virtual machine failed to come up and its tunnel never connected (`blackholeStatus` is `NONE`), the hint for code upload and deletion is different: retrying will not help. A galaxy that never started is retired by the platform automatically together with the app slot, usually within a day. After that, requests to the app id return `404`, and you create the app again. If the slot is still there after a day, start the galaxy from the dashboard or contact support.

## Upload path busy

Another upload is using the source upload path. A synchronous deploy returns `409 GALAXY_UPLOAD_BUSY` with `error.retryable: true`, `error.retryAfter: 10`, a matching `Retry-After: 10` header and `error.hint`. Wait at least 10 seconds and retry the same `POST /v1/infra/servers/:id/deploy` for this app.

The refusal itself preserves the app status and the version already running. When a build starts in the background after server creation, a new app initially stays in `provisioning`: its server response and dashboard card show a `buildHint` recommending the same deploy again. If the attempt remains stuck for 20 minutes, the next check within another 5 minutes marks the app `error`, preserving the busy-path reason and `buildHint`. Retry the same deploy after this transition too: a new attempt returns this slot to `provisioning`. The `/data` contents are preserved. A source build started after server creation is not retried automatically. Hermes retries its upload no sooner than 3 minutes later; the existing 30-minute wait limit ends the attempt while retaining the busy-path reason and hint. There is no need to delete the app, release a lock or call `/unstick`.

## See also

- [Create a server](./servers/create.md)
- [Full application deploy](./deploy/deploy.md)
- [Server logs](./deploy/logs.md)
- [Delete a server](./servers/delete.md)
- [Infrastructure](/docs/infra)
