For AI agents: markdown of this page — /docs-content-en/source-storage.md documentation index — /llms.txt
Source code storage
Source code storage automatically saves snapshots of your application's code on every deploy. If another person or a new AI session starts working on the application, the latest version can be downloaded so they can continue from the same point — the code is not lost.
Why it matters
When you iterate on an application, it is important not to lose a working version of the code. Storage does this automatically: on every successful deploy the platform saves a snapshot of the sources. If later another person or a new AI session starts working on the application, they download the latest version and continue from the same point, without manually sending archives back and forth.
A snapshot is tied to the application, not to a specific developer: the code stays with the application even when the team changes.
How it differs from Git and GitHub
This is not a version control system and not a replacement for Git. Storage solves a narrower task — a safety net and project handoff.
| Source code storage | Git / GitHub | |
|---|---|---|
| What it stores | whole archive snapshots of code | change history line by line |
| When it saves | automatically on every deploy | manually, by a developer's command |
| Are Git skills required | no | yes |
| Branches, merges, version comparison | no | yes |
Need team development with branches and history — use Git. Just need to avoid losing working code and be able to return to a previous version — storage is enough.
How it works
- You iterate on the application and run a deploy.
- The platform saves a snapshot of the sources by itself — the next version:
v1,v2,v3, … - At any time, view the list of versions and download the one you need.
- Before publishing the application, the platform checks that a fresh snapshot exists.
code edit → deploy → snapshot vN saved automatically
↓
list of versions → download any → continue work
Core concepts
- Snapshot — an archived copy of the application's source code at the moment of saving.
- Version
vN— the sequential number of a snapshot (v1,v2, …). The most recent one is considered current. - Tag — a
manualorpublishedlabel: marks a version as important so that automatic cleanup does not delete it. - Deduplication — if the code has not changed, no new snapshot is created; the already existing version is returned.
Access and endpoints
Base URL: https://vibecode.bitrix24.com/v1
Authorization: the X-Api-Key header. Snapshots can be managed by the application authorization key (vibe_app_*), the application author's personal key (vibe_api_*), or a Bitrix24 account administrator.
Allowed archive formats: application/gzip, application/x-tar, application/zip, application/octet-stream.
Body limit: 200 MB per request.
Endpoints:
| Method | Path | Action |
|---|---|---|
POST |
/v1/apps/:id/sources |
Save a snapshot |
GET |
/v1/apps/:id/sources |
List versions |
GET |
/v1/apps/:id/sources/:versionId |
Single-version metadata |
GET |
/v1/apps/:id/sources/:versionId/download |
Signed download link |
PATCH |
/v1/apps/:id/sources/:versionId |
Update tags / note |
POST |
/v1/apps/:id/sources/:versionId/tag |
Add or remove a tag |
DELETE |
/v1/apps/:id/sources/:versionId |
Delete a version |
POST |
/v1/apps/:id/sources/cleanup |
Bulk cleanup of old versions |
Saving sources
Since version 2026-05-23 the platform automatically saves the source bytes to storage on every successful deployment. This applies to:
POST /v1/infra/servers/:id/deploy { source: { content: <base64> } }— the inline bytes are saved as a new versionPOST /v1/infra/servers/:id/deploy { source: { url: <signed URL from storage> } }— the existing version is linked to the deployment identifierPOST /v1/infra/servers/:id/deploy { source: { versionId: 'vN' } }— the same
Galaxy apps accept only source.content. The source.url and source.versionId variants from the list above work on separate virtual machines, whose kind is STANDALONE. For a Galaxy app, whose kind is GALAXY_APP, the code source is passed only as inline source.content — a request with source.url or source.versionId is rejected with 400 GALAXY_DEPLOY_CONTENT_ONLY. See Galaxy apps for details.
Exception: deployment with an external URL (not from Vibecode storage) returns 409 SNAPSHOT_REQUIRED. To work around it — either first upload an archive via POST /v1/apps/:id/sources and deploy via {source: {versionId: 'vN'}}, or pass the X-Skip-Source-Snapshot: <reason> header to explicitly opt out.
Deploy response: the `source` block
The POST /v1/infra/servers/:id/deploy response includes a data.source block — the auto-save outcome. An AI agent or client reads it to tell whether the code reached storage:
{
"success": true,
"data": {
"status": "running",
"appUrl": "https://app-b7c1e2a4.vibecode.bitrix24.com",
"source": {
"autoSaved": true,
"savedVersionId": "v4",
"sha256": "a3f5d8b2c1e9f4...",
"linkedDeployId": "deploy:2026-05-21T10:15:30.000Z",
"skippedReason": null
}
}
}
| Field | Type | Description |
|---|---|---|
source.autoSaved |
boolean | true — snapshot saved; false — skipped (reason in skippedReason) |
source.savedVersionId |
string | absent | Identifier of the created version (vN) when autoSaved: true |
source.sha256 |
string | absent | SHA-256 of the saved archive |
source.linkedDeployId |
string | absent | Identifier of the deployment the snapshot is linked to |
source.skippedReason |
string | null | null on success; otherwise the skip reason (see below) |
The deployment completes successfully (200) regardless of the source block — auto-save is best-effort and never fails the deployment. Values of skippedReason:
skippedReason |
When |
|---|---|
feature-disabled-platform |
Source deposit is disabled at the platform level |
feature-disabled-portal |
The Bitrix24 account owner disabled source deposit for their Bitrix24 account |
external-url-or-toggles-off |
The source is an external URL (not from Vibecode storage) |
save-failed |
Transient storage error — retry the deploy or save manually via POST /v1/apps/:id/sources |
<header value> |
X-Skip-Source-Snapshot: <reason> was passed — the snapshot was skipped intentionally |
To confirm the code was deposited, check data.source.autoSaved === true and store savedVersionId for later rollback or handoff.
When a snapshot is not created
If source saving is disabled in the Bitrix24 account or the source is an external URL (not Vibecode storage), no snapshot is created and the deployment completes normally. To save a version explicitly, upload an archive via POST /v1/apps/:id/sources.
When `POST /sources` is still needed explicitly
Only three scenarios:
- Mark a version — set a
manualorpublishedtag so the version is stored indefinitely. - Save without deploying — save an intermediate result for handoff to another developer.
- Preparing for a rollback — a snapshot of the initial state before a risky change.
When to call
Typical scenario for an AI agent (since 2026-05-23 an explicit POST /sources call before deploy is not required — the platform saves automatically):
- Changed the code →
POST /v1/infra/servers/:id/deploy— sources are saved automatically. POST /v1/apps/:id/publish— publishes the application in the Bitrix24 catalog. Thepublishedtag is added to the snapshot automatically.- Repeated the cycle on the next change.
Scenario for handing the project to a new developer or a new AI session:
GET /v1/apps/:id/sources— list of available versions.GET /v1/apps/:id/sources/:versionId/download— signed link to the archive.- Download the archive, unpack it, and continue work.
For MCP clients: the save_sources tool packs the file tree into tar.gz on the client side and sends it in a single request. The load_sources tool downloads the latest version and unpacks it back into a file tree. A direct HTTP endpoint call is available for clients that pack the archive themselves.
Snapshot presence guarantee before publishing
POST /v1/apps/:id/publish checks for a snapshot no older than 10 minutes. If there is no snapshot or it is stale — 409 SNAPSHOT_REQUIRED is returned with a hint about which call to make before retrying publishing.
The check works only if source saving is enabled in the Bitrix24 account (enabled by default; the Bitrix24 account owner can disable it — see the "Disabling for a Bitrix24 account" section).
Rejection example:
{
"success": false,
"error": {
"code": "SNAPSHOT_REQUIRED",
"message": "Deploy requires a recent source snapshot. Call POST /v1/apps/:id/sources first.",
"hint": {
"requiredAction": "POST /v1/apps/:id/sources",
"toolName": "save_sources",
"freshnessWindowMinutes": 10,
"lastSnapshot": {
"versionId": "v3",
"timestamp": "2026-05-21T09:42:11.000Z",
"ageMinutes": 23
}
}
}
}
The hint.lastSnapshot field is null if there is not yet a single snapshot for the application.
Parameters for re-publishing the same version (not the most recent one):
sourceVersionIdin the body — formatv<N>, for example"sourceVersionId": "v3".- The
x-source-version: v3header — an alternative to the body.
Version lifetime (retention policy)
After saving, a version goes through automatic cleanup on a Grandfather-Father-Son schedule:
- The last 5 versions are always kept, regardless of tags.
- A daily grid over 14 days — one version for each calendar day (UTC).
- A weekly grid over 4 weeks — one version per every 7 days.
- The
publishedandmanualtags — kept indefinitely, never deleted automatically.
For on-demand custom cleanup — POST /v1/apps/:id/sources/cleanup. Versions with the published and manual tags are excluded from it as well.
Saving a snapshot
POST /v1/apps/:id/sources
Accepts the raw bytes of the source code archive in the request body. The format is determined by the Content-Type header. Metadata (tags, note, filename) is passed via additional headers.
Binary upload only (contract v2). The endpoint accepts the raw archive bytes in the request body (
--data-binary).multipart/form-datais no longer supported — such a request returns400 INVALID_CONTENT_TYPE. Early client versions that sentmultipart/form-datawith form fields (-F "tag=manual") must be updated: the body is the archive bytes themselves, and the tags, note, and filename are passed via theX-Tags/X-Note/X-Filenameheaders (not as form fields and not as query parameters). MCP clients use thesave_sourcestool, which packs and sends the archive in the correct format itself.
Path parameters
| Parameter | Type | Description |
|---|---|---|
id (path) |
UUID | Application identifier. Get it via GET /v1/apps. |
Request headers
| Header | Required | Description |
|---|---|---|
Content-Type |
yes | Archive format. Allowed values: application/gzip, application/x-tar, application/zip, application/octet-stream. Any other value → 400 INVALID_CONTENT_TYPE. |
X-Filename |
no | An arbitrary display filename (for example, app-v1.tar.gz). If not provided — the name is derived from Content-Type (for example, source.tar.gz for application/gzip). |
X-Tags |
no | Comma-separated tags. manual and published are recognized — they protect the version from automatic cleanup. |
X-Note |
no | An arbitrary note that is saved in the version record. |
X-AI-Session-Id |
no | AI session identifier. Groups snapshots in the manifest by session. |
Request body
Raw archive bytes, not multipart/form-data. Maximum size — 200 MB.
Response on successful save
HTTP 201:
{
"success": true,
"data": {
"versionId": "v3",
"id": "b7c1e2a4-9f5d-4c3a-8e21-0a1b2c3d4e5f",
"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
}
}
Response fields
| Field | Type | Description |
|---|---|---|
data.versionId |
string | Version identifier of the form v<N>. |
data.id |
string | Internal identifier of the snapshot record (UUID). |
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. |
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 — the existing version is returned. |
Idempotency
Re-saving an archive with the same contents yields the same sha256 and does not create a new version. The endpoint returns HTTP 201 with deduplicated: true and the versionId of the already existing version. If new tags or a note are passed on re-save — they are added to the existing version (tags are merged, the note is overwritten).
In both cases the App.lastSourceSavedAt mark is updated — the freshness check before POST /v1/apps/:id/publish accepts a deduplicated save on a par with a real one.
Response when saving is disabled
HTTP 200 — no snapshot created:
{
"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 Bitrix24 account). In either case POST /v1/apps/:id/publish does not check for a snapshot.
Examples
curl — tar.gz
curl -X POST https://vibecode.bitrix24.com/v1/apps/<APP_ID>/sources \
-H "X-Api-Key: YOUR_APP_KEY" \
-H "Content-Type: application/gzip" \
-H "X-Note: Added OAuth flow" \
-H "X-Tags: manual" \
--data-binary @app-sources.tar.gz
curl — zip
curl -X POST https://vibecode.bitrix24.com/v1/apps/<APP_ID>/sources \
-H "X-Api-Key: YOUR_APP_KEY" \
-H "Content-Type: application/zip" \
--data-binary @app-sources.zip
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': process.env.VIBE_APP_KEY,
'Content-Type': 'application/gzip',
'X-Note': 'Added OAuth flow',
},
body: archive,
},
)
const json = await res.json()
console.log(json.data.versionId, 'deduplicated:', json.data.deduplicated)
Error codes
| HTTP | Code | When returned |
|---|---|---|
| 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). |
| 403 | NOT_AUTHORIZED |
Only the application author, the application OAuth key, or a Bitrix24 account administrator can manage snapshots. |
| 404 | APP_NOT_FOUND |
The application does not exist, was deleted, or belongs to another portal. |
| 413 | — | The archive size exceeds the 200 MB limit. |
| 500 | SOURCE_STORAGE_ERROR |
Internal storage error. |
Updating version metadata
PATCH /v1/apps/:id/sources/:versionId
Updates the tags and/or note of an existing version without re-uploading the archive. Convenient if you need to add a tag or adjust a note after the fact.
Path parameters
| Parameter | Type | Description |
|---|---|---|
id (path) |
UUID | Application identifier. |
versionId (path) |
string | Version identifier of the form v<N>. |
Body fields
| Field | Type | Required | Description |
|---|---|---|---|
tags |
string[] | no | The new full list of tags. Replaces the existing tags entirely. If the field is absent — the tags are not changed. |
note |
string | null | no | Note. A string — overwrites the current one. null — clears the note. If the field is absent — the note is not changed. |
You can pass only tags, only note, or both fields at once.
Response
HTTP 200:
{
"success": true,
"data": {
"versionId": "v3",
"tags": ["manual"],
"note": "Final version before release"
}
}
Examples
curl — add the `manual` tag
curl -X PATCH https://vibecode.bitrix24.com/v1/apps/<APP_ID>/sources/v3 \
-H "X-Api-Key: YOUR_APP_KEY" \
-H "Content-Type: application/json" \
-d '{ "tags": ["manual"] }'
curl — clear the note
curl -X PATCH https://vibecode.bitrix24.com/v1/apps/<APP_ID>/sources/v3 \
-H "X-Api-Key: YOUR_APP_KEY" \
-H "Content-Type: application/json" \
-d '{ "note": null }'
JavaScript
await fetch(
`https://vibecode.bitrix24.com/v1/apps/${appId}/sources/v3`,
{
method: 'PATCH',
headers: {
'X-Api-Key': process.env.VIBE_APP_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ tags: ['manual'], note: 'Final version' }),
},
)
Error codes
| HTTP | Code | When returned |
|---|---|---|
| 400 | INVALID_METADATA |
The body failed validation (invalid tags or field types). |
| 400 | INVALID_VERSION_ID |
The versionId format does not match v<non-negative integer>. |
| 403 | NOT_AUTHORIZED |
Only the application author, the application OAuth key, or a Bitrix24 account administrator can manage snapshots. |
| 404 | APP_NOT_FOUND |
The application does not exist, was deleted, or belongs to another portal. |
| 404 | VERSION_NOT_FOUND |
A version with this versionId does not exist or was deleted. |
List versions
GET /v1/apps/:id/sources
Returns current (not deleted) versions, at most 500 per call, in descending order of save time — the most recent first.
Path parameters
| Parameter | Type | Description |
|---|---|---|
id (path) |
UUID | Application identifier. |
Response
HTTP 200:
{
"success": true,
"data": {
"totalVersions": 3,
"currentVersionId": "v3",
"totalSizeBytes": 552960,
"versions": [
{
"versionId": "v3",
"filename": "2026-05-21T10-15-30-000Z-v3.tar.gz",
"contentType": "application/gzip",
"timestamp": "2026-05-21T10:15:30.000Z",
"size": 184320,
"sha256": "a3f5d8b2c1e9f4...",
"tags": [],
"savedBy": {
"userId": "8f1a2b3c-...",
"session": "claude-session-2026-05-21"
},
"linkedDeployId": null,
"deployStatus": null,
"note": "Added OAuth flow"
},
{
"versionId": "v2",
"filename": "2026-05-20T18-02-11-000Z-v2-published.tar.gz",
"contentType": "application/gzip",
"timestamp": "2026-05-20T18:02:11.000Z",
"size": 184320,
"sha256": "9e7c4b2a1f8d3...",
"tags": ["published"],
"savedBy": {
"userId": "8f1a2b3c-...",
"session": null
},
"linkedDeployId": "publish:2026-05-20T18:05:42.000Z",
"deployStatus": "success",
"note": null
},
{
"versionId": "v1",
"filename": "2026-05-20T09-44-50-000Z-v1.tar.gz",
"contentType": "application/gzip",
"timestamp": "2026-05-20T09:44:50.000Z",
"size": 184320,
"sha256": "5a8d3e2f1c4b9...",
"tags": [],
"savedBy": { "userId": "8f1a2b3c-...", "session": null },
"linkedDeployId": null,
"deployStatus": null,
"note": null
}
]
}
}
Response fields
| Field | Type | Description |
|---|---|---|
data.totalVersions |
number | Total number of current versions. Counted without a cap, so on a long history it exceeds the length of the versions array. |
data.currentVersionId |
string | null | Identifier of the most recent version (v<N>). null if there are no versions. |
data.totalSizeBytes |
number | Total size of all archives in bytes. |
data.versions |
array | The list of versions, newest first. At most 500 entries are returned — on a longer history the array is truncated while totalVersions shows the real number. |
data.versions[].versionId |
string | Version identifier of the form v<N>. |
data.versions[].filename |
string | Filename in storage. Contains a -published or -manual suffix if the corresponding tag is set. |
data.versions[].contentType |
string | null | Archive type (application/gzip, application/zip, etc.). |
data.versions[].timestamp |
string | Save time (ISO 8601, UTC). |
data.versions[].size |
number | Archive size in bytes. |
data.versions[].sha256 |
string | SHA-256 of the archive contents. Used for deduplication. |
data.versions[].tags |
string[] | Active tags: manual, published. |
data.versions[].savedBy.userId |
string | null | Vibecode user identifier. |
data.versions[].savedBy.session |
string | null | AI session identifier (from the X-AI-Session-Id header). |
data.versions[].linkedDeployId |
string | null | Publication identifier (filled in after POST /v1/apps/:id/publish). |
data.versions[].deployStatus |
string | null | Publication status: success or failed. |
data.versions[].note |
string | null | Note from the X-Note field at save time or from PATCH. |
data.versions[].serverContext |
object | null | Server-level display context: serverId, serverName, serverDisplayName, linkedApp ({ appId, title } or null). Present on both server-scoped responses and app-scoped version items; linkedApp resolves from the server's current owner OAuth-app key at read time. |
Linked server versions (`linkedServerSources`)
The versions array lists only app-scoped versions. Versions stored under a server (via POST /v1/infra/servers/:id/sources or auto-save on deploy under a personal key) are not included there — they are returned in a separate additive field data.linkedServerSources, grouped by server:
{
"data": {
"totalVersions": 0,
"versions": [],
"linkedServerSources": [
{
"serverContext": {
"serverId": "8de64f8d-...",
"serverName": "srv-prod",
"serverDisplayName": "Prod",
"linkedApp": null
},
"totalVersions": 2,
"totalSizeBytes": 20,
"versions": [ /* same shape as versions[] items; v<N> numbering is per server */ ]
}
],
"linkedServerSourcesTruncated": false,
"linkedServerHint": {
"message": "This app has source versions stored under a server (server-keyed storage). List and download them via the server endpoint.",
"listEndpoint": "GET /v1/infra/servers/:serverId/sources",
"downloadEndpoint": "GET /v1/infra/servers/:serverId/sources/:versionId/download",
"docs": "https://vibecode.bitrix24.com/docs-content/source-storage.md"
}
}
}
| Field | Type | Description |
|---|---|---|
data.linkedServerSources[] |
array | Groups of server versions (one per server). Empty if there are no linked server versions. |
data.linkedServerSources[].serverContext |
object | The server: serverId, serverName, serverDisplayName, linkedApp (null for servers on a personal key). |
data.linkedServerSources[].totalVersions |
number | Exact number of versions on the server (not limited by the list size below). |
data.linkedServerSources[].totalSizeBytes |
number | Total size of that server's versions. |
data.linkedServerSources[].versions[] |
array | Server versions in the same shape as versions[]. The v<N> numbering is per server. |
data.linkedServerSourcesTruncated |
boolean | true if the version list was truncated for a very large history (the full list is on the server endpoint). |
data.linkedServerHint |
object | null | Present when linkedServerSources is non-empty. Points to the authoritative list and download endpoints for server versions. |
The field is populated when the caller is the app author (personal vibe_api_* key) or a portal administrator. When called with an OAuth-app key the section is empty (a machine key does not enumerate the author's personal servers). On a ?sha256= request the section is not computed (the existence probe stays lightweight). Download and the full per-server list are available via GET /v1/infra/servers/:serverId/sources (see serverContext.serverId); this response's versions / totalVersions / currentVersionId stay app-scoped and are unchanged.
Examples
curl
curl -H "X-Api-Key: YOUR_APP_KEY" \
https://vibecode.bitrix24.com/v1/apps/<APP_ID>/sources
JavaScript
const res = await fetch(
`https://vibecode.bitrix24.com/v1/apps/${appId}/sources`,
{ headers: { 'X-Api-Key': process.env.VIBE_APP_KEY } },
)
const { data } = await res.json()
console.log(`Versions: ${data.totalVersions}, latest: ${data.currentVersionId}`)
Error codes
| HTTP | Code | When returned |
|---|---|---|
| 403 | NOT_AUTHORIZED |
Only the application author, the application OAuth key, or a Bitrix24 account administrator can manage snapshots. |
| 404 | APP_NOT_FOUND |
The application does not exist, was deleted, or belongs to another portal. |
Downloading the archive
GET /v1/apps/:id/sources/:versionId/download
Returns a signed link to the version's archive. The link is valid for 30 minutes.
Path parameters
| Parameter | Type | Description |
|---|---|---|
id (path) |
UUID | Application identifier. |
versionId (path) |
string | Version identifier of the form v<N>. |
Response
HTTP 200:
{
"success": true,
"data": {
"url": "https://<storage-endpoint>/...",
"expiresAt": "2026-05-21T10:45:30.000Z"
}
}
Examples
curl
# Get the link
curl -H "X-Api-Key: YOUR_APP_KEY" \
https://vibecode.bitrix24.com/v1/apps/<APP_ID>/sources/v3/download
# Download the archive via the obtained link (no additional headers)
curl -o source-v3.tar.gz "<url from the response above>"
JavaScript
const { data } = await fetch(
`https://vibecode.bitrix24.com/v1/apps/${appId}/sources/v3/download`,
{ headers: { 'X-Api-Key': process.env.VIBE_APP_KEY } },
).then((r) => r.json())
const archive = await fetch(data.url)
const buffer = await archive.arrayBuffer()
// Next — unpack the archive (tar.gz: tar library; zip: JSZip or similar)
Error codes
| HTTP | Code | When returned |
|---|---|---|
| 400 | INVALID_VERSION_ID |
The versionId format does not match v<non-negative integer>. |
| 403 | NOT_AUTHORIZED |
Only the application author, the application OAuth key, or a Bitrix24 account administrator can manage snapshots. |
| 404 | APP_NOT_FOUND |
The application does not exist, was deleted, or belongs to another portal. |
| 404 | VERSION_NOT_FOUND |
A version with this versionId does not exist or was deleted. |
| 502 | SOURCE_DOWNLOAD_URL_FAILED |
Storage is temporarily unavailable — retry the request. |
Setting and removing a tag
POST /v1/apps/:id/sources/:versionId/tag
Two tags are recognized, both protect the version from automatic cleanup:
manual— the version is pinned manually by an operator.published— the version is fixed as published (this tag is also set automatically afterPOST /v1/apps/:id/publish).
Path parameters
| Parameter | Type | Description |
|---|---|---|
id (path) |
UUID | Application identifier. |
versionId (path) |
string | Version identifier of the form v<N>. |
Body fields
| Field | Type | Required | Description |
|---|---|---|---|
tag |
string | yes | manual or published. |
action |
string | yes | add — add the tag, remove — remove it. |
Response
HTTP 200:
{
"success": true,
"data": {
"versionId": "v3",
"tags": ["manual"]
}
}
Examples
curl
curl -X POST https://vibecode.bitrix24.com/v1/apps/<APP_ID>/sources/v3/tag \
-H "X-Api-Key: YOUR_APP_KEY" \
-H "Content-Type: application/json" \
-d '{ "tag": "manual", "action": "add" }'
JavaScript
await fetch(
`https://vibecode.bitrix24.com/v1/apps/${appId}/sources/v3/tag`,
{
method: 'POST',
headers: {
'X-Api-Key': process.env.VIBE_APP_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ tag: 'manual', action: 'add' }),
},
)
Error codes
| HTTP | Code | When returned |
|---|---|---|
| 400 | INVALID_TAG |
The tag is not in the manual, published set. |
| 400 | INVALID_ACTION |
The action is not equal to add or remove. |
| 400 | INVALID_VERSION_ID |
The versionId format does not match v<non-negative integer>. |
| 403 | NOT_AUTHORIZED |
Only the application author, the application OAuth key, or a Bitrix24 account administrator can manage snapshots. |
| 404 | APP_NOT_FOUND |
The application does not exist, was deleted, or belongs to another portal. |
| 404 | VERSION_NOT_FOUND |
A version with this versionId does not exist or was deleted. |
Deleting a version
DELETE /v1/apps/:id/sources/:versionId
Marks the version as deleted. A version cannot be restored via the API. A version with the published or manual tag is protected from deletion — first remove the tag via PATCH /v1/apps/:id/sources/:versionId with the body {"tags": []} (or while keeping other tags). The response includes hint.preservedTags — tags without protective labels that are worth keeping when removing the protection.
Path parameters
| Parameter | Type | Description |
|---|---|---|
id (path) |
UUID | Application identifier. |
versionId (path) |
string | Version identifier of the form v<N>. |
Response
HTTP 200:
{
"success": true,
"data": { "versionId": "v3" }
}
Examples
curl
curl -X DELETE https://vibecode.bitrix24.com/v1/apps/<APP_ID>/sources/v3 \
-H "X-Api-Key: YOUR_APP_KEY"
JavaScript
await fetch(
`https://vibecode.bitrix24.com/v1/apps/${appId}/sources/v3`,
{
method: 'DELETE',
headers: { 'X-Api-Key': process.env.VIBE_APP_KEY },
},
)
Error codes
| HTTP | Code | When returned |
|---|---|---|
| 400 | INVALID_VERSION_ID |
The versionId format does not match v<non-negative integer>. |
| 403 | NOT_AUTHORIZED |
Only the application author, the application OAuth key, or a Bitrix24 account administrator can manage snapshots. |
| 404 | APP_NOT_FOUND |
The application does not exist, was deleted, or belongs to another portal. |
| 404 | VERSION_NOT_FOUND |
A version with this versionId does not exist or was already deleted. |
| 409 | PROTECTED_BY_TAG |
The version is marked with the published or manual tag. First remove the tag via PATCH /v1/apps/:id/sources/:versionId. |
Bulk cleanup
POST /v1/apps/:id/sources/cleanup
Deletes old versions, keeping the keepLatest most recent ones (5 by default). Versions with the manual and published tags are excluded from cleanup regardless of keepLatest.
Path parameters
| Parameter | Type | Description |
|---|---|---|
id (path) |
UUID | Application identifier. |
Body fields
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
keepLatest |
number | no | 5 |
How many of the latest versions to keep. A non-negative integer. 0 will keep only versions with the manual and published tags. |
The body can be omitted — the default value applies.
Response
HTTP 200:
{
"success": true,
"data": {
"deletedVersions": ["v2", "v1"]
}
}
Examples
curl
curl -X POST https://vibecode.bitrix24.com/v1/apps/<APP_ID>/sources/cleanup \
-H "X-Api-Key: YOUR_APP_KEY" \
-H "Content-Type: application/json" \
-d '{ "keepLatest": 3 }'
JavaScript
const res = await fetch(
`https://vibecode.bitrix24.com/v1/apps/${appId}/sources/cleanup`,
{
method: 'POST',
headers: {
'X-Api-Key': process.env.VIBE_APP_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({ keepLatest: 3 }),
},
)
const { data } = await res.json()
console.log('Deleted versions:', data.deletedVersions.length)
Error codes
| HTTP | Code | When returned |
|---|---|---|
| 400 | INVALID_KEEP_LATEST |
The keepLatest value is not a non-negative integer. |
| 403 | NOT_AUTHORIZED |
Only the application author, the application OAuth key, or a Bitrix24 account administrator can manage snapshots. |
| 404 | APP_NOT_FOUND |
The application does not exist, was deleted, or belongs to another portal. |
Server-scoped source endpoints
This family mirrors the app-scoped one (/v1/apps/:id/sources…), but it is keyed by a server identifier rather than an application one. The request and response contracts are the same as the app-scoped operations (the same body format, the same headers, the same error codes) — only the path prefix changes.
Authorization: ownership (3-identity) — the server-owner key, the same user's personal key, or a Bitrix24 account administrator (ADMIN); checked via loadAndAuthorizeServer. No explicit scope is required: there is no route-level scope check, and vibe:storage is granted automatically.
| Method | Path | Action |
|---|---|---|
POST |
/v1/infra/servers/:id/sources |
Save a raw-bytes snapshot |
GET |
/v1/infra/servers/:id/sources |
List versions (+ ?sha256= probe) |
GET |
/v1/infra/servers/:id/sources/:versionId |
Single-version metadata |
GET |
/v1/infra/servers/:id/sources/:versionId/download |
Signed download link |
POST |
/v1/infra/servers/:id/sources/:versionId/tag |
Add or remove a tag |
PATCH |
/v1/infra/servers/:id/sources/:versionId |
Update tags / note |
DELETE |
/v1/infra/servers/:id/sources/:versionId |
Delete a version (soft-delete) |
POST |
/v1/infra/servers/:id/sources/cleanup |
Bulk cleanup |
POST …/sources— the same contract asPOST /v1/apps/:id/sources: the raw archive bytes in the body,Content-Typeone ofapplication/gzip,application/x-tar,application/zip,application/octet-stream, optionalX-Filename/X-Tags/X-Note/X-AI-Session-Idheaders, a 200 MB body cap, deduplication bysha256.GET …/sources— the server's version list. The optional?sha256=<64-hex>parameter is a lightweight probe for the existence of an archive with those contents.GET …/sources/:versionId— single-version metadata (the same shape as aversions[]item).GET …/sources/:versionId/download— a signed link to the archive, valid for 30 minutes.POST …/sources/:versionId/tag— body{ "tag": "manual" | "published", "action": "add" | "remove" }.PATCH …/sources/:versionId— updates the tags and/or note (three-state behavior: a string overwrites,nullclears, an absent field leaves it unchanged).DELETE …/sources/:versionId— soft-delete. A version with themanualorpublishedtag is protected — it returns409 PROTECTED_BY_TAG; remove the tag viaPATCHfirst.POST …/sources/cleanup— bulk cleanup: keeps thekeepLatestmost recent versions plus versions with themanual/publishedtags.
Every version in this family's responses carries a serverContext field (see "List versions" → data.versions[].serverContext) — the server-level display context.
Deploy from a snapshot
POST /v1/infra/servers/:id/deploy accepts the source.versionId field — an alternative to source.url and source.content. It lets you deploy to the server exactly the version that was saved via save_sources, without a separate file upload.
{
"source": {
"versionId": "v3"
},
"start": "node server.js"
}
The server resolves versionId into a signed link to the archive and performs the deploy. After a successful or failed deploy, the snapshot's linkedDeployId field is updated automatically.
Standalone virtual machines only. Deploy by source.versionId works on servers whose kind is STANDALONE. A Galaxy app, whose kind is GALAXY_APP, accepts sources on deploy only as inline source.content — a request with source.versionId or source.url is rejected with 400 GALAXY_DEPLOY_CONTENT_ONLY. See Galaxy apps for details.
Restriction: the server must match an application authorization key (vibe_app_*). Personal and management keys without a linked appId cannot resolve a snapshot — 400 SOURCE_VERSION_REQUIRES_APP is returned.
Behavior before publishing
Described above in the "Snapshot presence guarantee before publishing" section. Reference of POST /v1/apps/:id/publish error codes specific to the snapshot check:
| HTTP | Code | When returned |
|---|---|---|
| 409 | SNAPSHOT_REQUIRED |
There is no snapshot or it is older than 10 minutes (only when saving is enabled). The response includes a hint field describing the action. |
Checking Bitrix24 account state
GET /v1/me returns the data.capabilities.apps.sourceStorage block:
When saving is enabled:
{
"capabilities": {
"apps": {
"sourceStorage": {
"enabled": true,
"requiredBeforeDeploy": false,
"automaticOnDeploy": true,
"freshnessWindowMinutes": 10,
"limits": {
"maxBlobBytes": 209715200
}
}
}
}
}
When saving is disabled:
{
"capabilities": {
"apps": {
"sourceStorage": {
"enabled": false,
"requiredBeforeDeploy": false,
"automaticOnDeploy": false,
"disabledBy": "platform",
"readOperations": "available",
"reactivation": {
"scope": "platform",
"permission": "platform admin"
}
}
}
}
}
The disabledBy field — "platform" (the feature is not enabled at the platform level) or "portal" (the Bitrix24 account owner disabled it). The readOperations: "available" field means that listing versions and downloading remain available even when saving is disabled. The reactivation block indicates at which level (platform or Bitrix24 account) the feature is enabled and which permissions are required for it.
Disabling for a Bitrix24 account
The Bitrix24 account owner can disable source saving via the administration section (PATCH /api/admin/source-storage with the body { "sourceStorageEnabled": false }, requires an administrator session). When disabled:
POST /v1/apps/:id/sourcesreturns200 { skipped: true, reason: "DISABLED_FOR_PORTAL" }.POST /v1/apps/:id/publishdoes not check for a snapshot.GET /v1/apps/:id/sourcescontinues to return previously saved versions.GET /v1/apps/:id/sources/:versionId/downloadremains available.
The save history survives re-enabling — previously created versions remain available.
Behavior for AI models
The save_sources MCP tool wraps POST /v1/apps/:id/sources. The load_sources tool downloads and unpacks the latest snapshot into a file tree.
Since 2026-05-23 an explicit save_sources call before deployment is not required — the platform saves the bytes automatically. save_sources remains needed only for the three explicit scenarios (the "Saving sources" section above).
The channels through which the model learns about source saving:
- The descriptions of the
save_sourcesandload_sourcesMCP tools — visible on first contact. - The hint in the
409 SNAPSHOT_REQUIREDresponse (deployment with an external URL) — suggests uploading viaPOST /sourcesor passingX-Skip-Source-Snapshot. - The hint in the
409 SNAPSHOT_REQUIREDresponse (publishing without a fresh snapshot) — directs into thepublish → save_sources → publishcycle (relevant if the source is an external URL and auto-saving was skipped). - The
capabilities.apps.sourceStorageblock in theGET /v1/meresponse — a programmatic state check.
Source registry
The "App sources" page is available in your account via the left menu (the package icon). It shows a consolidated list of all deposited snapshots for convenient code handoff to a new developer or resuming an AI session.
Who sees what:
- Bitrix24 account member (MEMBER) — only their own snapshots (the ones they uploaded).
- Bitrix24 account administrator (ADMIN) — all snapshots of the Bitrix24 account, including snapshots of other members. Can download any snapshot.
How to download:
- Open "App sources" in the menu.
- Find the row you need (server or application).
- Click the "Download latest version" button — the browser will download the archive of the latest version.
For legacy applications (rows with the "Application" type) you can also open "Storage" to view the version history.
Note: the page works independently of the state of the platform deposit toggle — historical snapshots are available even if new saves are temporarily disabled.
Aggregated source registry — `GET /v1/me/sources`
The API twin of the "App sources" cabinet page. Returns a list of source-snapshot owners (servers and legacy apps) across everything the calling key owns. An account administrator (ADMIN) key sees the whole account.
Requires a portal-bound API key. A management key or a key with no portal binding → 403 PORTAL_KEY_REQUIRED.
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page |
number | 1 |
Page number. |
limit |
number | 25 |
Page size, maximum 100. |
search |
string | — | Filter by title / server name / owner name. |
Response
HTTP 200:
{
"success": true,
"data": [
{
"kind": "server",
"ownerKey": "8de64f8d-...",
"title": "Prod",
"server": {
"id": "8de64f8d-...",
"name": "srv-prod",
"displayName": "Prod",
"mode": "blackhole",
"status": "sleeping",
"blackholeStatus": "DISCONNECTED",
"deleted": false
},
"app": null,
"user": { "id": "8f1a2b3c-...", "name": "Alex" },
"latestVersionId": "v4",
"latestSavedAt": "2026-05-21T10:15:30.000Z",
"versionsCount": 4,
"totalSizeBytes": 552960,
"reachableViaApi": true,
"listEndpoint": "/v1/infra/servers/8de64f8d-.../sources",
"latestDownloadEndpoint": "/v1/infra/servers/8de64f8d-.../sources/v4/download"
}
],
"total": 1,
"page": 1,
"limit": 25
}
Row fields
| Field | Type | Description |
|---|---|---|
kind |
string | server or legacy-app. |
ownerKey |
string | Owner id of the row: the server id (kind: server) or the app id (kind: legacy-app). |
title |
string | Display title (server or application). |
server |
object | null | For kind: server: id, name, displayName, mode, status (lowercase), blackholeStatus, deleted. Otherwise null. |
app |
object | null | For kind: legacy-app: id, title. Otherwise null. |
user |
object | null | Owner: id, name. May be null. |
latestVersionId |
string | Identifier of the most recent version (vN). |
latestSavedAt |
string | Save time of the latest version (ISO 8601). |
versionsCount |
number | Number of versions. |
totalSizeBytes |
number | Total size of the archives in bytes. |
reachableViaApi |
boolean | Whether the record is reachable via the V1 endpoints (see below). |
listEndpoint |
string | null | Ready-to-use path to list the owner's versions, or null. |
latestDownloadEndpoint |
string | null | Ready-to-use path to download the latest version, or null. |
reachableViaApi.false(withlistEndpoint/latestDownloadEndpointequal tonull) for a server whose managing key was deleted (an orphan record) or that was itself soft-deleted, and for a soft-deleted legacy app. The row is still listed — for visibility — but the V1 per-owner drill-in would return404.- Server status. For a Black Hole server,
status: "sleeping"is the normal "parked" state (the server wakes on demand), not a failure. Pair it withblackholeStatusto tell "parked, wakes on demand" from a real problem. The value is the fleet-poller-maintained DB status (eventually consistent, ~15 s), not a live provider reconcile. - Contrast with
GET /v1/infra/servers. That list is scoped strictly to the calling key's servers, so a server bound to another of the owner's keys is absent there but present in this registry.