# 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

1. You iterate on the application and run a deploy.
2. The platform saves a snapshot of the sources by itself — the next version: `v1`, `v2`, `v3`, …
3. At any time, view the list of versions and download the one you need.
4. 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 `manual` or `published` label: 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 version
- `POST /v1/infra/servers/:id/deploy { source: { url: <signed URL from storage> } }` — the existing version is linked to the deployment identifier
- `POST /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](/docs/infra/galaxy) 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:

```json
{
  "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:

1. **Mark a version** — set a `manual` or `published` tag so the version is stored indefinitely.
2. **Save without deploying** — save an intermediate result for handoff to another developer.
3. **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):

1. Changed the code → `POST /v1/infra/servers/:id/deploy` — sources are saved automatically.
2. `POST /v1/apps/:id/publish` — publishes the application in the Bitrix24 catalog. The `published` tag is added to the snapshot automatically.
3. Repeated the cycle on the next change.

Scenario for handing the project to a new developer or a new AI session:

1. `GET /v1/apps/:id/sources` — list of available versions.
2. `GET /v1/apps/:id/sources/:versionId/download` — signed link to the archive.
3. 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:

```json
{
  "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):

- `sourceVersionId` in the body — format `v<N>`, for example `"sourceVersionId": "v3"`.
- The `x-source-version: v3` header — an alternative to the body.

## Version lifetime (retention policy)

After saving, a version goes through automatic cleanup on a Grandfather-Father-Son schedule:

1. **The last 5 versions** are always kept, regardless of tags.
2. **A daily grid over 14 days** — one version for each calendar day (UTC).
3. **A weekly grid over 4 weeks** — one version per every 7 days.
4. **The `published` and `manual` tags** — 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-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 itself.

### Path parameters

| Parameter | Type | Description |
|----------|-----|----------|
| `id` (path) | UUID | Application identifier. Get it via [`GET /v1/apps`](./apps.md). |

### 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`:

```json
{
  "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:

```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 Bitrix24 account). In either case `POST /v1/apps/:id/publish` does not check for a snapshot.

### Examples

#### curl — tar.gz

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

#### curl — zip

```bash
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

```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`:

```json
{
  "success": true,
  "data": {
    "versionId": "v3",
    "tags": ["manual"],
    "note": "Final version before release"
  }
}
```

### Examples

#### curl — add the `manual` tag

```bash
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

```bash
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

```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`:

```json
{
  "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:

```json
{
  "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

```bash
curl -H "X-Api-Key: YOUR_APP_KEY" \
  https://vibecode.bitrix24.com/v1/apps/<APP_ID>/sources
```

#### JavaScript

```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`:

```json
{
  "success": true,
  "data": {
    "url": "https://<storage-endpoint>/...",
    "expiresAt": "2026-05-21T10:45:30.000Z"
  }
}
```

### Examples

#### curl

```bash
# 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

```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 after `POST /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`:

```json
{
  "success": true,
  "data": {
    "versionId": "v3",
    "tags": ["manual"]
  }
}
```

### Examples

#### curl

```bash
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

```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`:

```json
{
  "success": true,
  "data": { "versionId": "v3" }
}
```

### Examples

#### curl

```bash
curl -X DELETE https://vibecode.bitrix24.com/v1/apps/<APP_ID>/sources/v3 \
  -H "X-Api-Key: YOUR_APP_KEY"
```

#### JavaScript

```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`:

```json
{
  "success": true,
  "data": {
    "deletedVersions": ["v2", "v1"]
  }
}
```

### Examples

#### curl

```bash
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

```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 as `POST /v1/apps/:id/sources`: the raw archive bytes in the body, `Content-Type` one of `application/gzip`, `application/x-tar`, `application/zip`, `application/octet-stream`, optional `X-Filename` / `X-Tags` / `X-Note` / `X-AI-Session-Id` headers, a 200 MB body cap, deduplication by `sha256`.
- **`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 a `versions[]` 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, `null` clears, an absent field leaves it unchanged).
- **`DELETE …/sources/:versionId`** — soft-delete. A version with the `manual` or `published` tag is protected — it returns `409 PROTECTED_BY_TAG`; remove the tag via `PATCH` first.
- **`POST …/sources/cleanup`** — bulk cleanup: keeps the `keepLatest` most recent versions plus versions with the `manual` / `published` tags.

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.

```json
{
  "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](/docs/infra/galaxy) 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:

```json
{
  "capabilities": {
    "apps": {
      "sourceStorage": {
        "enabled": true,
        "requiredBeforeDeploy": false,
        "automaticOnDeploy": true,
        "freshnessWindowMinutes": 10,
        "limits": {
          "maxBlobBytes": 209715200
        }
      }
    }
  }
}
```

When saving is disabled:

```json
{
  "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/sources` returns `200 { skipped: true, reason: "DISABLED_FOR_PORTAL" }`.
- `POST /v1/apps/:id/publish` does not check for a snapshot.
- `GET /v1/apps/:id/sources` continues to return previously saved versions.
- `GET /v1/apps/:id/sources/:versionId/download` remains 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_sources` and `load_sources` MCP tools — visible on first contact.
- The hint in the `409 SNAPSHOT_REQUIRED` response (deployment with an external URL) — suggests uploading via `POST /sources` or passing `X-Skip-Source-Snapshot`.
- The hint in the `409 SNAPSHOT_REQUIRED` response (publishing without a fresh snapshot) — directs into the `publish → save_sources → publish` cycle (relevant if the source is an external URL and auto-saving was skipped).
- The `capabilities.apps.sourceStorage` block in the `GET /v1/me` response — 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:**
1. Open "App sources" in the menu.
2. Find the row you need (server or application).
3. 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`:

```json
{
  "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` (with `listEndpoint` / `latestDownloadEndpoint` equal to `null`) 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 return `404`.
- **Server status.** For a Black Hole server, `status: "sleeping"` is the normal "parked" state (the server wakes on demand), not a failure. Pair it with `blackholeStatus` to 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.

## See also

- [Keys and authorization](./keys-auth.md)
- [Error codes](./errors.md)
- [MCP for AI](./mcp.md)
- [Application deployment](./infra/deploy.md)
