Для AI-агентов: markdown этой страницы — /docs-content/source-storage/save.md индекс документации — /llms.txt
Сохранить снапшот
Явное сохранение архива исходников: когда нужно пометить версию тегом, зафиксировать промежуточный результат без развёртывания или подготовить откат. При деплое снапшот создаётся автоматически.
Обзор хранилища и справочник эндпоинтов — Хранилище исходного кода.
Сохранение снапшота
POST /v1/apps/:id/sources
Принимает сырые байты архива исходного кода в теле запроса. Формат определяется заголовком Content-Type. Метаданные (теги, комментарий, имя файла) передаются через дополнительные заголовки.
Только бинарная загрузка (контракт v2). Эндпоинт принимает сырые байты архива в теле запроса (
--data-binary).multipart/form-dataбольше не поддерживается — такой запрос возвращает400 INVALID_CONTENT_TYPE. Ранние версии клиентов, которые слалиmultipart/form-dataс полями формы (-F "tag=manual"), нужно обновить: тело — это сами байты архива, а теги, комментарий и имя файла передаются заголовкамиX-Tags/X-Note/X-Filename(не полями формы и не query-параметрами). MCP-клиенты используют инструментsave_sources, который сам упаковывает и отправляет архив в правильном формате.
Параметры пути
| Параметр | Тип | Описание |
|---|---|---|
id (path) |
UUID | Идентификатор приложения. Получить — GET /v1/apps. |
Заголовки запроса
| Заголовок | Обязательный | Описание |
|---|---|---|
Content-Type |
да | Формат архива. Допустимые значения: application/gzip, application/x-tar, application/zip, application/octet-stream. Любое другое значение → 400 INVALID_CONTENT_TYPE. Заявленный формат сверяется с первыми байтами архива: прямое противоречие (application/gzip против zip-байт и наоборот) → 415 UNSUPPORTED_ARCHIVE_FORMAT. |
Content-Length |
да | Размер тела в байтах. Загрузка без него (Transfer-Encoding: chunked) не принимается → 411 MISSING_CONTENT_LENGTH. |
X-Filename |
нет | Произвольное имя файла для отображения (например, app-v1.tar.gz). Если не указан — имя выводится из Content-Type (например, source.tar.gz для application/gzip). Символы вне набора a-zA-Z0-9._- → 400 INVALID_FILENAME. |
X-Tags |
нет | Теги через запятую. Распознаются manual и published — они защищают версию от автоматической очистки. |
X-Note |
нет | Произвольный комментарий, который сохраняется в записи о версии. |
X-AI-Session-Id |
нет | Идентификатор AI-сессии. Группирует снапшоты в манифесте по сессии. |
Тело запроса
Сырые байты архива, не multipart/form-data. Максимальный размер — 500 МБ.
Тело принимается потоком, поэтому длину нужно объявить заранее: запрос обязан нести Content-Length. Загрузка чанками (Transfer-Encoding: chunked, без длины) отклоняется кодом 411 MISSING_CONTENT_LENGTH — платформа не станет копить архив в памяти, чтобы измерить его за клиента. Заявленная длина сверх потолка отбивается 413 ещё до чтения тела.
В curl длина проставляется сама, если тело берётся из файла (--data-binary @app.tar.gz). Ручной поток из пайпа (... | curl --data-binary @-) длины не даёт — сохраните архив во временный файл или задайте Content-Length явно.
Ответ при успешном сохранении
HTTP 201:
{
"success": true,
"data": {
"versionId": "v3",
"id": "cmszu9y12190j4bmj3hhsno39",
"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
}
}
Поля ответа
| Поле | Тип | Описание |
|---|---|---|
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. |
data.note |
string | null | Комментарий из заголовка X-Note или null. |
data.deduplicated |
boolean | true, если архив с тем же содержимым уже был сохранён у этого владельца — возвращена существующая версия. |
Идемпотентность
Повторное сохранение архива с тем же содержимым даёт тот же sha256 и не создаёт новую версию. Эндпоинт возвращает HTTP 201 с deduplicated: true и versionId уже существующей версии. Если при повторном сохранении переданы новые теги или комментарий — они добавляются к существующей версии (теги объединяются, комментарий перезаписывается).
Область действия — один владелец. Совпадение ищется среди версий того же приложения для POST /v1/apps/:id/sources и того же сервера для POST /v1/infra/servers/:id/sources. Один и тот же архив, сохранённый на двух разных серверах или под двумя приложениями, даёт две версии и два объекта в хранилище — у каждого владельца своя история версий.
Совпадение определяется по содержимому, а значит — только после того, как тело целиком принято: платформа считает sha256 на лету поверх приходящего потока. Поэтому повторное сохранение того же архива идёт столько же времени и передаёт столько же байт, сколько первое. Результат при этом прежний: новая версия не создаётся, в ответе deduplicated: true.
Дедуплицированное сохранение проверка перед POST /v1/apps/:id/publish принимает наравне с настоящим: сохранение отмечает версию как заново предъявленную, и возраст снапшота отсчитывается от этой отметки. Поле data.timestamp при этом остаётся временем создания версии — оно не сдвигается, поэтому имя файла в депо и место версии в политике хранения не меняются. Если после 409 SNAPSHOT_REQUIRED вы сохранили те же самые исходники, повторный publish пройдёт — менять байты, чтобы обновить снапшот, не нужно.
Ответ при отключённом сохранении
HTTP 200 — снапшот не создан:
{
"success": true,
"data": {
"skipped": true,
"reason": "DISABLED_GLOBALLY"
}
}
reason — "DISABLED_GLOBALLY" (платформа не включила функцию) или "DISABLED_FOR_PORTAL" (владелец портала отключил для своего портала). При любом из вариантов POST /v1/apps/:id/publish не проверяет наличие снапшота.
Примеры
curl — tar.gz
curl -X POST https://vibecode.bitrix24.tech/v1/apps/<APP_ID>/sources \
-H "X-Api-Key: YOUR_APP_KEY" \
-H "Content-Type: application/gzip" \
-H "X-Note: Добавил OAuth-флоу" \
-H "X-Tags: manual" \
--data-binary @app-sources.tar.gz
curl — zip
curl -X POST https://vibecode.bitrix24.tech/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.tech/v1/apps/${appId}/sources`,
{
method: 'POST',
headers: {
'X-Api-Key': process.env.VIBE_APP_KEY,
'Content-Type': 'application/gzip',
'X-Note': 'Добавил OAuth-флоу',
},
body: archive,
},
)
const json = await res.json()
console.log(json.data.versionId, 'deduplicated:', json.data.deduplicated)
Коды ошибок
| 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 |
Недостаточно средств на балансе — запись в хранилище приостановлена. Пополните баланс и повторите. |
| 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 |
Хранилище временно недоступно (сбой выдачи временных ключей доступа) — повторите запрос. |
Полный справочник кодов — Коды ошибок.