# Source code storage

Source code storage automatically saves snapshots of your application's code on every deploy. If someone else or a new AI session starts working on the application, they can download the latest version and continue from the same point — the code is not lost.

## Documentation sections

- [Automatic save on deploy](/docs/source-storage/auto-save) — what goes into a snapshot automatically, the `source` block in the deploy response, deploying from a saved version
- [Snapshot before publishing](/docs/source-storage/publish) — what is checked before publishing, the `409 SNAPSHOT_REQUIRED` refusal, publishing sources from a server
- [Save a snapshot](/docs/source-storage/save) — `POST /v1/apps/:id/sources`, the metadata headers, deduplication
- [List versions](/docs/source-storage/versions) — the application's version history and linked server versions
- [Get version metadata](/docs/source-storage/versions-get) — one version by its identifier
- [Download the archive](/docs/source-storage/versions-download) — a signed link to the version's archive
- [Version tags and notes](/docs/source-storage/metadata) — `PATCH` for metadata and setting a tag
- [Delete a version](/docs/source-storage/delete) — deleting a version by its identifier
- [Clear old versions](/docs/source-storage/cleanup) — bulk cleanup of the version history
- [Version lifetime and cleanup](/docs/source-storage/retention) — retention policy, versions of a deleted server, objects in storage
- [Server-keyed source endpoints](/docs/source-storage/servers) — the same family keyed by the server
- [Source registry and Bitrix24 account state](/docs/source-storage/registry) — `GET /v1/me/sources`, the page in your Vibecode account, `capabilities.apps.sourceStorage`

## Why it matters

When you are refining an application, it is important not to lose the working version of the code. Source code storage does that automatically: on every successful deploy the platform saves a snapshot of the sources. If someone else or a new AI session starts working on the application later, they download the latest version and continue from the same point, without manually passing archives around.

A snapshot belongs to the application rather than to a particular 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. Source code storage covers a narrower need — a safety net and project handoff.

| | Source code storage | Git / GitHub |
|---|---|---|
| What it stores | whole archive snapshots of the code | change history line by line |
| When it saves | automatically on every deploy | manually, on a developer's command |
| Git skills required | no | yes |
| Branches, merges, version comparison | no | yes |

If you need team development with branches and history, use Git. If you just need to avoid losing working code and to be able to return to a previous version, storage is enough.

## How it works

1. You refine the application and start a deploy.
2. The platform saves a source snapshot on its own — the next version: `v1`, `v2`, `v3`, …
3. At any time you can list the versions and download the one you need.
4. Before the application is published, the platform checks that a snapshot has been saved.

```
edit code  →  deploy  →  snapshot vN saved automatically
                                      ↓
        list 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 sequence number of a snapshot (`v1`, `v2`, …). The newest one is considered current.
- **Tag** — a `manual` or `published` label: marks a version as important so automatic cleanup does not delete it.
- **Deduplication** — if the code has not changed, no new snapshot is created and the existing version is returned. Applies within a single owner — a server or an application.

## Access

**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:** 500 MB per request.

## Save contract

The body of `POST …/sources` is the **raw archive bytes**, not `multipart/form-data`. Metadata travels in headers, not as form fields and not as query parameters.

| Header | Required | Description |
|-----------|--------------|----------|
| `Content-Type` | yes | An archive format from the list above. Any other value → `400 INVALID_CONTENT_TYPE` |
| `Content-Length` | yes | Body size in bytes. Without it (`Transfer-Encoding: chunked`) → `411 MISSING_CONTENT_LENGTH` |
| `X-Filename` | no | Display filename. If it is not provided, the name is derived from `Content-Type`. Characters outside `a-zA-Z0-9._-` → `400 INVALID_FILENAME` |
| `X-Tags` | no | Comma-separated tags. `manual` and `published` are recognized — they protect the version from automatic cleanup |
| `X-Note` | no | A note stored with the version record |
| `X-AI-Session-Id` | no | AI session identifier — groups snapshots in the manifest |

```bash
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-Tags: manual" \
  -H "X-Note: Added OAuth flow" \
  --data-binary @app-sources.tar.gz
```

**The code source for a deploy** — the `source` field of `POST /v1/infra/servers/:id/deploy` accepts three mutually exclusive forms:

| Form | What it is |
|-------|---------|
| `source.content` | An inline base64 archive — always works, including on a Galaxy app |
| `source.url` | A signed link from Vibecode storage. On a Galaxy app this must be enabled for you separately |
| `source.versionId` | An already saved version of the form `vN`. On a Galaxy app this must be enabled for you separately |

The full walkthrough of saving — [Save a snapshot](/docs/source-storage/save). Auto-save and deploying from a version — [Automatic save on deploy](/docs/source-storage/auto-save).

## Quick start

Handing the project over to another developer or a new AI session takes three calls:

```bash
# 1. What is already saved for this application
curl -H "X-Api-Key: YOUR_API_KEY" \
  https://vibecode.bitrix24.com/v1/apps/<APP_ID>/sources

# 2. A link to the archive of the version you need (valid for 30 minutes)
curl -H "X-Api-Key: YOUR_API_KEY" \
  https://vibecode.bitrix24.com/v1/apps/<APP_ID>/sources/v1/download

# 3. Download the archive using that link — no additional headers
curl -o source-v1.tar.gz "<url from the response above>"
```

## Full example

One continuous scenario: save a snapshot, list the versions, download the archive from its link and verify the checksum, then delete the version. The code runs in Node.js 18 or later: save it in an `.mjs` file, place the `app-sources.tar.gz` archive next to it, replace `<APP_ID>` with your application ID, and pass the key in the `VIBE_KEY` environment variable.

```javascript
import { readFile } from 'node:fs/promises'
import { createHash } from 'node:crypto'

const VIBE_KEY = process.env.VIBE_KEY
const SOURCES = 'https://vibecode.bitrix24.com/v1/apps/<APP_ID>/sources'

async function api(method, path = '', init = {}) {
  const res = await fetch(`${SOURCES}${path}`, {
    ...init,
    method,
    headers: { 'X-Api-Key': VIBE_KEY, ...init.headers },
  })
  const json = await res.json()
  if (!json.success) {
    throw new Error(`${method} ${path} → ${res.status} ${json.error.code}`)
  }
  return json.data
}

// 1. Save a snapshot: the body is the raw archive bytes,
//    fetch sets the body length itself
const archive = await readFile(
  new URL('./app-sources.tar.gz', import.meta.url),
)
const saved = await api('POST', '', {
  headers: { 'Content-Type': 'application/gzip' },
  body: archive,
})
if (saved.skipped) {
  throw new Error(`Source storage is disabled: ${saved.reason}`)
}
console.log(
  'Version:', saved.versionId,
  '| deduplicated:', saved.deduplicated,
)

// 2. List the application's versions
const list = await api('GET')
console.log(
  'Total versions:', list.totalVersions,
  '| current:', list.currentVersionId,
)

// 3. Get a link to the archive and download it —
//    the link is valid for 30 minutes and needs no key
const { url } = await api('GET', `/${saved.versionId}/download`)
const bytes = Buffer.from(await (await fetch(url)).arrayBuffer())
const sha256 = createHash('sha256').update(bytes).digest('hex')
console.log('Archive matches the saved one:', sha256 === saved.sha256)

// 4. Delete the version if this run created it. With deduplicated: true
//    the save returned an existing version with the same bytes —
//    the example leaves it alone
if (!saved.deduplicated) {
  const deleted = await api('DELETE', `/${saved.versionId}`)
  console.log('Deleted version:', deleted.versionId)
}
```

## Endpoint reference

**Application snapshots:**

| Method | Path | Action |
|-------|------|--------|
| `POST` | `/v1/apps/:id/sources` | [Save a snapshot](/docs/source-storage/save) |
| `GET` | `/v1/apps/:id/sources` | [List versions](/docs/source-storage/versions) |
| `GET` | `/v1/apps/:id/sources/:versionId` | [Get version metadata](/docs/source-storage/versions-get) |
| `GET` | `/v1/apps/:id/sources/:versionId/download` | [Download the archive](/docs/source-storage/versions-download) |
| `PATCH` | `/v1/apps/:id/sources/:versionId` | [Update version metadata](/docs/source-storage/metadata-update) |
| `POST` | `/v1/apps/:id/sources/:versionId/tag` | [Set or remove a tag](/docs/source-storage/metadata-tag) |
| `DELETE` | `/v1/apps/:id/sources/:versionId` | [Delete a version](/docs/source-storage/delete) |
| `POST` | `/v1/apps/:id/sources/cleanup` | [Clear old versions](/docs/source-storage/cleanup) |

**Server snapshots** — the same family keyed by the server. The differences and the full list of paths are on the [Server-keyed source endpoints](/docs/source-storage/servers) page.

**Aggregated registry:** `GET /v1/me/sources` — every snapshot owner the key can reach, see [Source registry](/docs/source-storage/registry).

## Error codes

Refusals specific to source storage:

| HTTP | Code | When returned |
|------|-----|---------------------|
| 400 | `INVALID_CONTENT_TYPE` | `Content-Type` is not in the list of allowed archive formats |
| 400 | `INVALID_SHA256` | The `sha256` parameter is not exactly 64 hexadecimal characters |
| 400 | `INVALID_VERSION_ID` | The `versionId` format does not match `v<non-negative integer>` |
| 400 | `INVALID_KEEP_LATEST` | The `keepLatest` value is not a non-negative integer |
| 400 | `SOURCE_VERSION_REQUIRES_APP` | Deploy by `versionId` onto a server owned by another key, and the server's owner is not bound to an application |
| 403 | `NOT_AUTHORIZED` | The key is neither an application key, the author's personal key, nor a portal administrator key |
| 403 | `PORTAL_KEY_REQUIRED` | The key is not bound to a portal — the aggregated registry is unavailable to such a key |
| 403 | `SOURCE_APP_ID_MISMATCH` | An authorization key `vibe_app_…` targets snapshots of a different application |
| 403 | `INFRA_FORBIDDEN_FOR_COWORK_KEY` | A save, a metadata edit, or a delete was made with a Cowork key — [Project key for deploy](/docs/cowork/deploy-key) |
| 403 | `EXTERNAL_COLLABORATOR_KEY_SERVER_MISMATCH` | The 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 |
| 403 | `EXTERNAL_COLLABORATOR_NOT_A_MEMBER` | An 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 |
| 404 | `APP_NOT_FOUND` | The application does not exist, was deleted, or belongs to another portal |
| 404 | `VERSION_NOT_FOUND` | No version with that `versionId` exists, or it was deleted |
| 404 | `SOURCE_VERSION_NOT_FOUND` | The version was not found in either the server's context or the application's |
| 409 | `SNAPSHOT_REQUIRED` | Publishing without a saved snapshot, or a deploy from an external URL |
| 409 | `PROTECTED_BY_TAG` | The version is protected by the `manual` or `published` tag |
| 410 | `SOURCE_VERSION_BYTES_PURGED` | The version record still exists, but the bytes have already been purged from storage |
| 411 | `MISSING_CONTENT_LENGTH` | The save request arrived without `Content-Length` |
| 415 | `UNSUPPORTED_ARCHIVE_FORMAT` | The first bytes of the archive contradict the declared `Content-Type` |
| 502 | `SOURCE_DOWNLOAD_URL_FAILED` | Storage is temporarily unavailable — retry the request |

The full reference of API error codes — [Error codes](/docs/errors).

## See also

- [Keys and authorization](/docs/keys-auth)
- [Error codes](/docs/errors)
- [MCP for AI](/docs/mcp)
- [Deploy API](/docs/infra/deploy)
- [Storage](/docs/storage)
- [Delete a server](/docs/infra/servers/delete)
