# Storage

File storage for Bitrix24 apps: upload, manage visibility, and download via temporary links.

**Scope:** `vibe:storage` | **Base URL:** `https://vibecode.bitrix24.com/v1` | **Authorization:** API key (`X-Api-Key`)

[Authorization](#authorization) · [Lifecycle](#object-lifecycle) · [Upload paths](#upload-paths) · [Visibility](#visibility-and-access) · [Object key](#object-key) · [Limits](#limits) · [Pricing](#pricing) · [Quick start](#quick-start) · [Full example](#full-example) · [Endpoints](#endpoint-reference) · [Errors](#error-codes)

## When to use

**Suitable for:**

- User avatars and app logos
- Form attachments: photos, scans, receipts
- Exports and generated reports (CSV, PDF)
- Video attachments in CRM deals
- App configuration backups
- Intermediate files in a data-processing pipeline

**Not suitable for:**

- Publishing files to the Bitrix24 account Drive — use the [Drive](/docs/entities/storages) section for that; storage is isolated per app and is not part of the Bitrix24 account's drive tree
- Storing secrets and keys — use server environment variables
- App logging and audit
- Scenarios not tied to an app — storage is always isolated per key

## Documentation sections

| Section | Description |
|---|---|
| [Upload](/docs/storage/upload) | Three upload paths: direct form (≤10 MB), presigned PUT (≤5 GB), multipart (≤5 TB) |
| [Objects](/docs/storage/objects) | List, retrieve, metadata, delete objects, and public access |

## Authorization

All requests to `/v1/storage/*` require an API key in the `X-Api-Key` header with the `vibe:storage` scope.

New keys receive the `vibe:storage` scope automatically. If a key was created before it existed — add the scope in the **API Keys** section of your account.

A missing scope returns `403 STORAGE_SCOPE_REQUIRED` on any `/v1/storage/*` call.

**Key types**

| Key type | Access | Storage area |
|---|---|---|
| `vibe_api_*` — personal API key | yes | namespace of the key owner in the Bitrix24 account |
| `vibe_app_*` — app key | yes | namespace of the app the key is bound to |

Management keys (`vibe_live_*`) are **not suitable** for `/v1/storage/*` — they have no Bitrix24 account binding. Any call returns `403 STORAGE_REQUIRES_PORTAL_BINDING`.

**Isolation.** Objects are isolated per key owner: a key from one Bitrix24 account cannot access objects of another Bitrix24 account — such a request returns `404 STORAGE_OBJECT_NOT_FOUND`, not 403, so as not to reveal the existence of another Bitrix24 account's resources.

## Object lifecycle

```
PENDING      — created, awaiting file upload (paths B and C)
    │
    ▼ (path A → immediately COMPLETED)
COMPLETED    — file uploaded, available for reading
    │
    ▼ (DELETE)
deletedAt    — soft delete: read → 410; data retained for another 30 days
    │
    ▼ (+30 days, scheduler)
deleted      — data erased from storage
```

Direct upload (path A) creates the object immediately in the `COMPLETED` state. Paths B and C create the object in the `PENDING` state — until `/complete` is called.

Objects in the `PENDING` state without completion are removed by a background scheduler after 24 hours.

## Upload paths

| Path | File size | Steps | When to use |
|---|---|---|---|
| **A — direct upload** | up to 10 MB | 1 | Server-side code, small files |
| **B — presigned PUT** | up to 5 GB | 3 | Browser upload without an intermediate server |
| **C — multipart upload** | up to 5 TB | 3 + N parts | Large files, parallel upload |

A detailed description of each path — [Upload](/docs/storage/upload).

## Visibility and access

Each object has a `visibility` attribute:

- `PRIVATE` (default) — download via an authorized `GET /v1/storage/objects/:key` request, which returns a 302 redirect to a presigned URL valid for 10 minutes.
- `PUBLIC` — additionally available via a permanent URL of the form `https://vibecode.bitrix24.com/v1/public-storage/:portalId/:objectId` without authorization.

For `PUBLIC` objects, serving files with content types `text/html`, `application/javascript`, `application/x-javascript`, `image/svg+xml` is blocked. An attempt to upload a file with such a type at `visibility=PUBLIC` returns `415 STORAGE_FORBIDDEN_CONTENT_TYPE`.

Anonymous access via `public-storage` is enabled on the Bitrix24 account side and is off by default. While it is off, requests to the public URL return `503 STORAGE_PUBLIC_GET_DISABLED_FOR_PORTAL`.

**A permanent source for external tools.** The presigned URL from `GET /v1/storage/objects/:key` is valid for 10 minutes and requires an `X-Api-Key` header, so it does not work as a permanent source for tools that do not send authorization headers — importing data into spreadsheets, report schedulers, external dashboards. For that scenario, set the object to `PUBLIC` and use its permanent URL `https://vibecode.bitrix24.com/v1/public-storage/:portalId/:objectId`, which is served without authorization while anonymous access is enabled on the Bitrix24 account side. For `PUBLIC` reports, content types such as `text/csv`, `application/json`, and similar are suitable. The types `text/html`, `application/javascript`, and `image/svg+xml` are blocked.

## Object key

The object key is a unique identifier within the namespace bound to your key (the owner's personal namespace or the app's namespace). Rules:

- Length: 1–1024 characters.
- Allowed characters: `a-z`, `A-Z`, `0-9`, `.`, `_`, `/`, `-`.
- Cannot start with `/` or `.`.
- Cannot contain the sequence `..`.
- The `/` character is a path separator, used for grouping by prefix.

In URL path parameters, slashes in the key must be encoded: `encodeURIComponent('users/42/report.csv')` returns `'users%2F42%2Freport.csv'`.

## Limits

| Parameter | Value |
|---|---|
| Maximum file size (path A) | 10 MB |
| Maximum file size (path B) | 5 GB |
| Maximum file size (path C) | 5 TB |
| Object key length | 1–1024 characters |
| Presigned download URL validity | 10 minutes |
| Soft-delete period | 30 days |
| Objects in the PENDING state | removed after 24 hours |
| Public URL request limit | 60 requests/min from a single IP |

## Pricing

Storage runs on a pay-as-you-go scheme. The platform charges for three resources.

| Resource | Unit | Field in `GET /v1/me` |
|---|---|---|
| Storage | Ꝟ per GB-month | `costRateGbMonthVibes` |
| Outbound traffic | Ꝟ per GB | `costRateGbEgressVibes` |
| Write operations | Ꝟ per 1,000 operations | `costRateOps1kVibes` |

Rates are set by the platform and differ from instance to instance. The current values are returned in the `storage` block of the `GET /v1/me` response.

At a zero balance, write requests are blocked for 24 hours. Reading objects continues to work. Topping up the balance unblocks writing immediately.

Current usage and a cost forecast are in the `storage` block of the `GET /v1/me` response. The Bitrix24 account owner also sees aggregate spending across apps and can export a report on the **Storage** page in the Bitrix24 account admin panel.

## Use in AI agents

Storage is available to AI models and agents without reading this page:

- The `storage` block in the `GET /v1/me` response — storage state, rates, upload paths, and the list of endpoints.
- The `vibe_storage_*` tools of the MCP server (upload, presigned links, list, delete, spending control) — see [MCP for AI](/docs/mcp).

## Quick start

**Upload a file (path A):**

```bash
curl -X POST https://vibecode.bitrix24.com/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"
```

**Get a download link:**

```bash
curl -I \
  -H "X-Api-Key: YOUR_API_KEY" \
  "https://vibecode.bitrix24.com/v1/storage/objects/reports%2Fmarch.csv"
# HTTP/1.1 302 Found
# Location: <temporary link to the object, valid for 10 minutes>
```

## Full example

Full cycle: upload → list → get link → delete.

```javascript
const KEY = 'YOUR_API_KEY'
const BASE = 'https://vibecode.bitrix24.com/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('Uploaded:', 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('Objects:', total, '| first:', 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('Link (10 min):', 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:', deleted.key, '| deletedAt:', deleted.deletedAt)
```

## Common scenarios

### Large file via multipart upload

Uploading a large file in parts with cancellation on error (to avoid paying for unfinished parts):

```javascript
const KEY = 'YOUR_API_KEY'
const BASE = 'https://vibecode.bitrix24.com/v1'
const PART_SIZE = 8 * 1024 * 1024 // 8 MB

// file — File or Blob, e.g. from <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('Uploaded:', 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
}
```

### Bulk cleanup by prefix

Deleting all of a user's files when they are disabled: paginated iteration by prefix and deletion of each object.

```javascript
const KEY = 'YOUR_API_KEY'
const BASE = 'https://vibecode.bitrix24.com/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('Objects deleted:', count)
```

## Endpoint reference

### Upload

| Method | Path | Description |
|---|---|---|
| `POST` | [`/v1/storage/objects/upload`](/docs/storage/upload/direct) | Direct upload of a file up to 10 MB in a single `multipart/form-data` request |
| `POST` | [`/v1/storage/objects`](/docs/storage/upload/presigned) | Create a presigned URL to upload a file up to 5 GB directly to storage |
| `POST` | [`/v1/storage/objects/complete`](/docs/storage/upload/complete) | Confirm completion of a presigned-URL upload |
| `POST` | [`/v1/storage/objects/multipart/create`](/docs/storage/upload/multipart-create) | Initiate a multipart upload: obtain a session identifier and URLs for the parts |
| `POST` | [`/v1/storage/objects/multipart/complete`](/docs/storage/upload/multipart-complete) | Assemble the object from uploaded parts by their `partNumber` and `etag` |
| `POST` | [`/v1/storage/objects/multipart/abort`](/docs/storage/upload/multipart-abort) | Cancel a multipart upload and remove unfinished parts |

### Objects

| Method | Path | Description |
|---|---|---|
| `GET` | [`/v1/storage/objects`](/docs/storage/objects/list) | List objects with prefix filtering and cursor pagination |
| `GET` | [`/v1/storage/objects/:key`](/docs/storage/objects/get) | Download an object (302 redirect → presigned URL) |
| `HEAD` | [`/v1/storage/objects/:key`](/docs/storage/objects/head) | Object metadata without a response body |
| `DELETE` | [`/v1/storage/objects/:key`](/docs/storage/objects/delete) | Soft-delete an object |

### Public access

| Method | Path | Description |
|---|---|---|
| `GET` | [`/v1/public-storage/:portalId/:objectId`](/docs/storage/objects/public-get) | Download a `PUBLIC` object without an API key |
| `HEAD` | [`/v1/public-storage/:portalId/:objectId`](/docs/storage/objects/public-head) | Metadata of a `PUBLIC` object without an API key |

## Error codes

| Code | HTTP | When it occurs |
|---|---|---|
| `STORAGE_SCOPE_REQUIRED` | 403 | API key does not have the `vibe:storage` scope |
| `STORAGE_NO_AUTH_CONTEXT` | 401 | Request without usable app credentials |
| `STORAGE_KEY_REQUIRED` | 400 | The `key` field is not passed or is empty |
| `STORAGE_INVALID_KEY` | 400 | The key contains invalid characters, starts with `/` or `.`, or contains `..` |
| `STORAGE_INVALID_PATH` | 400 | The key forms a path injection |
| `STORAGE_CONTENT_TYPE_REQUIRED` | 400 | The `contentType` field is not passed (paths B and C) |
| `STORAGE_INVALID_VISIBILITY` | 400 | `visibility` is not equal to `PUBLIC` or `PRIVATE` |
| `STORAGE_INVALID_SIZE` | 400 | `sizeBytes` is not a non-negative integer |
| `STORAGE_INVALID_TTL` | 400 | `ttlSeconds` is outside the 60–86400 range |
| `STORAGE_FILE_REQUIRED` | 400 | The file part of the form is not passed (path A) |
| `STORAGE_MULTIPART_PARSE_FAILED` | 400 | Failed to parse `multipart/form-data` |
| `STORAGE_OBJECT_ID_REQUIRED` | 400 | The `objectId` parameter is not passed |
| `STORAGE_INVALID_TOTAL_SIZE` | 400 | `totalSize` is not a positive integer (path C) |
| `STORAGE_INVALID_PART_SIZE` | 400 | `partSize` is not a positive integer (path C) |
| `STORAGE_PARTS_REQUIRED` | 400 | The `parts` array is not passed or is empty |
| `STORAGE_INVALID_PART` | 400 | An element of the `parts` array does not match the `{ partNumber, etag }` format |
| `STORAGE_INVALID_PARTS` | 400 | The set of parts was rejected by storage when assembling the multipart upload |
| `STORAGE_TOO_MANY_PARTS` | 400 | Too many parts for a multipart upload; increase `partSize` |
| `STORAGE_INVALID_LIMIT` | 400 | `limit` is not a positive integer |
| `STORAGE_UPLOAD_TOO_LARGE` | 413 | File size exceeds 10 MB (path A) |
| `STORAGE_OBJECT_TOO_LARGE` | 413 | `totalSize` exceeds 5 TB (path C) |
| `STORAGE_FORBIDDEN_CONTENT_TYPE` | 415 | Content type is not allowed for `PUBLIC` objects |
| `STORAGE_OBJECT_NOT_FOUND` | 404 | Object not found or does not belong to the calling app |
| `STORAGE_UPLOAD_NOT_PENDING` | 409 | The object is already uploaded or is in an invalid state |
| `STORAGE_UPLOAD_PENDING` | 409 | Attempt to read an object whose upload is not complete |
| `STORAGE_UPLOAD_NOT_FOUND_IN_BUCKET` | 409 | The PUT request to the presigned URL was not performed |
| `STORAGE_MULTIPART_IN_PROGRESS` | 409 | The object has an unfinished multipart upload; call `/multipart/abort` |
| `STORAGE_OBJECT_DELETED` | 410 | The object is marked as deleted |
| `BILLING_INSUFFICIENT` | 402 | Portal balance is zero — writing is paused for 24 hours (reading works) |
| `STORAGE_RATE_LIMIT_EXCEEDED` | 429 | Exceeded the limit of 60 requests/min to the public URL from a single IP |
| `STORAGE_PUBLIC_GET_DISABLED_FOR_PORTAL` | 503 | Public access is disabled at the portal level |
| `STORAGE_FEATURE_DISABLED` | 503 | Storage is temporarily disabled |
| `STORAGE_STS_UNAVAILABLE` | 503 | The temporary-credentials issuance service is unavailable |
| `STORAGE_BUCKET_ERROR` | 502 | Error accessing the object storage |
| `STORAGE_OBJECT_MISSING_IN_BUCKET` | 502 | The object is recorded in the database but absent from storage |
| `STORAGE_OBJECT_STREAM_FAILED` | 502 | Error while transferring object data |
| `STORAGE_OBJECT_HEAD_FAILED` | 502 | Error while retrieving object metadata from storage |

General API error codes — [Error codes](/docs/errors).

## See also

- [Upload](/docs/storage/upload)
- [Objects](/docs/storage/objects)
- [Keys and authorization](/docs/keys-auth)
- [Scopes](/docs/scopes)
