## 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`](/docs/storage/upload/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`](/docs/storage/upload/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

```bash
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

```bash
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_CONTENT_TYPE` | `contentType` is not a single `type/subtype` media type with optional `;name=value` parameters, for example it is a comma-separated list |
| 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 a browser renders as a page, runs or sniffs are forbidden: HTML and XHTML, SVG, JavaScript, any XML (`*/xml`, `*+xml`), XSL, `multipart/*`, `message/*`, `unknown/unknown`, `application/unknown`, `*/*`, with or without parameters — [Error codes](/docs/errors) |
| 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](/docs/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`](/docs/storage/upload/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](/docs/storage/upload/direct) (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

- [Upload](/docs/storage/upload)
- [Complete multipart upload](/docs/storage/upload/multipart-complete)
- [Abort multipart upload](/docs/storage/upload/multipart-abort)
- [Direct file upload](/docs/storage/upload/direct)
- [Storage](/docs/storage)
