For AI agents: markdown of this page — /docs-content-en/storage/upload/direct.md documentation index — /llms.txt

Direct file upload

POST /v1/storage/objects/upload

Uploads a file up to 10 MB in a single multipart/form-data request — the server accepts the file and returns the resulting storage object.

Uploading again under the same key replaces the object content. This also applies to a soft-deleted object when it belongs to the same owner tuple, has COMPLETED status, and has no open multipart session. The row is revived with the same object.id, createdAt, key, physical address, owner tuple, and visibility; deletedAt is cleared, while sizeBytes, sha256, contentType, and contentUpdatedAt are updated to match the new bytes.

This does not restore the deleted content: Path A writes new bytes over the old ones. For a PUBLIC object, a previously issued anonymous URL with the same object.id starts working again and serves the new bytes. To replace or revive an object, omit visibility or pass the stored value — visibility cannot change (STORAGE_VISIBILITY_MISMATCH). Deleted PENDING rows and rows with a multipart session stay occupied and return 409 STORAGE_KEY_DELETED.

Replacement and revival work the same way for an app-bound key and for a personal developer key: uploading again under the same key does not create a second object.

For Path A direct uploads, concurrent requests to the same physical address run one at a time. A request waits for its turn for no more than 60 seconds; if safe write ownership cannot be confirmed, the server returns 409 STORAGE_KEY_CONFLICT before writing to object storage. Retry the entire request. The guarantee also covers a personal key (appId = null). Path C serializes every key, but replaces a live object only for an app-bound key; it accepts a personal key after explicit DELETE. Path B is excluded.

Request fields (body)

The request body must be in multipart/form-data format.

Parameter Type Required Default Description
file file yes Binary file no larger than 10 MB. The MIME type is determined from the Content-Type header of the form part. If the header is absent, application/octet-stream is used
key string yes Logical object key: 1 to 1024 characters. Allowed characters: a-z, A-Z, 0-9, ., _, /, -. Must not start with / or . and must not contain ..
visibility string no PRIVATE Object visibility: PRIVATE or PUBLIC. On a new object an omitted field means PRIVATE. When replacing or reviving an existing object visibility does not change: omit the field or pass the stored value

Examples

curl — personal key

Terminal
curl -X POST https://vibecode.bitrix24.com/v1/storage/objects/upload \
  -H "X-Api-Key: YOUR_API_KEY" \
  -F "file=@avatar.png;type=image/png" \
  -F "key=users/42/avatar.png" \
  -F "visibility=PRIVATE"

curl — OAuth application

Terminal
curl -X POST https://vibecode.bitrix24.com/v1/storage/objects/upload \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -F "file=@avatar.png;type=image/png" \
  -F "key=users/42/avatar.png" \
  -F "visibility=PRIVATE"

JavaScript — personal key

javascript
const formData = new FormData()
formData.append('file', fileInput.files[0])
formData.append('key', 'users/42/avatar.png')
formData.append('visibility', 'PRIVATE')

const res = await fetch('https://vibecode.bitrix24.com/v1/storage/objects/upload', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
  },
  body: formData,
})

if (!res.ok) {
  const { error } = await res.json()
  console.error(error.code, error.message)
  return
}
const { object } = await res.json()
console.log('Object ID:', object.id)

JavaScript — OAuth application

javascript
const formData = new FormData()
formData.append('file', fileInput.files[0])
formData.append('key', 'users/42/avatar.png')
formData.append('visibility', 'PRIVATE')

const res = await fetch('https://vibecode.bitrix24.com/v1/storage/objects/upload', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
  },
  body: formData,
})

const { object } = await res.json()

Response fields

Field Type Description
object.id string Object identifier
object.key string Logical object key
object.contentType string File MIME type
object.sizeBytes string File size in bytes (string)
object.sha256 string File SHA-256 hash. Always computed on direct upload
object.visibility string Visibility: PRIVATE or PUBLIC
object.uploadStatus string Upload status: COMPLETED
object.createdAt string Creation date (ISO 8601)
object.deletedAt string Deletion date or null
object.contentUpdatedAt string (ISO 8601) When the content last became current: a replacement, or the first completion of the upload. Service updates do not move it. null for objects last written before this field existed

Response example

JSON
{
  "object": {
    "id": "cmpf9b1gp003omr0zkc0qjojn",
    "key": "users/42/avatar.png",
    "contentType": "image/png",
    "sizeBytes": "20480",
    "sha256": "10068f1908d45621f11b50a1d23ddc0720a669c15f45a43d883604e7793537cd",
    "visibility": "PRIVATE",
    "uploadStatus": "COMPLETED",
    "createdAt": "2026-05-21T08:56:32.809Z",
    "deletedAt": null
  }
}

Error response example

415 — the request passed a MIME type that is not allowed for PUBLIC objects because of cross-site scripting risks:

JSON
{
  "success": false,
  "error": {
    "code": "STORAGE_FORBIDDEN_CONTENT_TYPE",
    "message": "Content-Type text/html is not allowed for PUBLIC objects (XSS-prone)"
  }
}

Errors

HTTP Code Description
403 STORAGE_SCOPE_REQUIRED The API key lacks the vibe:storage scope
401 STORAGE_NO_AUTH_CONTEXT The request was made without authorization
400 STORAGE_FILE_REQUIRED The file field was not provided
400 STORAGE_KEY_REQUIRED The key field was not provided
400 STORAGE_INVALID_KEY The key value violates the format rules
400 STORAGE_INVALID_VISIBILITY Invalid visibility value
400 STORAGE_VISIBILITY_MISMATCH Replacing content cannot change object visibility. Omit the field or pass the current value — it is named in the error message
409 STORAGE_KEY_DELETED The deleted row is not eligible for Path A revival: it is not a COMPLETED object without a multipart session. An eligible deleted object returns 200 with the same id and visibility instead
409 STORAGE_MULTIPART_IN_PROGRESS The object has a multipart upload in progress — complete or abort it
409 STORAGE_UPLOAD_PENDING A presigned URL was issued for this key and the upload is not confirmed yet
409 STORAGE_KEY_OWNED_ELSEWHERE The name is taken by an object of the other ownership kind (app-shared vs per-employee). Another employee does not block the same name
409 STORAGE_KEY_CONFLICT The object state changed, or a concurrent direct upload (Path A) did not obtain write ownership within 60 seconds or the concurrency limit. Object storage is untouched until confirmation; retry the entire request. Serialization also covers a personal key (appId = null). Path C serializes every key, but replaces a live object only for an app-bound key; it accepts a personal key after explicit DELETE. Path B is excluded
400 STORAGE_MULTIPART_PARSE_FAILED Failed to parse the request body as multipart/form-data
413 STORAGE_UPLOAD_TOO_LARGE The file exceeds 10 MB — use multipart upload (path B is temporarily disabled)
415 STORAGE_FORBIDDEN_CONTENT_TYPE For PUBLIC objects the types text/html, application/javascript, application/x-javascript, image/svg+xml are forbidden
402 BILLING_INSUFFICIENT Insufficient balance
503 STORAGE_FEATURE_DISABLED Storage is disabled for the portal
502 STORAGE_BUCKET_ERROR Error while interacting with storage

Full list of common API errors — Errors.

Known specifics

Files larger than 10 MB. Path B is temporarily disabled and answers 503 STORAGE_PRESIGNED_UPLOAD_DISABLED, so upload anything above 10 MB with multipart upload (Path C).

Type check for PUBLIC objects. The check runs against the declared Content-Type of the form part before the file is written — if it fails, the request is rejected without accessing storage.

See also