For AI agents: markdown of this page — /docs-content-en/storage.md documentation index — /llms.txt
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 · Lifecycle · Upload paths · Visibility · Object key · Limits · Pricing · Quick start · Full example · Endpoints · Errors
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 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 | Three upload paths: direct form (≤10 MB), presigned PUT (≤5 GB), multipart (≤5 TB) |
| 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.
Visibility and access
Each object has a visibility attribute:
PRIVATE(default) — download via an authorizedGET /v1/storage/objects/:keyrequest, which returns a 302 redirect to a presigned URL valid for 10 minutes.PUBLIC— additionally available via a permanent URL of the formhttps://vibecode.bitrix24.com/v1/public-storage/:portalId/:objectIdwithout 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
storageblock in theGET /v1/meresponse — 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.
Quick start
Upload a file (path A):
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:
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.
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):
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.
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 |
Direct upload of a file up to 10 MB in a single multipart/form-data request |
POST |
/v1/storage/objects |
Create a presigned URL to upload a file up to 5 GB directly to storage |
POST |
/v1/storage/objects/complete |
Confirm completion of a presigned-URL upload |
POST |
/v1/storage/objects/multipart/create |
Initiate a multipart upload: obtain a session identifier and URLs for the parts |
POST |
/v1/storage/objects/multipart/complete |
Assemble the object from uploaded parts by their partNumber and etag |
POST |
/v1/storage/objects/multipart/abort |
Cancel a multipart upload and remove unfinished parts |
Objects
| Method | Path | Description |
|---|---|---|
GET |
/v1/storage/objects |
List objects with prefix filtering and cursor pagination |
GET |
/v1/storage/objects/:key |
Download an object (302 redirect → presigned URL) |
HEAD |
/v1/storage/objects/:key |
Object metadata without a response body |
DELETE |
/v1/storage/objects/:key |
Soft-delete an object |
Public access
| Method | Path | Description |
|---|---|---|
GET |
/v1/public-storage/:portalId/:objectId |
Download a PUBLIC object without an API key |
HEAD |
/v1/public-storage/:portalId/:objectId |
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.