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

Create multipart upload

POST /v1/storage/objects/multipart/create

Opens a multipart upload session for files up to 5 TB. Returns the object identifier, the session identifier, and presigned URLs for uploading parts. After all parts are uploaded, call POST /v1/storage/objects/multipart/complete to assemble the object.

On an unused key, Path C creates a new object. It replaces, in place, a live app-bound COMPLETED object owned by the same tuple: the response returns the existing objectId, while reads return the old content until /multipart/complete publishes the new bytes. A successful complete response confirms the new version; 502 STORAGE_BUCKET_ERROR can mean publication without a confirmed result. A confirmed abort before publication preserves the old content.

Path C does not replace a live object owned by a personal key (appId = null). Delete the object with DELETE /v1/storage/objects/{key} first, then start the multipart upload again under the same key. This operation is non-atomic: the object is unavailable between deletion and successful /multipart/complete. An eligible deleted personal-key COMPLETED row is reused with its existing objectId and visibility; other deleted states return 409 STORAGE_KEY_DELETED.

Request fields (body)

The request body is JSON.

Parameter Type Required Default Description
key string yes Logical object key: from 1 to 1024 characters, allowed a-z, A-Z, 0-9, ., _, /, -; must not start with / or ., must not contain ..
contentType string yes File MIME type, e.g. video/mp4 or application/zip
totalSize number yes Total file size in bytes; positive integer; no more than 5 TB (5,497,558,138,880 bytes)
partSize number no 8388608 Size of one part in bytes; from 5 MB (5,242,880) to 5 GB (5,368,709,120); the last part may be smaller than the minimum
visibility string no PRIVATE for a new object Object visibility: PRIVATE or PUBLIC. On replacement, omit the field or pass the stored value

Examples

curl — personal key

Terminal
curl -X POST https://vibecode.bitrix24.com/v1/storage/objects/multipart/create \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "videos/lecture-01.mp4",
    "contentType": "video/mp4",
    "totalSize": 209715200
  }'

curl — OAuth application

Terminal
curl -X POST https://vibecode.bitrix24.com/v1/storage/objects/multipart/create \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "videos/lecture-01.mp4",
    "contentType": "video/mp4",
    "totalSize": 209715200
  }'

JavaScript — personal key

javascript
const file = fileInput.files[0]

// Step 1 — open a multipart upload session
const createRes = await fetch(
  'https://vibecode.bitrix24.com/v1/storage/objects/multipart/create',
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      key: 'videos/lecture-01.mp4',
      contentType: file.type,
      totalSize: file.size,
    }),
  }
)
const { objectId, parts } = await createRes.json()

// Step 2 — upload the parts in parallel, save the ETag from the response headers
const PART_SIZE = 8 * 1024 * 1024
const uploadedParts = await Promise.all(
  parts.map(async ({ partNumber, uploadUrl }) => {
    const start = (partNumber - 1) * PART_SIZE
    const end = Math.min(start + PART_SIZE, file.size)

    const putRes = await fetch(uploadUrl, {
      method: 'PUT',
      body: file.slice(start, end),
    })
    const etag = putRes.headers.get('ETag')
    return { partNumber, etag }
  })
)

// Step 3 — finalize object assembly
const completeRes = await fetch(
  'https://vibecode.bitrix24.com/v1/storage/objects/multipart/complete',
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_API_KEY',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ objectId, parts: uploadedParts }),
  }
)
const { object } = await completeRes.json()
console.log('Object ID:', object.id)

JavaScript — OAuth application

javascript
const file = fileInput.files[0]

const createRes = await fetch(
  'https://vibecode.bitrix24.com/v1/storage/objects/multipart/create',
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      key: 'videos/lecture-01.mp4',
      contentType: file.type,
      totalSize: file.size,
    }),
  }
)
const { objectId, parts } = await createRes.json()

const PART_SIZE = 8 * 1024 * 1024
const uploadedParts = await Promise.all(
  parts.map(async ({ partNumber, uploadUrl }) => {
    const start = (partNumber - 1) * PART_SIZE
    const end = Math.min(start + PART_SIZE, file.size)

    const putRes = await fetch(uploadUrl, { method: 'PUT', body: file.slice(start, end) })
    const etag = putRes.headers.get('ETag')
    return { partNumber, etag }
  })
)

const completeRes = await fetch(
  'https://vibecode.bitrix24.com/v1/storage/objects/multipart/complete',
  {
    method: 'POST',
    headers: {
      'X-Api-Key': 'YOUR_APP_KEY',
      'Authorization': 'Bearer USER_SESSION_TOKEN',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ objectId, parts: uploadedParts }),
  }
)
const { object } = await completeRes.json()

Response fields

Field Type Description
objectId string Object identifier in storage
uploadId string Multipart upload session identifier
partCount number Number of parts, computed from totalSize and partSize
parts array Array of part descriptors
parts[].partNumber number Part number (starting from 1)
parts[].uploadUrl string Presigned URL for uploading the part via PUT; its signature binds the exact size of this part, including the final remainder
parts[].expiresAt string Part URL expiry (ISO 8601), taken from the URL signature. May be shorter than the 24-hour session

Response example

JSON
{
  "objectId": "cmpfg012a01aco510mrr6u632",
  "uploadId": "00065252B4EA33D1",
  "partCount": 2,
  "parts": [
    {
      "partNumber": 1,
      "uploadUrl": "https://storage.example.com/upload?partNumber=1&uploadId=00065252B4EA33D1&...",
      "expiresAt": "2026-05-22T12:03:56.398Z"
    },
    {
      "partNumber": 2,
      "uploadUrl": "https://storage.example.com/upload?partNumber=2&uploadId=00065252B4EA33D1&...",
      "expiresAt": "2026-05-22T12:03:56.398Z"
    }
  ]
}

Error response example

413 — the file exceeds the 5 TB limit:

JSON
{
  "success": false,
  "error": {
    "code": "STORAGE_OBJECT_TOO_LARGE",
    "message": "Object exceeds 5 TB cap"
  }
}

Errors

HTTP Code Description
403 STORAGE_SCOPE_REQUIRED The API key lacks the vibe:storage scope
409 STORAGE_KEY_EXISTS A personal-key (appId = null) address already has a live object. Delete the object, then start the upload again. The operation is non-atomic: the new object is unavailable until completion succeeds
409 STORAGE_KEY_DELETED The deleted row is not eligible for a Path C upload. Only a personal-key COMPLETED row with no active multipart session can be reused
409 STORAGE_MULTIPART_IN_PROGRESS A multipart upload for this key is already in progress — complete or abort it
409 STORAGE_UPLOAD_PENDING The key holds an unfinished presigned reservation
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 Retryable conflict: a concurrent write owns the address, or the object changed while the session was being created. Retry the same create operation
401 STORAGE_NO_AUTH_CONTEXT The request was made without authorization
400 STORAGE_INVALID_PATH The caller identifier contains invalid path characters
400 STORAGE_KEY_REQUIRED The key field was not provided
400 STORAGE_INVALID_KEY The key value violates the format rules
400 STORAGE_CONTENT_TYPE_REQUIRED The contentType field was not provided
400 STORAGE_INVALID_VISIBILITY Invalid visibility value
400 STORAGE_VISIBILITY_MISMATCH A replacement cannot change object visibility — omit visibility or pass the stored value
400 STORAGE_INVALID_TOTAL_SIZE totalSize is not a positive integer
400 STORAGE_INVALID_PART_SIZE partSize is outside the allowed range (5 MB – 5 GB) or is not a positive integer
413 STORAGE_OBJECT_TOO_LARGE totalSize exceeds 5 TB
415 STORAGE_FORBIDDEN_CONTENT_TYPE For PUBLIC objects the types text/html, application/javascript, application/x-javascript, image/svg+xml are forbidden — Error codes
400 STORAGE_TOO_MANY_PARTS The computed number of parts exceeds 10,000 — increase partSize
402 BILLING_INSUFFICIENT Insufficient balance
507 STORAGE_QUOTA_EXCEEDED The declared totalSize does not fit the remaining portal storage volume cap. The refusal is returned before the session is created, so no parts are uploaded. The cap is optional, is switched on by the platform, and applies only to portals that have no commercial Bitrix24 plan and never had one
503 STORAGE_FEATURE_DISABLED Storage is disabled for the portal
503 STORAGE_STS_UNAVAILABLE The temporary credentials issuance service is unavailable
502 STORAGE_BUCKET_ERROR Error while interacting with storage

Full list of common API errors — Errors.

Known specifics

Presigned URLs for parts do not require authorization headers. PUT requests to these URLs are sent directly to storage without X-Api-Key or Authorization — all necessary credentials are already signed into the URL itself.

Automatic cleanup — after 24 hours. After 24 hours, the scheduler attempts to abort an incomplete session. Unuploaded parts are deleted only after the provider confirms the abort. If the abort is not confirmed, the session is retained and releasing it requires another abort attempt or operator action. Each presigned part URL has its own lifetime: it is taken from the URL signature, returned in parts[].expiresAt and may be shorter than 24 hours. Upload parts as early as you can, going by parts[].expiresAt, not by the automatic-cleanup deadline. To free resources early, call POST /v1/storage/objects/multipart/abort.

Publishing the new version. During a multipart replacement, reads return the previous version until /multipart/complete publishes the new bytes. A successful complete response confirms the new version under the same objectId; 502 STORAGE_BUCKET_ERROR can mean publication happened without a confirmed result. A confirmed /multipart/abort before publication preserves the previous version. Automatic cleanup releases parts that are confirmed incomplete or finalizes a recorded publication of the new version; an unconfirmed outcome retains the session for operator reconciliation.

Smaller files. For files up to 10 MB use direct upload (Path A). Path B is temporarily disabled and answers 503 STORAGE_PRESIGNED_UPLOAD_DISABLED, so the intermediate size range goes through multipart upload as well.

See also