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
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
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
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
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
{
"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:
{
"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.