Für KI-Agenten: Markdown dieser Seite — /docs-content-en/source-storage/save.md Dokumentationsindex — /llms.txt

Dokumentationsartikel sind derzeit auf Englisch verfügbar.

Save a snapshot

POST /v1/apps/:id/sources

Saving a source archive explicitly: when you need to tag a version, record an intermediate result without deploying, or prepare a rollback.

The request body is the raw archive bytes, and the format is declared by the Content-Type header. Tags, the note, and the filename are passed via additional headers. When you deploy, a snapshot is created automatically.

Binary upload only (contract v2). The endpoint accepts the raw archive bytes in the request body (--data-binary). multipart/form-data is no longer supported — such a request returns 400 INVALID_CONTENT_TYPE. Early client versions that sent multipart/form-data with form fields (-F "tag=manual") must be updated: the body is the archive bytes themselves, and the tags, note, and filename are passed via the X-Tags / X-Note / X-Filename headers (not as form fields and not as query parameters). MCP clients use the save_sources tool, which packs and sends the archive in the correct format for you.

Parameters

Parameter Type Required Description
id (path) UUID yes Application identifier. Get it via GET /v1/apps.
Content-Type (header) string yes Archive format. Allowed values: application/gzip, application/x-tar, application/zip, application/octet-stream. Any other value → 400 INVALID_CONTENT_TYPE. The declared format is checked against the first bytes of the archive: a direct contradiction (application/gzip declared for zip bytes, or the other way round) → 415 UNSUPPORTED_ARCHIVE_FORMAT.
Content-Length (header) number yes Body size in bytes. An upload without it (Transfer-Encoding: chunked) is not accepted → 411 MISSING_CONTENT_LENGTH.
X-Filename (header) string no An arbitrary display filename, for example app-v1.tar.gz. If it is not provided, the name is derived from Content-Type, which gives source.tar.gz for application/gzip. Characters outside the a-zA-Z0-9._- set → 400 INVALID_FILENAME.
X-Tags (header) string no Comma-separated tags. manual and published are recognized — they protect the version from automatic cleanup.
X-Note (header) string no An arbitrary note that is saved in the version record.
X-AI-Session-Id (header) string no AI session identifier. Groups snapshots in the manifest by session.

Request fields (body)

The body is raw archive bytes, not multipart/form-data. Maximum size — 500 MB.

The body is accepted as a stream, so its length has to be declared up front: the request must carry Content-Length. A chunked upload (Transfer-Encoding: chunked, no length) is rejected with 411 MISSING_CONTENT_LENGTH — the platform will not buffer the archive in memory to measure it for the client. A declared length above the cap is rejected with 413 before the body is read.

In curl the length is set automatically when the body comes from a file (--data-binary @app.tar.gz). Piping the body in by hand (... | curl --data-binary @-) gives no length — save the archive to a temporary file or set Content-Length explicitly.

Examples

curl — personal key

Terminal
curl -X POST https://vibecode.bitrix24.com/v1/apps/<APP_ID>/sources \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/gzip" \
  -H "X-Note: Added OAuth flow" \
  -H "X-Tags: manual" \
  --data-binary @app-sources.tar.gz

curl — OAuth application

Terminal
curl -X POST https://vibecode.bitrix24.com/v1/apps/<APP_ID>/sources \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/gzip" \
  -H "X-Note: Added OAuth flow" \
  -H "X-Tags: manual" \
  --data-binary @app-sources.tar.gz

JavaScript — personal key

javascript
const archive = await fs.readFile('app-sources.tar.gz')

const res = await fetch(
  `https://vibecode.bitrix24.com/v1/apps/${appId}/sources`,
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/gzip',
      'X-Note': 'Added OAuth flow',
      'X-Tags': 'manual',
    },
    body: archive,
  },
)
const json = await res.json()
console.log(json.data.versionId, 'deduplicated:', json.data.deduplicated)

JavaScript — OAuth application

javascript
const archive = await fs.readFile('app-sources.tar.gz')

const res = await fetch(
  `https://vibecode.bitrix24.com/v1/apps/${appId}/sources`,
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
      'Content-Type': 'application/gzip',
      'X-Note': 'Added OAuth flow',
      'X-Tags': 'manual',
    },
    body: archive,
  },
)
const json = await res.json()
console.log(json.data.versionId, 'deduplicated:', json.data.deduplicated)

For a zip archive, only one value changes in the same examples: Content-Type: application/zip, and the body is the bytes of the zip file.

Response fields

Field Type Description
data.versionId string Version identifier of the form v<N>.
data.id string Internal identifier of the snapshot record. Returned only in the save response: the version list and the single-version metadata do not carry this field.
data.filename string Name of the archive file in storage.
data.contentType string Archive type: application/gzip, application/zip, application/x-tar, or application/octet-stream.
data.sha256 string SHA-256 of the archive contents. Used for deduplication within one owner.
data.size number Archive size in bytes.
data.timestamp string Save time (ISO 8601, UTC).
data.tags string[] Active version tags: manual, published.
data.note string | null Note from the X-Note header or null.
data.deduplicated boolean true if an archive with the same contents was already saved for this owner — the existing version is returned.
data.skipped boolean Arrives instead of the version fields when saving is disabled. Always true.
data.reason string Why the save was skipped: DISABLED_GLOBALLY or DISABLED_FOR_PORTAL. Arrives together with skipped.

Response example

HTTP 201 — the snapshot was saved:

JSON
{
  "success": true,
  "data": {
    "versionId": "v3",
    "id": "cmszu9y12190j4bmj3hhsno39",
    "filename": "2026-05-21T10-15-30-000Z-v3.tar.gz",
    "contentType": "application/gzip",
    "sha256": "a3f5d8b2c1e9f4...",
    "size": 184320,
    "timestamp": "2026-05-21T10:15:30.000Z",
    "tags": [],
    "note": null,
    "deduplicated": false
  }
}

HTTP 200 — saving is disabled, no snapshot created:

JSON
{
  "success": true,
  "data": {
    "skipped": true,
    "reason": "DISABLED_GLOBALLY"
  }
}

reason"DISABLED_GLOBALLY" (the platform has not enabled the feature) or "DISABLED_FOR_PORTAL" (the Bitrix24 account owner disabled it for their account). In either case POST /v1/apps/:id/publish does not check for a snapshot.

Error response example

400 — Content-Type is not in the list of allowed archive formats:

JSON
{
  "success": false,
  "error": {
    "code": "INVALID_CONTENT_TYPE",
    "message": "Content-Type \"text/plain\" not allowed. Allowed: application/gzip, application/x-tar, application/zip, application/octet-stream",
    "hint": {
      "method": "binary (not multipart)",
      "acceptedContentTypes": [
        "application/gzip",
        "application/x-tar",
        "application/zip",
        "application/octet-stream"
      ],
      "mcpToolName": "save_sources",
      "docsUrl": "/docs/source-storage"
    }
  }
}

The main hint fields are shown. The full response also carries explanation with a breakdown of the format, metadataHeaders with the list of metadata headers, and exampleCurl with a ready-made command.

Errors

HTTP Code Description
400 INVALID_CONTENT_TYPE Content-Type is not in the list of allowed archive formats. multipart/form-data is not accepted — send the raw archive bytes.
400 INVALID_BLOB The request body is not a binary buffer — for example, text was passed.
400 INVALID_FILENAME The X-Filename header contains characters outside the a-zA-Z0-9._- set.
400 INVALID_TAGS A tag from the X-Tags header contains invalid characters (a-zA-Z0-9_- are allowed).
400 STORAGE_UPLOAD_LENGTH_MISMATCH The declared Content-Length did not match the actual body size. The check happens at the end of the stream, so the refusal arrives after the bytes have been sent. Recompute the length and retry.
402 BILLING_INSUFFICIENT Insufficient balance — storage writes are paused. Top up the balance and retry.
507 STORAGE_QUOTA_EXCEEDED The snapshot does not fit into the portal's remaining storage cap. The cap is optional: it is switched on by the platform and applies only to portals that have no commercial Bitrix24 plan and never had one. Usage is counted across the whole portal, not per application.
403 SOURCE_APP_ID_MISMATCH The call was made with an authorization key vibe_app_… issued for a different application. Such a key can access only its own application's snapshots, even when both applications were created by the same author.
403 NOT_AUTHORIZED Only the application author, the application OAuth key, or a Bitrix24 account administrator can manage snapshots.
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 APP_NOT_FOUND The application does not exist, was deleted, or belongs to another portal.
411 MISSING_CONTENT_LENGTH The request arrived without Content-Length (a chunked upload). Declare the body length and retry.
413 The archive size exceeds the 500 MB limit.
415 UNSUPPORTED_ARCHIVE_FORMAT The first bytes of the archive directly contradict the declared Content-Type: application/gzip was declared while a zip was sent, or the other way round. The response body carries error.hint.declared and error.hint.detected. The types application/x-tar and application/octet-stream never trigger this refusal — their signature cannot be read from the first bytes.
500 SOURCE_STORAGE_ERROR Internal storage error.
503 STORAGE_STS_UNAVAILABLE Storage is temporarily unavailable (a failure to issue temporary access credentials) — retry the request.

Full list of common API errors — Errors.

Known specifics

Re-saving the same archive does not create a version. A match is determined by the sha256 of the contents. The endpoint returns HTTP 201 with deduplicated: true and the versionId of the existing version. If new tags or a note are passed on a re-save, they are added to the existing version: tags are merged, the note is overwritten.

Deduplication is scoped to a single owner. Matching happens only among the versions of the same application for POST /v1/apps/:id/sources and the same server for POST /v1/infra/servers/:id/sources. The very same archive saved on two different servers or under two applications yields two versions and two objects in storage — each owner keeps its own version history.

A deduplicated save costs the same as the first one. A match is determined by contents, so it can be established only after the body has been received in full: the platform computes sha256 on the fly over the incoming stream. That is why re-saving the same archive takes as long as the first save and transfers the same number of bytes.

A deduplicated save clears 409 SNAPSHOT_REQUIRED. The check before POST /v1/apps/:id/publish accepts it on a par with a real one: the save marks the version as presented again, and the snapshot's age is counted from that mark. The data.timestamp field still reports when the version was created — it does not move, so the stored file name and the version's place in the retention policy stay the same. If you re-save the very same sources after a 409 SNAPSHOT_REQUIRED, the next publish goes through — you do not have to change the bytes to refresh the snapshot.

See also