Para agentes de IA: markdown desta página — /docs-content-en/infra/deploy.md índice da documentação — /llms.txt
Os artigos da documentação estão disponíveis atualmente em inglês.
Deploy API
Deploying applications to BLACKHOLE servers without SSH. All operations go through the tunnel agent: commands run on the server, files are uploaded through the tunnel, logs are read from the system journal (the journalctl utility). AI agents should use this group of endpoints for the full cycle: "pull code from git → install dependencies → start the service → read logs".
Requirements: server in BLACKHOLE mode, running status, blackholeStatus: CONNECTED. In OPEN mode the Deploy API returns NOT_BLACKHOLE.
There is no need to wake a sleeping standalone virtual machine in advance: a deploy, a command, a file upload and a journal read wake a server in sleeping status on their own and wait until it is ready within the same request — up to 6.5 minutes. If the wake does not complete in that window, the request returns 503 WAKE_TIMEOUT, the server returns to sleeping, and a repeat request is safe. Details — Full application deploy, the "Known specifics" section.
This is the contract for a Black Hole VM (
kind: "STANDALONE"). For a Galaxy application (kind: "GALAXY_APP") theCONNECTEDrequirement does not apply — the deploy builds the container itself. The full model and deploy contract for a Galaxy application — Galaxy app.
Galaxy application OOM → release to a dedicated server. If a Galaxy application deploy fails with
502 GALAXY_APP_START_FAILEDspecifically due to OOM (the application outgrew the container memory limit of 512 MB), the response contains a structured hinterror.hintwithrecoveryAction: "graduate-to-dedicated-vm". The recommended action is to recreate the application on a separate virtual machine:POST /v1/infra/serverswithplacement: "dedicated"andgraduateFrom(the identifier of the failed Galaxy application, which will be deleted after the server is created), thenPOST /v1/infra/servers/:id/deploywith the same source code. Duplicate-creation protection via theIdempotency-Keyheader does not extend to this release — together withgraduateFromit returns400 IDEMPOTENCY_UNSUPPORTED_WITH_GRADUATION. This is allowed when the Bitrix24 account'sserverCreationpolicy permits creating servers and you are within quota. A dedicated virtual machine is billed (it sleeps when idle). The hint is added only when the cause is OOM, not on an ordinary crash.
Response format: all endpoints return JSON by default (201/200 with {"success": true, ...}). For /deploy and /exec a streaming mode is available via ?stream=true — an SSE (Server-Sent Events) stream with line-by-line progress is returned. For AI agents and MCP clients always use JSON (do not add ?stream=true) — they cannot parse SSE.
Rate limits:
| Limit | Value |
|---|---|
| Operations per minute per server | 10 |
Concurrent exec/deploy |
1 per server |
exec timeout |
1–600 seconds (default 300) |
Inline body size (base64 in /upload, /deploy source.content) |
96 MB of body, about 72 MB of archive. Over the cap — 413 INLINE_SOURCE_TOO_LARGE |
File size via URL (/upload url, /deploy source.url) |
500 MB |
Archive size of a saved version (/deploy source.versionId) |
500 MB |
Multipart archive size in /deploy |
500 MB as long as the archive streams into storage. Otherwise about 72 MB — conditions |
Scope: vibe:infra
Run a command
POST /v1/infra/servers/:id/exec
Runs a shell command on a BLACKHOLE server through the tunnel agent. The full standard output (stdout), error stream (stderr), and return code are passed to the client. By default the response is JSON, returned after the command finishes (recommended for AI agents and scripts). If you need line-by-line progress, pass ?stream=true and read the stdout/stderr/exit SSE events. A failure during command execution arrives in streaming mode as an error event.
For AI agents and MCP clients, always use JSON mode (without ?stream=true) — they cannot parse SSE. The official Vibecode MCP client (the manage_server_deploy tool) drops this parameter itself and always works in JSON mode.
Streaming gives you live progress, but it does not raise the execution time limit: the limit is the same in both modes and equals timeout + 30 seconds, while timeout itself is capped at 600 seconds. For work that clearly will not fit within that limit, start a background job — see "Background jobs" below.
Which servers it runs on
The server kind arrives in the kind field of the GET /v1/infra/servers and GET /v1/infra/servers/:id responses.
kind |
Command runs | What to keep in mind |
|---|---|---|
STANDALONE — a standalone virtual machine |
yes | The contract of this page applies in full, including workdir and env |
GALAXY_APP — an application in a galaxy |
yes, inside the application 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. Failures during execution arrive with status 502, not 200 — except EXEC_BUSY, which arrives as 409 (see below) |
GALAXY — the machine carrying application containers |
no — 403 GALAXY_HOST_EXEC_FORBIDDEN |
The machine carries containers belonging to different keys of a single Bitrix24 account, so it is not a command target. Run the command on the application, by its own ID (kind equals GALAXY_APP). For the machine's free disk space, look at the galaxy card in your Vibecode account — see Galaxy app |
Parameters
| Parameter | In | Type | Required | Default | Description |
|---|---|---|---|---|---|
id |
path | string (UUID) | yes | — | BLACKHOLE server ID, status: running, blackholeStatus: CONNECTED |
stream |
query | string | no | — | true — SSE streaming mode. For a JSON response, do not pass the parameter: JSON is returned by default |
Request fields (body)
| Field | Type | Required | Description |
|---|---|---|---|
command |
string | yes | Shell command. 1–10,000 characters |
timeout |
number | no | Execution timeout in seconds: 1–600. Default 300 |
workdir |
string | no | Working directory. Up to 500 characters |
env |
object | no | Environment variables: { "KEY": "value" }. Only string values |
Examples
curl — personal key
curl -X POST "https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/exec" \
-H "X-Api-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"command": "ls -la /opt/app", "timeout": 30}'
curl — OAuth application
curl -X POST "https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/exec" \
-H "X-Api-Key: YOUR_APP_KEY" \
-H "Authorization: Bearer USER_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{"command": "npm ci --production", "workdir": "/opt/app", "timeout": 180}'
JavaScript — personal key
const res = await fetch(
`https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/exec`,
{
method: 'POST',
headers: {
'X-Api-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
command: 'node -v',
timeout: 10,
}),
}
)
const { data } = await res.json()
console.log(`exit ${data.exitCode} in ${data.duration}s:\n${data.stdout}`)
JavaScript — OAuth application
// A command with environment variables and a working directory
const res = await fetch(
`https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/exec`,
{
method: 'POST',
headers: {
'X-Api-Key': 'YOUR_APP_KEY',
'Authorization': 'Bearer USER_SESSION_TOKEN',
'Content-Type': 'application/json',
},
body: JSON.stringify({
command: 'npx tsx scripts/seed.ts',
workdir: '/opt/app',
env: { DATABASE_URL: 'postgresql://localhost/mydb' },
timeout: 60,
}),
}
)
Response fields
| Field | Type | Description |
|---|---|---|
success |
boolean | true if the command ran (including a non-zero exitCode) |
data.exitCode |
number | The process's return code |
data.stdout |
string | The command's standard output |
data.stderr |
string | The command's error stream |
data.duration |
number | Execution time in seconds |
data.truncated |
boolean | true if stdout/stderr were truncated at the size limit (5 MB per stream) |
Response example
{
"success": true,
"data": {
"exitCode": 0,
"stdout": "Linux epd65hdv07p8g89c0c06 6.8.0-107-generic #107-Ubuntu SMP PREEMPT_DYNAMIC Fri Mar 13 19:51:50 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux\n",
"stderr": "",
"duration": 5,
"truncated": false
}
}
Error response example
409 — another operation is already running on the server. This is a failure before the command starts, so the HTTP status reflects the outcome. A Galaxy application returns the same status when the host's shared exec channel is busy — that one arises during execution, but it means the same thing: busy, retry:
{
"success": false,
"error": {
"code": "EXEC_BUSY",
"message": "Another operation is running on this server",
"retryable": true,
"retryAfter": 10
}
}
The response also carries a Retry-After HTTP header (in seconds, equal to the retryAfter field) — this is a short poll interval: keep retrying with it until the lock clears. The full upper bound — when the lock auto-expires — is in error.hint.autoExpiresInSeconds.
Errors
The endpoint's errors fall into two groups, and they are delivered differently. A failure before the command starts arrives with a real HTTP status. A failure during execution arrives in the response body — with one exception: a busy exec channel (EXEC_BUSY) on a Galaxy application arrives as a real 409, because it is a "busy" refusal rather than a failure.
Errors before the command starts
| HTTP | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR |
The request schema is violated (empty command, invalid timeout, workdir longer than 500 characters) |
| 400 | NOT_BLACKHOLE |
Server in OPEN mode — the Deploy API is unavailable |
| 400 | COMMAND_TOO_LONG |
The command is longer than 10,000 characters. The response carries a hint: ship large payloads and scripts via /upload, then run bash /path/script.sh |
| 400 | GALAXY_EXEC_NO_WORKDIR_ENV |
A command for a Galaxy application (kind: "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 |
| 401 | MISSING_API_KEY |
The X-Api-Key header was not provided |
| 401 | INVALID_API_KEY |
Invalid or expired API key |
| 402 | ACCOUNT_FROZEN |
The Vibecode balance is frozen. The request is rejected before the operation and a sleeping server is not woken — top up the balance and repeat |
| 403 | SERVER_WAKE_BLOCKED |
The server is asleep and waking is blocked by the platform — an ended trial or an administrative block. A command does not bring such a server up, and a retry will not help until the block is lifted. For details on the block, see Wake a server |
| 403 | WRONG_KEY |
The server exists but is not yours. Operations on the application's content — deploy, exec, upload and log reads — are open on any one of three grounds: the server's managing key, a key whose application is bound to that server (Application.serverId), or membership in that server's development team (both roles, Developer and Administrator). Access tokens and the icon upload require the managing key. Managing the machine itself is also open to the Administrator role on the team. The response carries a hint with a two-step recovery. See Server access recovery |
| 403 | GALAXY_HOST_EXEC_FORBIDDEN |
The command was sent to the ID of a galaxy host machine (kind: "GALAXY"). Run it on the application, by its own ID — see "Which servers it runs on" above |
| 403 | INFRA_FORBIDDEN_FOR_COWORK_KEY |
The call was made with a Cowork/Code key — such a key works with data only and cannot perform write operations. To issue a key that can, see Project key for deploy |
| 404 | NOT_FOUND |
There is no server with this id, or it was deleted. A server that exists but is not yours answers 403 WRONG_KEY — the code tells "no such server" apart from "no rights to it" |
| 409 | EXEC_BUSY |
Another /exec or /deploy is already running on the server. The response carries a Retry-After header and retryable: true / retryAfter (seconds) fields — retry at that interval. Or release the lock via /lock. If EXEC_BUSY persists after releasing the lock, unstick the channel via POST /v1/infra/servers/:id/unstick. On a galaxy host and a Galaxy application unsticking is not available to the owner (409 GALAXY_UNSTICK_UNSUPPORTED): the exec channel is shared by every application on the host — retry after 30-60 s, and if it persists contact support |
| 409 | SERVER_NOT_READY |
The server is not ready for the operation: it is not running or the tunnel is not connected. The response carries a hint field with the reason and the next step. This code does not describe a sleeping server — the platform wakes that one itself. Start a stopped server with /start or restore the tunnel with /repair, then repeat the request. The "listed as connected but the Gateway has no live tunnel" case on this route arrives as 502 TUNNEL_NOT_FOUND (see below) |
| 409 | WAKE_IN_PROGRESS |
Another request is already waking this sleeping server. Wait for it to finish and repeat |
| 422 | VM_MISSING |
The server record has no virtual machine on the provider side, so there is nothing to wake. Delete the server and create a new one |
| 502 | WAKE_FAILED |
While the sleeping server was being woken it entered an unexpected state. Repeat the request |
| 502 | PROVIDER_ERROR |
The cloud provider returned an error while starting the sleeping server's virtual machine. The tunnel is not involved here, so /repair will not help |
| 409 | GALAXY_APP_NOT_READY |
Galaxy applications only (kind: "GALAXY_APP"): the platform did not resolve the name of the application's container on the host and did not run the command. Two conditions. The container name failed validation — message then reads invalid on-host name. Or the application has no subdomain or no link to its galaxy host — message then reads Galaxy app is missing its subdomain or host link. Read the application's current state with GET /v1/infra/servers/:id |
| 429 | RATE_LIMITED |
The limit of 10 operations per minute per server was exceeded |
| 503 | WAKE_TIMEOUT |
The sleeping server did not come up within the allotted 6.5 minutes — either the machine never started or the tunnel never connected. The server returns to sleeping, so a repeat request is safe. See "Known specifics" |
Errors during execution
The HTTP status does not reflect the command's outcome. On a standalone virtual machine (
kind: "STANDALONE") the connection is held open for the whole run, and the200status is sent before execution begins — so both a successful and a failed command arrive with the code200. The failure marker is thesuccess: falsefield and theerrorobject in the response body. Checksuccessin the body, not the HTTP status: a client that branches on the status will treat a failed command as successful. For a Galaxy application (kind: "GALAXY_APP") the same error arrives with the status502.
| Code | Description |
|---|---|
EXEC_TIMEOUT |
The timeout was exceeded (or the default 300 seconds). The agent terminates the whole process group forcibly (SIGKILL, no grace period). The response carries a hint object — its fields are described below |
EXEC_FAILED |
An execution error on the agent |
EXEC_NO_EXIT |
The stream ended without sending an exit status. What the command managed to do on the server is unknown, so this is not a success: the agent may have stopped responding, the tunnel may have dropped mid-command, or very large output may have overrun the channel after the command had already finished. data carries the output collected up to the break (stdout, stderr); the exitCode, duration and truncated fields are absent — the platform does not know their values. The response carries a hint with the recovery path. Applies to a standalone virtual machine (kind: "STANDALONE") only; on a Galaxy application this case still arrives as success: true with exitCode: -1 |
EXEC_BUSY |
The agent's own exec mutex is busy — it is already running another command. This differs from the platform lock (409 before the start): it arrives during execution, in the body as success: false (on STANDALONE — with HTTP 200, on GALAXY_APP — with 409, together with a Retry-After header and the retryable: true / retryAfter fields). The response carries a hint with the recovery path: on STANDALONE — via /unstick, on GALAXY_APP — retry and contact support, because an application owner cannot unstick the host's shared channel |
CONTAINER_NOT_READY |
The galaxy host answered, but the application container the command was meant to run in is not there yet: the slot was created and never deployed, a deploy is in flight, or the container is restarting. Arrives with the status 502. The response carries a hint that leads to deploying the application rather than to the host — the host is healthy, so waking or repairing it changes nothing. Applies to a Galaxy application (kind: "GALAXY_APP") only |
The `hint` fields on `EXEC_TIMEOUT`
| Field | Type | Description |
|---|---|---|
hint.reason |
string | What happened: the command did not finish within the allotted time, and the whole process group was terminated immediately, with no grace period |
hint.recovery |
string | What to do: the upper bound for timeout is 600 seconds, so run longer operations as a background job (systemd-run --unit=<name>) and follow it via GET /v1/infra/servers/:id/logs with the ?service=<name> parameter; for progress within the timeout pass ?stream=true |
hint.recoveryAction |
string | A pointer to the documentation: docs: /docs/infra/deploy/exec |
The hint arrives both in the JSON response and in the SSE error event of streaming mode. An agent-side EXEC_BUSY carries a hint object of the same shape — on a standalone virtual machine (kind: "STANDALONE") its recoveryAction points to POST /v1/infra/servers/:id/unstick. On a Galaxy application that path is closed to the owner, so its recoveryAction describes retrying and contacting support.
A tunnel communication failure arrives in the same shape — with its own code in the error.code field (for example TUNNEL_NOT_FOUND or GATEWAY_TIMEOUT: …). Some gateway codes carry details after a colon (GATEWAY_UNREACHABLE: …, GATEWAY_TIMEOUT: …), so match error.code by prefix, not by an exact comparison. In streaming mode (?stream=true) the same errors arrive as an SSE error event. The event data carries code and message, and for EXEC_TIMEOUT, GATEWAY_TIMEOUT and an agent-side EXEC_BUSY it also carries a hint. The GATEWAY_TIMEOUT hint states the key point: no exit status came back, so the outcome of the command is unknown and it may still be running on the server — do not re-run it blindly, check the state first. These events carry no success field — the error event itself is the failure marker.
Full list of common API errors — Errors.
Known specifics
- A sleeping standalone virtual machine is woken by the command itself. If the server (
kind: "STANDALONE") is insleepingstatus, the platform starts the wake and waits for it within the same request — up to 6.5 minutes — and only then runs the command. A separate/wakecall is not needed, but the client's request timeout must be longer than that window. If the server does not come up,503 WAKE_TIMEOUTarrives and the server returns tosleeping— the command never ran, so a repeat request is safe. - The command runs through
/bin/sh. Thecommandfield is executed by the/bin/shinterpreter — a minimal POSIX shell (dash), notbash. Constructs specific tobash(set -o pipefail,[[ … ]], arrays) are not available there and fail with an error. Run them explicitly — viacommand: "bash -c 'set -o pipefail; …'"or as a script filecommand: "/bin/bash /opt/app/script.sh". - Server-level locking. While an
/execor/deployis running, a second such call returns 409EXEC_BUSY. If the previous/deploywas interrupted (for example, failed at thehealthcheckstep or the client aborted the connection on a timeout), the server lock remains held until it expires automatically — for/deploythis is up to 15 minutes, for/exec— until the command'stimeoutplus 90 seconds has elapsed. The lock can be released immediately by callingDELETE /v1/infra/servers/:id/lock, after which/execworks again. There is no need to recreate the server. - The maximum size of
stdout/stderris 5 MB per stream. If the command's output is larger,truncated: truearrives and the tail is cut off. The command still ran to completion, andexitCodeis correct. For large output, redirect to a file:command: "my-cmd > /opt/app/output.log 2>&1"and then read it via/exec cat /opt/app/output.log. - The
.envfrom/deployis not picked up automatically. Systemd loads.envwhen the service starts, but not for one-off commands via/exec. If you needDATABASE_URL/ API keys, pass them in theenvfield:{ env: { DATABASE_URL: "..." } }. - The two timeouts together give
timeout + 30seconds. The agent kills the process exactly attimeout; the Gateway waits another 30 seconds for the final events and only then returnsEXEC_TIMEOUT. - Keeping the connection alive in JSON mode. If the command runs longer than 15 seconds, the server periodically sends spaces in the response body — this prevents timeouts in nginx and intermediate proxies. The spaces go inside the already-opened JSON object, so the body looks like JSON (
{) from its very first byte instead of starting with spaces: strict clients and ordinary parsers read it the same way, and you do not need to trim anything by hand. - Client timeout: make sure it is inter-byte, not total. The keepalive spaces reset "silence" timeouts, so the classic Python socket timeouts (
urlopen(..., timeout=300), therequestsread timeout) will not by themselves abort a long command in JSON mode. Two other things do abort it. First — clients and wrappers with a true wall-clock deadline for the whole request (for example,aiohttpwithClientTimeout(total=…)or an agent framework's own deadline): keepalive cannot help against those. Second — the server-sidetimeoutof the command itself (300 seconds by default): once it expires, the agent kills the process and you getEXEC_TIMEOUTregardless of the client settings — do not mistake it for a client-side disconnect. In practice: in Python set split timeouts —requests.post(url, json=body, headers=headers, timeout=(10, 60))— 10 seconds for connect and 60 for the inter-byte pause. If the client does drop the connection, the server-side lock persists for up totimeout + 90seconds (EXEC_BUSYfor repeated calls). Commands longer than a few minutes are better run as a background job — see "Background jobs" below. On a sleeping server this rule does not apply right away: the spaces start only after the wake, so up to 6.5 minutes of complete silence precede the first byte of the response, and thetimeout=(10, 60)recipe aborts the request before the command even starts. Either raise the inter-byte timeout above that window, or wake the machine in advance with/wakeandwait=true, then call/execon a machine that is already up.
Unsticking a stuck channel
If /exec still returns EXEC_BUSY after DELETE /v1/infra/servers/:id/lock (and /deploy fails with the code DEPLOY_FAILED and the message "Another command is running"), the agent-side exec mutex is stuck: the server lock is released, but a background process keeps the channel busy. That case is handled by POST /v1/infra/servers/:id/unstick — its parameters, response and every refusal code are covered there.
Choosing the mode
Three call modes and when each one fits:
| When | How | Monitoring |
|---|---|---|
| The command fits into 600 seconds, no progress needed | { "command": "...", "timeout": 600 } without ?stream=true — a blocking JSON response |
exitCode, stdout, stderr in the response body |
The command fits into timeout, live progress needed |
the same request with ?stream=true — SSE events stdout / stderr / exit |
read the events as they arrive |
| The command runs longer than 600 seconds, or the channel must not stay busy for its whole duration | a background job systemd-run --unit=<name> — /exec returns immediately |
GET /v1/infra/servers/:id/logs?service=<name> |
Do not set the client-side overall request deadline lower than the command's timeout — the client would cut the connection before the server can answer. A sleeping server adds up to 6.5 minutes of wake time to that deadline — the example below assumes a machine that is already up. An example of split timeouts in Python:
import requests
r = requests.post(
f"{VIBE_URL}/v1/infra/servers/{SERVER_ID}/exec",
headers={"X-Api-Key": VIBE_API_KEY},
json={"command": "npm ci --production", "workdir": "/opt/app", "timeout": 600},
timeout=(10, 60), # 10 seconds to connect, 60 for a pause between bytes
)
Raising the timeout of /exec does not help the install step — deploy steps have their own 300-second limit, separate from the 600 seconds of /exec.
Important: the timeout default is 300 seconds, not 600. The 600 ceiling exists, but to use it the field must be passed explicitly. A command planned for nine minutes and started without timeout is killed at the fifth minute.
Background jobs
The timeout of /exec is capped at 600 seconds, and when it expires the agent terminates the whole process group forcibly (SIGKILL, no grace period). Run anything longer — package installation, a database dump restore, a heavy build — as a background job: /exec returns instantly, the channel is freed (a second call will not hit EXEC_BUSY), and the process keeps running independently of the HTTP connection.
Recommended — a transient systemd unit
{ "command": "systemctl reset-failed restore-db 2>/dev/null; systemd-run --unit=restore-db /bin/bash /opt/data/restore.sh", "timeout": 30 }
The job gets its own cgroup and lives independently of the exec session. Manage it with standard calls:
- status:
{ "command": "systemctl is-active restore-db" }—active(running),inactive(finished successfully),failed(crashed). - logs: journald picks up the unit's output automatically — read it via
GET /v1/infra/servers/:id/logs?service=restore-db. systemctl reset-failed <name>before a rerun — otherwise systemd refuses to create a unit with the name of the failed job.
Lightweight — background with output redirect
{ "command": "(cd /opt/app && npm run build > /tmp/build.log 2>&1 &)", "timeout": 10 }
Redirecting both streams to a file is mandatory — it releases the exec session's pipes. Progress: { "command": "tail -20 /tmp/build.log" }.
Anti-pattern — bare `nohup`
nohup cmd & without a redirect does not work: in a pipe (rather than a terminal) nohup does not redirect the output, the background process inherits the exec session's pipe, and the agent waits for it to close right up to the timeout — then forcibly terminates the whole process group, including your "background" process. Always add > file 2>&1.
Pass secrets (database passwords and the like) via the env field, not inside command — the agent logs executed commands (the first 200 characters). Also keep in mind that the install/preStart steps of /deploy have their own 300-second timeout — move heavy installations into a one-off /exec or a background job, not into the install script.