Для 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 — личный ключ
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-приложение
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 — личный ключ
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-приложение
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:
{
"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 — сохранение отключено, снапшот не создан:
{
"success": true,
"data": {
"skipped": true,
"reason": "DISABLED_GLOBALLY"
}
}
reason — "DISABLED_GLOBALLY" (платформа не включила функцию) или "DISABLED_FOR_PORTAL" (владелец портала отключил для своего портала). При любом из вариантов POST /v1/apps/:id/publish не проверяет наличие снапшота.
Пример ответа при ошибке
400 — Content-Type не входит в список допустимых форматов архива:
{
"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 вы сохранили те же самые исходники, повторная публикация пройдёт — менять байты, чтобы обновить снапшот, не нужно.