## 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.

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

## Examples

### curl — personal key

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

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

## 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_MULTIPART_PARSE_FAILED` | Failed to parse the request body as `multipart/form-data` |
| 413 | `STORAGE_UPLOAD_TOO_LARGE` | The file exceeds 10 MB — use a presigned URL |
| 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](/docs/errors).

## Known specifics

**Files larger than 10 MB.** For files from 10 MB to 5 GB use a [presigned URL](/docs/storage/upload/presigned) (Path B). For files larger than 5 GB — [multipart upload](/docs/storage/upload/multipart-create) (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

- [Upload](/docs/storage/upload)
- [Create presigned URL](/docs/storage/upload/presigned)
- [Complete upload](/docs/storage/upload/complete)
- [Multipart upload](/docs/storage/upload/multipart-create)
- [Storage](/docs/storage)
