# Быстрый старт API

> Первый ролик через Slicer API за 10 минут — загрузка файла, цена, запуск монтажа, ожидание и скачивание на curl, JavaScript и Python.

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

За несколько запросов вы загрузите видео, узнаете цену монтажа, запустите его и скачаете готовый вертикальный ролик. Сначала пройдём путь по шагам на `curl`, затем — тот же сценарий одним скриптом на JavaScript и Python.

Что понадобится:

-   аккаунт Slicer и токены на балансе (первый ролик бесплатный, с водяным знаком);
-   видео с речью — например, `interview.mp4`;
-   `curl` и [`jq`](https://jqlang.org/) для шагов в терминале, Node.js 20+ или Python 3.9+ для скриптов.

## По шагам

### 1. Создайте API-токен

Откройте **Профиль** → «Для разработчиков» → «Создать токен» в веб-приложении или в приложении для iPhone и iPad. Выберите набор «Монтаж» — он разрешает загрузку, запуск монтажа и правки. Токен показывается один раз: скопируйте его сразу. Подробно — [Токены и области доступа](https://docs.slicer.media/api/authentication/).

### 2. Сохраните токен в переменной окружения

Все примеры документации берут токен из `SLICER_TOKEN`:

Терминал:

```bash
export SLICER_TOKEN="slc_…"
export API=https://api.slicer.media/v1
```

Проверьте, что токен работает:

Терминал:

```bash
curl -s "$API/me" -H "Authorization: Bearer $SLICER_TOKEN" | jq .data.wallet
```

Ответ:

```json
{ "balance": 240, "held": 0, "available": 240, "freeMontageAvailable": false, "montageTokens": 24 }
```

### 3. Загрузите видео

Создайте загрузку — в ответе будет одноразовая ссылка `uploadUrl`. Файл отправляется на неё командой `curl -T`, без заголовка с токеном: разрешение уже в самой ссылке.

Терминал:

```bash
FILE=interview.mp4
SIZE=$(wc -c < "$FILE" | tr -d ' ')

CREATED=$(curl -s "$API/uploads" \
  -H "Authorization: Bearer $SLICER_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"fileName\":\"$FILE\",\"size\":$SIZE}")

UPLOAD_ID=$(echo "$CREATED" | jq -r .data.upload.id)
UPLOAD_URL=$(echo "$CREATED" | jq -r .data.uploadUrl)

curl -s -T "$FILE" "$UPLOAD_URL" | jq .data.status
```

После приёма файла сервер проверяет его. Дождитесь статуса `ready`:

Терминал:

```bash
until [ "$(curl -s "$API/uploads/$UPLOAD_ID" -H "Authorization: Bearer $SLICER_TOKEN" | jq -r .data.status)" = "ready" ]; do
  sleep 2
done
```

Если статус стал `failed`, причина — в поле `error` (например, формат не поддерживается). Другие способы загрузки — по частям и по публичному адресу — описаны в разделе [Загрузка файлов](https://docs.slicer.media/api/uploads/).

### 4. Узнайте цену

Запрос на создание проекта без `confirmCost` ничего не создаёт и не тратит — он только называет цену:

Терминал:

```bash
BODY="{\"uploadIds\":[\"$UPLOAD_ID\"],\"title\":\"Монтаж за 3 минуты\",\"settings\":{\"templateId\":\"bold\"}}"

QUOTE=$(curl -s "$API/projects" \
  -H "Authorization: Bearer $SLICER_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$BODY")

echo "$QUOTE" | jq .data
COST=$(echo "$QUOTE" | jq .data.cost)
```

Ответ:

```json
{ "status": "quote", "cost": 24, "free": false, "available": 240, "enough": true, "spendLimitLeft": 200 }
```

`enough: false` — токенов не хватает, пополните баланс в приложении. Если у аккаунта ещё есть бесплатный первый ролик, придёт `"cost": 0, "free": true`.

### 5. Запустите монтаж

Повторите тот же запрос с `confirmCost`, равным названной цене. Заголовок `Idempotency-Key` защищает от двойного запуска, если запрос придётся повторить из-за сбоя сети ([Идемпотентность](https://docs.slicer.media/api/idempotency/)).

Терминал:

```bash
PROJECT_ID=$(curl -s "$API/projects" \
  -H "Authorization: Bearer $SLICER_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d "$(echo "$BODY" | jq --argjson cost "$COST" '. + {confirmCost: $cost}')" | jq -r .data.id)

echo "Проект $PROJECT_ID"
```

Если цена за это время изменилась, придёт ошибка `409 cost_changed` с новой ценой — запросите цену заново.

### 6. Дождитесь результата

Монтаж занимает несколько минут. Опрашивайте проект раз в 5 секунд — или получайте [вебхук](https://docs.slicer.media/api/webhooks/) `project.succeeded`:

Терминал:

```bash
while true; do
  STATUS=$(curl -s "$API/projects/$PROJECT_ID" -H "Authorization: Bearer $SLICER_TOKEN" | jq -r '.data | "\(.status) \(.stage) \(.progress)%"')
  echo "$STATUS"
  case "$STATUS" in succeeded*|failed*|cancelled*) break ;; esac
  sleep 5
done
```

Монтаж отдаёт одну версию ролика: режиссёр сам пересматривает готовый результат и исправляет порядок и логику, прежде чем проект станет готовым. Прежние версии после пересборки — в [Версиях ролика](https://docs.slicer.media/api/montage/#versions).

### 7. Скачайте ролик

Терминал:

```bash
curl -f -o reel.mp4 "$API/projects/$PROJECT_ID/result" \
  -H "Authorization: Bearer $SLICER_TOKEN"
```

Ролик готов. Тот же проект открывается в веб-приложении по ссылке из поля `appUrl` — там его можно доработать в редакторе.

## Скрипт целиком

Тот же сценарий одним файлом: загрузка → цена → запуск → ожидание → скачивание. Скрипт принимает путь к видео аргументом.

**JavaScript**

quickstart.mjs:

```js
// Node.js 20+: node quickstart.mjs interview.mp4
import { createWriteStream, openAsBlob } from 'node:fs';
import { stat } from 'node:fs/promises';
import { basename } from 'node:path';
import { randomUUID } from 'node:crypto';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';

const API = 'https://api.slicer.media/v1';
const TOKEN = process.env.SLICER_TOKEN;
const file = process.argv[2] ?? 'interview.mp4';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function api(method, path, body, headers = {}) {
  const response = await fetch(`${API}${path}`, {
    method,
    headers: {
      Authorization: `Bearer ${TOKEN}`,
      ...(body ? { 'Content-Type': 'application/json' } : {}),
      ...headers,
    },
    body: body ? JSON.stringify(body) : undefined,
  });
  const json = await response.json();
  if (!json.ok) throw new Error(`${response.status} ${json.error.code}: ${json.error.message}`);
  return json.data;
}

// 1. Загрузка: создать и отправить файл по одноразовой ссылке (без токена)
const { size } = await stat(file);
const created = await api('POST', '/uploads', { fileName: basename(file), size });
const put = await fetch(created.uploadUrl, { method: 'PUT', body: await openAsBlob(file) });
const putJson = await put.json();
if (!putJson.ok) throw new Error(`Загрузка: ${putJson.error.message}`);

let upload = putJson.data;
while (upload.status !== 'ready') {
  if (upload.status === 'failed' || upload.status === 'aborted') throw new Error(`Файл не принят: ${upload.error}`);
  await sleep(2000);
  upload = await api('GET', `/uploads/${upload.id}`);
}

// 2. Цена: без confirmCost ничего не запускается
const request = { uploadIds: [upload.id], title: 'Монтаж за 3 минуты', settings: { templateId: 'bold' } };
const quote = await api('POST', '/projects', request);
console.log(`Цена: ${quote.cost} токенов, доступно: ${quote.available}`);
if (!quote.enough) throw new Error('Не хватает токенов — пополните баланс в приложении Slicer');

// 3. Запуск с подтверждённой ценой
let project = await api('POST', '/projects', { ...request, confirmCost: quote.cost }, { 'Idempotency-Key': randomUUID() });

// 4. Ожидание
while (project.status === 'queued' || project.status === 'processing') {
  console.log(`${project.stage ?? 'в очереди'}: ${project.progress}%`);
  await sleep(5000);
  project = await api('GET', `/projects/${project.id}`);
}
if (project.status !== 'succeeded') throw new Error(`Монтаж не завершён: ${project.error?.message ?? project.status}`);

// 5. Скачивание потоком
const output = `reel.${project.result.container}`;
const video = await fetch(project.result.url, { headers: { Authorization: `Bearer ${TOKEN}` } });
if (!video.ok) throw new Error(`Скачивание: ${video.status}`);
await pipeline(Readable.fromWeb(video.body), createWriteStream(output));
console.log(`Готово: ${output}. Проект в приложении: ${project.appUrl}`);
```

**Python**

quickstart.py:

```python
# Python 3.9+: pip install requests && python quickstart.py interview.mp4
import os
import sys
import time
import uuid

import requests

API = "https://api.slicer.media/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['SLICER_TOKEN']}"

def api(method, path, json=None, headers=None):
    response = session.request(method, API + path, json=json, headers=headers, timeout=60)
    body = response.json()
    if not body["ok"]:
        error = body["error"]
        raise RuntimeError(f"{response.status_code} {error['code']}: {error['message']}")
    return body["data"]

path = sys.argv[1] if len(sys.argv) > 1 else "interview.mp4"

# 1. Загрузка: создать и отправить файл по одноразовой ссылке (без токена)
created = api("POST", "/uploads", {"fileName": os.path.basename(path), "size": os.path.getsize(path)})
with open(path, "rb") as file:
    put = requests.put(created["uploadUrl"], data=file, timeout=3600)
put_body = put.json()
if not put_body["ok"]:
    raise RuntimeError(f"Загрузка: {put_body['error']['message']}")

upload = put_body["data"]
while upload["status"] != "ready":
    if upload["status"] in ("failed", "aborted"):
        raise RuntimeError(f"Файл не принят: {upload['error']}")
    time.sleep(2)
    upload = api("GET", f"/uploads/{upload['id']}")

# 2. Цена: без confirmCost ничего не запускается
request = {"uploadIds": [upload["id"]], "title": "Монтаж за 3 минуты", "settings": {"templateId": "bold"}}
quote = api("POST", "/projects", request)
print(f"Цена: {quote['cost']} токенов, доступно: {quote['available']}")
if not quote["enough"]:
    raise RuntimeError("Не хватает токенов — пополните баланс в приложении Slicer")

# 3. Запуск с подтверждённой ценой
project = api(
    "POST",
    "/projects",
    {**request, "confirmCost": quote["cost"]},
    headers={"Idempotency-Key": str(uuid.uuid4())},
)

# 4. Ожидание
while project["status"] in ("queued", "processing"):
    print(f"{project['stage'] or 'в очереди'}: {project['progress']}%")
    time.sleep(5)
    project = api("GET", f"/projects/{project['id']}")
if project["status"] != "succeeded":
    raise RuntimeError(f"Монтаж не завершён: {(project['error'] or {}).get('message', project['status'])}")

# 5. Скачивание потоком
output = f"reel.{project['result']['container']}"
with session.get(project["result"]["url"], stream=True, timeout=600) as video:
    video.raise_for_status()
    with open(output, "wb") as out:
        for chunk in video.iter_content(chunk_size=1 << 20):
            out.write(chunk)
print(f"Готово: {output}. Проект в приложении: {project['appUrl']}")
```

> **Совет.** Ссылка `uploadUrl` одноразовая и живёт не дольше часа: она принимает один файл заявленного размера и ничего не отдаёт. Поэтому её можно передать другой программе или агенту, не раскрывая токен.

## Что дальше

[Монтаж и ценаНастройки монтажа, версии ролика, пересборка и повтор.](https://docs.slicer.media/api/montage/)[Правки роликаВырезать слова, добавить хук и плашки, поменять стиль — бесплатно.](https://docs.slicer.media/api/editing/)[Экспорт и публикацияПараметры файла и публикация в соцсети после рендера.](https://docs.slicer.media/api/export/)[ВебхукиУведомления о готовом ролике вместо опроса.](https://docs.slicer.media/api/webhooks/)
