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
# 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
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
// 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
// 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
{
"success": true,
"data": {
"path": "/opt/app/source.tar.gz",
"size": 1048576,
"extracted": true
}
}
Error response example
403 — the path is not allowed for upload:
{
"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 insleepingstatus, 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/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 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, thedetectArchiveExt()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 usingCOPYFILE_DISABLE=1 tar -czf ...so that the sidecars never end up in the archive. - Windows PowerShell ZIP — works on agent ≥ 1.2.3.
Compress-Archiveon Windows writes a ZIP with literal\in file names. Since version 1.2.3 the agent automatically normalizes these paths via thenormalize_windows_pathsstep. For older agent versions usetar.gz(via WSL / Git Bash) or host a ready-made tar archive and upload it viaurl. - 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>/. modeis applied to the archive, not to the unpacked files. Withextract: truethe permissions of the unpacked files are determined by the archive's contents, andmodeis set only on the saved archive. If you need to change permissions inside, do it via/execwithchmod.- The client VM itself downloads
url; resilient mode is rolled out gradually. This route intentionally does not use the full/deployplatform 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 withsource.url. In resilient mode the client timeout must be longer than the server wake plus the five-minute budget.