Для AI-агентов: markdown этой страницы — /docs-content/source-storage/save.md индекс документации — /llms.txt

Сохранить снапшот

POST /v1/apps/:id/sources

Явное сохранение архива исходников: когда нужно пометить версию тегом, зафиксировать промежуточный результат без развёртывания или подготовить откат.

При деплое снапшот создаётся автоматически.

Параметры

Параметр Тип Обяз. Описание
id (path) string да Идентификатор приложения. Получить — GET /v1/apps.
Content-Type (header) string да Формат архива. Допустимые значения: application/gzip, application/x-tar, application/zip, application/octet-stream. Любое другое значение → 400 INVALID_CONTENT_TYPE. Заявленный формат сверяется с первыми байтами архива: прямое противоречие (application/gzip против zip-байт и наоборот) → 415 UNSUPPORTED_ARCHIVE_FORMAT.
Content-Length (header) number да Размер тела в байтах. Загрузка без него (Transfer-Encoding: chunked) не принимается → 411 MISSING_CONTENT_LENGTH.
X-Filename (header) string нет Произвольное имя файла для отображения, например app-v1.tar.gz. Если не указан — имя выводится из Content-Type, для application/gzip это source.tar.gz. Символы вне набора a-zA-Z0-9._- → 400 INVALID_FILENAME.
X-Tags (header) string нет Теги через запятую, пробелы вокруг запятых отбрасываются. В теге — латиница, цифры, _ и -, иначе 400 INVALID_TAGS. manual и published защищают версию от автоматической очистки, остальные теги — произвольные метки.
X-Note (header) string нет Произвольный комментарий, который сохраняется в записи о версии. fetch в JavaScript пропускает в заголовках только символы Latin-1: кириллица в X-Note даст TypeError ещё до отправки запроса.
X-AI-Session-Id (header) string нет Идентификатор AI-сессии. Группирует снапшоты в манифесте по сессии.

Поля запроса (body)

Тело — сырые байты архива, не multipart/form-data. Максимальный размер — 500 МБ.

Тело принимается потоком, поэтому длину нужно объявить заранее: запрос обязан нести Content-Length. Загрузка чанками (Transfer-Encoding: chunked, без длины) отклоняется кодом 411 MISSING_CONTENT_LENGTH — платформа не станет копить архив в памяти, чтобы измерить его за клиента. Заявленная длина сверх потолка отбивается 413 ещё до чтения тела.

В curl длина проставляется автоматически при --data-binary @app.tar.gz и при чтении из стандартного ввода через --data-binary @-.

Примеры

Примеры JavaScript выполняются в Node.js: сохраните код в файл с расширением .mjs. Подставьте идентификатор приложения вместо <APP_ID> и положите архив app-sources.tar.gz рядом с файлом примера.

curl — личный ключ

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

curl — OAuth-приложение

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

JavaScript — личный ключ

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

const appId = '<APP_ID>'
const archive = await readFile(new URL('./app-sources.tar.gz', import.meta.url))

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

JavaScript — OAuth-приложение

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

const appId = '<APP_ID>'
const archive = await readFile(new URL('./app-sources.tar.gz', import.meta.url))

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

Для zip-архива в тех же примерах меняется одно значение: Content-Type: application/zip, тело — байты zip-файла.

Поля ответа

Поле Тип Описание
success boolean true при успешном ответе, в том числе при пропуске сохранения.
data object Результат сохранения: поля версии или skipped и reason, если сохранение отключено.
data.versionId string Идентификатор версии вида v<N>.
data.id string Внутренний идентификатор записи снапшота. Приходит только в ответе на сохранение: список версий и метаданные одной версии этого поля не отдают.
data.filename string Имя файла архива в хранилище.
data.contentType string Тип архива: application/gzip, application/zip, application/x-tar или application/octet-stream.
data.sha256 string SHA-256 содержимого архива. Используется для дедупликации в пределах владельца.
data.size number Размер архива в байтах.
data.timestamp string Время сохранения (ISO 8601, UTC).
data.tags string[] Теги версии. manual и published защищают её от автоматической очистки, остальные — произвольные метки из X-Tags. Если архив уже был сохранён (deduplicated: true), теги из запроса добавляются к тегам существующей версии.
data.note string | null Комментарий из заголовка X-Note или null. При повторном сохранении того же архива переданный комментарий заменяет предыдущий.
data.deduplicated boolean true, если архив с тем же содержимым уже был сохранён у этого владельца — возвращена существующая версия.
data.skipped boolean Приходит вместо полей версии, когда сохранение отключено. Всегда true.
data.reason string Причина пропуска: DISABLED_GLOBALLY или DISABLED_FOR_PORTAL. Приходит вместе с skipped.

Пример ответа

HTTP 201 — снапшот сохранён. При повторной отправке того же архива статус также 201, а data.deduplicated — true:

JSON
{
  "success": true,
  "data": {
    "versionId": "v3",
    "id": "cmuye5vkc0kd74bou8q1fnreo",
    "filename": "2026-10-07T17-38-38-988Z-v3-manual.tar.gz",
    "contentType": "application/gzip",
    "sha256": "4cd663d7b491eb3dcbe46f433a3822077410873e302737ce093c84c7a915af08",
    "size": 159,
    "timestamp": "2026-10-07T17:38:38.988Z",
    "tags": ["manual"],
    "note": "OAuth sign-in",
    "deduplicated": false
  }
}

HTTP 200 — сохранение отключено, снапшот не создан:

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

reason — "DISABLED_GLOBALLY" (платформа не включила функцию) или "DISABLED_FOR_PORTAL" (владелец портала отключил для своего портала). При любом из вариантов POST /v1/apps/:id/publish не проверяет наличие снапшота.

Пример ответа при ошибке

400 — Content-Type не входит в список допустимых форматов архива:

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

Показаны основные поля hint. Полный ответ несёт ещё explanation с разбором формата, metadataHeaders со списком заголовков метаданных и exampleCurl с готовой командой.

Ошибки

HTTP Код Описание
400 INVALID_CONTENT_TYPE Content-Type не входит в список допустимых форматов архива. Формат multipart/form-data не принимается — отправляйте сырые байты архива.
400 INVALID_BLOB Тело запроса не является бинарным буфером, например передан текст.
400 INVALID_FILENAME Заголовок X-Filename содержит символы вне набора a-zA-Z0-9._-.
400 INVALID_TAGS Тег из заголовка X-Tags содержит недопустимые символы (разрешены a-zA-Z0-9_-).
400 STORAGE_UPLOAD_LENGTH_MISMATCH Объявленный Content-Length не совпал с фактическим размером тела. Проверяется в конце потока, поэтому отказ приходит после передачи байтов. Пересчитайте длину и повторите.
402 BILLING_INSUFFICIENT Недостаточно средств на балансе — запись в хранилище приостановлена. Пополните баланс и повторите.
507 STORAGE_QUOTA_EXCEEDED Снапшот не помещается в остаток потолка объёма хранилища портала. Потолок необязательный, включается платформой и действует только для порталов, у которых нет и никогда не было коммерческого тарифа Битрикс24. Считается весь портал, а не отдельное приложение.
403 SOURCE_APP_ID_MISMATCH Вызов сделан ключом авторизации vibe_app_…, выписанным на другое приложение. Такой ключ обращается только к снапшотам своего приложения, даже если оба приложения создал один автор.
403 NOT_AUTHORIZED Только автор приложения, OAuth-ключ приложения или администратор портала могут управлять снапшотами.
403 INFRA_FORBIDDEN_FOR_COWORK_KEY Вызов сделан ключом Cowork/Code — такой ключ работает только с данными, изменяющие операции ему закрыты. Что делать — Проектный ключ для деплоя.
404 APP_NOT_FOUND Приложение не существует, удалено или принадлежит другому порталу.
411 MISSING_CONTENT_LENGTH Запрос пришёл без Content-Length (загрузка чанками). Объявите длину тела и повторите.
413 — Размер архива превышает лимит в 500 МБ.
415 UNSUPPORTED_ARCHIVE_FORMAT Первые байты архива прямо противоречат заявленному Content-Type: объявлен application/gzip, а прислан zip, или наоборот. Тело ответа несёт error.hint.declared и error.hint.detected. Типы application/x-tar и application/octet-stream под этот отказ не попадают — их сигнатура не читается в первых байтах.
500 SOURCE_STORAGE_ERROR Внутренняя ошибка хранилища.
503 STORAGE_STS_UNAVAILABLE Хранилище временно недоступно (сбой выдачи временных ключей доступа) — повторите запрос.

Полный список общих ошибок API — Ошибки.

Известные особенности

Область действия дедупликации — один владелец. Совпадение ищется среди версий того же приложения для POST /v1/apps/:id/sources и того же сервера для POST /v1/infra/servers/:id/sources. Один и тот же архив, сохранённый на двух разных серверах или под двумя приложениями, даёт две версии и два объекта в хранилище — у каждого владельца своя история версий.

При повторном сохранении архив передаётся целиком. Платформа считает sha256 по мере получения данных и определяет совпадение только после приёма всего тела запроса.

Дедуплицированное сохранение снимает 409 SNAPSHOT_REQUIRED. Проверка перед POST /v1/apps/:id/publish принимает его наравне с настоящим: сохранение отмечает версию как заново предъявленную, и возраст снапшота отсчитывается от этой отметки. Поле data.timestamp при этом остаётся временем создания версии — оно не сдвигается, поэтому имя файла в депо и место версии в политике хранения не меняются. Если после 409 SNAPSHOT_REQUIRED вы сохранили те же самые исходники, повторная публикация пройдёт — менять байты, чтобы обновить снапшот, не нужно.

Смотрите также