# Хранилище

Хранилище файлов для Битрикс24-приложений: загружайте, управляйте видимостью и скачивайте по временным ссылкам.

**Скоуп:** `vibe:storage` | **Базовый URL:** `https://vibecode.bitrix24.tech/v1` | **Авторизация:** API-ключ (`X-Api-Key`)

[Авторизация](#авторизация) · [Жизненный цикл](#жизненный-цикл-объекта) · [Пути загрузки](#пути-загрузки) · [Видимость](#видимость-и-доступ) · [Ключ объекта](#ключ-объекта) · [Лимиты](#лимиты) · [Тарификация](#тарификация) · [Быстрый старт](#быстрый-старт) · [Полный пример](#полный-пример) · [Эндпоинты](#справочник-эндпоинтов) · [Ошибки](#коды-ошибок)

## Когда использовать

**Подходит для:**

- Аватаров пользователей и логотипов приложения
- Вложений в формах: фотографии, сканы, чеки
- Экспортов и сгенерированных отчётов (CSV, PDF)
- Видеовложений в CRM-сделках
- Резервных копий конфигурации приложения
- Промежуточных файлов в цепочке обработки данных

**Не подходит для:**

- Публикации файлов в Диск Битрикс24 портала — для этого предназначен раздел [Диск](/docs/entities/storages). Хранилище изолировано по приложению и не входит в дисковое дерево портала
- Хранения секретов и ключей — используйте переменные окружения сервера
- Журналирования и аудита приложения
- Сценариев без привязки к приложению — хранилище всегда изолировано по ключу

## Разделы документации

| Раздел | Описание |
|---|---|
| [Загрузка](/docs/storage/upload) | Три пути загрузки: прямая форма (≤10 МБ), предподписанный PUT (временно отключён), составные части (≤5 ТБ) |
| [Объекты](/docs/storage/objects) | Список, получение, метаданные, удаление объектов и публичный доступ |

## Авторизация

Все запросы к `/v1/storage/*` требуют API-ключ в заголовке `X-Api-Key` со скоупом `vibe:storage`.

В форме создания ключа скоуп `vibe:storage` отмечен заранее. Если у ключа его нет — добавьте скоуп в разделе **API-ключи** личного кабинета.

Отсутствие скоупа возвращает `403 STORAGE_SCOPE_REQUIRED` на любом вызове `/v1/storage/*`.

**Типы ключей**

| Тип ключа | Доступ | Область хранилища |
|---|---|---|
| `vibe_api_*` — личный API-ключ | да | пространство имён владельца ключа на портале |
| `vibe_app_*` — ключ приложения | да | пространство имён приложения, к которому привязан ключ |

Менеджмент-ключи (`vibe_live_*`) к `/v1/storage/*` **не подходят** — у них нет привязки к порталу. Любой вызов вернёт `403 STORAGE_REQUIRES_PORTAL_BINDING`.

**Изоляция.** Объекты изолированы по владельцу ключа: ключ одного портала не получит доступ к объектам другого портала — такой запрос вернёт `404 STORAGE_OBJECT_NOT_FOUND`, а не 403, чтобы не раскрывать существование чужих ресурсов.

## Жизненный цикл объекта

```
PENDING      — создан, ожидает загрузки файла (пути B и C)
    │
    ▼ (путь A → сразу COMPLETED)
COMPLETED    — файл загружен, доступен для чтения
    │
    ▼ (DELETE)
deletedAt    — мягкое удаление: чтение → 410; данные хранятся ещё 30 суток
    │
    ▼ (+30 суток, планировщик)
удалён       — данные стёрты из хранилища
```

Прямая загрузка (путь A) создаёт объект сразу в состоянии `COMPLETED`. Пути B и C создают объект в состоянии `PENDING` — до вызова `/complete`.

Объекты в состоянии `PENDING` без завершения удаляются фоновым планировщиком через 24 часа.

## Пути загрузки

| Путь | Размер файла | Шагов | Когда применять |
|---|---|---|---|
| **A — прямая загрузка** | до 10 МБ | 1 | Серверный код, небольшие файлы |
| **B — предподписанный PUT** | до 5 ГБ | 3 | Временно отключён: оба шага отвечают `503 STORAGE_PRESIGNED_UPLOAD_DISABLED`. Для загрузки из браузера используйте путь C |
| **C — составная загрузка** | до 5 ТБ | 3 + N частей | Крупные файлы, параллельная загрузка |

Для прямой загрузки по пути A с ключом, привязанным к приложению, конкурентные запросы к одному физическому адресу выполняются по очереди. Запрос ждёт свою очередь не более 60 секунд; если безопасно получить право записи не удалось, сервер возвращает `409 STORAGE_KEY_CONFLICT` до записи в объектное хранилище. Повторите весь запрос. Эта гарантия не распространяется на личные ключи без привязки к приложению (`appId = null`) и пути B/C.

Подробное описание каждого пути — [Загрузка](/docs/storage/upload).

## Видимость и доступ

Каждый объект имеет атрибут `visibility`:

- `PRIVATE` (по умолчанию) — скачивание через авторизованный запрос `GET /v1/storage/objects/:key`, который возвращает перенаправление 302 на предподписанный URL, действующий 10 минут.
- `PUBLIC` — после завершения загрузки (`COMPLETED`) дополнительно доступен через постоянный URL вида `https://vibecode.bitrix24.tech/v1/public-storage/:portalId/:objectId` без авторизации. Пока объект находится в `PENDING`, публичный URL возвращает `404 STORAGE_OBJECT_NOT_FOUND`.

Для `PUBLIC`-объектов заблокирована отдача файлов с типами содержимого `text/html`, `application/javascript`, `application/x-javascript`, `image/svg+xml`. Попытка загрузить файл с таким типом при `visibility=PUBLIC` возвращает `415 STORAGE_FORBIDDEN_CONTENT_TYPE`.

Анонимный доступ через `public-storage` включается на стороне портала и по умолчанию выключен. Пока он выключен, запросы к публичному URL возвращают `503 STORAGE_PUBLIC_GET_DISABLED_FOR_PORTAL`.

**Постоянный источник для внешних инструментов.** Предподписанный URL из `GET /v1/storage/objects/:key` действует 10 минут и запрашивается с заголовком `X-Api-Key`, поэтому как постоянный источник для инструментов, которые не отправляют заголовки авторизации — импорт данных в электронные таблицы, планировщики отчётов, внешние дашборды — он не подходит. Для такого сценария завершите загрузку `PUBLIC`-объекта и используйте его постоянный URL `https://vibecode.bitrix24.tech/v1/public-storage/:portalId/:objectId` — он отдаётся без авторизации, пока анонимный доступ включён на стороне портала. Для `PUBLIC`-отчётов подходят типы содержимого `text/csv`, `application/json` и подобные. Типы `text/html`, `application/javascript` и `image/svg+xml` заблокированы.

## Ключ объекта

Ключ объекта — уникальный идентификатор в пространстве имён, привязанном к вашему ключу (личное пространство владельца или пространство приложения). Правила:

- Длина: 1–1024 символа.
- Допустимые символы: `a-z`, `A-Z`, `0-9`, `.`, `_`, `/`, `-`.
- Не может начинаться с `/` или `.`.
- Не может содержать последовательность `..`.
- Символ `/` — разделитель пути, используется для группировки по префиксу.

В параметрах пути URL слеши в ключе необходимо кодировать: `encodeURIComponent('users/42/report.csv')` возвращает `'users%2F42%2Freport.csv'`.

**Снимки исходного кода.** Платформа сохраняет исходники приложения в это же хранилище — они видны в `GET /v1/storage/objects` по префиксу `source/`. Личный ключ видит снимки серверов своего владельца, ключ приложения — только снимки этого приложения. Убирают их операциями с исходниками: `DELETE /v1/infra/servers/:id/sources/:versionId` для одной версии и `POST /v1/infra/servers/:id/sources/cleanup` для массовой очистки, у приложений — те же операции с префиксом `/v1/apps/:id/sources`. Что лежит под этим префиксом и почему один и тот же ключ встречается в выдаче несколько раз — [Хранилище исходного кода](/docs/source-storage).

## Лимиты

| Параметр | Значение |
|---|---|
| Максимальный размер файла (путь A) | 10 МБ |
| Максимальный размер файла (путь B, временно отключён) | 5 ГБ |
| Максимальный размер файла (путь C) | 5 ТБ |
| Длина ключа объекта | 1–1024 символа |
| Срок действия предподписанного URL для скачивания | 10 минут |
| Период мягкого удаления | 30 суток |
| Объекты в состоянии PENDING | удаляются через 24 часа |
| Лимит запросов к публичному URL | 60 запросов/мин с одного IP |

## Тарификация

Хранилище работает по схеме оплаты по факту использования. Плата берётся за три ресурса.

| Ресурс | Единица | Поле в `GET /v1/me` |
|---|---|---|
| Хранение | Ꝟ за ГБ в месяц | `costRateGbMonthVibes` |
| Исходящий трафик | Ꝟ за ГБ | `costRateGbEgressVibes` |
| Операции записи | Ꝟ за 1 000 операций | `costRateOps1kVibes` |

Ставки задаёт платформа, и они различаются от инстанса к инстансу. Актуальные значения возвращает блок `storage` ответа `GET /v1/me`.

При нулевом балансе запросы на запись блокируются на 24 часа. Чтение объектов продолжает работать. Пополнение баланса разблокирует запись немедленно.

Текущее использование и прогноз расходов — в блоке `storage` ответа `GET /v1/me`. Владелец портала также видит сводный расход по приложениям и может выгрузить отчёт на странице **Хранилище** в админ-панели портала.

## Использование в AI-агентах

AI-моделям и агентам хранилище доступно без чтения этой страницы:

- Блок `storage` в ответе `GET /v1/me` — состояние хранилища, ставки, пути загрузки и список эндпоинтов.
- Инструменты `vibe_storage_*` MCP-сервера (загрузка, предподписанные ссылки, список, удаление, контроль расхода) — см. [MCP для AI](/docs/mcp).

## Быстрый старт

**Загрузить файл (путь A):**

```bash
curl -X POST https://vibecode.bitrix24.tech/v1/storage/objects/upload \
  -H "X-Api-Key: YOUR_API_KEY" \
  -F "key=reports/march.csv" \
  -F "visibility=PRIVATE" \
  -F "file=@march.csv;type=text/csv"
```

**Получить ссылку для скачивания:**

```bash
curl -I \
  -H "X-Api-Key: YOUR_API_KEY" \
  "https://vibecode.bitrix24.tech/v1/storage/objects/reports%2Fmarch.csv"
# HTTP/1.1 302 Found
# Location: <временная ссылка на объект, действует 10 минут>
```

## Полный пример

Полный цикл: загрузка → список → получение ссылки → удаление.

```javascript
const KEY = 'YOUR_API_KEY'
const BASE = 'https://vibecode.bitrix24.tech/v1'

const form = new FormData()
form.append('key', 'users/42/report.csv')
form.append('visibility', 'PRIVATE')
form.append('file', new Blob(['id,name\n1,Alice\n2,Bob'], { type: 'text/csv' }), 'report.csv')

const uploadRes = await fetch(`${BASE}/storage/objects/upload`, {
  method: 'POST',
  headers: { 'X-Api-Key': KEY },
  body: form,
})
const { object } = await uploadRes.json()
console.log('Загружен:', object.key, '| uploadStatus:', object.uploadStatus)

const listRes = await fetch(`${BASE}/storage/objects?prefix=users/42`, {
  headers: { 'X-Api-Key': KEY },
})
const { data, total } = await listRes.json()
console.log('Объектов:', total, '| первый:', data[0]?.key)

const getRes = await fetch(`${BASE}/storage/objects/${encodeURIComponent(object.key)}`, {
  headers: { 'X-Api-Key': KEY },
  redirect: 'manual',
})
const downloadUrl = getRes.headers.get('location')
console.log('Ссылка (10 мин):', downloadUrl)

const delRes = await fetch(`${BASE}/storage/objects/${encodeURIComponent(object.key)}`, {
  method: 'DELETE',
  headers: { 'X-Api-Key': KEY },
})
const { object: deleted } = await delRes.json()
console.log('Удалён:', deleted.key, '| deletedAt:', deleted.deletedAt)
```

## Типовые сценарии

### Большой файл составной загрузкой

Загрузка крупного файла по частям с отменой при ошибке (чтобы не оплачивать незавершённые части):

```javascript
const KEY = 'YOUR_API_KEY'
const BASE = 'https://vibecode.bitrix24.tech/v1'
const PART_SIZE = 8 * 1024 * 1024 // 8 МБ

// file — File или Blob, например из <input type="file">
const init = await fetch(`${BASE}/storage/objects/multipart/create`, {
  method: 'POST',
  headers: { 'X-Api-Key': KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    key: 'videos/demo.mp4',
    contentType: 'video/mp4',
    totalSize: file.size,
    partSize: PART_SIZE,
    visibility: 'PRIVATE',
  }),
})
const { objectId, parts } = await init.json()

try {
  const uploaded = []
  for (const part of parts) {
    const chunk = file.slice((part.partNumber - 1) * PART_SIZE, part.partNumber * PART_SIZE)
    const res = await fetch(part.uploadUrl, { method: 'PUT', body: chunk })
    uploaded.push({ partNumber: part.partNumber, etag: res.headers.get('etag') })
  }
  const done = await fetch(`${BASE}/storage/objects/multipart/complete`, {
    method: 'POST',
    headers: { 'X-Api-Key': KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ objectId, parts: uploaded }),
  })
  const { object } = await done.json()
  console.log('Загружен:', object.key, object.sizeBytes)
} catch (err) {
  await fetch(`${BASE}/storage/objects/multipart/abort`, {
    method: 'POST',
    headers: { 'X-Api-Key': KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({ objectId }),
  })
  throw err
}
```

### Массовая чистка по префиксу

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

```javascript
const KEY = 'YOUR_API_KEY'
const BASE = 'https://vibecode.bitrix24.tech/v1'

async function deleteByPrefix(prefix) {
  let cursor = null
  let removed = 0
  do {
    const url = new URL(`${BASE}/storage/objects`)
    url.searchParams.set('prefix', prefix)
    url.searchParams.set('limit', '500')
    if (cursor) url.searchParams.set('cursor', cursor)

    const page = await fetch(url, { headers: { 'X-Api-Key': KEY } })
    const { data, cursor: next } = await page.json()

    for (const obj of data) {
      await fetch(`${BASE}/storage/objects/${encodeURIComponent(obj.key)}`, {
        method: 'DELETE',
        headers: { 'X-Api-Key': KEY },
      })
      removed++
    }
    cursor = next
  } while (cursor)
  return removed
}

const count = await deleteByPrefix('users/42/')
console.log('Удалено объектов:', count)
```

## Справочник эндпоинтов

### Загрузка

| Метод | Путь | Описание |
|---|---|---|
| `POST` | [`/v1/storage/objects/upload`](/docs/storage/upload/direct) | Прямая загрузка файла до 10 МБ одним `multipart/form-data`-запросом |
| `POST` | [`/v1/storage/objects`](/docs/storage/upload/presigned) | Создать предподписанный URL для загрузки файла до 5 ГБ напрямую в хранилище. Временно отключён |
| `POST` | [`/v1/storage/objects/complete`](/docs/storage/upload/complete) | Подтвердить завершение загрузки по предподписанному URL. Временно отключён |
| `POST` | [`/v1/storage/objects/multipart/create`](/docs/storage/upload/multipart-create) | Инициировать составную загрузку: получить идентификатор сессии и URL для частей |
| `POST` | [`/v1/storage/objects/multipart/complete`](/docs/storage/upload/multipart-complete) | Собрать объект из загруженных частей по их `partNumber` и `etag` |
| `POST` | [`/v1/storage/objects/multipart/abort`](/docs/storage/upload/multipart-abort) | Отменить составную загрузку и удалить незавершённые части |

### Объекты

| Метод | Путь | Описание |
|---|---|---|
| `GET` | [`/v1/storage/objects`](/docs/storage/objects/list) | Список объектов с фильтрацией по префиксу и курсорной пагинацией |
| `GET` | [`/v1/storage/objects/:key`](/docs/storage/objects/get) | Скачать объект (перенаправление 302 → предподписанный URL) |
| `HEAD` | [`/v1/storage/objects/:key`](/docs/storage/objects/head) | Метаданные объекта без тела ответа |
| `DELETE` | [`/v1/storage/objects/:key`](/docs/storage/objects/delete) | Мягкое удаление объекта |

### Публичный доступ

| Метод | Путь | Описание |
|---|---|---|
| `GET` | [`/v1/public-storage/:portalId/:objectId`](/docs/storage/objects/public-get) | Скачать завершённый `PUBLIC`-объект без API-ключа |
| `HEAD` | [`/v1/public-storage/:portalId/:objectId`](/docs/storage/objects/public-head) | Метаданные завершённого `PUBLIC`-объекта без API-ключа |

## Коды ошибок

| Код | HTTP | Когда возникает |
|---|---|---|
| `STORAGE_SCOPE_REQUIRED` | 403 | API-ключ не имеет скоупа `vibe:storage` |
| `STORAGE_NO_AUTH_CONTEXT` | 401 | Запрос без пригодных учётных данных приложения |
| `STORAGE_KEY_REQUIRED` | 400 | Поле `key` не передано или пустое |
| `STORAGE_INVALID_KEY` | 400 | Ключ содержит недопустимые символы, начинается с `/` или `.`, содержит `..` |
| `STORAGE_INVALID_PATH` | 400 | Ключ формирует инъекцию пути |
| `STORAGE_CONTENT_TYPE_REQUIRED` | 400 | Поле `contentType` не передано (пути B и C) |
| `STORAGE_INVALID_VISIBILITY` | 400 | `visibility` не равен `PUBLIC` или `PRIVATE` |
| `STORAGE_INVALID_SIZE` | 400 | `sizeBytes` не является неотрицательным целым числом |
| `STORAGE_INVALID_TTL` | 400 | `ttlSeconds` вне диапазона 60–86400 |
| `STORAGE_FILE_REQUIRED` | 400 | Файловая часть в форме не передана (путь A) |
| `STORAGE_MULTIPART_PARSE_FAILED` | 400 | Ошибка разбора `multipart/form-data` |
| `STORAGE_OBJECT_ID_REQUIRED` | 400 | Параметр `objectId` не передан |
| `STORAGE_INVALID_TOTAL_SIZE` | 400 | `totalSize` не является положительным целым числом (путь C) |
| `STORAGE_INVALID_PART_SIZE` | 400 | `partSize` не является положительным целым числом (путь C) |
| `STORAGE_PARTS_REQUIRED` | 400 | Массив `parts` не передан или пуст |
| `STORAGE_INVALID_PART` | 400 | Элемент массива `parts` не соответствует формату `{ partNumber, etag }` |
| `STORAGE_INVALID_PARTS` | 400 | Набор частей отклонён хранилищем при сборке составной загрузки |
| `STORAGE_TOO_MANY_PARTS` | 400 | Слишком много частей для составной загрузки. Увеличьте `partSize` |
| `STORAGE_INVALID_LIMIT` | 400 | `limit` не является положительным целым числом |
| `STORAGE_UPLOAD_TOO_LARGE` | 413 | Размер файла превышает 10 МБ (путь A) |
| `STORAGE_OBJECT_TOO_LARGE` | 413 | `totalSize` превышает 5 ТБ (путь C) |
| `STORAGE_FORBIDDEN_CONTENT_TYPE` | 415 | Тип содержимого недопустим для `PUBLIC`-объектов |
| `STORAGE_OBJECT_NOT_FOUND` | 404 | Объект не найден или не принадлежит вызывающему приложению; публичный GET/HEAD также возвращает этот код для `PENDING` и `PRIVATE` |
| `STORAGE_UPLOAD_NOT_PENDING` | 409 | Объект уже загружен или находится в недопустимом состоянии |
| `STORAGE_UPLOAD_PENDING` | 409 | Загрузка объекта не завершена: при чтении — объект ещё не готов, при заливке — по ключу висит незавершённая бронь предподписанной ссылки |
| `STORAGE_UPLOAD_NOT_FOUND_IN_BUCKET` | 409 | PUT-запрос к предподписанному URL не был выполнен |
| `STORAGE_MULTIPART_IN_PROGRESS` | 409 | У объекта есть незавершённая составная загрузка. Вызовите `/multipart/abort`; заменить или удалить объект до этого нельзя |
| `STORAGE_KEY_DELETED` | 409 | Объект под этим ключом удалён и держит имя до физической уборки (30 дней). Восстановление пока не поддерживается — используйте другой ключ |
| `STORAGE_KEY_OWNED_ELSEWHERE` | 409 | Логическое имя занято другим объектом приложения: общие и персональные файлы хранятся по разным адресам |
| `STORAGE_KEY_EXISTS` | 409 | Объект уже существует. Заменить содержимое через предподписанную ссылку нельзя — используйте прямую заливку (путь A) |
| `STORAGE_KEY_CONFLICT` | 409 | Конфликт состояния объекта либо конкурентная прямая загрузка (путь A) с ключом приложения не получила право записи в пределах 60 секунд или лимита конкурентности. До подтверждения объектное хранилище не затрагивается; повторите весь запрос. Для личного ключа (`appId = null`) и путей B/C сериализация не обещана |
| `STORAGE_VISIBILITY_MISMATCH` | 400 | Замена содержимого не может изменить видимость объекта. Не передавайте поле `visibility` либо передайте текущее значение |
| `STORAGE_OBJECT_DELETED` | 410 | Объект помечен как удалённый |
| `BILLING_INSUFFICIENT` | 402 | Нулевой баланс портала — запись приостановлена на 24 часа (чтение работает) |
| `STORAGE_RATE_LIMIT_EXCEEDED` | 429 | Превышен лимит 60 запросов/мин к публичному URL с одного IP |
| `STORAGE_PUBLIC_GET_DISABLED_FOR_PORTAL` | 503 | Публичный доступ отключён на уровне портала |
| `STORAGE_FEATURE_DISABLED` | 503 | Хранилище временно отключено |
| `STORAGE_PRESIGNED_UPLOAD_DISABLED` | 503 | Путь B временно отключён. Используйте прямую загрузку (путь A) или составную загрузку (путь C) |
| `STORAGE_STS_UNAVAILABLE` | 503 | Сервис выдачи временных учётных данных недоступен |
| `STORAGE_BUCKET_ERROR` | 502 | Ошибка при обращении к объектному хранилищу |
| `STORAGE_OBJECT_MISSING_IN_BUCKET` | 502 | Объект зафиксирован в базе, но отсутствует в хранилище |
| `STORAGE_OBJECT_STREAM_FAILED` | 502 | Ошибка при передаче данных объекта |
| `STORAGE_OBJECT_HEAD_FAILED` | 502 | Ошибка при получении метаданных объекта из хранилища |

Общие коды ошибок API — [Коды ошибок](/docs/errors).

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

- [Загрузка](/docs/storage/upload)
- [Объекты](/docs/storage/objects)
- [Хранилище исходного кода](/docs/source-storage)
- [Ключи и авторизация](/docs/keys-auth)
- [Скоупы](/docs/scopes)
