# Генерация видео

> Генерируйте видео из текстовых промптов через RouterAI API.

RouterAI поддерживает генерацию видео через модели, у которых `"video"` указан в `output_modalities`. Генерация **асинхронная**: вы создаёте задачу, а готовое видео получаете позже — опросом статуса (polling) или через webhook на ваш `callback_url`.

Полный жизненный цикл:

1. **Создание** — `POST /api/v1/videos` возвращает `id` задачи и `polling_url`.
2. **Ожидание** — опрашивайте статус (`GET /api/v1/videos/{id}`) **или** получите webhook, если указали `callback_url`.
3. **Скачивание** — когда статус `completed`, заберите mp4 через content-эндпоинт.

## Поиск моделей

Модели для генерации видео — это модели с `"video"` в `output_modalities`. Найти их можно:

- на [странице моделей](/models), отфильтровав по выходным модальностям;
- запросом общего каталога моделей `GET /api/v1/models` — видео-модели отличаются значением `video` в `architecture.output_modalities`:

```bash
curl https://routerai.ru/api/v1/models \
  -H "Authorization: Bearer $ROUTERAI_API_KEY"
```

## Шаг 1. Создание генерации

Отправьте `POST /api/v1/videos` с обязательными `model` и `prompt`. Необязательное поле `callback_url` — HTTPS-адрес, на который придёт webhook о готовности (см. [Шаг 3](#шаг-3-webhook-о-готовности)).

```bash
curl https://routerai.ru/api/v1/videos \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ROUTERAI_API_KEY" \
  -d '{
    "model": "x-ai/grok-imagine-video",
    "prompt": "Кот в скафандре медленно плывёт в невесомости на космической станции, кинематографичный свет",
    "aspect_ratio": "16:9",
    "duration": 4,
    "resolution": "480p",
    "callback_url": "https://example.com/hooks/routerai-video"
  }'
```

Ответ `202 Accepted` — задача принята:

```json
{
  "id": "Cq4gNzomlZDrNyy72GHC",
  "status": "pending",
  "polling_url": "https://routerai.ru/api/v1/videos/Cq4gNzomlZDrNyy72GHC"
}
```

- `id` — идентификатор задачи (используется во всех последующих запросах).
- `status` — текущий статус (`pending` сразу после создания).
- `polling_url` — URL для опроса статуса (указывает на домен RouterAI).

## Шаг 2. Поллинг статуса

Опрашивайте статус задачи, пока он не станет терминальным:

```bash
curl https://routerai.ru/api/v1/videos/Cq4gNzomlZDrNyy72GHC \
  -H "Authorization: Bearer $ROUTERAI_API_KEY"
```

Статусы:

| Статус | Тип | Значение |
|---|---|---|
| `pending` | промежуточный | задача создана |
| `in_progress` | промежуточный | видео генерируется |
| `completed` | успех | готово — можно скачивать |
| `failed` | терминальная ошибка | ошибка генерации (см. поле `error`) |
| `cancelled` | терминальная ошибка | задача отменена |
| `expired` | терминальная ошибка | срок хранения результата истёк |

Ответ для готовой задачи (`completed`):

```json
{
  "id": "Cq4gNzomlZDrNyy72GHC",
  "status": "completed",
  "polling_url": "https://routerai.ru/api/v1/videos/Cq4gNzomlZDrNyy72GHC",
  "unsigned_urls": [
    "https://routerai.ru/api/v1/videos/Cq4gNzomlZDrNyy72GHC/content?index=0"
  ],
  "usage": { "cost": 18.2 }
}
```

`unsigned_urls` — ссылки на content-эндпоинт RouterAI; по ним скачивается готовое видео (см. [Шаг 4](#шаг-4-скачивание-видео)).

<Info>
  Вместо опроса в цикле удобнее указать `callback_url` при создании — тогда RouterAI сам уведомит вас о готовности, и поллинг не нужен.
</Info>

## Шаг 3. Webhook о готовности

Если при создании указан `callback_url`, RouterAI отправит на него `POST` с JSON-телом, когда задача достигнет терминального статуса (`completed`, `failed`, `expired`, `cancelled`). Это избавляет от постоянного опроса статуса.

### Что приходит

```json
{
  "type": "video.generation.completed",
  "created_at": "2026-01-01T00:00:00.000Z",
  "data": {
    "id": "Cq4gNzomlZDrNyy72GHC",
    "status": "completed",
    "generation_id": "gen-abc123",
    "model": "x-ai/grok-imagine-video",
    "unsigned_urls": [
      "https://routerai.ru/api/v1/videos/Cq4gNzomlZDrNyy72GHC/content?index=0"
    ],
    "usage": { "cost": 18.2 }
  }
}
```

- `type` — тип события, одно из:
  - `video.generation.completed`
  - `video.generation.failed`
  - `video.generation.expired`
  - `video.generation.cancelled`
- `data.unsigned_urls` — ссылки на content-эндпоинт RouterAI (только при `completed`).
- `data.usage.cost` — стоимость в рублях (только при `completed`).
- `data.error` — текст ошибки (для `failed`/`expired`).

Заголовки запроса:

| Заголовок | Значение |
|---|---|
| `Content-Type` | `application/json` |
| `X-RouterAI-Timestamp` | момент отправки, unix-секунды |
| `X-RouterAI-Signature` | HMAC-SHA256 в hex (см. ниже) |

### Подпись

Подпись считается над строкой `"<timestamp>.<body>"`, где `body` — **сырое тело запроса** (та же JSON-строка, что пришла):

```
signature = HMAC_SHA256(secret, "<X-RouterAI-Timestamp>.<raw_body>")
```

Особенность RouterAI: **отдельный секрет не нужен** — секретом служит SHA-256-дайджест вашего API-ключа в hex. То есть вы вычисляете секрет прямо из своего ключа:

```
secret = sha256_hex(ROUTERAI_API_KEY)
```

Так подпись можно проверить, имея только свой API-ключ. Запрос подписывается ключом, которым была создана задача.

<Warning>
  Считайте HMAC именно по полученным байтам тела, а не по результату `JSON.parse` + повторной сериализации — иначе порядок ключей и пробелы изменят дайджест, и сверка ложно не сойдётся.
</Warning>

### Как проверить подпись

1. Прочитайте `X-RouterAI-Timestamp` и **сырое** тело запроса (до парсинга JSON).
2. Вычислите `secret = sha256_hex(api_key)`.
3. Пересчитайте `HMAC_SHA256(secret, "<ts>.<raw_body>")` и сравните с `X-RouterAI-Signature` **в постоянном времени** (constant-time), не обычным `==`.
4. Проверьте свежесть `timestamp` (например, ±5 минут) — защита от повторного воспроизведения (replay).

#### Node.js (Express)

```js
const crypto = require("crypto");

// важно получить сырое тело: app.use(express.raw({ type: "application/json" }))
app.post("/hooks/routerai-video", (req, res) => {
  const apiKey = process.env.ROUTERAI_API_KEY;
  const ts = req.get("X-RouterAI-Timestamp");
  const sig = req.get("X-RouterAI-Signature");
  const rawBody = req.body; // Buffer

  // свежесть метки времени (анти-replay)
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
    return res.status(401).end();
  }

  // секрет = sha256(api_key) в hex
  const secret = crypto.createHash("sha256").update(apiKey).digest("hex");
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${ts}.${rawBody}`)
    .digest("hex");

  const ok =
    sig &&
    sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  if (!ok) return res.status(401).end();

  const event = JSON.parse(rawBody.toString("utf8"));
  // ... ваша логика: event.type, event.data.unsigned_urls ...
  res.status(200).end();
});
```

#### Python (Flask)

```python
import os, hmac, hashlib, time
from flask import request, abort

@app.post("/hooks/routerai-video")
def routerai_video():
    api_key = os.environ["ROUTERAI_API_KEY"]
    ts = request.headers.get("X-RouterAI-Timestamp", "")
    sig = request.headers.get("X-RouterAI-Signature", "")
    raw = request.get_data()  # bytes, сырое тело

    if abs(time.time() - int(ts)) > 300:
        abort(401)

    # секрет = sha256(api_key) в hex
    secret = hashlib.sha256(api_key.encode()).hexdigest()
    expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(sig, expected):
        abort(401)

    event = request.get_json()
    # ... ваша логика: event["type"], event["data"]["unsigned_urls"] ...
    return "", 200
```

### Доставка и ретраи

- Доставка считается успешной при ответе **HTTP 2xx**. Любой другой код или таймаут — неуспех.
- При неуспехе RouterAI повторяет отправку: до **10 попыток** с экспоненциальной задержкой (несколько часов суммарно).
- Бюджет одной попытки — около **8 секунд** (на установку соединения и ответ).
- Endpoint должен быть **публично доступен** — RouterAI не подключается к адресам внутри приватных сетей (защита от SSRF).

## Шаг 4. Скачивание видео

Когда статус `completed`, скачайте готовое видео через content-эндпоинт. `index` — позиция ссылки в массиве `unsigned_urls` (по умолчанию `0`):

```bash
curl "https://routerai.ru/api/v1/videos/Cq4gNzomlZDrNyy72GHC/content?index=0" \
  -H "Authorization: Bearer $ROUTERAI_API_KEY" \
  -o video.mp4
```

Эндпоинт отдаёт бинарный mp4. Если модель вернула несколько видео, в `unsigned_urls` будет несколько ссылок — скачайте каждую, меняя `index` (`?index=1`, `?index=2`, …).

## Устранение неполадок

**Не приходит webhook?**

- Убедитесь, что `callback_url` — валидный **HTTPS**-адрес и публично доступен (приватные сети блокируются).
- Endpoint должен отвечать **2xx**; при ошибках/таймаутах RouterAI ретраит, но после исчерпания попыток перестаёт.
- Webhook отправляется только при терминальном статусе (`completed`/`failed`/`expired`/`cancelled`).

**Подпись не сходится?**

- Считайте HMAC по **сырым байтам** тела, без `JSON.parse` + пересериализации.
- Секрет — это `sha256_hex(api_key)` именно того ключа, которым создавалась задача, а не сам ключ и не отдельный секрет.
- Подписываемая строка — `"<X-RouterAI-Timestamp>.<raw_body>"` (timestamp, точка, тело).

**Статус `failed` или `expired`?**

- Смотрите `data.error` (в webhook) или поле `error` в ответе поллинга.
- `expired` означает, что срок хранения результата истёк — создайте задачу заново.
