# Tegas API — Quick start

Base URL: `https://api.tegas.ai` (mirror: `https://api.tegasai.ru`).
Full reference: [docs.tegas.ai](https://docs.tegas.ai) · schema: `https://api.tegas.ai/v1/openapi.json`.

1. Create a key in the cabinet: **Settings → API → Create key**. Keep it secret; it is shown once.
   Send it in every request as `Authorization: Bearer tg_live_...`.
2. Write a scenario (`POST /v1/scenarios`) — Tessa returns a complete scenario in one call.
3. Start the video (`POST /v1/videos`) with the returned `scenario_id`.
4. Poll `GET /v1/videos/{video_id}` every 10 seconds until `status` is `completed` (then `file_url` is set)
   or `failed` (tokens are refunded).

## curl

```bash
# 2. Write a scenario
curl -X POST https://api.tegas.ai/v1/scenarios \
  -H "Authorization: Bearer tg_live_..." -H "Content-Type: application/json" \
  -d '{"idea":"30-second ad for a coffee shop, warm morning mood","language":"en","duration_sec":30,"aspect":"9:16"}'

# 3. Start the video with the returned scenario_id
curl -X POST https://api.tegas.ai/v1/videos \
  -H "Authorization: Bearer tg_live_..." -H "Idempotency-Key: order-1234" -H "Content-Type: application/json" \
  -d '{"scenario_id":"scn_...","quality":"720p"}'

# 4. Poll until completed / failed
curl https://api.tegas.ai/v1/videos/vid_... -H "Authorization: Bearer tg_live_..."
```

## Python (`requests`)

```python
import time
import uuid

import requests

BASE = "https://api.tegas.ai"
HEADERS = {"Authorization": "Bearer tg_live_...", "Content-Type": "application/json"}


def api(method: str, path: str, **kwargs) -> dict:
    r = requests.request(method, f"{BASE}{path}", headers={**HEADERS, **kwargs.pop("headers", {})}, timeout=60, **kwargs)
    if not r.ok:
        err = r.json().get("error", {})
        raise RuntimeError(f"{r.status_code} {err.get('code')}: {err.get('message')}")
    return r.json() if r.content else {}


# 2. Write a scenario
scenario = api("POST", "/v1/scenarios", json={
    "idea": "30-second ad for a coffee shop, warm morning mood",
    "language": "en",
    "duration_sec": 30,
    "aspect": "9:16",
})
print(scenario["text"])

# 3. Start the video — Idempotency-Key makes retries safe (no double charge)
video = api("POST", "/v1/videos",
            headers={"Idempotency-Key": f"order-{uuid.uuid4()}"},
            json={"scenario_id": scenario["scenario_id"], "quality": "720p"})

# 4. Poll every 10 s until the job finishes
while video["status"] not in ("completed", "failed"):
    time.sleep(10)
    video = api("GET", f"/v1/videos/{video['video_id']}")
    print(video["status"], f"{video['progress']}%")

if video["status"] == "completed":
    print("Ready:", video["file_url"])
else:
    print("Failed:", video["error"])  # tokens are refunded automatically
```

## JavaScript (`fetch`)

```js
const BASE = "https://api.tegas.ai";
const HEADERS = { Authorization: "Bearer tg_live_...", "Content-Type": "application/json" };

async function api(method, path, { body, headers } = {}) {
  const r = await fetch(`${BASE}${path}`, {
    method,
    headers: { ...HEADERS, ...headers },
    body: body ? JSON.stringify(body) : undefined,
  });
  if (!r.ok) {
    const { error } = await r.json().catch(() => ({ error: {} }));
    throw new Error(`${r.status} ${error?.code ?? ""}: ${error?.message ?? r.statusText}`);
  }
  return r.status === 204 ? null : r.json();
}

const sleep = (ms) => new Promise((res) => setTimeout(res, ms));

// 2. Write a scenario
const scenario = await api("POST", "/v1/scenarios", {
  body: {
    idea: "30-second ad for a coffee shop, warm morning mood",
    language: "en",
    duration_sec: 30,
    aspect: "9:16",
  },
});
console.log(scenario.text);

// 3. Start the video — Idempotency-Key makes retries safe (no double charge)
let video = await api("POST", "/v1/videos", {
  headers: { "Idempotency-Key": `order-${crypto.randomUUID()}` },
  body: { scenario_id: scenario.scenario_id, quality: "720p" },
});

// 4. Poll every 10 s until the job finishes
while (video.status !== "completed" && video.status !== "failed") {
  await sleep(10_000);
  video = await api("GET", `/v1/videos/${video.video_id}`);
  console.log(video.status, `${video.progress}%`);
}

if (video.status === "completed") console.log("Ready:", video.file_url);
else console.log("Failed:", video.error); // tokens are refunded automatically
```

## Claude Code / MCP

No code needed: Tegas ships an MCP server at `https://api.tegas.ai/mcp` (RU mirror: `https://api.tegasai.ru/mcp`).
Connect it to Claude Code with your API key in a header:

```bash
claude mcp add --transport http tegas https://api.tegas.ai/mcp --header "Authorization: Bearer tg_live_..."
```

Any MCP client with Streamable HTTP transport (Cursor, Claude Desktop with a custom connector, etc.) works the same way;
`X-Tegas-Key: tg_live_...` is accepted as an alternative header.

The connector requires an active Tegas plan on your account — get one at https://tegas.ai/app/plans
(https://tegasai.ru/app/plans for the RU mirror) before calling most tools.

Tools:

| Tool | Paid | What it does |
|---|---|---|
| `tegas_account` | no | balance, plan, limits, price per second by quality |
| `tegas_voices` | no | voices for a language |
| `tegas_upload_reference` | no | uploads one image (base64 or a local file) and returns `reference_id` |
| `tegas_write_scenario` | no | Tessa writes a scenario from an idea, no questions |
| `tegas_find_heroes` | no | brief of characters / hero product with the price per sheet |
| `tegas_draw_heroes` | **yes** | draws hero sheets — requires `confirm=true` |
| `tegas_start_video` | **yes** | starts the render — requires `confirm=true` |
| `tegas_video_status` | no | status of one video |
| `tegas_wait_video` | no | polls every 10 s until completed / failed / timeout |
| `tegas_list_videos` | no | recent videos |
| `tegas_edit_video` | no | edits scenes / voice / subtitles / music / volumes of a completed video and rebuilds it |
| `tegas_upload_music` | no | uploads one track (base64 or a local file) and returns a `url` for `tegas_edit_video`'s `music.url` |

Paid tools never spend tokens on the first call: with `confirm=false` (the default) they return the price and ask the
agent to get your consent; only after you say "yes" the agent calls again with `confirm=true`. Every start carries an
`Idempotency-Key` (returned as `idempotency_key`), so a retry after a timeout never charges twice.
The resource `tegas://quickstart` gives the agent this document.

`tegas_upload_reference` takes the picture itself, not a link: a remote agent passes `image_base64` (raw base64 or a
`data:image/png;base64,...` URL), while `file_path` reads a file from the machine of the MCP server and therefore works
only on local / self-hosted runs with `MCP_ALLOW_LOCAL_FILES=true` (on our hosted connector it answers
`local_files_disabled` and asks for base64). The whole flow in two lines:

```text
tegas_upload_reference(image_base64="iVBORw0KGgo...", label="hero")      -> reference_id "ref_3f9c2a7b1d4e5f60a1b2"
tegas_write_scenario(idea="...", reference_urls=["ref_3f9c2a7b1d4e5f60a1b2"]) -> scenario_id -> tegas_start_video(scenario_id=...)
```

Example prompt in Claude Code:

> Make a 15-second vertical video about a cozy coffee shop in Russian, 720p. Show me the scenario and the price first.

The agent will call `tegas_write_scenario`, show you the text, quote the price via `tegas_start_video` (confirm=false),
wait for your "yes", start the render and return the `file_url` when `tegas_wait_video` finishes.

`tegas_edit_video` is free and rebuilds a completed video with the changes you ask for - it takes minutes, same as the
original render, so the agent should tell you what it is changing and then wait again with `tegas_wait_video`. Example:

> Change the voice to a male one and make the subtitles the "contrast" style.

```text
tegas_voices(language="ru") -> pick a male voice id
tegas_edit_video(video_id="vid_...", voice={"id": "v_ru_male_2"}, subtitles={"style": "contrast"})
tegas_wait_video(video_id="vid_...") -> file_url of the rebuilt video
```

## For agents

- Using Claude Code or another MCP client? Connect `https://api.tegas.ai/mcp` instead of calling REST by hand — see "Claude Code / MCP" above.

- One call `POST /v1/scenarios` returns a complete scenario; no questions are asked.
- If you want Tessa to ask up to 3 clarifying questions, use `POST /v1/conversations` and
  `POST /v1/conversations/{cid}/messages` until the reply has `complete: true` (it then carries `scenario`).
- `POST /v1/videos` accepts either `scenario_id` (from Tessa) or your own `scenario_text` — exactly one of them.
- `POST /v1/videos` takes an optional `watermark` (bool): the Tegas watermark on the finished video; omit it
  to keep the account default. It can also be switched later with a watermark-only call to
  `POST /v1/videos/{video_id}/edits` (see "Правки готового ролика" below).
- Always send `Idempotency-Key` on `POST /v1/videos` so retries never double-charge.
- Check the price before starting: `GET /v1/prices` (tokens per second by quality) and `GET /v1/me` (balance).
- Errors are `{"error": {"code", "message", "details"?}}`; `429` carries `Retry-After` — wait and retry. Full list below.
- Read-only routes (`GET /v1/me`, `/v1/prices`, `/v1/voices`, `/v1/videos`, `/v1/videos/{id}`, `/v1/conversations/{id}`) need only an API key; an active plan is required for the `POST` routes that write a scenario, upload a reference, draw hero sheets or start a video.
- Upload images once with `POST /v1/references` and pass `reference_id` instead of a URL; draw consistent characters with `POST /v1/scenarios/{id}/heroes` → `/heroes/draw`; get notified with `webhook_url` instead of polling. Details in the sections below.
- A limited number of videos may be in progress at once; a `429 rate_limited` with `details.active` means wait for a running one to finish.

## Референс файлом

Есть фото продукта или героя? Загрузите его один раз — получите `reference_id` и подставляйте в запросы,
ссылка не нужна. Принимаются `jpeg`, `png`, `webp`, `heic` до 10 МБ, один файл за запрос; референс живёт 7 дней,
как сценарий. Размеры: минимальная сторона 360 px, максимальная 4096 px — иначе `422 validation_error`
с `details.reason = "too_small"` / `"too_large_dimensions"` и фактическими `width`/`height`. Те же ограничения
действуют для референсов по ссылке (`422 reference_unavailable`, `details.reason = "too_small"`). У `heic`
размеры не проверяются заранее — их проверит рендер.

```bash
curl -X POST https://api.tegas.ai/v1/references \
  -H "Authorization: Bearer tg_live_..." \
  -F file=@photo.jpg -F label=hero
```

Ответ `201`:

```json
{
  "reference_id": "ref_3f9c2a7b1d4e5f60a1b2",
  "url": "https://files.tegas.ai/.../reference.jpg",
  "label": "hero",
  "description": "a woman in a red jacket holding a white coffee cup",
  "expires_at": "2026-09-19T10:00:00+00:00"
}
```

`description` — короткое описание картинки, которое Тесса использует в сценарии. Дальше передавайте
`{"reference_id": "..."}` вместо `{"url": ...}` в `references` у `POST /v1/scenarios`, `POST /v1/conversations`
и `POST /v1/videos` (в одном списке можно смешивать оба вида):

```bash
curl -X POST https://api.tegas.ai/v1/scenarios \
  -H "Authorization: Bearer tg_live_..." -H "Content-Type: application/json" \
  -d '{"idea":"30-second ad for a coffee shop","language":"en","duration_sec":30,"aspect":"9:16",
       "references":[{"reference_id":"ref_3f9c2a7b1d4e5f60a1b2"}]}'
```

Слишком большой файл — `413 too_large` (по `Content-Length`, до чтения тела) или `422 validation_error`
с `details.reason = "too_large"`; не картинка — `422` с `details.reason = "not_image"`.

## Своя музыка

Свой трек при старте ролика поставить нельзя — рендер этого не умеет. Загрузите его отдельно и подставьте
в правку готового ролика (`POST /v1/videos/{video_id}/edits`, поле `music.url`). Принимаются `mp3`, `wav`,
`m4a` до 25 МБ, один файл за запрос; тип определяется по содержимому файла, а не по заголовку клиента.
Запись живёт 7 дней, как сценарий, и доступна только вашему ключу.

```bash
curl -X POST https://api.tegas.ai/v1/music \
  -H "Authorization: Bearer tg_live_..." \
  -F file=@track.mp3 -F label=intro
```

Ответ `201`:

```json
{
  "music_id": "mus_8b1d4e5f60a1b2c3d4e5",
  "url": "https://files.tegas.ai/.../music.mp3",
  "label": "intro",
  "expires_at": "2026-09-21T10:00:00+00:00"
}
```

Возвращённый `url` передавайте в `music.url` правки:

```bash
curl -X POST https://api.tegas.ai/v1/videos/123/edits \
  -H "Authorization: Bearer tg_live_..." -H "Content-Type: application/json" \
  -d '{"music":{"enabled":true,"url":"https://files.tegas.ai/.../music.mp3"},"volumes":{"music":25}}'
```

Слишком большой файл — `413 too_large` (по `Content-Length`, до чтения тела) или `422 validation_error`
с `details.reason = "too_large"`; не музыка — `422` с `details.reason = "not_audio"`; пустой файл —
`422` с `details.reason = "empty"`.

## Правки готового ролика

Готовый ролик можно пересобрать — это бесплатно, токены за правки не списываются.
Один вызов `POST /v1/videos/{video_id}/edits` применяет сразу все изменения: тексты озвучки и субтитров
по сценам, своё видео в сцену, голос, стиль и вид субтитров, свою музыку и громкости.
Править можно только свои ролики: чужой `video_id` — `404 not_found`.

```bash
# Текст озвучки первой сцены и стиль субтитров — одним вызовом
curl -X POST https://api.tegas.ai/v1/videos/123/edits \
  -H "Authorization: Bearer tg_live_..." -H "Content-Type: application/json" \
  -d '{
        "scenes": [{"index": 0, "voiceover_text": "Новый текст первой сцены"}],
        "subtitles": {"enabled": true, "style": "tiktok",
                      "appearance": {"alignment": "center", "text_color": "#FFCC00"}}
      }'
```

Ответ `202`:

```json
{"video_id": "123", "status": "processing", "edits_accepted": ["scenes", "subtitles"]}
```

`edits_accepted` — группы изменений, которые ушли в сборку. После правки ролик снова
`processing`: дождитесь `completed` тем же опросом, что и при запуске.

```bash
curl -s https://api.tegas.ai/v1/videos/123 -H "Authorization: Bearer tg_live_..." | jq .status
```

Смена `voice.id`, `voice.gender` или `voice.language` переозвучивает **все** сцены, у которых есть текст
озвучки — это гарантирует сам рендер, даже если в той же правке меняются другие сцены, добавляется своё
видео или новая музыка; сцены без своего `voiceover_text` озвучиваются прежним текстом. Если вебхук для
ролика уже был заведён, он вернётся в очередь и новый терминальный статус придёт снова; если вебхука не
было, его можно передать прямо в правке (`webhook_url`, `webhook_secret`).

Субтитры нельзя одновременно выключить и поменять им стиль или оформление в одной и той же правке:
`subtitles.enabled: false` вместе с `style` или `appearance` вернёт `422 validation_error`. Выключение
субтитров и их последующая перестройка — это две разные правки.

Водяной знак (`watermark: true|false`) меняется отдельным вызовом: рендер пересобирает финальное видео
через свой отдельный шаг, поэтому `watermark` нельзя передавать вместе с любой другой группой изменений
в одной правке — это вернёт `422 validation_error`. Отдельный вызов с одним только `watermark` вернёт
`edits_accepted: ["watermark"]`.

Пока ролик пересобирается, вторая правка получит `409 video_busy`:

```json
{"error": {"code": "video_busy", "message": "The video is being rebuilt, wait for status completed"}}
```

Тело обязано содержать хотя бы одно изменение, иначе `422 nothing_to_change`. Адреса `scenes[].video_url`
и `music.url` проверяются так же, как референсы (только `https` и публичный хост), иначе `422
validation_error` с точным путём поля в `details.fields`.

Что правкой сделать нельзя: удалить сцену, сменить соотношение сторон или качество — рендер этого не умеет.

## Герои

Чтобы герой и продукт выглядели одинаково во всех сценах, нарисуйте им «листы» — референсы, которые рендер
будет держать перед глазами. Два шага после `POST /v1/scenarios`:

1. `POST /v1/scenarios/{scenario_id}/heroes` — бриф: Тесса читает сценарий и предлагает, кого рисовать.
   Бесплатно. Тело `{"style": "3d_cartoon"}` (`photorealistic` | `3d_cartoon` | `2d_animation` | `anime`;
   пусто — по сценарию).

   ```json
   {
     "items": [
       {"index": 0, "kind": "character", "name": "Barista Anna", "prompt": "A cheerful barista in her 30s, ..."},
       {"index": 1, "kind": "object", "name": "Signature latte", "prompt": "A tall glass of layered latte, ..."}
     ],
     "price_each_tokens": 2
   }
   ```

   `price_each_tokens` — цена одного листа (сейчас 2 T); проверьте баланс в `GET /v1/me`.
2. `POST /v1/scenarios/{scenario_id}/heroes/draw` — рисование, списывает рендер. Передайте индексы из брифа
   `{"items": [0, 1]}` (1–3 штуки) или свои правки `{"items": [{"kind": "character", "name": "...", "prompt": "..."}]}`.
   Ответ `201`:

   ```json
   {
     "items": [
       {"reference_id": "ref_...", "kind": "character", "name": "Barista Anna", "url": "https://files.tegas.ai/..."},
       {"reference_id": "ref_...", "kind": "object", "name": "Signature latte", "url": "https://files.tegas.ai/..."}
     ],
     "price_tokens": 4
   }
   ```

Дальше ничего делать не нужно: `POST /v1/videos {"scenario_id": ...}` подхватит нарисованные листы сам — они
уже лежат в референсах сценария с метками `hero: <name>` и `product: <name>`. Повторный `draw` того же героя
заменяет его лист, а не добавляет второй. Не хватает токенов — `402 insufficient_tokens`, бриф не делали —
`422 validation_error` с `details.reason = "no_brief"`.

```bash
curl -X POST https://api.tegas.ai/v1/scenarios/scn_.../heroes \
  -H "Authorization: Bearer tg_live_..." -H "Content-Type: application/json" -d '{"style":"3d_cartoon"}'

curl -X POST https://api.tegas.ai/v1/scenarios/scn_.../heroes/draw \
  -H "Authorization: Bearer tg_live_..." -H "Content-Type: application/json" -d '{"items":[0,1]}'
```

## Вебхуки

Вместо опроса `GET /v1/videos/{id}` можно попросить нас сообщить о результате. В `POST /v1/videos` добавьте
`webhook_url` (только `https` на публичном хосте) и, желательно, `webhook_secret` (16–128 символов) — им мы
подпишем тело:

```json
{
  "scenario_id": "scn_...",
  "quality": "720p",
  "webhook_url": "https://example.com/hooks/tegas",
  "webhook_secret": "change-me-to-a-long-random-string"
}
```

Когда ролик готов или упал, мы делаем `POST webhook_url` с JSON:

```json
{
  "event": "video.completed",
  "video": {
    "video_id": "vid_...",
    "status": "completed",
    "progress": 100,
    "duration_sec": 30,
    "file_url": "https://files.tegas.ai/.../video.mp4",
    "error": null,
    "price_tokens": 120,
    "created_at": "2026-09-12T10:00:00+00:00",
    "completed_at": "2026-09-12T10:06:12+00:00"
  },
  "sent_at": "2026-09-12T10:06:15+00:00"
}
```

`event` — `video.completed` или `video.failed` (тогда в `video.error` лежит `{code, message}`, токены возвращены).
`video` — тот же объект, что отдаёт `GET /v1/videos/{id}`.

Заголовки:

| header | значение |
|---|---|
| `X-Tegas-Event` | `video.completed` / `video.failed` |
| `X-Tegas-Delivery` | uuid доставки — новый на каждую попытку; используйте `video.video_id` для идемпотентности |
| `X-Tegas-Signature` | `sha256=<hex HMAC-SHA256(webhook_secret, raw body)>` — только если задан `webhook_secret` |
| `User-Agent` | `tegas-webhooks/1` |

**Проверка подписи.** Считайте HMAC от сырых байтов тела (до разбора JSON) и сравнивайте безопасно.

Python (Flask/FastAPI — любой фреймворк, важно взять `raw body`):

```python
import hashlib
import hmac

SECRET = b"change-me-to-a-long-random-string"


def verify(raw_body: bytes, signature_header: str) -> bool:
    expected = "sha256=" + hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header or "")


# FastAPI
# @app.post("/hooks/tegas")
# async def hook(request: Request):
#     raw = await request.body()
#     if not verify(raw, request.headers.get("X-Tegas-Signature")):
#         return Response(status_code=401)
#     event = json.loads(raw)
#     ...  # обработайте event["video"] по video_id идемпотентно
#     return Response(status_code=204)
```

Node (Express — отключите JSON-парсер на этом роуте, нужны сырые байты):

```js
import crypto from "node:crypto";
import express from "express";

const SECRET = "change-me-to-a-long-random-string";
const app = express();

function verify(rawBody, signatureHeader) {
  const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(rawBody).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(signatureHeader ?? "");
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/hooks/tegas", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.body, req.get("X-Tegas-Signature"))) return res.sendStatus(401);
  const { event, video } = JSON.parse(req.body.toString("utf8"));
  // обработайте по video.video_id идемпотентно
  res.sendStatus(204);
});
```

Ожидания к приёмнику и повторы:

- Ответьте любым `2xx` в течение 10 секунд — тяжёлую работу делайте после ответа.
- Не `2xx` или сетевая ошибка — повторим: 5 попыток с паузами 1, 5, 15, 60 и 300 секунд, затем `failed`.
  Одна и та же доставка может прийти дважды (например, если вы ответили медленно) — делайте обработку
  идемпотентной по `video.video_id`.
- Если ролик не завершился за 2 часа, вебхук переводится в `failed` с `last_status = "timeout"`;
  сам ролик при этом можно по-прежнему опросить.
- Состояние вебхука видно в `GET /v1/videos/{id}` → `webhook: {state, attempts, last_status}`
  (`pending` | `delivering` | `done` | `failed`).
- Вебхук — дополнение к опросу, не замена: при сомнениях опросите `GET /v1/videos/{id}`.

## Error codes

| HTTP | code | meaning |
|---|---|---|
| 400 | invalid_request | the renderer rejected the request parameters; see `details.upstream` |
| 401 | invalid_key | missing, unknown or revoked API key |
| 402 | insufficient_tokens | not enough tokens for this video |
| 403 | no_active_plan | an active plan is required for this call (POST routes only) |
| 404 | not_found | the object does not exist or belongs to another account (also an expired or foreign `reference_id`, see `details.reference_id`) |
| 402 | insufficient_tokens | not enough tokens for a hero sheet (`POST /heroes/draw`) |
| 409 | idempotency_conflict | same `Idempotency-Key` with a different body, or the same request is still in progress |
| 409 | conversation_complete | the conversation already produced a scenario — start a new one |
| 409 | conversation_too_long | the conversation hit its message limit — start a new one |
| 413 | too_large | `POST /v1/references`: the request body exceeds the file limit (`details.max_bytes`) |
| 422 | validation_error | a field is invalid; see `details.fields` (or `details.field` + `details.reason`: `too_large`, `not_image`, `empty`, `too_small`, `too_large_dimensions` (with `width`/`height`/`min_side`/`max_side`) for uploads; `no_brief` for `/heroes/draw` before a brief) |
| 422 | scenario_invalid | Tessa could not produce a valid scenario (`details.warnings`), or `scenario_text` has no episodes (`details.reason = "no_episodes"`) |
| 422 | reference_unavailable | a reference image could not be downloaded, or its shorter side is under 360 px (`details.reason = "too_small"`, `details.index`) |
| 429 | rate_limited | rate or concurrency limit; wait `Retry-After` seconds |
| 500 | server_misconfigured | the API is not configured on the server side — contact support |
| 502 | upstream_error | renderer, wallet or model did not respond; `details.upstream` has the raw reason |

A failed video is not an HTTP error: `GET /v1/videos/{id}` returns `status: "failed"` with `error.code` equal to
`generation_failed` (rendering failed, tokens are refunded) or `ip_infringement` (blocked by the rights filter).

## Privacy & support

Data handling: https://tegas.ai/privacy-policy. Support: support@tegas.ai.
