For AI agents: markdown of this page — /docs-content-en/infra/deploy/upload.md documentation index — /llms.txt

Upload a file

POST /v1/infra/servers/:id/upload

Writes a file to a BLACKHOLE server through the tunnel agent. Two source options: inline base64 content in the request body — up to 96 MB, which is about 72 MB of the file itself — or a URL from which the agent downloads the file on its own, up to 500 MB. On servers enrolled in the resilient URL-download rollout, the agent makes up to four attempts with backoff inside a shared five-minute budget; before enrollment, the existing URL-download behavior is preserved. Automatic unpacking of tar.gz / tar.bz2 / zip archives is supported — the format is detected by the file signature (magic bytes) or by the URL extension.

Parameters

Parameter In Type Required Description
id path string (UUID) yes BLACKHOLE server ID, status: running, blackholeStatus: CONNECTED

Request fields (body)

Field Type Required Description
path string yes Path on the server, 1–500 characters. For archives — where to place the uploaded archive before unpacking
content string yes (or url) Base64 content of the file. Maximum 96 MB per request body, which is about 72 MB of the file itself: base64 runs roughly a third larger than the raw bytes. A body over the cap is refused with 413 INLINE_SOURCE_TOO_LARGE. Send a larger file through the url field
url string yes (or content) HTTPS URL from which the agent downloads the file. Up to 500 MB
mode string no File permissions in octal format: 0644, 0755. Applied to the file at path
extract boolean no Unpack the archive after upload. Defaults to false. Requires content or url with an archive
extractTo string no Directory for unpacking (when extract: true). Defaults to the directory from path. Validated before the upload starts: an absolute path inside one of the roots /opt, /tmp, /root, /var/log, /var/lib, /etc/systemd, /etc/nginx, not pointing at key or schedule directories. Otherwise — 400 INVALID_EXTRACT_TO

You must pass exactly one of: content or url.

Examples

curl — personal key

Terminal
# Uploading a file by URL with automatic unpacking
curl -X POST https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/upload \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://github.com/user/repo/archive/main.tar.gz",
    "path": "/opt/app/source.tar.gz",
    "extract": true,
    "extractTo": "/opt/app"
  }'

# Inline base64 content — a small file (package.json)
CONTENT=$(echo '{"name":"my-app","version":"1.0.0"}' | base64)
curl -X POST https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/upload \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"content\":\"$CONTENT\",\"path\":\"/opt/app/package.json\",\"mode\":\"0644\"}"

curl — OAuth application

Terminal
curl -X POST https://vibecode.bitrix24.com/v1/infra/servers/SERVER_ID/upload \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/config.json","path":"/etc/myapp/config.json"}'

JavaScript — personal key

javascript
// Uploading and unpacking an archive
const res = await fetch(
  `https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/upload`,
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      url: 'https://github.com/user/app/archive/main.tar.gz',
      path: '/opt/app/source.tar.gz',
      extract: true,
      extractTo: '/opt/app',
    }),
  }
)
const { data } = await res.json()
console.log(`Wrote ${data.size} bytes at ${data.path}${data.extracted ? ', unpacked' : ''}`)

JavaScript — OAuth application

javascript
// Inline base64 content — a small config
const content = Buffer.from('NODE_ENV=production\n').toString('base64')
await fetch(
  `https://vibecode.bitrix24.com/v1/infra/servers/${serverId}/upload`,
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      content,
      path: '/opt/app/.env',
      mode: '0600',
    }),
  }
)

Response fields

Field Type Description
success boolean true on a successful write
data.path string The final file path on the server
data.size number Size in bytes
data.extracted boolean true if the archive was unpacked after upload

Response example

JSON
{
  "success": true,
  "data": {
    "path": "/opt/app/source.tar.gz",
    "size": 1048576,
    "extracted": true
  }
}

Error response example

403 — the path is not allowed for upload:

JSON
{
  "success": false,
  "error": {
    "code": "UPLOAD_PATH_DENIED",
    "message": "Upload to system path is not allowed"
  }
}

Errors

HTTP Code Description
400 VALIDATION_ERROR Schema violation: both content and url at once, or neither; an invalid path
413 INLINE_SOURCE_TOO_LARGE The body carrying inline content is over 96 MB, which is about 72 MB of the file itself. The decision is based on the Content-Length header before the body is read, so the refusal is deterministic — re-sending the same body fails identically. It saves no traffic: the platform accepts the whole body first, and the refusal arrives once the upload has finished. error.hint carries four strings — reason, recovery, recoveryAction and note: this route puts an arbitrary file on the machine, so the recipe here is to host the file where the machine can fetch it and send url instead of the bytes
400 NOT_BLACKHOLE Server in OPEN mode — the Deploy API is unavailable
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. An upload 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
400 INVALID_EXTRACT_TO The extractTo path is not acceptable — see the field description in the parameters. The response text names the rule that was violated. The check runs before the upload starts
400 GALAXY_HOST_NOT_A_DEPLOY_TARGET The upload was addressed to a galaxy host machine ID (kind: "GALAXY"). The machine carries application containers and is not an upload target: put application files into the deploy archive, or create them with a command through /exec against the application ID. See Galaxy app
400 GALAXY_APP_USE_GALAXY_ROUTE The upload was addressed to the ID of a Galaxy application (kind: "GALAXY_APP"). The write would land on the shared server filesystem rather than inside the application container. Files reach such an application through an image rebuild from sources — the source field in the deploy body
403 UPLOAD_PATH_DENIED Uploading to system directories (/root/.ssh, /etc/passwd, /boot, …) is forbidden
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 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
409 EXEC_BUSY The server command channel is busy with another operation — including the unzip preflight before extracting a zip. This is not a failed unzip install and not a reason to change the archive format. The response carries retryable: true, a hint, and a Retry-After header; retry the request
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
429 RATE_LIMITED The limit of 10 operations per minute per server was exceeded
429 DEPLOY_BACKEND_BUSY Too many concurrent uploads that carry content in the request body. The platform caps the number of concurrent calls with inline content so that memory is not exhausted: the counter is shared by /upload, /deploy and server creation carrying a source.content field. The response carries the Retry-After: 30 header — retry in 30 seconds. Or pass the archive by link (url): a URL upload does not consume an inline-memory slot
502 UNZIP_PREFLIGHT_FAILED Failed to install unzip on the server before unpacking the zip archive (a real apt failure, not a busy channel). To avoid depending on this step, use a .tar.gz archive. A busy channel on this step arrives as 409 EXEC_BUSY
500 UPLOAD_URL_DENIED / UPLOAD_DOWNLOAD_FAILED / UPLOAD_TOO_LARGE / UPLOAD_INTEGRITY_MISMATCH / UPLOAD_EXTRACT_FAILED, or another agent code The URL was denied by network policy; the source remained unreachable after the retries were exhausted; the archive was too large, failed integrity checks, or could not be extracted; or another write error occurred. On enrolled servers, known URL-download codes carry a stable error.message without the original link or raw network-library text; other codes retain their own message
502 TUNNEL_NOT_FOUND / GATEWAY_UNREACHABLE: … No live tunnel to the server, or the Gateway is unreachable — call /repair and retry. GATEWAY_UNREACHABLE carries details after the colon — match error.code by prefix
503 GATEWAY_TIMEOUT: … The Gateway did not respond in time. The code carries details after the colon — compare by prefix. In resilient URL mode the stable message says that the download outcome could not be confirmed; retrying is safe
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"

Full list of common API errors — Errors.

When resilient URL mode is enabled, request validation and the blocking VM wake finish before the response opens and retain the HTTP statuses from the table. Before the long download, the platform opens HTTP 200 and keeps the JSON response alive with whitespace; determine the download's final outcome from success and error.code in the body. Agent/Gateway failures from an already started URL upload therefore arrive under HTTP 200 rather than 500/502/503. The HTTP-status contract is unchanged for content, URLs before enrollment, and failures before the long phase.

Known specifics

  • A sleeping standalone virtual machine is woken by the upload itself. If the server (kind: "STANDALONE") is in sleeping status, the platform starts the wake and waits for it within the same request — up to 6.5 minutes — and only then writes the file. A separate /wake call is not needed, but the client's request timeout must be longer than that window. If the server does not come up, 503 WAKE_TIMEOUT arrives and the server returns to sleeping — the file was not written, so a repeat request is safe.
  • Automatic archive format detection with extract: true. The agent detects the format by the file signature (magic bytes, the detectArchiveExt() function): it supports .tar.gz, .zip, .tar.bz2. The format does not need to be specified explicitly. For a URL, the agent also checks the extension.
  • macOS archives are cleaned automatically. If the archive contains AppleDouble sidecars (._*) or .DS_Store, the agent removes them after unpacking — this prevents false positives from scanners like Tailwind v4 oxide. When creating an archive on macOS, we recommend using COPYFILE_DISABLE=1 tar -czf ... so that the sidecars never end up in the archive.
  • Windows PowerShell ZIP — works on agent ≥ 1.2.3. Compress-Archive on Windows writes a ZIP with literal \ in file names. Since version 1.2.3 the agent automatically normalizes these paths via the normalize_windows_paths step. For older agent versions use tar.gz (via WSL / Git Bash) or host a ready-made tar archive and upload it via url.
  • List of forbidden paths. The agent blocks uploads to: /root/.ssh/ (protecting SSH keys), /boot, /etc/shadow, /etc/passwd, system service files. For your application, use /opt/<app>/ or /var/lib/<app>/.
  • mode is applied to the archive, not to the unpacked files. With extract: true the permissions of the unpacked files are determined by the archive's contents, and mode is set only on the saved archive. If you need to change permissions inside, do it via /exec with chmod.
  • The client VM itself downloads url; resilient mode is rolled out gradually. This route intentionally does not use the full /deploy platform relay. On enrolled servers, the agent makes up to four attempts with backoff inside a shared five-minute budget: DNS/TCP/TLS/header/body interruptions and transient HTTP statuses are retried, while permanent URL-policy, HTTP, size, integrity, and extraction errors terminate immediately. Before enrollment, the server keeps the existing URL contract and timeout. In either mode the path still depends on that VM's egress or NAT. To deploy application source without depending on VM networking, use full deploy with source.url. In resilient mode the client timeout must be longer than the server wake plus the five-minute budget.

See also