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 |
|---|---|---|---|---|
Parameterid |
Inpath | Typestring (UUID) | Requiredyes | DescriptionBLACKHOLE server ID, status: running, blackholeStatus: CONNECTED |
Request fields (body)
| Field | Type | Required | Description |
|---|---|---|---|
Fieldpath |
Typestring | Requiredyes | DescriptionPath on the server, 1–500 characters. For archives — where to place the uploaded archive before unpacking |
Fieldcontent |
Typestring | Requiredyes (or url) |
DescriptionBase64 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 |
Fieldurl |
Typestring | Requiredyes (or content) |
DescriptionHTTPS URL from which the agent downloads the file. Up to 500 MB |
Fieldmode |
Typestring | Requiredno | DescriptionFile permissions in octal format: 0644, 0755. Applied to the file at path |
Fieldextract |
Typeboolean | Requiredno | DescriptionUnpack the archive after upload. Defaults to false. Requires content or url with an archive |
FieldextractTo |
Typestring | Requiredno | DescriptionDirectory 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 |
|---|---|---|
Fieldsuccess |
Typeboolean | Descriptiontrue on a successful write |
Fielddata.path |
Typestring | DescriptionThe final file path on the server |
Fielddata.size |
Typenumber | DescriptionSize in bytes |
Fielddata.extracted |
Typeboolean | Descriptiontrue 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 |
|---|---|---|
| HTTP400 | CodeVALIDATION_ERROR |
DescriptionSchema violation: both content and url at once, or neither; an invalid path |
| HTTP413 | CodeINLINE_SOURCE_TOO_LARGE |
DescriptionThe 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 |
| HTTP400 | CodeNOT_BLACKHOLE |
DescriptionServer in OPEN mode — the Deploy API is unavailable |
| HTTP401 | CodeMISSING_API_KEY |
DescriptionThe X-Api-Key header was not provided |
| HTTP401 | CodeINVALID_API_KEY |
DescriptionThe key is not recognized — no such key exists on the platform |
| HTTP402 | CodeACCOUNT_FROZEN |
DescriptionThe Vibecode balance is frozen. The request is rejected before the operation and a sleeping server is not woken — top up the balance and repeat |
| HTTP402 | CodeINT_TARIFF_REQUIRED |
DescriptionThe portal is on a free Bitrix24 plan, so uploading a file is refused just like creating a new app: the check comes first, before the server is woken, and the refusal leaves nothing behind. The app that is already running keeps responding. The upgrade path is in error.alternatives and error.details.upgradeUrl. Plan and access explains which access path applies to the portal |
| HTTP402 | CodeINT_VIBE_PLUS_REQUIRED |
DescriptionAccount access is narrowed to a paid Vibe+ plan, and an ordinary commercial Bitrix24 plan is no longer enough, so uploading a file is refused as well. The link in error.details.upgradeUrl opens the plan offer in the Bitrix24 account. An account on a trial plan keeps its trial access and does not receive this code |
| HTTP403 | CodePORTAL_TARIFF_UNREADABLE |
DescriptionThe 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 |
| HTTP403 | CodeSERVER_WAKE_BLOCKED |
DescriptionThe server is asleep and waking is blocked — for example, platform access has ended or an administrative block is in place. 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 |
| HTTP403 | CodeWRONG_KEY |
DescriptionThe 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 |
| HTTP400 | CodeINVALID_EXTRACT_TO |
DescriptionThe 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 |
| HTTP400 | CodeGALAXY_HOST_NOT_A_DEPLOY_TARGET |
DescriptionThe 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 |
| HTTP400 | CodeGALAXY_APP_USE_GALAXY_ROUTE |
DescriptionThe 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 |
| HTTP403 | CodeUPLOAD_PATH_DENIED |
DescriptionUploading to system directories (/root/.ssh, /etc/passwd, /boot, …) is forbidden |
| HTTP403 | CodeINFRA_FORBIDDEN_FOR_COWORK_KEY |
DescriptionThe 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 |
| HTTP403 | CodeEXTERNAL_COLLABORATOR_KEY_SERVER_MISMATCH |
DescriptionThe call was made with an external-collaborator key, and the address names a server other than the one the key was issued for. Such a key is valid for exactly one server, and the response does not reveal whether the requested server exists. Working with another server requires the key issued for that external membership |
| HTTP403 | CodeEXTERNAL_COLLABORATOR_NOT_A_MEMBER |
DescriptionAn external-collaborator key addresses its own server, but access is not granted right now: the membership was removed, it expired, or external development collaborators are switched off for the Bitrix24 account that owns the server. The code does not tell these reasons apart and reveals nothing about the server. Membership is re-read on every request, so revoked access stops working immediately |
| HTTP404 | CodeNOT_FOUND |
DescriptionThere 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" |
| HTTP409 | CodeSERVER_NOT_READY |
DescriptionThe server is not ready for the operation: it is not running, the tunnel is not connected, or a successful non-empty Gateway snapshot confirms that a tunnel stored as CONNECTED is absent. 409 is returned only when repair is not confirmed successful; an unavailable or empty snapshot does not confirm absence and the API uses the stored status. The platform attempts to restore a confirmed-missing tunnel before writing the file; if it cannot, the response carries a hint. This code does not describe a sleeping server — the platform wakes that one itself. Restore the tunnel with /repair, then repeat the request. A sleeping server with a live plan-change operation (resize) returns 409 EXEC_BUSY before waking; wait for that operation to finish. |
| HTTP409 | CodeWAKE_IN_PROGRESS |
DescriptionAnother request is already waking this sleeping server. Wait for it to finish and repeat |
| HTTP409 | CodeEXEC_BUSY |
DescriptionA sleeping server with a live plan-change operation (resize) returns 409 EXEC_BUSY before waking; wait for that operation to finish. 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 |
| HTTP422 | CodeVM_MISSING |
DescriptionThe server record has no virtual machine on the provider side, so there is nothing to wake. 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. The error.userMessage field arrives only when both steps are available |
| HTTP422 | CodeVM_POINTER_LOST |
DescriptionThe 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 |
| HTTP502 | CodeWAKE_FAILED |
DescriptionServer readiness could not be confirmed. This response does not mean that the server was stopped. Check the current server state before taking another action |
| HTTP502 | CodePROVIDER_ERROR |
DescriptionThe cloud provider returned an error while starting the sleeping server's virtual machine. The tunnel is not involved here, so /repair will not help |
| HTTP429 | CodeRATE_LIMITED |
DescriptionThe limit of 10 operations per minute per server was exceeded. The current limit is returned in the x-ratelimit-limit header |
| HTTP429 | CodeDEPLOY_BACKEND_BUSY |
DescriptionToo 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 |
| HTTP502 | CodeUNZIP_PREFLIGHT_FAILED |
DescriptionFailed 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 |
| HTTP500 | CodeUPLOAD_URL_DENIED / UPLOAD_DOWNLOAD_FAILED / UPLOAD_TOO_LARGE / UPLOAD_INTEGRITY_MISMATCH / UPLOAD_EXTRACT_FAILED, or another agent code |
DescriptionThe 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 |
| HTTP502 | CodeTUNNEL_NOT_FOUND / GATEWAY_UNREACHABLE: … |
DescriptionNo 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 |
| HTTP503 | CodeGATEWAY_TIMEOUT: … |
DescriptionThe 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 |
| HTTP503 | CodeWAKE_NO_CAPACITY |
DescriptionStartup has already been refused for lack of capacity. Read the current server state before trying again |
| HTTP503 | CodeWAKE_TIMEOUT |
DescriptionThe 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" |
For the full list of common API errors, see 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. A sleeping server with a live plan-change operation (resize) returns409 EXEC_BUSYbefore waking; wait for that operation to finish. - 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 end the download 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.