# Загрузка файлов

> Как загрузить видео, фото и аудио в Slicer через API — одноразовая ссылка для curl -T, файл целиком, по частям или импорт по публичному адресу.

Источник: https://docs.slicer.media/api/uploads/

Монтаж собирается из загруженных материалов: видео, фото и аудио. Любой файл сначала становится **загрузкой** — у неё есть `id`, статус и сведения о медиа. Готовые загрузки (`status: ready`) передаются в `uploadIds` при [создании проекта](https://docs.slicer.media/api/montage/).

## Четыре способа

| Способ | Когда удобен | Токен при отправке файла |
| --- | --- | --- |
| Одноразовая ссылка `uploadUrl` | проще всего; файл отправляет другая программа или ИИ-агент | не нужен |
| Файл целиком | файл у вашего сервиса, загрузка одним запросом | нужен |
| По частям | большие файлы и нестабильная сеть: части параллельно, повтор части | нужен |
| Импорт по адресу | файл уже лежит по публичному `https`\-адресу | не нужен — файл скачает Slicer |

## Создать загрузку

Для первых трёх способов загрузка создаётся заранее — сервер должен знать имя и размер файла:

Терминал:

```bash
curl https://api.slicer.media/v1/uploads \
  -H "Authorization: Bearer $SLICER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"fileName":"interview.mp4","size":48211337}'
```

Ответ:

```json
{
  "ok": true,
  "data": {
    "upload": {
      "id": "7d0c6a4e-2f7b-4f5e-9a0e-3b8f2c1d9e61",
      "fileName": "interview.mp4",
      "purpose": "source",
      "origin": "client",
      "sizeBytes": 48211337,
      "status": "uploading",
      "partSize": 8388608,
      "partCount": 6,
      "uploadedParts": [],
      "progress": null,
      "media": null,
      "error": null,
      "createdAt": "2026-10-16T09:12:04.000Z"
    },
    "uploadUrl": "https://api.slicer.media/v1/upload-links/slc_ul_…",
    "uploadUrlExpiresAt": "2026-10-16T10:12:04.000Z"
  }
}
```

-   `fileName` — с расширением: по нему определяется вид материала (видео, фото или аудио).
-   `size` — точный размер файла в байтах.
-   `purpose` — назначение; для материалов монтажа — `source` (по умолчанию).

## Одноразовая ссылка

`uploadUrl` принимает `PUT` всего файла **без токена** — разрешение заключено в самой ссылке:

Терминал:

```bash
curl -T interview.mp4 "$UPLOAD_URL"
```

Свойства ссылки:

-   **одна попытка** — ссылка гаснет при первом запросе, даже если он оборвался; для повтора создайте новую загрузку или отправьте на ту же загрузку файл целиком с токеном (ниже);
-   **не дольше часа** — время окончания в `uploadUrlExpiresAt`;
-   **только запись** — по ссылке нельзя ничего прочитать или узнать об аккаунте;
-   размер тела должен совпадать с заявленным `size`, заголовок `Content-Length` обязателен (`curl -T` ставит его сам).

После приёма файла загрузка завершается сама, ответ — объект загрузки. Ссылку удобно передать ИИ-агенту или скрипту на другой машине, не раскрывая токен.

## Файл целиком

Тот же приём, но с токеном и на постоянный адрес загрузки:

Терминал:

```bash
curl -X PUT "https://api.slicer.media/v1/uploads/$UPLOAD_ID/content" \
  -H "Authorization: Bearer $SLICER_TOKEN" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @interview.mp4
```

Сервер сам режет поток на части, шифрует их и завершает загрузку. `Content-Length` должен быть равен заявленному размеру.

## По частям

Для больших файлов: части по `partSize` байт (8 МиБ), нумерация с 1, последняя — остаток. Части можно отправлять параллельно и повторять — уже принятые перечислены в `uploadedParts`. Когда все части на месте, вызовите `complete`.

**JavaScript**

upload-parts.mjs:

```js
// Node.js 20+: node upload-parts.mjs video.mp4
import { open, stat } from 'node:fs/promises';
import { basename } from 'node:path';

const API = 'https://api.slicer.media/v1';
const auth = { Authorization: `Bearer ${process.env.SLICER_TOKEN}` };
const file = process.argv[2];

async function call(method, path, body, headers = {}) {
  const response = await fetch(`${API}${path}`, { method, headers: { ...auth, ...headers }, body });
  const json = await response.json();
  if (!json.ok) throw new Error(`${response.status} ${json.error.code}: ${json.error.message}`);
  return json.data;
}

const { size } = await stat(file);
const { upload } = await call('POST', '/uploads', JSON.stringify({ fileName: basename(file), size }), {
  'Content-Type': 'application/json',
});

const handle = await open(file);
const queue = Array.from({ length: upload.partCount }, (_, i) => i + 1);

async function worker() {
  for (let part = queue.shift(); part != null; part = queue.shift()) {
    const offset = (part - 1) * upload.partSize;
    const length = Math.min(upload.partSize, size - offset);
    const { buffer } = await handle.read(Buffer.alloc(length), 0, length, offset);
    for (let attempt = 1; ; attempt += 1) {
      try {
        await call('PUT', `/uploads/${upload.id}/parts/${part}`, buffer, { 'Content-Type': 'application/octet-stream' });
        break;
      } catch (error) {
        if (attempt === 3) throw error;
      }
    }
  }
}

await Promise.all(Array.from({ length: 4 }, worker)); // 4 части одновременно
await handle.close();
const ready = await call('POST', `/uploads/${upload.id}/complete`);
console.log(ready.status, ready.media);
```

**Python**

upload_parts.py:

```python
# python upload_parts.py video.mp4
import os
import sys
from concurrent.futures import ThreadPoolExecutor

import requests

API = "https://api.slicer.media/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SLICER_TOKEN']}"}
path = sys.argv[1]
size = os.path.getsize(path)

def call(method, url, **kwargs):
    body = requests.request(method, API + url, headers=HEADERS, timeout=300, **kwargs).json()
    if not body["ok"]:
        raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
    return body["data"]

upload = call("POST", "/uploads", json={"fileName": os.path.basename(path), "size": size})["upload"]

def send(part):
    offset = (part - 1) * upload["partSize"]
    with open(path, "rb") as file:
        file.seek(offset)
        chunk = file.read(min(upload["partSize"], size - offset))
    for attempt in range(3):
        try:
            return call("PUT", f"/uploads/{upload['id']}/parts/{part}", data=chunk)
        except (RuntimeError, requests.RequestException):
            if attempt == 2:
                raise

with ThreadPoolExecutor(max_workers=4) as pool:  # 4 части одновременно
    list(pool.map(send, range(1, upload["partCount"] + 1)))

ready = call("POST", f"/uploads/{upload['id']}/complete")
print(ready["status"], ready["media"])
```

Части считаются в отдельной корзине лимита — 1200 запросов в минуту ([Лимиты](https://docs.slicer.media/api/rate-limits/)). Начатую по частям загрузку нельзя продолжить целиком (`409 upload_in_parts`) — продолжайте частями.

## Импорт по адресу

Если файл уже лежит в интернете, Slicer скачает его сам:

Терминал:

```bash
curl https://api.slicer.media/v1/uploads/import \
  -H "Authorization: Bearer $SLICER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://media.example.com/clips/product-demo.mp4"}'
```

-   Только `https` и только публичные адреса: адреса внутренних сетей не принимаются, каждое перенаправление проверяется заново.
-   Имя файла берётся из адреса. Если в адресе нет расширения (`…/download?id=42`), передайте `fileName`: `"fileName": "product-demo.mp4"`.
-   Импорт идёт в фоне: загрузка сразу приходит со статусом `uploading`, поле `progress` показывает долю скачанного (0–1). Затем — `processing` и `ready` или `failed` с причиной в `error`.
-   Одновременно — до 3 импортов на аккаунт, скачивание — до 30 минут. Ограничения размера и формата — те же, что у обычной загрузки.

Ссылки на страницы видеохостингов (не на сам файл) не подходят: нужен прямой адрес файла.

## Статусы загрузки

| Статус | Что происходит |
| --- | --- |
| `uploading` | ждём файл или части; у импорта — скачиваем |
| `processing` | файл получен, сервер проверяет формат и готовит обложку |
| `ready` | можно использовать в монтаже; сведения о файле — в `media` |
| `failed` | файл не принят; причина — в `error` |
| `aborted` | загрузка отменена (`DELETE /v1/uploads/{id}`) |

Узнать статус — `GET /v1/uploads/{id}` или [вебхуки](https://docs.slicer.media/api/webhooks/) `upload.ready` и `upload.failed`. Поле `media` готовой загрузки: `kind` (`video`, `image`, `audio`), `durationSec`, `width`, `height`, `hasAudio`.

## Форматы и ограничения

Ограничения те же, что в приложении, и приходят в каталоге `GET /v1/styles` → `materialLimits`:

-   видео — `mp4`, `mov`, `m4v`, `webm`, `mkv`, `avi`; фото — `jpg`, `jpeg`, `png`, `webp`; аудио — `mp3`, `m4a`, `aac`, `wav`, `ogg`, `opus`, `flac`;
-   до 20 материалов в проекте, из них видео, фото и аудио — каждого вида в своих пределах;
-   формат проверяется не только по расширению, но и по содержимому файла.

Подробнее о размерах, длительности и разрешении — [Форматы и ограничения файлов](https://docs.slicer.media/guide/help/files/). Незавершённые загрузки удаляются через сутки, исходники проекта хранятся 7 дней после монтажа (`sourcesAvailable` в проекте), готовые ролики — пока вы их не удалите.

## Ошибки загрузки

| Код | Статус | Причина |
| --- | --- | --- |
| `unsupported_format` | 400 | расширение или содержимое файла не поддерживается |
| `file_too_large` | 400 | файл больше допустимого для своего вида |
| `empty_file` | 400 | файл пустой |
| `not_a_video`, `not_an_image`, `not_audio`, `media_unreadable` | 400 | содержимое не совпадает с расширением или файл повреждён |
| `resolution_too_low`, `resolution_too_high`, `image_too_large` | 400 | разрешение вне допустимых пределов |
| `audio_too_long` | 400 | аудио длиннее допустимого |
| `too_many_uploads`, `daily_upload_limit` | 400 | много незавершённых загрузок или исчерпан суточный объём |
| `bad_part`, `bad_part_size` | 400 | неверный номер или размер части |
| `parts_missing` | 400 | `complete` вызван до отправки всех частей |
| `length_required` | 411 | нет заголовка `Content-Length` |
| `bad_content_length`, `body_too_short`, `body_too_long` | 400 | размер тела не совпадает с заявленным `size` |
| `upload_not_active` | 409 | загрузка уже завершена или отменена |
| `upload_in_parts` | 409 | загрузка уже идёт по частям |
| `link_not_found` | 404 | ссылка на загрузку не найдена |
| `link_expired`, `link_used` | 410 | срок ссылки истёк или она уже использована |
| `bad_url`, `file_name_required` | 400 | адрес импорта не `https` или в нём нет имени файла |
| `too_many_imports` | 429 | уже идут 3 импорта — дождитесь окончания |

## Что дальше

[Монтаж и ценаЗапустить монтаж из готовых загрузок.](https://docs.slicer.media/api/montage/)[Справочник: загрузкиВсе эндпоинты загрузок.](https://docs.slicer.media/api/reference/uploads/)
