## Upload a new file version

`POST /v1/files/:id/versions`

Replaces the content of a file on Bitrix24 Drive and saves it in the version history. The new content is passed as Base64 in the JSON request body, and the file name does not change.

## Parameters

| Parameter | Type | Required | Description |
|----------|-----|:-----:|----------|
| `id` (path) | number | yes | File ID. Get it: `GET /v1/files?folderId=X` |

## Request fields (body)

| Field | Type | Required | Description |
|------|-----|:-----:|----------|
| `content` | string | yes | New file content in Base64: the standard alphabet, `=` padding at the end, no spaces or line breaks. An empty string is not accepted. The whole request body is limited to 70 MiB: Base64 inflates data by a third, so one request fits a file of about 50 MiB |
| `expectedContentGeneration` | number | no | Expected value of the file's `globalContentVersion`, a non-negative integer. If the file has a different value, the upload does not run and `409 DISK_VERSION_CONFLICT` is returned. Current value: [`GET /v1/files/:id`](./get.md) |

## Examples

### curl — personal key

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/files/205/versions" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"content":"UmVwb3J0IHYy","expectedContentGeneration":3}'
```

### curl — OAuth application

```bash
curl -X POST "https://vibecode.bitrix24.com/v1/files/205/versions" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"content":"UmVwb3J0IHYy","expectedContentGeneration":3}'
```

### JavaScript — personal key

```javascript
const content = btoa('Report v2')

const res = await fetch('https://vibecode.bitrix24.com/v1/files/205/versions', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ content, expectedContentGeneration: 3 }),
})

const { data, meta } = await res.json()
if (meta.joinedIntoPreviousVersion) {
  console.log('The latest version was overwritten; the history did not grow')
}
```

### JavaScript — OAuth application

```javascript
const content = btoa('Report v2')

const res = await fetch('https://vibecode.bitrix24.com/v1/files/205/versions', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ content, expectedContentGeneration: 3 }),
})

const { data, meta } = await res.json()
```

## Response fields

| Field | Type | Description |
|------|-----|----------|
| `success` | boolean | Always `true` on success |
| `data` | object | The file after the upload — the same fields as in the [`GET /v1/files/:id`](./get.md) response |
| `data.globalContentVersion` | number | File version counter. Grows by one after the upload |
| `data.size` | number | Size of the new content in bytes |
| `meta.joinedIntoPreviousVersion` | boolean\|null | `true` — the upload overwrote the latest version, and the history did not grow. `false` — a new version appeared in the history. `null` — the file history could not be read, so no comparison was made |
| `meta.inTrash` | boolean | `true` — the file is in the trash, and its content was replaced anyway |

## Response example

```json
{
  "success": true,
  "data": {
    "id": 205,
    "name": "report.txt",
    "code": null,
    "storageId": 1,
    "type": "file",
    "folderId": 27,
    "deletedType": 0,
    "globalContentVersion": 4,
    "fileId": 369,
    "size": 9,
    "createdAt": "2026-10-15T17:09:58.000Z",
    "updatedAt": "2026-10-15T17:09:59.000Z",
    "deletedAt": null,
    "createdBy": 1,
    "updatedBy": 1,
    "deletedBy": null,
    "downloadUrl": "https://vibecode.bitrix24.com/v1/files/205/download",
    "detailUrl": "https://example.bitrix24.com/company/personal/user/1/disk/file/report.txt"
  },
  "meta": {
    "joinedIntoPreviousVersion": true,
    "inTrash": false
  }
}
```

## Error response example

409 — `expectedContentGeneration` did not match the file's `globalContentVersion`:

```json
{
  "success": false,
  "error": {
    "code": "DISK_VERSION_CONFLICT",
    "message": "The content generation changed. Read the file before uploading."
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|----------|
| 409 | `DISK_VERSION_CONFLICT` | `expectedContentGeneration` did not match the file's `globalContentVersion`. The content is not changed |
| 400 | `INVALID_PARAMS` | `content` is empty or not in standard Base64 |
| 400 | `INVALID_PARAMS` | `expectedContentGeneration` is not a non-negative integer |
| 400 | `INVALID_ID` | `id` is not a positive integer |
| 401 | `MISSING_API_KEY` | The `X-Api-Key` header is missing |
| 401 | `TOKEN_MISSING` | Access tokens for the Bitrix24 portal are unavailable |
| 403 | `SCOPE_DENIED` | The key lacks the `disk` scope |
| 403 | `WRITE_BLOCKED_READONLY_KEY` | The key is in read-only mode. The address of the page where the mode is changed is in `error.details.switchUrl` |
| 404 | `ENTITY_NOT_FOUND` | There is no file with this `id`, or it is not available to the key |
| 413 | `PAYLOAD_TOO_LARGE` | The request body is larger than 70 MiB |
| 422 | `DISK_FILE_NOT_SAVED` | Bitrix24 refused the write without an error code — the refusal text is in `error.message` |
| 422 | `DISK_FILE_NOT_SAVED` | Bitrix24 did not confirm the upload. Read the file before retrying |
| 429 | `LARGE_BODY_BACKEND_BUSY` | The platform is processing other large requests. Retry after `error.retryAfter` seconds |
| 502 | `DISK_OUTCOME_UNKNOWN` | The outcome of the write is unknown, and the platform did not retry the request. Before retrying, read the file with [`GET /v1/files/:id`](./get.md) and compare `globalContentVersion` with the previous value: if it grew, the upload went through |

Full list of common API errors — [Errors](/docs/errors).

## Known specifics

- **An upload within five minutes of a file change overwrites the latest version.** If the same user changed the file less than five minutes ago, counting from `updatedAt`, the new content replaces the latest version instead of adding a new one. The previous content of that version disappears from the history, and `meta.joinedIntoPreviousVersion` is `true`. Any change to the file by this user starts the countdown, including creation and renaming. To keep the current content as a separate version, upload the new content when at least five minutes have passed since the file's `updatedAt`.
- **`expectedContentGeneration` is checked before the write, not during it.** The platform reads the file, compares the value, and only then uploads the content. This check does not detect a write by another client between the comparison and the upload.
- **The file name does not change.** The content is saved under the file's current name, with the same extension. To rename the file, use [`PATCH /v1/files/:id`](./update.md).

## See also

- [File version history](/docs/entities/files/versions)
- [Restore content from a version](/docs/entities/files/version-restore)
- [Upload file](/docs/entities/files/upload)
- [Get file](/docs/entities/files/get)
