# Artillect API — изображения, видео и чат

REST API: **генерация изображений** (async poll), **генерация видео** (async poll), **Topaz upscale** (async poll, API-only) и **LLM chat** (sync).  
Async: images/videos/upscale submit → poll (или push через webhooks). Chat: один запрос → ответ.

> **Обратная совместимость:** все изменения аддитивны. Старые запросы (без новых полей `project_id`, `billing_source`, без заголовков `Idempotency-Key`) работают как раньше. Новые поля в ответах (`request_id` и т.п.) можно игнорировать.

---

## Base URL

| Окружение      | URL                         | Примечание                                                |
| -------------- | --------------------------- | --------------------------------------------------------- |
| **Production** | `https://app.artillect.pro` | `main` → `deploy-prod`, prod MariaDB; public API v1 здесь |
| **Staging**    | `https://dev.artillect.pro` | `develop` → `deploy-staging`, отдельная MariaDB `:13306`  |

Все запросы — к тому же origin, где у пользователя аккаунт.  
`verification_url` и URL картинок формируются из домена запроса.

Документация: `https://app.artillect.pro/docs/artillect-api.md` (staging: `https://dev.artillect.pro/docs/artillect-api.md`).

**OpenAPI 3.1:** [`/docs/artillect-openapi.yaml`](./artillect-openapi.yaml) — `https://app.artillect.pro/docs/artillect-openapi.yaml` (staging: `https://dev.artillect.pro/docs/artillect-openapi.yaml`). **Interactive reference (Scalar):** [`/docs`](/docs) — `https://app.artillect.pro/docs` (Try-it: paste `art_…` Bearer; auth persists in browser localStorage). **MCP install (Claude/Cursor):** [`/docs/mcp`](/docs/mcp). **Curated use cases (UGC / overnight / ecommerce):** [`/docs/use-cases`](/docs/use-cases) · markdown [`artillect-use-cases.md`](./artillect-use-cases.md). **Features (mechanics):** [`/docs/features`](/docs/features) · [`artillect-features.md`](./artillect-features.md). Core v1 paths + schemas; model-specific params → `GET /api/v1/models/`.

### Changelog (integrator-facing)

| Version | Notes                                                                                                             |
| ------- | ----------------------------------------------------------------------------------------------------------------- |
| 1.1.7   | 3D: `POST /api/v1/mesh/generations/` (Meshy v6 t2m/i2m, GLB) + MCP `generate_mesh`; `estimate` `type: "mesh"`     |
| 1.1.6   | Аудио: `POST /api/v1/audio/generations/` (Suno V5.5, музыка) + MCP `generate_audio`; `estimate` `type: "audio"`   |
| 1.1.5   | SSRF harden (input + webhook delivery); daily per-key spend cap; chat per-key submit RL; MCP HTTP Bearer-only     |
| 1.1.4   | DeepSeek chat: `thinking` disabled by default; clearer empty/`max_tokens` errors (no silent sanitize)             |
| 1.1.3   | Chat default → `gpt-5-5` (KIE Claude capacity); `PROVIDER_BUSY` for tight resources                               |
| 1.1.2   | Per-key usage: `request_count`, `tokens_spent`, `last_route` on key list / `/api` UI                              |
| 1.1.1   | API key `name` / `last_used_at` / `scopes`; `PATCH /api/v1/keys/{id}`; `403 INSUFFICIENT_SCOPE`                   |
| 1.1.0   | Scalar Try-it auth persist; OpenAPI↔routes CI drift gate; project manage + webhooks deliveries already in surface |
| 1.0     | Initial OpenAPI 3.1 + Scalar docs                                                                                 |

### Trailing slash

Канон URL — **со слешем** в конце (`/api/v1/device/code/`).  
В приложении включён `skipTrailingSlashRedirect`: Next **не** отвечает 308 на вариант без `/`, оба пути обслуживаются.

Клиент приложения нормализует URL через `authFetch` / `appFetch`. Внешним клиентам всё равно лучше слать канон со `/` (curl без `-L` и без слеша раньше терял POST body на 308).

---

## Лимиты ввода (все модели)

Источник в коде: `src/lib/generation/kieInputLimits.js`, registry `imageModels.ts` / `llmModels.ts`.  
Для **images** лимит — поле `prompt` (символы Unicode, `String.length`). Для **chat** — сумма всех `messages[].content` (только строки).

### Images (`POST /api/v1/images/generations/`)

| `model`                 | max `prompt` (символов) | Источник                                                                            | Другое на вход                               |
| ----------------------- | ----------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------- |
| `gpt-image-2` (default) | **20 000**              | [KIE OpenAPI](https://docs.kie.ai/market/gpt/gpt-image-2-text-to-image) `maxLength` | i2i: до **16** ref (`input_urls` / `images`) |
| `gpt-image-15`          | **20 000**              | [KIE GPT Image 1.5](https://docs.kie.ai/market/gpt-image/1-5-text-to-image)         | i2i: до **16** refs; `quality` medium\|high  |
| `nano-banana`           | **20 000**              | KIE Google Nano Banana (legacy)                                                     | i2i: до **10** refs                          |
| `nano-banana-2`         | **20 000**              | [KIE Nano Banana 2](https://docs.kie.ai/market/google/nanobanana2)                  | i2i: до **16** refs                          |
| `nano-banana-pro`       | **10 000**              | [KIE Nano Banana Pro](https://docs.kie.ai/market/google/pro-image-to-image)         | i2i: до **8** refs                           |
| `seedream-4`            | **5 000**               | KIE/FAL ByteDance Seedream 4.0                                                      | i2i: до **10** refs                          |
| `seedream-4-5`          | **5 000**               | KIE/FAL ByteDance Seedream 4.5                                                      | i2i: до **10** refs                          |
| `seedream-5-lite`       | **5 000**               | KIE Seedream 5 Lite                                                                 | i2i: до **14** refs                          |
| `flux2-pro`             | **20 000**              | KIE Flux 2 Pro                                                                      | i2i: до **8** refs; resolution 1K\|2K        |
| `flux2-max`             | **20 000**              | KIE Flux 2 Max (тот же KIE backend, что flux2-pro)                                  | i2i: до **8** refs; resolution 1K\|2K        |
| `flux2-flex`            | **20 000**              | KIE Flux 2 Flex                                                                     | i2i: до **8** refs; resolution 1K\|2K        |
| `grok-imagine`          | **20 000**              | KIE Grok Imagine                                                                    | i2i: до **1** ref                            |
| `wan-27-image`          | **20 000**              | KIE Wan 2.7 Image                                                                   | i2i: до **9** refs; default resolution 2K    |
| `wan-27-image-pro`      | **20 000**              | KIE Wan 2.7 Image Pro                                                               | i2i: до **9** refs; default resolution 2K    |
| `cosmos-3-super`        | **20 000**              | FAL Nvidia Cosmos 3S                                                                | t2i only (refs → `400`)                      |
| `mai-image-25`          | **20 000**              | FAL Microsoft MAI Image 2.5                                                         | i2i: до **8** refs                           |
| `krea-v2-large`         | **20 000**              | FAL Krea 2 Large                                                                    | style refs на t2i, max **10**                |
| `krea-v2-medium`        | **20 000**              | FAL Krea 2 Medium                                                                   | style refs на t2i, max **10**                |
| `krea-v2-medium-turbo`  | **20 000**              | FAL Krea 2 Medium Turbo                                                             | style refs на t2i, max **10**                |
| `qwen-multiple-angles`  | **20 000**              | FAL Qwen Multiple Angles (`angles`)                                                 | **обязателен** 1 ref; `prompt` опционален    |

Превышение → `400 PROMPT_TOO_LONG`. Одинаковый лимит для text-to-image и image-to-image (поле `prompt`).

### Chat (`POST /api/v1/chat/completions/`)

| `model`             | Лимит символов на вход            | Лимит сообщений | Docs                                                                            | Vision (картинки)         |
| ------------------- | --------------------------------- | --------------- | ------------------------------------------------------------------------------- | ------------------------- |
| `gpt-5-5` (default) | **200 000** суммарно в `content`¹ | **32**          | [KIE GPT 5.5](https://docs.kie.ai/market/chat/gpt-5-5.md)                       | ✓ (URL / Artillect media) |
| `claude-sonnet-4-6` | **200 000** суммарно в `content`¹ | **32**          | [KIE Claude Sonnet 4.6](https://docs.kie.ai/market/claude/claude-sonnet-4-6.md) | ✓ (URL / Artillect media) |
| `claude-opus-4-8`   | **200 000** суммарно в `content`¹ | **32**          | [KIE Claude Opus 4.8](https://docs.kie.ai/market/claude/claude-opus-4-8.md)     | ✓ (URL / Artillect media) |
| `deepseek-v4-flash` | **200 000** суммарно в `content`¹ | **32**          | [DeepSeek pricing](https://api-docs.deepseek.com/quick_start/pricing)           | ✗                         |

¹ **200 000** — guardrail Artillect API (защита от abuse), **не** документированный char-cap KIE. KIE может отклонить запрос раньше по **токенам** (зависит от модели и длины истории; см. `usage.input_tokens` в ответе).

**Vision-лимиты (модели с vision):** max **4** картинки на сообщение, max **8** суммарно на запрос, max **10MB** на файл (зеркалируется в Artillect media перед провайдером).  
**Видео во входе chat — не поддерживается.** `deepseek-v4-flash` — только текст (картинки → `400 CHAT_VISION_UNSUPPORTED`).

**Выход chat (не вход):** Claude / DeepSeek — `max_tokens` по умолчанию `4096`, можно задать в теле (`max_tokens`, 1–8192). GPT 5.5 — лимит выхода задаёт провайдер.

Превышение 32 сообщений или 200k символов → `400 MESSAGES_TOO_LONG`. Слишком много картинок → `400 MESSAGES_TOO_MANY_IMAGES`.

---

## Обзор

```
1. POST /api/v1/device/code/        → device_code + verification_url
2. ⛔ ПОКАЗАТЬ verification_url пользователю → ждать «выдал разрешение»
3. POST /api/v1/device/token/       → poll до api_key (art_...)
4. POST /api/v1/images/generations/ → job id (images)
5. GET  /api/v1/images/generations/{id}/ → poll image
6. POST /api/v1/videos/generations/ → job id (video)
7. GET  /api/v1/videos/generations/{id}/ → poll video
8. POST /api/v1/upscale/generations/ → job id (Topaz upscale)
9. GET  /api/v1/upscale/generations/{id}/ → poll upscale
10. GET /api/v1/projects/            → список доступных проектов
11. GET /api/v1/projects/{id}/elements/ → element library проекта
12. GET /api/v1/projects/{id}/tags/      → теги проекта (+ POST/PATCH/DELETE)
13. POST /api/v1/switchx/generations/ → job id (SwitchX / Beeble)
14. GET  /api/v1/switchx/generations/{id}/ → poll SwitchX
15. POST /api/v1/audio/generations/  → job id (Suno V5.5, музыка)
16. GET  /api/v1/audio/generations/{id}/ → poll аудио
17. POST /api/v1/mesh/generations/   → job id (Meshy v6, 3D mesh)
18. GET  /api/v1/mesh/generations/{id}/ → poll mesh (GLB)
19. POST /api/v1/chat/completions/   → LLM ответ (sync; опц. SSE gpt-5-5)
20. GET  /api/v1/models/             → список моделей и лимитов (без auth)
```

### Вспомогательные эндпоинты

| Эндпоинт                                                | Назначение                                                                                |
| ------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `GET /api/v1/me/`                                       | Кто я: `user_id`, `email`, `token_balance` (проверить ключ)                               |
| `GET /api/v1/balance/`                                  | Личный баланс токенов                                                                     |
| `POST /api/v1/estimate/`                                | Узнать цену **до** генерации (без списания), `{type, ...body}`                            |
| `POST /api/v1/files/`                                   | Загрузить файл (multipart `file`) → media key + URL для повторного использования на входе |
| `POST /api/v1/files/from-url/`                          | Зеркалировать публичный HTTPS URL в media (SSRF-гард) → тот же `{ key, url }`             |
| `POST /api/v1/files/presign/`                           | Presigned PUT для image/audio (не video) → `{ upload_url, url, required_headers }`        |
| `GET /api/v1/images/generations/`                       | История image-генераций (пагинация)                                                       |
| `GET /api/v1/videos/generations/`                       | История video-генераций (пагинация)                                                       |
| `GET /api/v1/upscale/generations/`                      | История upscale-генераций (пагинация)                                                     |
| `GET /api/v1/switchx/generations/`                      | История SwitchX-генераций (пагинация)                                                     |
| `GET /api/v1/audio/generations/`                        | История аудио-генераций (пагинация)                                                       |
| `GET /api/v1/mesh/generations/`                         | История 3D-генераций (пагинация)                                                          |
| `GET /api/v1/projects/`                                 | Проекты, доступные API-пользователю (owned + member)                                      |
| `POST /api/v1/projects/`                                | Создать проект `{ name, description?, status?, folder_id? }`                              |
| `GET /api/v1/projects/{id}/`                            | Один проект + роль вызывающего                                                            |
| `PATCH /api/v1/projects/{id}/`                          | Обновить проект (owner): `{ name?, description?, status? }`                               |
| `DELETE /api/v1/projects/{id}/`                         | Удалить проект (owner; не личный хаб «Мои генерации»)                                     |
| `GET /api/v1/projects/roles/`                           | Каталог ролей участников (`slug` / `title` / `level`)                                     |
| `GET /api/v1/projects/{id}/members/`                    | Участники проекта (`?state=active_or_invited\|active\|…`)                                 |
| `POST /api/v1/projects/{id}/members/`                   | Пригласить `{ user_id\|email, role?, token_limit? }` (owner)                              |
| `PATCH /api/v1/projects/{id}/members/{userId}/`         | Роль / лимит / `action: remove\|activate` (owner)                                         |
| `DELETE /api/v1/projects/{id}/members/{userId}/`        | Удалить участника (owner)                                                                 |
| `DELETE /api/v1/projects/{id}/members/me/`              | Выйти из проекта (не owner)                                                               |
| `PATCH /api/v1/projects/{id}/members/me/`               | Сайдбар: `{ folder_id?, is_favorite? }`                                                   |
| `GET /api/v1/projects/{id}/generations/`                | Лента: `source`, `kind`, `q`, `tag_ids`, `date`/`date_from`/`date_to`, `mine_only`, …     |
| `PATCH /api/v1/projects/{id}/generations/{genId}/`      | Move (`project_id`) / favorite mask (`favorited`); genId = DB id или external_job_id      |
| `DELETE /api/v1/projects/{id}/generations/{genId}/`     | Удалить генерацию                                                                         |
| `POST /api/v1/projects/{id}/generations/{genId}/copy/`  | Копия в другой проект `{ project_id }`                                                    |
| `GET /api/v1/projects/{id}/favorites/`                  | Избранное проекта: counts + items                                                         |
| `GET /api/v1/projects/{id}/analytics/`                  | Сводка: members/gens/tokens + recent ledger/audit                                         |
| `GET /api/v1/projects/{id}/billing/`                    | Роль вызывающего + `token_limit` / `tokens_spent`                                         |
| `GET /api/v1/folders/`                                  | Папки сайдбара текущего пользователя                                                      |
| `POST /api/v1/folders/`                                 | Создать папку `{ name, parent_id? }`                                                      |
| `GET /api/v1/projects/{id}/elements/`                   | Element library проекта (`@имя` для видео)                                                |
| `POST /api/v1/projects/{id}/elements/`                  | Создать элемент `{ name, kind, media_json, preview_url }`                                 |
| `GET /api/v1/projects/{id}/elements/{elementId}/`       | Один элемент                                                                              |
| `PATCH /api/v1/projects/{id}/elements/{elementId}/`     | Обновить элемент (`name?`, `kind?`, `media_json?`, `preview_url?`)                        |
| `DELETE /api/v1/projects/{id}/elements/{elementId}/`    | Soft-delete элемента                                                                      |
| `POST /api/v1/projects/{id}/elements/reorder/`          | `{ ordered_ids: string[] }`                                                               |
| `GET /api/v1/projects/{id}/tags/`                       | Теги проекта (custom shared; без лайков/системных)                                        |
| `POST /api/v1/projects/{id}/tags/`                      | Создать тег `{ name, color? }`                                                            |
| `PATCH /api/v1/projects/{id}/tags/{tagId}/`             | Переименовать / перекрасить                                                               |
| `DELETE /api/v1/projects/{id}/tags/{tagId}/`            | Удалить тег и связи                                                                       |
| `GET /api/v1/projects/{id}/generations/{genId}/likes/`  | Кто лайкнул + `liked_by_me`                                                               |
| `POST /api/v1/projects/{id}/generations/{genId}/likes/` | Свой лайк add/remove/toggle                                                               |
| `POST /api/v1/projects/{id}/generations/{genId}/tags/`  | Assign custom tags на существующую генерацию                                              |
| `POST /api/v1/images/generations/{id}/cancel/`          | Отменить + вернуть токены (если не завершено)                                             |
| `POST /api/v1/videos/generations/{id}/cancel/`          | То же для видео                                                                           |
| `POST /api/v1/upscale/generations/{id}/cancel/`         | То же для upscale                                                                         |
| `POST /api/v1/switchx/generations/{id}/cancel/`         | То же для SwitchX                                                                         |
| `POST /api/v1/audio/generations/{id}/cancel/`           | То же для аудио                                                                           |
| `POST /api/v1/mesh/generations/{id}/cancel/`            | То же для 3D-мешей                                                                        |
| `POST /api/v1/webhooks/`                                | Зарегистрировать HTTPS webhook для текущего API-ключа                                     |
| `GET /api/v1/webhooks/`                                 | Текущая подписка (`secret_last4`, без полного secret)                                     |
| `DELETE /api/v1/webhooks/`                              | Удалить подписку                                                                          |
| `POST /api/v1/webhooks/rotate-secret/`                  | Новый signing secret (показывается один раз)                                              |
| `GET /api/v1/webhooks/deliveries/`                      | История доставок (outbox): `limit`, `cursor`, `status`, `job_id`                          |
| `GET /api/v1/webhooks/deliveries/{event_id}/`           | Деталь доставки + `payload`                                                               |

**Webhooks (MVP):** одна HTTPS-подписка на API-ключ (URL должен резолвиться в публичные IP; redirects при delivery не следуем). По умолчанию события `generation.completed` / `generation.failed` для jobs с `client_source=public_api`. Опция `include_studio: true` — также Studio jobs того же пользователя. Poll остаётся source of truth.

### Webhook signing cookbook

Заголовок: `X-Artillect-Signature: sha256=<hex>` = HMAC-SHA256(`secret`, **raw body bytes as UTF-8 string**).

Правила:

1. Сверяй подпись по **сырому** телу запроса — не `JSON.stringify(JSON.parse(body))` (пробелы/порядок ключей сломают HMAC).
2. Сравнивай через timing-safe equal (`crypto.timingSafeEqual` / `hmac.compare_digest`).
3. Дедуп по `payload.id` (`event_id`, вид `evt_…`) — доставки могут ретраиться.
4. Статусы outbox: `pending` → `delivered` | `dead` (после исчерпания попыток). Смотри `GET /api/v1/webhooks/deliveries/`.

Node:

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

function verify(secret, rawBody, header) {
  const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  try {
    return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(String(header || "").trim()));
  } catch {
    return false;
  }
}
```

Python:

```python
import hashlib, hmac

def verify(secret: str, raw_body: bytes, header: str) -> bool:
    digest = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    expected = f"sha256={digest}"
    return hmac.compare_digest(expected, (header or "").strip())
```

**История генераций** (`GET .../generations/` для images, videos, upscale, switchx): каждый item — `{ id, status, model, created_at, cost_tokens, tokens_charged, project_id, preview_text, source }`, плюс `next_cursor`. Query `?source=all` (default) \| `public_api` \| `studio` фильтрует по `client_source` в БД. Новые Public image jobs: id `{uuid}-api-image` (legacy poll/cancel: `{uuid}-api`). Project feed: `GET /api/v1/projects/{id}/generations/`. Poll по id — `{ id, status, model, cost_tokens, … }` (video/switchx также `task`).

**Отмена:** `POST .../generations/{id}/cancel/` доступна для **images, videos, upscale и switchx** (best-effort: статус cancelled + refund, если job ещё не terminal).

**Оценка цены:** `POST /api/v1/estimate/` с тем же телом, что и submit, плюс `type: "image" | "video" | "chat" | "switchx" | "upscale"` → `{ cost_tokens }`. Ничего не списывает.

**Несколько картинок и seed:** для image-генерации можно передать `n` (кол-во картинок, где модель поддерживает — см. `GET /models`) и `seed` (воспроизводимость). Цена масштабируется на `n`. В poll вернётся массив `images`.

**Загрузка файлов:** `POST /api/v1/files/` (multipart), `POST /api/v1/files/from-url/` (`{ "url": "https://…" }`), или `POST /api/v1/files/presign/` (image/audio only → PUT на `upload_url` с `required_headers`) → `{ key, url }`. Лимиты: изображение 10MB, видео 200MB, аудио 50MB. Video **не** поддерживает presign (нужен server transform).

**Стабильные ссылки на результат:** для завершённых генераций API **предпочитает** постоянное хранилище Artillect (`…/api/backend/media/download/?key=…`). Если зеркалирование ещё не успело — в ответе может быть временный CDN провайдера; скачайте сразу (см. poll §).

**Images:** omit `model` → `gpt-image-2` (обратная совместимость). Неизвестный `model` → `400 MODEL_UNKNOWN` (как у video).  
**Video:** omit `model` → `kling-3-turbo`.  
**Chat:** omit `model` → `gpt-5-5`. Без встроенных system prompts — только ваши `messages`.  
(Claude Sonnet/Opus остаются в каталоге; при capacity у KIE → `503 PROVIDER_BUSY`, retry или `gpt-5-5`.)  
По умолчанию списание — с **личного баланса**, результаты — в проект **«Мои генерации {userId}»** (отдельная запись в БД).  
Это **не** `users.default_project_id` (стартовая страница в UI — может быть любой проект, например «зверолес»).

Альтернатива device flow: `POST /api/v1/keys` с session JWT в браузере (см. таблицу эндпоинтов), либо страница **`/api`** в браузере — создание/копирование/удаление ключей.

Опционально при создании: `{ "name": "ci", "scopes": ["images:write", "models:read"] }`. Список ключей отдаёт `name`, `last_used_at`, `scopes` (секрет никогда). `PATCH /api/v1/keys/{id}` — переименовать / сменить scopes. Пустые/`null` scopes = полный доступ `["*"]` (обратная совместимость). Недостающий scope на Public API → `403 INSUFFICIENT_SCOPE`.

### Выбор проекта и источника списания (`project_id`, `billing_source`)

Опциональные поля тела для **всех** submit-эндпоинтов (images / videos / chat):

| Поле             | Значения          | Поведение                                                                                                    |
| ---------------- | ----------------- | ------------------------------------------------------------------------------------------------------------ |
| `project_id`     | int               | Проект, куда писать генерацию. **Omit** → «Мои генерации» + личный баланс (как раньше).                      |
| `billing_source` | `owner` \| `self` | С чьего баланса списывать. Обязателен **только** если `project_id` — чужой проект, где вы редактор (editor). |
| `tag_ids`        | int[]             | **Опционально.** Сразу повесить custom-теги проекта на новую генерацию. Неверный id молча пропускается.      |
| `tag_id`         | int               | То же, один тег (удобный алиас).                                                                             |

Логика:

- `project_id` не задан (или = ваш личный hub) → личный баланс. `billing_source` игнорируется. **Полная обратная совместимость.**
- `project_id` — проект, которым вы **владеете** → списание с вашего баланса.
- `project_id` — **чужой** проект, где вы **editor**:
  - `billing_source: "owner"` → списание с **владельца** проекта (проверяется роль editor и `token_limit`).
  - `billing_source: "self"` → списание с **вашего** личного баланса.
  - поле не передано → `400 BILLING_SOURCE_REQUIRED`.
- Нет доступа к проекту → `403 PROJECT_ACCESS_DENIED`. Несуществующий → `400 PROJECT_NOT_FOUND`.

### Теги проекта (`tag_ids` на submit + CRUD)

Теги в UI — короткие цветные метки на генерациях (до **10** custom на проект). Личные «лайки» и системные метки (архив и т.п.) **не** отдаются в Public API и не принимаются в `tag_ids`.

| Метод    | Путь                                  | Назначение                                            |
| -------- | ------------------------------------- | ----------------------------------------------------- |
| `GET`    | `/api/v1/projects/{id}/tags/`         | Список `{ items: [{ id, name, color, sort_order }] }` |
| `POST`   | `/api/v1/projects/{id}/tags/`         | Создать: `{ name, color? }` → `201 { tag }`           |
| `PATCH`  | `/api/v1/projects/{id}/tags/{tagId}/` | `{ name?`, `color? }` → `{ tag }`                     |
| `DELETE` | `/api/v1/projects/{id}/tags/{tagId}/` | Удалить тег и все связи                               |

Права: смотреть — любой участник проекта; create/update/delete — owner / editor / admin / client (**не** viewer).

На submit (images / videos / upscale / switchx) можно передать опциональные `tag_ids: [1, 2]` или `tag_id: 1` вместе с `project_id`. Теги должны принадлежать этому проекту. Поле необязательное — omit = генерация без тегов.

Пример:

```http
POST https://app.artillect.pro/api/v1/images/generations/
Authorization: Bearer art_xxxxxxxx
Content-Type: application/json

{
  "prompt": "hero shot",
  "project_id": 42,
  "tag_ids": [7]
}
```

### Идемпотентность и `request_id`

- Заголовок **`Idempotency-Key`** на **всех** async submit и chat:
  - `POST /api/v1/images/generations/`
  - `POST /api/v1/videos/generations/`
  - `POST /api/v1/upscale/generations/`
  - `POST /api/v1/switchx/generations/`
  - `POST /api/v1/chat/completions/`
- Повтор с тем же ключом (тот же пользователь) вернёт **тот же** ответ без повторного списания/сабмита (TTL 24 ч). В replay-ответе — заголовок `Idempotent-Replay: true`.
- Если тот же ключ ещё **в обработке** (параллельный запрос) → `409 IDEMPOTENCY_CONFLICT`.
- Каждый ответ содержит заголовок **`X-Request-Id`** (и поле `request_id` в теле ошибок) — указывайте его при обращении в поддержку.

### Rate limits

Дорогие async submit (`POST /api/v1/images|videos|upscale|switchx/generations/`) и sync chat ограничены **дважды**: per-IP окно (30–60 запросов/мин в зависимости от модальности) и **per-API-key** минимальный интервал **3 с** между submit одним ключом (Redis, fail-closed в production). При превышении — `429` с кодом `RATE_LIMITED`, заголовками `Retry-After` и `RateLimit-*`. Идемпотентный replay (`Idempotency-Key`) не обходит per-key throttle.

**Daily spend cap (per key / OIDC identity):** когда `PUBLIC_API_KEY_DAILY_TOKEN_CAP` > 0 (в production по умолчанию **500000**, если env не задан; `0` = выкл), submit отклоняется с `429 SPEND_CAP_EXCEEDED`, если UTC-дневной счётчик токенов по ключу уже ≥ cap. Счётчик растёт при успешном charge.

---

## 1. Получить API-ключ (device flow)

> **⛔ СТОП — обязательно для CLI, скриптов и AI-агентов**
>
> После `POST /api/v1/device/code/` **не вызывайте** `POST /api/v1/device/token/`, пока пользователь **явно не подтвердил** выдачу ключа в браузере.
>
> **Порядок (строго):**
>
> 1. `POST /api/v1/device/code/`
> 2. **Сразу показать пользователю** (не только в лог): полный `verification_url` и запасной `user_code`
> 3. **Остановиться** и дождаться «выдал разрешение» / «готово» — не poll «на всякий случай»
> 4. Только после этого — `POST /api/v1/device/token/` с `device_code`
>
> Poll до подтверждения всегда вернёт `{ "status": "pending" }` — это нормально, не повод молча крутить цикл.

### 1.1 Запросить код

```http
POST https://app.artillect.pro/api/v1/device/code/
```

Ответ:

```json
{
  "device_code": "secret-token-for-polling",
  "user_code": "X7KM-9P2Q",
  "verification_url": "https://app.artillect.pro/connect?code=X7KM-9P2Q",
  "expires_in": 900,
  "interval": 5,
  "instructions": "Open verification_url in a browser..."
}
```

| Поле               | Описание                                       |
| ------------------ | ---------------------------------------------- |
| `device_code`      | Секрет для poll (`POST /api/v1/device/token/`) |
| `user_code`        | Код `XXXX-XXXX` для пользователя               |
| `verification_url` | **Готовая ссылка** — открыть в браузере        |
| `instructions`     | Краткая подсказка от API                       |

### 1.2 Подтверждение в браузере (до poll)

**Кто:** человек, залогиненный в Artillect.  
**Где:** полный `verification_url` из ответа — **не** `/connect/` без `?code=`.

| URL                       | Что видит пользователь                           |
| ------------------------- | ------------------------------------------------ |
| `/connect?code=X7KM-9P2Q` | Код уже подставлен — нажать «Выдать API-ключ»    |
| `/connect/` без `?code=`  | Заглушка `XXXX-XXXX`, ввести `user_code` вручную |

| Что показать пользователю | Зачем                                        |
| ------------------------- | -------------------------------------------- |
| `verification_url`        | Открыть в браузере, нажать «Выдать API-ключ» |
| `user_code`               | Запасной вариант на `/connect/`              |

Ключ **не показывается в браузере** — приложение забирает его через poll (§1.3).

### 1.3 Poll ключа (только после §1.2)

```http
POST https://app.artillect.pro/api/v1/device/token/
Content-Type: application/json

{"device_code": "secret-token-for-polling"}
```

Pending: `{ "status": "pending", "interval": 5 }`

Готово:

```json
{
  "status": "completed",
  "api_key": "art_xxxxxxxx",
  "usage": "Use Authorization: Bearer <api_key> for /api/v1/images/generations"
}
```

| Поле    | Описание                                                     |
| ------- | ------------------------------------------------------------ |
| `usage` | Подсказка, как использовать ключ в заголовке `Authorization` |

Истёк или неверный `device_code`: HTTP **410**, `code: DEVICE_CODE_EXPIRED`.

Poll каждые **5 с**, таймаут кода **15 мин**.

**API-ключ (`art_...`) долгоживущий** — сохраните в `ARTILLECT_API_KEY`. Полное значение больше не вернётся; при потере — новый device flow.

### Node.js — получение ключа

```javascript
const BASE = "https://app.artillect.pro";

/** В CLI/агенте: дождаться явного подтверждения пользователя. */
async function waitForUserApproval() {
  // Реализация на вашей стороне: readline, UI-кнопка, сообщение в чате и т.д.
  throw new Error("Implement: block until user confirms browser approval");
}

export async function obtainArtillectApiKey(onOpenUrl) {
  const start = await fetch(`${BASE}/api/v1/device/code/`, { method: "POST" });
  if (!start.ok) throw new Error(await start.text());
  const { device_code, user_code, verification_url, interval } = await start.json();

  // ⛔ Gate: показать ссылку и код, затем ждать подтверждения
  onOpenUrl?.(verification_url, user_code);
  await waitForUserApproval();

  for (let i = 0; i < 180; i++) {
    await new Promise((r) => setTimeout(r, (interval || 5) * 1000));
    const poll = await fetch(`${BASE}/api/v1/device/token/`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ device_code }),
    });
    const body = await poll.json();
    if (body.status === "completed") return body.api_key;
    if (body.status === "expired") throw new Error("Device code expired or invalid");
  }
  throw new Error("Timeout waiting for approval");
}
```

---

## 2. Создать генерацию

```http
POST https://app.artillect.pro/api/v1/images/generations/
Authorization: Bearer art_xxxxxxxx
Content-Type: application/json
```

### Модели (`model`)

| `model`                | Описание                          | max `prompt`     | t2i | i2i (refs)                    |
| ---------------------- | --------------------------------- | ---------------- | --- | ----------------------------- |
| `gpt-image-2`          | **Default** — если `model` опущен | **20 000** симв. | да  | да (`input_urls` / `images`)  |
| `gpt-image-15`         | GPT Image 1.5                     | **20 000** симв. | да  | да, max **16** refs           |
| `nano-banana`          | Nano Banana (legacy)              | **20 000** симв. | да  | да, max **10** refs           |
| `nano-banana-2`        | Nano Banana 2                     | **20 000** симв. | да  | да, max **16** refs           |
| `nano-banana-pro`      | Nano Banana Pro                   | **10 000** симв. | да  | да, max **8** refs            |
| `seedream-4`           | Seedream 4.0 (ByteDance)          | **5 000** симв.  | да  | да, max **10** refs           |
| `seedream-4-5`         | Seedream 4.5 (ByteDance)          | **5 000** симв.  | да  | да, max **10** refs           |
| `seedream-5-lite`      | Seedream 5 Lite (ByteDance)       | **5 000** симв.  | да  | да, max **14** refs           |
| `flux2-pro`            | Flux 2 Pro                        | **20 000** симв. | да  | да, max **8** refs            |
| `flux2-max`            | Flux 2 Max                        | **20 000** симв. | да  | да, max **8** refs            |
| `flux2-flex`           | Flux 2 Flex                       | **20 000** симв. | да  | да, max **8** refs            |
| `grok-imagine`         | Grok Imagine                      | **20 000** симв. | да  | да, max **1** ref             |
| `wan-27-image`         | Wan 2.7 Image                     | **20 000** симв. | да  | да, max **9** refs            |
| `wan-27-image-pro`     | Wan 2.7 Image Pro                 | **20 000** симв. | да  | да, max **9** refs            |
| `cosmos-3-super`       | Nvidia Cosmos 3S (FAL)            | **20 000** симв. | да  | нет (refs → `400`)            |
| `mai-image-25`         | MAI Image 2.5 (Microsoft, FAL)    | **20 000** симв. | да  | да, max **8** refs            |
| `krea-v2-large`        | Krea 2 Large (FAL)                | **20 000** симв. | да  | style refs на t2i, max **10** |
| `krea-v2-medium`       | Krea 2 Medium (FAL)               | **20 000** симв. | да  | style refs на t2i, max **10** |
| `krea-v2-medium-turbo` | Krea 2 Medium Turbo (FAL)         | **20 000** симв. | да  | style refs на t2i, max **10** |
| `qwen-multiple-angles` | Qwen Multiple Angles (FAL angles) | **20 000** симв. | нет | **обязателен** 1 ref image    |

Роутинг как в UI: есть `input_urls` или `images` → **image-to-image** (edit); иначе text-to-image. Исключения: Krea — style refs на t2i; `qwen-multiple-angles` — всегда `angles` (1 ref, camera params).

Алиасы: `nano-banana-2-gen`, `nano-banana-pro-edit`, `gpt-image-2-gen`, `gpt-image-1.5`, `flux-2-pro`, `wan-2.7-image`, `seedream-4.5`, `seedream-45`, `seedream-5-lite-edit`, … → та же семья.

`output_format` — Nano Banana семьи; у GPT / Seedream / Flux / Grok / Wan игнорируется (формат отдаёт провайдер).

`gpt-image-15`: поле `quality` (`medium`\|`high`); `resolution` `2K`/`4K`/`high` → `high`, иначе `medium`.

**Text-to-image** (default model, без поля `model`):

```json
{
  "prompt": "Photorealistic red apple on white background",
  "aspect_ratio": "1:1",
  "resolution": "1K"
}
```

**Text-to-image** (Nano Banana 2):

```json
{
  "model": "nano-banana-2",
  "prompt": "Product photo of a red apple",
  "aspect_ratio": "1:1",
  "resolution": "2K",
  "output_format": "png"
}
```

**Image-to-image** — внешний HTTPS URL (проверен на dev):

```json
{
  "model": "nano-banana-2",
  "prompt": "Make it look like a watercolor painting",
  "input_urls": ["https://images.unsplash.com/photo-1560806887-1e4cd0b6cbd6?w=512"],
  "aspect_ratio": "auto",
  "resolution": "1K"
}
```

**Image-to-image** — уже загруженный Artillect media path (без повторного fetch):

```json
{
  "prompt": "Make it look like a watercolor painting",
  "images": ["/api/backend/media/download/?key=2026%2F06%2F21%2Fabc-api%2Finput%2F0.jpg"],
  "aspect_ratio": "auto",
  "resolution": "1K"
}
```

Алиас поля UI: `images` (массив HTTPS URL или `/api/backend/media/download/?key=...`).

Ответ **202**:

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000-api",
  "status": "queued",
  "model": "gpt-image-2",
  "cost_tokens": 4,
  "project_id": 123,
  "poll_url": "/api/v1/images/generations/550e8400-e29b-41d4-a716-446655440000-api/"
}
```

Poll URL (относительный): `` `${BASE}${poll_url}` `` → `https://app.artillect.pro/api/v1/images/generations/550e8400-...-api/`.  
`project_id` — id проекта «Мои генерации» в БД.

### Параметры

| Поле                    | Обяз. | Значения                                                                                                                           | Default       |
| ----------------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `prompt`                | да    | string; max символов **зависит от модели** (см. ниже)                                                                              | —             |
| `model`                 | нет   | см. таблицу моделей выше (`gpt-image-2`, `gpt-image-15`, `nano-banana*`, `seedream-*`, `flux2-*`, `grok-imagine`, `wan-27-image*`) | `gpt-image-2` |
| `aspect_ratio`          | нет   | см. каталог модели (`auto`, `1:1`, `21:9`, …)                                                                                      | из каталога   |
| `resolution`            | нет   | `1K`, `2K`, `4K` (где есть в каталоге; Wan default **2K**)                                                                         | из каталога   |
| `quality`               | нет   | `medium`, `high` — только `gpt-image-15`                                                                                           | `medium`      |
| `output_format`         | нет   | `png`, `jpg`/`jpeg` — Nano Banana семьи                                                                                            | из каталога   |
| `input_urls` / `images` | нет   | string[], max зависит от модели (см. таблицу)                                                                                      | —             |

**Лимит `prompt` (символы, как в [KIE Market](https://docs.kie.ai)):**

| `model`                       | max `prompt` |
| ----------------------------- | ------------ |
| `gpt-image-2`                 | **20 000**   |
| `gpt-image-15`                | **20 000**   |
| `nano-banana`                 | **20 000**   |
| `nano-banana-2`               | **20 000**   |
| `nano-banana-pro`             | **10 000**   |
| `seedream-4` / `seedream-4-5` | **5 000**    |
| `seedream-5-lite`             | **5 000**    |
| `flux2-pro` / `flux2-flex`    | **20 000**   |
| `grok-imagine`                | **20 000**   |
| `wan-27-image` / `-pro`       | **20 000**   |

Превышение → `400 PROMPT_TOO_LONG`.

Есть непустой `input_urls` или `images` (валидный HTTPS URL или Artillect media path) → **image-to-image** (edit).  
**Без них** — **text-to-image** (это не ошибка; `INPUT_URLS_REQUIRED` не возвращается).

**Input (image-to-image):** внешние **HTTPS** URL на **публичных** хостах сервер **скачивает и кладёт в Artillect media (MinIO)** перед KIE (`redirect` не следуем; private/metadata IP блокируются). Провайдер получает `https://{origin}/api/backend/media/download/?key=...`. Уже загруженные пути `/api/backend/media/download/?key=...` принимаются как есть. Max **10MB** на файл. `http://` и внутренние адреса → `4xx`.

**Какие URL сработают:** прямой GET без auth, `Content-Type: image/*`. На dev проверен `images.unsplash.com`. `picsum.photos`, Wikimedia, hotlink-защита и часть CDN **могут вернуть 403/404** при server-side fetch → `INPUT_URL_FETCH_FAILED` (до submit, без списания).

**Загрузка файла без внешнего URL:** `POST /api/v1/files/` (multipart `file`) → `{ key, url }` — полученный `url`/`key` можно передать в `input_urls` / слоты видео.

Опционально: `project_id`, `billing_source`, `tag_ids` / `tag_id` (см. «Выбор проекта…» и «Теги проекта» выше).

---

## 3. Poll результата

```http
GET https://app.artillect.pro/api/v1/images/generations/{id}/
Authorization: Bearer art_xxxxxxxx
```

Poll каждые **2–5 с** (или по `recommended_poll_interval_sec` / заголовку `Retry-After` в ответе). Статусы: `queued` → `running` → `completed` | `failed` | `cancelled`.

Во время `running` поле `progress` часто **долго остаётся 55** и прыгает на `100` только при `completed` — это нормально, не признак зависания.

**Пример ответа (ещё в работе):**

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000-api",
  "status": "running",
  "model": "gpt-image-2",
  "cost_tokens": 4,
  "images": [],
  "error": null,
  "progress": 55,
  "provider_sync": "fresh",
  "recommended_poll_interval_sec": 5
}
```

**Если KIE временно не отвечает на проверку статуса** — poll **не падает с ошибкой**. Ответ **200**, последнее известное состояние, `provider_sync: "deferred"` и поле `hint`:

```json
{
  "status": "running",
  "progress": 55,
  "provider_sync": "deferred",
  "hint": "Could not refresh provider status in time. The generation may still be running — keep polling this URL every few seconds.",
  "recommended_poll_interval_sec": 5
}
```

Это **не отказ генерации**: задача уже принята на submit (**202**), KIE может доделать картинку в фоне. **Продолжайте poll** того же `id` каждые несколько секунд (рекомендуем до **15 минут**).

### 3.1 Таймауты и надёжность (images)

| Фаза                                        | Что происходит                                                | Ошибка?                                                                  | Что делать клиенту                                                                                       |
| ------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
| **POST submit**                             | Artillect отправляет задачу в KIE и сразу отдаёт `id`         | **202** + `poll_url` — успех                                             | Сохранить `id`, начать poll                                                                              |
| **POST submit**, KIE не подтвердил за ~50 с | Задача **не создана**, токены **не списаны** (или возвращены) | **504** `PROVIDER_TIMEOUT`, `phase: "submit"`, `retryable: true`, `hint` | Повторить POST (безопасно)                                                                               |
| **GET poll**, KIE отвечает                  | Актуальный статус                                             | **200**, `provider_sync: "fresh"`                                        | Если `queued`/`running` — poll снова через 2–5 с                                                         |
| **GET poll**, KIE не ответил вовремя        | Отдаём **кэш** последнего статуса                             | **200**, `provider_sync: "deferred"`, `hint`                             | **Не считать ошибкой** — poll снова; генерация может завершиться позже                                   |
| **GET poll**, долгая генерация (минуты)     | Норма для тяжёлых моделей                                     | **200**, статус `running`                                                | Терпеливо poll до `completed` / `failed`                                                                 |
| Edge **502** HTML / обрыв                   | Сбой до приложения или таймаут edge (Caddy → контейнер)       | Не JSON от API                                                           | Повторить; при живом API ответ JSON (**504** submit/chat, **200** poll). Edge — см. `docs/DEV_DEPLOY.md` |

**Главное правило:** после успешного **202** единственный источник правды — **poll по `id`**. Отдельный неудачный poll (deferred) **не отменяет** задачу.

Заголовок **`Retry-After`** на poll (секунды) дублирует `recommended_poll_interval_sec`, пока статус `queued` или `running`.

Health-check доступности API (без генерации): `GET /api/v1/health/` — не путать с `device/code`.

**failed:**

```json
{
  "status": "failed",
  "images": [],
  "error": "Could not download input_urls[0] (HTTP 403)...",
  "progress": 0
}
```

Текст в `error` — человекочитаемый; машинный код смотрите в ответе submit (`INPUT_URL_FETCH_FAILED` и т.д.) или в UI проекта.

**completed** (вариант A — Artillect media, **предпочтительный**, когда backend успел зеркалировать):

```json
{
  "status": "completed",
  "images": [
    { "url": "https://app.artillect.pro/api/backend/media/download/?key=2026%2F06%2F21%2F..." }
  ],
  "progress": 100
}
```

**completed** (вариант B — CDN провайдера, fallback пока зеркало не готово):

```json
{
  "status": "completed",
  "images": [{ "url": "https://tempfile.aiquickdraw.com/images/chatgpt/file_....png" }],
  "progress": 100
}
```

### Формат URL (input vs output)

| Фаза                       | Формат                                               | Примечание                                          |
| -------------------------- | ---------------------------------------------------- | --------------------------------------------------- |
| Input после зеркалирования | `{BASE}/api/backend/media/download/?key={minio_key}` | Стабильный, свой storage                            |
| Output (предпочтительно)   | `{BASE}/api/backend/media/download/?key=...`         | Когда `result_images` уже в MinIO                   |
| Output (fallback)          | CDN KIE (`tempfile.aiquickdraw.com`, …)              | Временный; скачивайте сразу, часто нужен User-Agent |

`key` — object key в MinIO (Artillect media store).

**Скачивание результата:** `GET` по `images[0].url` **без** Authorization для Artillect media.  
CDN KIE часто требует **User-Agent** (иначе HTTP 403):

```bash
curl -L -A "Mozilla/5.0" -o result.png "https://tempfile.aiquickdraw.com/..."
```

```python
requests.get(url, headers={"User-Agent": "Mozilla/5.0"}, timeout=120)
```

CDN может качаться **медленно** или обрываться по таймауту — сохраняйте файл сразу после `completed`, если URL ещё CDN. Input зеркалируется в MinIO при успешном fetch; output — по мере finalize.

---

## 3b. Видео-генерация

Async, как images: `POST /api/v1/videos/generations/` → `id` → poll `GET /api/v1/videos/generations/{id}/`.

```http
POST https://app.artillect.pro/api/v1/videos/generations/
Authorization: Bearer art_xxxxxxxx
Content-Type: application/json
```

### Модели и медиа-слоты

Медиа задаётся **явными именованными слотами**. Задача (`t2v` / `i2v` / `flf2v` / `ref2v` / `v2v` / `edit` / `motion` / `avatar` / `lipsync`) определяется **детерминированно** по заполненным слотам; опционально можно передать `"task"` для явного выбора.

| Слот               | Тип      | Описание                                                                         |
| ------------------ | -------- | -------------------------------------------------------------------------------- |
| `start_image_url`  | string   | Первый кадр (i2v)                                                                |
| `end_image_url`    | string   | Последний кадр (только с `start_image_url` → keyframe-видео)                     |
| `source_video_url` | string   | Исходное видео для **edit** / **v2v** (Kling O3)                                 |
| `reference_images` | string[] | Референс-изображения                                                             |
| `reference_videos` | string[] | Референс-видео (Seedance ref2v; для Kling O3 `[0]` = alias `source_video_url`)   |
| `reference_audios` | string[] | Референс-аудио                                                                   |
| `audio_url`        | string   | Аудио для **avatar** / **lipsync** (alias `reference_audios[0]`)                 |
| `motion_video_url` | string   | Видео движения для **motion** (alias `reference_videos[0]` / `source_video_url`) |

**Промпт и `@`-токены:** API нормализует регистр: `@video1` → `@Video1`, `@image2` → `@Image2`, `@audio3` → `@Audio3`.  
Индексация: `reference_videos[0]` = `@Video1`, `reference_images[0]` = `@Image1`, `reference_audios[0]` = `@Audio1` (порядок массива сохраняется).

**Element library:** если в промпте есть `@имя` из библиотеки проекта — передайте `project_id` (проект, где создан элемент). Элементы резолвятся из `project_element_library` (`kind=image|video`). Список элементов: `GET /api/v1/projects/{id}/elements/`. Альтернатива `@` в промпте — массив `element_names: ["hero", "bg"]` (имена без `@`).

Поддерживают element library: `kling-3`, `kling-o3`, `seedance-2`, `seedance-2-mini`, `happy-horse`, `happy-horse-1-1`, `wan-27`, `veo-31`, `grok-imagine-video`.

Задача выбирается автоматически: как только в промпте есть `@элемент` (или задан `element_names`), генерация уходит на reference-эндпоинт модели — задавать `task` вручную не нужно.

**`kling-3` с элементами требует `start_image_url`** (стартовая сцена). Без него — `400 VIDEO_START_FRAME_REQUIRED`. Стартовый кадр — это фон/сцена, элементы (`@имя`) накладываются поверх.

URL — HTTPS (сервер скачает и зеркалирует в Artillect media) или готовый `/api/backend/media/download/?key=...`. Лимиты: изображение **10MB**, видео **200MB**, аудио **50MB**.

**Матрица слотов по моделям** (любое нарушение → `400` до списания):

| `model`                   | t2v |    i2v / flf2v    | ref2v                     | edit / v2v                             | Extra params                                                                                                                                                                                                 | Разрешения                             |
| ------------------------- | :-: | :---------------: | ------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------- |
| `kling-3-turbo` (default) |  ✓  |     start / —     | ✗                         | ✗                                      | —                                                                                                                                                                                                            | 720p, 1080p                            |
| `happy-horse-1-1`         |  ✓  |       start       | images 1–9                | ✗                                      | —                                                                                                                                                                                                            | 720p, 1080p                            |
| `happy-horse`             |  ✓  |       start       | images 1–9                | **edit** (`source_video_url`)          | —                                                                                                                                                                                                            | 720p, 1080p                            |
| `seedance-2-mini`         |  ✓  | start / start+end | img≤9, vid≤3, aud≤3, Σ≤15 | ✗                                      | `generate_audio`                                                                                                                                                                                             | 480p, 720p                             |
| `seedance-2`              |  ✓  | start / start+end | img≤9, vid≤3, aud≤3, Σ≤15 | ✗                                      | `seedance_mode` quality\|fast (default **quality**), `generate_audio`                                                                                                                                        | quality: 480p–**4k**; fast: 480p, 720p |
| `kling-3`                 |  ✓  | start / start+end | images + @elements        | ✗                                      | `kling_mode` std\|pro\|4k (default pro), `generate_audio`, `multi_shots` + `multi_prompt`                                                                                                                    | по tier                                |
| `kling-o3`                |  ✓  | start / start+end | refs + @elements + frames | **edit**, **v2v** (`source_video_url`) | `kling_mode` pro\|4k; **v2v** `duration` 3–15 (default 10); **edit** без `duration`; `shot_type`; `multi_prompt`; `keep_audio` (edit/v2v); `cfg_scale` 0–1 (default 0.5, t2v–v2v, не edit); `generate_audio` | pro/4k                                 |
| `kling-26`                |  ✓  | start / start+end | ✗                         | ✗                                      | `generate_audio`                                                                                                                                                                                             | —                                      |
| `wan-27`                  |  ✓  | start / start+end | img≤5, vid≤5, aud≤1       | **edit** (`source_video_url`)          | —                                                                                                                                                                                                            | 720p, 1080p                            |
| `veo-31`                  |  ✓  | start / start+end | images 1–3                | ✗                                      | —                                                                                                                                                                                                            | 720p, 1080p, 4k                        |
| `grok-imagine-video`      |  ✓  |       start       | ✗                         | **edit** (`source_video_url`)          | ≠ image `grok-imagine`                                                                                                                                                                                       | 480p, 720p                             |
| `grok-imagine-15`         |  ✗  |       start       | ✗                         | ✗                                      | **i2v only**                                                                                                                                                                                                 | 480p, 720p                             |
| `ltx-23`                  |  ✓  | start / start+end | ✗                         | ✗                                      | `generate_audio` (avatar/audio-to-video — не в Public API)                                                                                                                                                   | 1080p, 1440p, 2160p                    |
| `luma-ray-32`             |  ✓  | start / start+end | ✗                         | **v2v** (`source_video_url`)           | —                                                                                                                                                                                                            | 540p, 720p, 1080p                      |

### Avatar / Omni / Motion / Lip-sync (wave 2)

| `model`               | task               | Required media                                                                | Prompt          | Extra                                                |
| --------------------- | ------------------ | ----------------------------------------------------------------------------- | --------------- | ---------------------------------------------------- |
| `gemini-omni`         | t2v/i2v/ref2v/edit | ref2v: `reference_images` and/or `motion_video_url`; edit: `source_video_url` | required (edit) | KIE quota 7 units on ref2v; expensive at 4k          |
| `gemini-omni-flash`   | t2v/i2v/ref2v/edit | i2v: `start_image_url`; ref2v: `reference_images`; edit: `source_video_url`   | required        | FAL; frames XOR refs                                 |
| `kling-3-motion`      | motion             | `start_image_url` + `motion_video_url`                                        | optional        | `character_orientation`, `background_source`         |
| `kling-26-motion`     | motion             | same                                                                          | optional        | Kling 2.6 motion                                     |
| `wan-22`              | motion, swap       | `start_image_url` + `motion_video_url` / `source_video_url`                   | optional        | `task`: `motion` (default) \| `swap`; 480p/580p/720p |
| `kling-ai-avatar-pro` | avatar             | `start_image_url` + `audio_url`                                               | **required**    | talking head                                         |
| `omnihuman`           | avatar             | same                                                                          | optional        | `pe_fast_mode`, `seed`, `resolution` (720/1080)      |
| `volcengine`          | lipsync            | `source_video_url` + `audio_url`                                              | optional        | `lipsync_mode` lite\|basic                           |
| `sync-v3`             | lipsync            | same                                                                          | optional        | `sync_mode` (cut_off default)                        |
| `heygen`              | lipsync            | same                                                                          | optional        | precision only (`heygen_mode: speed` → 400)          |

**Motion transfer example:**

```json
{
  "model": "kling-3-motion",
  "prompt": "Dance like the reference",
  "start_image_url": "https://.../character.jpg",
  "motion_video_url": "https://.../dance.mp4",
  "resolution": "1080p"
}
```

**Avatar example:**

```json
{
  "model": "kling-ai-avatar-pro",
  "prompt": "Natural talking head",
  "start_image_url": "https://.../face.jpg",
  "audio_url": "https://.../voice.mp3"
}
```

**Lip-sync example (prompt optional):**

```json
{
  "model": "volcengine",
  "source_video_url": "https://.../talk.mp4",
  "audio_url": "https://.../dub.mp3",
  "lipsync_mode": "lite"
}
```

Коды ошибок wave 2: `MOTION_IMAGE_REQUIRED`, `MOTION_VIDEO_REQUIRED`, `AVATAR_IMAGE_REQUIRED`, `AUDIO_REQUIRED`, `GEMINI_OMNI_QUOTA_EXCEEDED`, `REFERENCE_MEDIA_REQUIRED`, `HEYGEN_MODE_UNSUPPORTED`.

`aspect_ratio` — по каталогу модели. Default `16:9`. Для **`kling-3` + start frame** можно опустить / `"auto"` — KIE auto-adapt по кадру (см. секцию ниже).  
`seedance-2` + `seedance_mode: fast` + `resolution: 4k` → `400 VIDEO_RESOLUTION_UNSUPPORTED`.

Коды ошибок видео: `VIDEO_SLOT_UNSUPPORTED`, `VIDEO_FRAMES_REFS_EXCLUSIVE`, `VIDEO_END_REQUIRES_START`, `VIDEO_TOO_MANY_REFS`, `VIDEO_TASK_UNSUPPORTED`, `VIDEO_TIER_TASK_UNSUPPORTED`, `SOURCE_VIDEO_REQUIRED`, `DURATION_NOT_SUPPORTED`, `SHOT_TYPE_INVALID`, `MULTI_PROMPT_INVALID`, `CFG_SCALE_INVALID`, `CFG_SCALE_OUT_OF_RANGE`, `CFG_SCALE_UNSUPPORTED`, `PROJECT_ID_REQUIRED_FOR_ELEMENTS`, `ELEMENT_NOT_FOUND`. Также общие `PROMPT_REQUIRED`, `PROMPT_TOO_LONG` (max 5000 симв.), `INPUT_VIDEO_*`, `INPUT_AUDIO_*`.

**Text-to-video:**

```json
{
  "model": "kling-3-turbo",
  "prompt": "A cat surfing a wave",
  "resolution": "1080p",
  "duration": "5",
  "aspect_ratio": "16:9"
}
```

**Image-to-video (первый кадр):**

```json
{
  "model": "seedance-2-mini",
  "prompt": "Camera slowly zooms in",
  "start_image_url": "https://images.unsplash.com/photo-...",
  "resolution": "720p",
  "duration": 5
}
```

**Keyframe (start+end, только Seedance):**

```json
{
  "model": "seedance-2-mini",
  "prompt": "Morph between the two frames",
  "start_image_url": "https://.../a.jpg",
  "end_image_url": "https://.../b.jpg"
}
```

**Reference-to-video (Seedance, 3 видео → @Video1..3):**

```json
{
  "model": "seedance-2",
  "seedance_mode": "quality",
  "prompt": "Transfer motion from @Video1 onto the character in @Image1",
  "reference_videos": ["https://.../a.mp4", "https://.../b.mp4", "https://.../c.mp4"],
  "reference_images": ["https://.../char.jpg"],
  "resolution": "1080p",
  "duration": 5
}
```

**Kling O3 — edit видео:**

```json
{
  "model": "kling-o3",
  "kling_mode": "pro",
  "task": "edit",
  "prompt": "Change the jacket to red, keep everything else the same",
  "source_video_url": "https://.../clip.mp4"
}
```

**Kling O3 — edit с референсами и keep_audio:**

```json
{
  "model": "kling-o3",
  "task": "edit",
  "prompt": "Change the jacket to red",
  "source_video_url": "https://.../clip.mp4",
  "reference_images": ["https://.../style.jpg"],
  "keep_audio": true,
  "shot_type": "customize"
}
```

`reference_images` в edit/v2v маппятся в provider `image_urls`. CamelCase-алиасы: `shotType`, `multiPrompt`, `keepAudio`, `cfgScale`, `startImageUrl`, `sourceVideoUrl`, …

**`cfg_scale` (только `kling-o3`, FAL):** насколько строго модель следует промпту. Float **0–1**, default **0.5** (0 — больше свободы, 1 — строго по тексту). Задачи: t2v / i2v / flf2v / ref2v / v2v; **не** edit. У **`kling-3`** (KIE `kling-3.0/video`) этого параметра в provider API **нет** — передача `cfg_scale` → `400 CFG_SCALE_UNSUPPORTED`.

### Multi-shot (`multi_prompt`) — Kling 3 и Kling O3

Обе модели поддерживают сценарий из нескольких шотов. Вместо одного `prompt` передаётся массив `multi_prompt` с отдельным промптом и длительностью на каждую сцену.

**Общие правила:**

| Параметр               | Kling 3 (`kling-3`, KIE)                                                           | Kling O3 (`kling-o3`, FAL)                                         |
| ---------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Флаг режима            | `multi_shots: true` (обязателен)                                                   | `shot_type: "customize"`                                           |
| Сцен                   | 2–5                                                                                | 2–5                                                                |
| Длительность сцены     | 1–12 с (целое)                                                                     | 1–12 с, **строка** `"1"`…`"12"`                                    |
| Суммарная длительность | 3–15 с (`duration` = сумма)                                                        | 3–15 с (`duration` = сумма, **строка** `"3"`…`"15"`)               |
| Промпт сцены           | ≤500 символов                                                                      | ≤500 символов                                                      |
| Верхний `prompt`       | не нужен при `multi_shots` + `multi_prompt`                                        | не нужен при `shot_type: customize` + `multi_prompt`               |
| Задачи                 | t2v, i2v (**не** flf2v: end frame запрещён)                                        | t2v, i2v, flf2v, ref2v, v2v (не edit)                              |
| Валидация              | те же лимиты, что UI (`MULTI_PROMPT_INVALID` / `MULTI_SHOT_END_FRAME_UNSUPPORTED`) | то же; `intelligent` + `multi_prompt` → `MULTI_PROMPT_UNSUPPORTED` |

> **Важно для Kling O3:** FAL принимает `duration` только как **строковый литерал** (`"3"`, `"5"`, `"10"`), в том числе внутри каждого элемента `multi_prompt`. Число `3` без кавычек даст ошибку валидации provider (`literal error`).

**Kling 3 — multi-shot (ручной сценарий):**

```json
{
  "model": "kling-3",
  "kling_mode": "pro",
  "multi_shots": true,
  "multi_prompt": [
    { "prompt": "A dog runs across a sunny field", "duration": 3 },
    { "prompt": "The dog stops and looks at the camera", "duration": 3 }
  ],
  "duration": "6",
  "generate_audio": true,
  "start_image_url": "https://.../scene.jpg"
}
```

При multi-shot Kling 3 использует только `start_image_url` (первый кадр); end frame недоступен. `@элементы` и `@ImageN` можно указывать в промптах отдельных сцен.

### Kling 3 — aspect auto-adapt (в т.ч. ultrawide / 21:9)

По [KIE Kling 3.0](https://docs.kie.ai/market/kling/kling-3-0.md): enum `aspect_ratio` только **`16:9` | `9:16` | `1:1`**. Значения вроде `21:9` в enum **нет** — `aspect_ratio: "21:9"` не сработает.

Отдельно в доке описан **Aspect Ratio Auto-Adaptation**: если переданы `image_urls` (start / start+end), `aspect_ratio` **можно не слать** — провайдер подстраивает соотношение под кадр(ы). Явный `16:9` / `9:16` / `1:1` по-прежнему форсит выбранный ratio.

Как вызвать через этот API:

1. Модель `kling-3` (single-shot или multi-shot).
2. Обязательно `start_image_url` — картинка нужного соотношения, напр. **21:9** (task станет `i2v`). Multi-shot: только первый кадр (`image_urls[0]` у провайдера).
3. **Не передавайте** `aspect_ratio` — или явно `"auto"`. Тогда Artillect не подставит default `16:9`.
4. Чистый **t2v** (без `start_image_url`) auto-adapt **недоступен** — только `16:9` / `9:16` / `1:1`.
5. Явный `aspect_ratio: "16:9"` (и т.п.) форсит enum, даже если кадр шире.

```json
{
  "model": "kling-3",
  "kling_mode": "pro",
  "prompt": "Camera slowly pans across the desert highway",
  "start_image_url": "https://.../scene-21x9.jpg",
  "duration": "5",
  "generate_audio": true
}
```

Multi-shot то же самое — `multi_shots` + `multi_prompt` + `start_image_url` нужного AR, без `aspect_ratio`:

```json
{
  "model": "kling-3",
  "kling_mode": "pro",
  "multi_shots": true,
  "multi_prompt": [
    { "prompt": "Wide establishing shot of the canyon", "duration": 4 },
    { "prompt": "Hero walks into frame from the left", "duration": 4 }
  ],
  "duration": "8",
  "start_image_url": "https://.../scene-21x9.jpg",
  "generate_audio": true
}
```

> Это поведение **KIE Kling 3.0** (раздел Aspect Ratio Auto-Adaptation). Имеет смысл только при наличии start-кадра. Для t2v / без кадра — не работает.

**Kling O3 — multi-shot, ручной режим (`customize`):**

```json
{
  "model": "kling-o3",
  "kling_mode": "pro",
  "shot_type": "customize",
  "multi_prompt": [
    { "prompt": "Wide shot: city skyline at dawn", "duration": "4" },
    { "prompt": "Close-up: hero wakes up and smiles", "duration": "3" }
  ],
  "duration": "7",
  "aspect_ratio": "16:9",
  "generate_audio": false
}
```

`prompt` на верхнем уровне **не передавайте** — provider использует только `multi_prompt`.

**Kling O3 — multi-shot, авто-режим (`intelligent`):**

Модель сама режет один промпт на сцены. Передаётся обычный `prompt`, **без** `multi_prompt`:

```json
{
  "model": "kling-o3",
  "kling_mode": "pro",
  "shot_type": "intelligent",
  "prompt": "A short story: a cat explores an abandoned library, finds a glowing book, and opens it",
  "duration": "10",
  "aspect_ratio": "16:9"
}
```

**Kling O3 — multi-shot ref2v с элементами:**

```json
{
  "model": "kling-o3",
  "shot_type": "customize",
  "multi_prompt": [
    { "prompt": "@hero walks into frame from the left", "duration": "5" },
    { "prompt": "@hero picks up @Image1 and examines it", "duration": "5" }
  ],
  "duration": "10",
  "project_id": 42,
  "element_names": ["hero"],
  "reference_images": ["https://.../prop.jpg"],
  "start_image_url": "https://.../scene.jpg"
}
```

CamelCase-алиасы: `multiShots`, `multiPrompt`, `shotType`, `startImageUrl`, `generateAudio`.

**Kling O3 — v2v (замена персонажа, @Image1 + исходное видео):**

```json
{
  "model": "kling-o3",
  "task": "v2v",
  "prompt": "Replace the character in the video with the person from @Image1",
  "source_video_url": "https://.../clip.mp4",
  "reference_images": ["https://.../face.jpg"],
  "duration": 10
}
```

`duration` (секунды, **3–15**, default **10**) задаёт длину результата для **v2v**. Для **edit** поле не принимается — длина выхода следует `source_video_url`.

**Element library (@имя из проекта):**

```json
{
  "model": "seedance-2-mini",
  "project_id": 42,
  "prompt": "Animate @hero walking through the scene, style like @Image1",
  "element_names": ["hero"],
  "reference_images": ["https://.../style.jpg"]
}
```

`element_names` — опционально, если не хотите писать `@имя` в промпте (имена из `GET .../elements/`).

---

## Проекты и element library

Discovery + управление проектами (без платежей / покупки токенов). Платежи намеренно не в Public API.

### Список / создание проектов

```http
GET https://app.artillect.pro/api/v1/projects/?limit=50
Authorization: Bearer art_xxxxxxxx
```

```http
POST https://app.artillect.pro/api/v1/projects/
Authorization: Bearer art_xxxxxxxx
Content-Type: application/json

{ "name": "Campaign A", "description": "optional", "status": "active", "folder_id": null }
```

Query GET: `limit` (1–100, default **50**), `cursor` (id проекта для следующей страницы), `status` (`active` default | `all`).

Ответ list:

```json
{
  "items": [
    {
      "id": 42,
      "name": "зверолес",
      "description": null,
      "status": "active",
      "owner_user_id": 7,
      "role": "owner",
      "is_personal_generations_hub": false,
      "created_at": "2025-06-01T12:00:00.000Z",
      "updated_at": "2025-06-01T12:00:00.000Z"
    },
    {
      "id": 123,
      "name": "Мои генерации",
      "status": "active",
      "owner_user_id": 7,
      "role": "owner",
      "is_personal_generations_hub": true,
      "created_at": "2025-01-01T00:00:00.000Z",
      "updated_at": "2025-01-01T00:00:00.000Z"
    }
  ],
  "next_cursor": null
}
```

`is_personal_generations_hub: true` — проект по умолчанию, куда попадают генерации без `project_id`.  
`role: "owner"` — ваш проект; иначе slug роли участника (`editor`, …). Для чужого проекта при submit нужен `billing_source` (см. выше).

### Один проект / update / delete

```http
GET https://app.artillect.pro/api/v1/projects/42/
PATCH https://app.artillect.pro/api/v1/projects/42/
DELETE https://app.artillect.pro/api/v1/projects/42/
Authorization: Bearer art_xxxxxxxx
```

PATCH body: `{ "name"?, "description"?, "status"?: "active"|"closed"|"archived" }` — только owner.  
DELETE — только owner; личный хаб «Мои генерации» удалить нельзя.

### Роли и участники

```http
GET /api/v1/projects/roles/
GET /api/v1/projects/42/members/?state=active_or_invited
POST /api/v1/projects/42/members/
{ "email": "editor@example.com", "role": "editor", "token_limit": 5000 }
PATCH /api/v1/projects/42/members/99/
{ "role": "viewer", "token_limit": null }
DELETE /api/v1/projects/42/members/99/
DELETE /api/v1/projects/42/members/me/
```

`token_limit` можно задавать только роли `editor`. Приглашение в личный хаб запрещено.

### Лента / move / copy / delete / favorites

Лента: `GET /api/v1/projects/42/generations/?q=hero&tag_ids=1,2&mine_only=1&date_from=2026-07-01&date_to=2026-07-28`  
Move: `PATCH .../generations/{genId}/` `{ "project_id": 99 }`  
Favorite mask: `{ "favorited": 1 }`  
Copy: `POST .../generations/{genId}/copy/` `{ "project_id": 99 }`  
Delete: `DELETE .../generations/{genId}/`  
Favorites list: `GET .../favorites/`  
Analytics: `GET .../analytics/` · Billing context: `GET .../billing/`  
Folders: `GET|POST /api/v1/folders/`

`genId` — numeric DB id **или** `external_job_id` из ленты.

### Элементы проекта

```http
GET https://app.artillect.pro/api/v1/projects/42/elements/?kinds=image,video
Authorization: Bearer art_xxxxxxxx
```

Query: `limit` (1–200, default **100**), `cursor` (element id), `kinds` — фильтр `image`, `video` (через запятую).

POST create: `{ "name", "kind": "image"|"video", "media_json", "preview_url" }` · GET/PATCH/DELETE на `.../elements/{elementId}/` · reorder `POST .../elements/reorder/` `{ "ordered_ids": ["1","2"] }`.

System archive/favorite: `POST .../generations/{genId}/tags/` `{ "system_slugs": ["__archive__"], "mode": "add" }` (`__favorite__` тоже). `genId` = DB id или `external_job_id` из ленты (то же для likes).

**Не в Public API (пока):** Gemini/audio element create (KIE+billing), share-public links, paginated ledger/audit сверх analytics, feed/meta facets, payments. MCP project/element tools — **есть** (см. `/docs/mcp`).

Ответ:

```json
{
  "project_id": 42,
  "items": [
    {
      "id": "9001",
      "project_id": 42,
      "name": "hero",
      "kind": "image",
      "mention": "@hero",
      "preview_url": "https://app.artillect.pro/api/backend/media/download/?key=...",
      "image_urls": ["https://app.artillect.pro/api/backend/media/download/?key=..."],
      "sort_order": 0,
      "created_at": "2026-03-01T10:00:00.000Z"
    }
  ],
  "next_cursor": null
}
```

**Использование в генерации:** передайте `project_id` того же проекта и `@hero` в `prompt` **или** `element_names: ["hero"]` в `POST /api/v1/videos/generations/`. Модели: kling-3, kling-o3, seedance-2, seedance-2-mini, happy-horse, happy-horse-1-1, wan-27, veo-31, grok-imagine-video.

### Теги проекта

```http
GET https://app.artillect.pro/api/v1/projects/42/tags/
Authorization: Bearer art_xxxxxxxx
```

```json
{
  "items": [{ "id": 7, "name": "Hero", "color": "#7C5CFF", "sort_order": 0 }],
  "request_id": "…"
}
```

```http
POST https://app.artillect.pro/api/v1/projects/42/tags/
Authorization: Bearer art_xxxxxxxx
Content-Type: application/json

{ "name": "Hero", "color": "#7C5CFF" }
```

```http
PATCH https://app.artillect.pro/api/v1/projects/42/tags/7/
Authorization: Bearer art_xxxxxxxx
Content-Type: application/json

{ "name": "Hero2", "color": "#22C55E" }
```

```http
DELETE https://app.artillect.pro/api/v1/projects/42/tags/7/
Authorization: Bearer art_xxxxxxxx
```

Коды: `PROJECT_ACCESS_DENIED`, `PROJECT_NOT_FOUND`, `PROJECT_ID_INVALID`, `ELEMENT_NOT_FOUND`, `PROJECT_ID_REQUIRED_FOR_ELEMENTS`, `TAG_ID_INVALID`, `TAG_NOT_FOUND`, `TAG_NAME_TAKEN`, `TAG_LIMIT_REACHED`, `VIEWER_READONLY`, `SYSTEM_TAG_PROTECTED`.

### Лайки и теги на существующей генерации

Лайк в UI — персональная метка участника (не путать с custom-тегами). Любой участник проекта может лайкнуть; список лайкнувших виден всем с доступом к проекту.

```http
GET https://app.artillect.pro/api/v1/projects/42/generations/9001/likes/
Authorization: Bearer art_xxxxxxxx
```

```json
{
  "generation_id": 9001,
  "project_id": 42,
  "result_index": -1,
  "count": 2,
  "liked_by_me": true,
  "liked_by": [
    { "user_id": 7, "name": "Rustam" },
    { "user_id": 12, "name": "Anna" }
  ],
  "request_id": "…"
}
```

Query: опционально `result_index` (слайс multi-image; без параметра — все лайки на генерации).

```http
POST https://app.artillect.pro/api/v1/projects/42/generations/9001/likes/
Authorization: Bearer art_xxxxxxxx
Content-Type: application/json

{ "mode": "toggle" }
```

`mode`: `add` | `remove` | `toggle` (default). Ответ — тот же shape, что у GET.

Повесить custom-тег на уже созданную генерацию (нужна роль не-viewer):

```http
POST https://app.artillect.pro/api/v1/projects/42/generations/9001/tags/
Authorization: Bearer art_xxxxxxxx
Content-Type: application/json

{ "tag_ids": [7], "mode": "add" }
```

Коды: `GENERATION_NOT_FOUND`, `GENERATION_ID_INVALID`, `TAG_IDS_REQUIRED`, `TAG_NOT_CUSTOM`, `VIEWER_READONLY`.

---

Ответ **202** (video submit):

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000-api-video",
  "status": "queued",
  "model": "seedance-2-mini",
  "task": "i2v",
  "cost_tokens": 55,
  "project_id": 123,
  "poll_url": "/api/v1/videos/generations/550e8400-...-api-video/"
}
```

### Poll видео

```http
GET https://app.artillect.pro/api/v1/videos/generations/{id}/
Authorization: Bearer art_xxxxxxxx
```

Статусы и `provider_sync`/`hint`/`recommended_poll_interval_sec` — как у images (видео рендерится дольше, рекомендуемый интервал **8 с**). Готово:

```json
{
  "status": "completed",
  "model": "seedance-2-mini",
  "task": "i2v",
  "cost_tokens": 55,
  "videos": [{ "url": "https://tempfile.aiquickdraw.com/.../video.mp4" }],
  "progress": 100
}
```

CDN видео — временный; скачивайте сразу (User-Agent как у images).

---

## Upscale (Topaz + Crystal)

Topaz и Crystal upscale через FAL. Topaz — **только Public API** (не в UI `/enhance`). Crystal — тот же backend, что Studio `/enhance` (`crystal-image-upscaler`, `crystal-video-upscaler`). Async poll, как images/video.

```http
POST https://app.artillect.pro/api/v1/upscale/generations/
Authorization: Bearer art_xxxxxxxx
Content-Type: application/json
```

Poll:

```http
GET https://app.artillect.pro/api/v1/upscale/generations/{id}/
```

Job id: `{uuid}-api-upscale`. Discovery: `GET /api/v1/models/` → секция `upscale`.

### Модели

| `model` (slug)                | Вход        | FAL endpoint                       |
| ----------------------------- | ----------- | ---------------------------------- |
| `topaz-image` (default image) | `image_url` | `fal-ai/topaz/upscale/image`       |
| `topaz-video`                 | `video_url` | `fal-ai/topaz/upscale/video`       |
| `crystal-image`               | `image_url` | `clarityai/crystal-upscaler`       |
| `crystal-video`               | `video_url` | `clarityai/crystal-video-upscaler` |

**Поле `model` vs `enhancement_model` (Topaz only):**

- `model` — slug Artillect API (`topaz-image`, `topaz-video`, `crystal-image`, `crystal-video`).
- `enhancement_model` — Topaz-модель (FAL field `model`: `Standard V2`, `Proteus`, `Gaia 2`, …). Alias: `topaz_model`.
- **Shorthand:** если `model` = имя Topaz-модели (например `"Gaia 2"`) и передан `video_url` / `image_url`, slug выводится автоматически.

URL — HTTPS или `/api/backend/media/download/?key=...` (как у images/video).

### Цена (1 USD cent = 1 Artillect token, округление **ceil**)

**Video** (`topaz-video`) — за секунду выходного видео (передайте `duration`, `width`, `height`, `upscale_factor` для точной оценки):

| Выходное разрешение (height после upscale) | tokens / сек |
| ------------------------------------------ | ------------ |
| ≤ 720p                                     | 1            |
| 720p–1080p                                 | 2            |
| > 1080p                                    | 8            |

- `target_fps: 60` → **×2** к цене.
- `enhancement_model: "Gaia 2"` → **×0.5** (половина цены).
- Итог: `ceil(duration × tokens_per_sec × fps_multiplier × model_multiplier)`.

**Image** (`topaz-image`) — по выходным мегапикселям (`width × height × upscale_factor²`):

| Output MP | tokens |
| --------- | ------ |
| ≤ 24      | 8      |
| ≤ 48      | 16     |
| ≤ 96      | 32     |
| ≤ 512     | 136    |

**Crystal image** (`crystal-image`) — `ceil(output_MP × 1.6)` tokens; используйте `scale_factor` (не `upscale_factor`), опционально `creativity` 0–1.

**Crystal video** (`crystal-video`) — base **10** + input MP (`per_megapixel_crystal`) + `duration` (per second) + FPS tier (`fps` / 30, ceil). Передайте `duration`, `width`, `height`, `upscale_factor`, `fps` для точной оценки.

Оценка до submit: `POST /api/v1/estimate/` с `type: "upscale"` и тем же телом.

### Пример — image

```json
{
  "model": "topaz-image",
  "image_url": "https://example.com/photo.jpg",
  "enhancement_model": "Standard V2",
  "upscale_factor": 2,
  "width": 1920,
  "height": 1080,
  "output_format": "jpeg",
  "face_enhancement": true,
  "sharpen": 0.3,
  "denoise": 0.2
}
```

### Пример — video

```json
{
  "model": "topaz-video",
  "video_url": "https://example.com/clip.mp4",
  "enhancement_model": "Proteus",
  "upscale_factor": 2,
  "width": 1280,
  "height": 720,
  "duration": 12,
  "target_fps": 30,
  "compression": 0.5,
  "noise": 0.3
}
```

Shorthand (slug не нужен):

```json
{
  "model": "Gaia 2",
  "video_url": "https://example.com/clip.mp4",
  "duration": 10
}
```

### Параметры `topaz-image`

Все параметры опциональны (есть defaults); можно задать любое допустимое число в диапазоне.

| Параметр                      | Тип / default                                          | Описание                                                                                                                                                                                       |
| ----------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enhancement_model`           | enum, default `Standard V2`                            | Topaz model → FAL `model`. Значения: `Low Resolution V2`, `Standard V2`, `CGI`, `High Fidelity V2`, `Text Refine`, `Recovery`, `Redefine`, `Recovery V2`, `Standard MAX`, `Wonder`, `Wonder 3` |
| `upscale_factor`              | number 1–4, default **2**                              | Масштаб (выход ≈ вход × factor)                                                                                                                                                                |
| `width`, `height`             | integer, default **1024**                              | Размер входа (px) — для billing MP tiers                                                                                                                                                       |
| `crop_to_fill`                | boolean, default **false**                             | Crop to fill после upscale                                                                                                                                                                     |
| `output_format`               | `jpeg` \| `png`, default **jpeg**                      | Формат файла                                                                                                                                                                                   |
| `subject_detection`           | `All` \| `Foreground` \| `Background`, default **All** | Standard / Recovery V2                                                                                                                                                                         |
| `face_enhancement`            | boolean, default **true**                              | Standard / Recovery V2                                                                                                                                                                         |
| `face_enhancement_creativity` | 0–1, default **0**                                     | При `face_enhancement=true`                                                                                                                                                                    |
| `face_enhancement_strength`   | 0–1, default **0.8**                                   | При `face_enhancement=true`                                                                                                                                                                    |
| `sharpen`                     | 0–1                                                    | Standard V2, Low Res V2, CGI, High Fidelity V2, Text Refine, Redefine                                                                                                                          |
| `denoise`                     | 0–1                                                    | Те же модели                                                                                                                                                                                   |
| `fix_compression`             | 0–1                                                    | Standard V2, Low Res V2, High Fidelity V2, Text Refine                                                                                                                                         |
| `strength`                    | 0.01–1                                                 | **Text Refine** only                                                                                                                                                                           |
| `creativity`                  | 1–6                                                    | **Redefine** only                                                                                                                                                                              |
| `texture`                     | 1–5                                                    | **Redefine** only                                                                                                                                                                              |
| `prompt`                      | string, max 1024                                       | **Redefine** — generative prompt                                                                                                                                                               |
| `autoprompt`                  | boolean                                                | **Redefine** — auto prompt                                                                                                                                                                     |
| `detail`                      | 0–1                                                    | **Recovery V2** only                                                                                                                                                                           |
| `enhancement_strength`        | `low` \| `medium` \| `high`                            | **Wonder 3** only (omit = Topaz auto)                                                                                                                                                          |

FAL reference: [topaz/upscale/image](https://fal.ai/models/fal-ai/topaz/upscale/image/llms.txt)

### Параметры `topaz-video`

| Параметр            | Тип / default              | Описание                                                     |
| ------------------- | -------------------------- | ------------------------------------------------------------ |
| `enhancement_model` | enum, default **Proteus**  | FAL `model`. Gaia 2 = half price. См. список в `GET /models` |
| `upscale_factor`    | number 1–4, default **2**  | Масштаб                                                      |
| `width`, `height`   | integer, default **1024**  | Вход (px) — tier pricing по выходной height                  |
| `duration`          | number ≥1, default **5**   | Длина видео (сек) — **обязательно для точного billing**      |
| `target_fps`        | integer 16–60              | Frame interpolation; **60 → ×2 price**                       |
| `compression`       | 0–1                        | Удаление артефактов сжатия                                   |
| `noise`             | 0–1                        | Шумоподавление                                               |
| `halo`              | 0–1                        | Halo reduction                                               |
| `grain`             | 0–0.1 (step 0.01)          | Film grain                                                   |
| `recover_detail`    | 0–1                        | Recover original detail                                      |
| `H264_output`       | boolean, default **false** | H.264 вместо H.265/HEVC                                      |

Video enhancement models: `Proteus`, `Artemis HQ/MQ/LQ`, `Nyx`, `Nyx Fast`, `Nyx XL`, `Nyx HF`, `Gaia HQ/CG/2`, `Starlight Precise 1/2/2.5`, `Starlight HQ/Mini/Sharp/Fast 1/Fast 2`.

FAL reference: [topaz/upscale/video](https://fal.ai/models/fal-ai/topaz/upscale/video/llms.txt)

### Параметры `crystal-image`

| Поле           | Тип                       | Описание                        |
| -------------- | ------------------------- | ------------------------------- |
| `scale_factor` | number 1–4, default **2** | Масштаб (выход ≈ вход × factor) |
| `creativity`   | number 0–1, default **0** | Генеративная креативность       |
| `width`        | integer, default 1024     | Ширина входа (оценка цены)      |
| `height`       | integer, default 1024     | Высота входа (оценка цены)      |

Alias `model`: `crystal-image-upscaler`.

### Параметры `crystal-video`

| Поле               | Тип                       | Описание                          |
| ------------------ | ------------------------- | --------------------------------- |
| `upscale_factor`   | number 1–4, default **2** | Масштаб                           |
| `duration`         | number, default **5**     | Длительность в секундах (billing) |
| `width` / `height` | integer, default 1024     | Размер входа (оценка)             |
| `fps`              | integer, default **24**   | FPS (tier pricing)                |

Alias `model`: `crystal-video-upscaler`.

### Poll upscale

Ответ **202** (submit):

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000-api-upscale",
  "status": "queued",
  "model": "topaz-image",
  "cost_tokens": 8,
  "poll_url": "/api/v1/upscale/generations/550e8400-...-api-upscale/"
}
```

Готово (image):

```json
{
  "status": "completed",
  "model": "topaz-image",
  "images": [{ "url": "https://.../upscaled.jpg" }],
  "progress": 100
}
```

Готово (video):

```json
{
  "status": "completed",
  "model": "topaz-video",
  "videos": [{ "url": "https://.../upscaled.mp4" }],
  "progress": 100
}
```

Коды ошибок: `MODEL_UNKNOWN`, `IMAGE_URL_REQUIRED`, `VIDEO_URL_REQUIRED`, `INVALID_MEDIA_URL`, `PARAM_OUT_OF_RANGE`, `ENUM_INVALID`.

---

## Аудио (Suno V5.5 — музыка)

Async submit → poll, как images/video. **Только музыка** (`generate-music`): TTS, клонирование голоса и диалоги в Public API не выведены.

```http
POST https://app.artillect.pro/api/v1/audio/generations/
Authorization: Bearer art_xxxxxxxx
Content-Type: application/json
Idempotency-Key: optional-unique-key

{
  "model": "suno-v55",
  "prompt": "dreamy lo-fi beat for late night coding",
  "style": "lo-fi hip-hop, warm tape saturation",
  "instrumental": true
}
```

### Модели и параметры

| slug                 | Провайдер | Цена       | Примечание                           |
| -------------------- | --------- | ---------- | ------------------------------------ |
| `suno-v55` (default) | KIE Suno  | **8** ток. | Aliases: `suno`, `suno-v5.5`, `V5_5` |

| Поле                   | Тип              | Заметка                                                                             |
| ---------------------- | ---------------- | ----------------------------------------------------------------------------------- |
| `prompt`               | string, required | Текст песни при `custom_mode: true`, иначе описание (max **500** символов)          |
| `style`                | string           | Стиль/жанр (max **1000**); **обязателен** при `custom_mode: true`; иначе = `prompt` |
| `title`                | string           | Название трека (max 80; default `Generated Track`)                                  |
| `custom_mode`          | boolean          | default `false`; `true` → `prompt` = лирика (max **5000**), нужен `style`           |
| `instrumental`         | boolean          | default `false` — без вокала                                                        |
| `negative_tags`        | string           | Что исключить                                                                       |
| `vocal_gender`         | `m` \| `f`       | Только в custom mode                                                                |
| `style_weight`         | number 0–1       | Насколько строго следовать стилю                                                    |
| `weirdness_constraint` | number 0–1       | Креативность                                                                        |
| `audio_weight`         | number 0–1       | Влияние аудио                                                                       |

Оценка цены: `POST /api/v1/estimate/` с `type: "audio"`.

### Poll аудио

Ответ **202** (submit):

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000-api-audio",
  "status": "queued",
  "model": "suno-v55",
  "cost_tokens": 8,
  "poll_url": "/api/v1/audio/generations/550e8400-...-api-audio/"
}
```

Готово (Suno обычно отдаёт два трека):

```json
{
  "status": "completed",
  "model": "suno-v55",
  "audio": [{ "url": "https://.../track-1.mp3" }, { "url": "https://.../track-2.mp3" }],
  "tracks": [{ "url": "https://.../track-1.mp3", "title": "Night Drive", "duration": 183.4 }],
  "progress": 100
}
```

Suno отдаёт результат через callback — трек может появиться через несколько минут; при `provider_sync: "deferred"` продолжайте poll (`recommended_poll_interval_sec` = 10). Webhooks для аудио пока не рассылаются — используйте poll.

Коды ошибок: `MODEL_UNKNOWN`, `PROMPT_REQUIRED`, `PROMPT_TOO_LONG`, `STYLE_REQUIRED`, `STYLE_TOO_LONG`, `TITLE_TOO_LONG`, `PARAMETER_INVALID`.

---

## SwitchX (Beeble video compositing)

Отдельный endpoint — **не** смешивается с Kling/Seedance `videos/generations`.

```http
POST https://app.artillect.pro/api/v1/switchx/generations/
Authorization: Bearer art_xxxxxxxx
Content-Type: application/json
Idempotency-Key: optional-unique-key

{
  "model": "switchx",
  "generation_type": "video",
  "source_url": "https://example.com/source.mp4",
  "reference_image_url": "https://example.com/ref.jpg",
  "alpha_mode": "auto",
  "max_resolution": 1080,
  "frame_count": 90,
  "prompt": "Place the character into the scene"
}
```

Обязательно: `source_url`. Нужен `prompt` **или** `reference_image_url`. Для `alpha_mode: "custom"|"select"` — `alpha_url`.

Ответ `202`:

```json
{
  "id": "550e8400-...-api-switchx",
  "status": "queued",
  "model": "switchx",
  "cost_tokens": 90,
  "poll_url": "/api/v1/switchx/generations/550e8400-...-api-switchx/"
}
```

Poll:

```http
GET https://app.artillect.pro/api/v1/switchx/generations/{id}/
```

Формат ответа как у video poll (`status`, `videos`, `progress`, `cost_tokens`, …). Цена: блоки по 30 кадров × множитель 1080p (×3). Оценка: `POST /api/v1/estimate/` с `type: "switchx"`.

---

## 3D-меши (Meshy v6)

Асинхронная генерация 3D-моделей: **`meshy-v6-t2m`** (текст → меш) и **`meshy-v6-i2m`** (изображение → меш). Провайдер — FAL, сервис `generate-mesh`, база **80 токенов**. Запекание PBR-текстур (`texture-pbr`) в Public API **не** выведено.

```http
POST https://app.artillect.pro/api/v1/mesh/generations/
Authorization: Bearer art_xxxxxxxx
Content-Type: application/json
Idempotency-Key: optional-unique-key

{
  "model": "meshy-v6-t2m",
  "prompt": "a small brass lantern, game-ready asset",
  "topology": "triangle",
  "target_polycount": 30000,
  "art_style": "realistic"
}
```

Image-to-3D — тот же роут с картинкой:

```json
{
  "model": "meshy-v6-i2m",
  "image_url": "https://example.com/toy.png",
  "should_texture": true
}
```

**Выбор модели.** `model` можно не слать: при наличии `image_url` (алиасы `input_url`, `image`, `input_urls[0]`) выбирается `meshy-v6-i2m`, иначе `meshy-v6-t2m` (default). Альтернатива — `task: "t2m" | "i2m"`. `prompt` (≤ 2000 символов) обязателен для t2m; для i2m опционален (идёт в подпись генерации). Слать картинку в t2m нельзя — `IMAGE_NOT_SUPPORTED`.

**Входное изображение** проходит тот же SSRF-гард, что и images: только публичные HTTPS-URL (без redirect / приватных IP), base64 data URI или media-URL из `POST /api/v1/files/`. Файл зеркалируется в Artillect media, провайдер читает уже нашу ссылку.

| Параметр                  | Модели | Значения                             |
| ------------------------- | ------ | ------------------------------------ |
| `topology`                | обе    | `triangle` (default) \| `quad`       |
| `target_polycount`        | обе    | 100–300000, default 30000            |
| `symmetry_mode`           | обе    | `off` \| `auto` (default) \| `on`    |
| `should_remesh`           | обе    | bool, default `true`                 |
| `enable_pbr`              | обе    | bool, default `false`                |
| `is_a_t_pose`             | обе    | bool, default `false`                |
| `texture_prompt`          | обе    | строка                               |
| `mode`                    | t2m    | `full` (default) \| `preview`        |
| `art_style`               | t2m    | `realistic` (default) \| `sculpture` |
| `enable_prompt_expansion` | t2m    | bool, default `false`                |
| `seed`                    | t2m    | 0–4294967295                         |
| `should_texture`          | i2m    | bool, default `true`                 |

Ответ `202`:

```json
{
  "id": "550e8400-...-api-mesh",
  "status": "queued",
  "model": "meshy-v6-t2m",
  "task": "t2m",
  "cost_tokens": 80,
  "poll_url": "/api/v1/mesh/generations/550e8400-...-api-mesh/"
}
```

Poll — `GET /api/v1/mesh/generations/{id}/`; готовый ответ отдаёт `mesh[]` (GLB первым):

```json
{
  "status": "completed",
  "model": "meshy-v6-t2m",
  "task": "t2m",
  "mesh": [
    {
      "url": "https://app.artillect.pro/api/backend/media/download/model.glb?key=...",
      "format": "glb"
    }
  ],
  "progress": 100,
  "cost_tokens": 80
}
```

Отмена/refund — `POST /api/v1/mesh/generations/{id}/cancel/`; история — `GET /api/v1/mesh/generations/`. Оценка цены: `POST /api/v1/estimate/` с `type: "mesh"` (для i2m передайте любой валидный `image_url` — он не скачивается).

Коды ошибок: `MODEL_UNKNOWN`, `TASK_INVALID`, `MODEL_TASK_MISMATCH`, `PROMPT_REQUIRED`, `PROMPT_TOO_LONG`, `IMAGE_URL_REQUIRED`, `IMAGE_NOT_SUPPORTED`, `TEXTURE_PARAMS_UNSUPPORTED`, `SERVICE_UNSUPPORTED`, `ENUM_INVALID`, `PARAM_OUT_OF_RANGE`, `INPUT_URL_INVALID`.

> Webhooks для `-api-mesh` пока не рассылаются — используйте poll.

---

## 4. Chat completions (LLM)

Синхронный запрос: один `POST` → текст ответа. Poll не нужен.  
История сохраняется в проект **«Мои генерации»** (`kind: text`, `service: generate-chat`).

**Диалог / память:** сервер **не** хранит conversation id для Public API. Клиент (или MCP) передаёт полный `messages[]` с предыдущими ходами. Для DeepSeek стабильный system-префикс даёт disk KV-cache (~дешевле на длинных диалогах).

**`assistant` (additive):**

| Значение      | Поведение                                                                                                                                            |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| omit / `none` | Как раньше — **без** серверного system prompt (BC)                                                                                                   |
| `artillect`   | Подставляет studio system prompt «Artillect Assistant» (карта продукта), если в `messages` ещё нет `role:system`. Опционально `locale`: `ru` / `en`. |

```http
POST https://app.artillect.pro/api/v1/chat/completions/
Authorization: Bearer art_xxxxxxxx
Content-Type: application/json
```

### Модели (`model`)

| `model`             | Описание                          | Стоимость (токены) | max вход (символы)²   |
| ------------------- | --------------------------------- | ------------------ | --------------------- |
| `gpt-5-5`           | **Default** — если `model` опущен | 2                  | **200 000** суммарно¹ | [KIE doc](https://docs.kie.ai/market/chat/gpt-5-5.md)             |
| `claude-sonnet-4-6` | Claude Sonnet 4.6                 | 1                  | **200 000** суммарно¹ | [KIE doc](https://docs.kie.ai/market/claude/claude-sonnet-4-6.md) |
| `claude-opus-4-8`   | Claude Opus 4.8                   | 3                  | **200 000** суммарно¹ | [KIE doc](https://docs.kie.ai/market/claude/claude-opus-4-8.md)   |
| `deepseek-v4-flash` | DeepSeek V4 Flash (прямой API)    | 2                  | **200 000** суммарно¹ | [DeepSeek](https://api-docs.deepseek.com/quick_start/pricing)     |

¹ Сумма `messages[].content` (только текст); max **32** сообщения. См. сводную таблицу в начале дока.

**Vision (картинки):** `claude-sonnet-4-6`, `gpt-5-5`, `claude-opus-4-8`. Форматы тела (backward-compatible):

```json
{
  "messages": [
    {
      "role": "user",
      "content": "What is in this image?",
      "images": ["https://example.com/photo.jpg"]
    }
  ]
}
```

или OpenAI-style:

```json
{
  "role": "user",
  "content": [
    { "type": "text", "text": "What is in this image?" },
    { "type": "image_url", "url": "https://example.com/photo.jpg" }
  ]
}
```

Лимиты: **4** картинки/сообщение, **8** суммарно, **10MB**/файл. Внешние URL зеркалируются в Artillect media.  
**Видео во входе chat не поддерживается.** `deepseek-v4-flash` — text-only (картинки → `400 CHAT_VISION_UNSUPPORTED`).  
**Видео / PDF:** у GPT 5.5 в KIE есть `input_file` для PDF/доков, но Artillect API v1 это пока не экспонирует.

**Дефолты:** `stream: false` (sync JSON); GPT — `reasoning.effort: low`; Claude / DeepSeek — `max_tokens: 4096`. Без tools и web search.

**Streaming (опционально):** `stream: true` — только для **`gpt-5-5`**. Ответ `Content-Type: text/event-stream` с OpenAI-ish чанками (`object: chat.completion.chunk`, `choices[0].delta.content`), затем `data: [DONE]`. В финальном чанке — `finish_reason: stop`, `usage`, `cost_tokens`. Другие модели → `400 STREAM_UNSUPPORTED`. Заголовок `Idempotency-Key` со stream несовместим (`400 STREAM_IDEMPOTENCY_UNSUPPORTED`). MCP `chat_completion` всегда sync.

**Запрос (sync):**

```json
{
  "model": "gpt-5-5",
  "messages": [{ "role": "user", "content": "Explain recursion in one paragraph." }]
}
```

**Запрос (SSE):**

```json
{
  "model": "gpt-5-5",
  "stream": true,
  "messages": [{ "role": "user", "content": "Say hello in one sentence." }]
}
```

Роли: `system`, `user`, `assistant`. Для Claude `system` склеивается с первым `user` (ограничение провайдера).  
Лимиты: до **32** сообщений; суммарно до **200 000** символов во всех `content` (guardrail Artillect; у KIE/DeepSeek потолок — в **токенах** контекста).  
Превышение → `400 MESSAGES_TOO_LONG`.

Aliases для DeepSeek: `deepseek-v4`, `deepseek-4-flash`, `deepseek`.

**Ответ 200:**

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000-api-chat",
  "model": "claude-sonnet-4-6",
  "content": "Recursion is when a function calls itself...",
  "usage": { "input_tokens": 12, "output_tokens": 84 },
  "cost_tokens": 1,
  "project_id": 123
}
```

`usage` — из ответа KIE, может содержать `null` если провайдер не вернул счётчики.

### 4.1 Таймауты (chat)

Chat **синхронный**: ответ приходит в том же HTTP-запросе (до ~120 с ожидания KIE / DeepSeek). Poll нет.

| Ситуация                          | HTTP                       | Поля                                                                                         |
| --------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------- |
| Успех                             | **200**                    | `content`, `usage`, …                                                                        |
| KIE / DeepSeek не ответил вовремя | **504** `PROVIDER_TIMEOUT` | `phase: "chat"`, `retryable: true`, `hint` — токены **возвращены**, можно **повторить POST** |
| Ошибка провайдера                 | **502** `CHAT_FAILED`      | см. `error`                                                                                  |

Долгая генерация картинки **не относится** к chat — у images свой async poll (§3.1).

`project_id` / `billing_source` поддерживаются как у images (см. «Выбор проекта и источника списания»).

---

## 5. Пример (Python)

```python
import os, time, requests

BASE = os.environ.get("ARTILLECT_BASE_URL", "https://app.artillect.pro").rstrip("/")
API_KEY = os.environ["ARTILLECT_API_KEY"]
HDR = {"Authorization": f"Bearer {API_KEY}"}
UA = {"User-Agent": "Mozilla/5.0"}

def generate_image(prompt: str) -> str:
    r = requests.post(
        f"{BASE}/api/v1/images/generations/",
        headers=HDR,
        json={"prompt": prompt, "aspect_ratio": "1:1", "resolution": "1K"},
        timeout=60,
    )
    r.raise_for_status()
    body = r.json()
    job_id = body["id"]
    poll_url = f"{BASE}{body['poll_url']}"
    poll: dict = {}

    for _ in range(120):
        time.sleep(poll.get("recommended_poll_interval_sec", 3))
        p = requests.get(poll_url, headers=HDR, timeout=60)
        p.raise_for_status()
        poll = p.json()
        # provider_sync "deferred" — KIE не ответил на этот poll; генерация может идти дальше
        if poll.get("provider_sync") == "deferred":
            continue
        if poll["status"] == "completed":
            img_url = poll["images"][0]["url"]
            img = requests.get(img_url, headers=UA, timeout=120)
            img.raise_for_status()
            return img_url
        if poll["status"] in ("failed", "cancelled"):
            raise RuntimeError(poll.get("error") or poll["status"])
    raise TimeoutError("generation timeout")
```

---

## 6. Пример (bash, end-to-end)

Нужны: `bash`, `curl`, `jq`. Если `ARTILLECT_API_KEY` уже в env — device flow пропускается.

```bash
#!/usr/bin/env bash
set -euo pipefail

BASE="${ARTILLECT_BASE_URL:-https://app.artillect.pro}"
BASE="${BASE%/}"
CURL=(curl -sS -L)
HDR=(-H "Content-Type: application/json")

# --- 1–3. Device flow (пропуск, если ключ уже есть) ---
if [[ -z "${ARTILLECT_API_KEY:-}" ]]; then
  read -r DEVICE_CODE USER_CODE VERIFICATION_URL INTERVAL < <(
    "${CURL[@]}" -X POST "$BASE/api/v1/device/code/" \
      | jq -r '[.device_code, .user_code, .verification_url, (.interval // 5)] | @tsv'
  )

  echo "⛔ Откройте в браузере (залогиньтесь в Artillect):"
  echo "   $VERIFICATION_URL"
  echo "   Запасной код: $USER_CODE"
  read -r -p "Нажмите Enter после «Выдать API-ключ»…"

  while true; do
    RESP=$("${CURL[@]}" -X POST "$BASE/api/v1/device/token/" "${HDR[@]}" \
      -d "$(jq -n --arg dc "$DEVICE_CODE" '{device_code:$dc}')")
    STATUS=$(jq -r .status <<<"$RESP")
    [[ "$STATUS" == "completed" ]] && ARTILLECT_API_KEY=$(jq -r .api_key <<<"$RESP") && break
    [[ "$STATUS" == "expired" ]] && echo "Device code expired" >&2 && exit 1
    sleep "$INTERVAL"
  done
  echo "export ARTILLECT_API_KEY=$ARTILLECT_API_KEY"
fi

AUTH=(-H "Authorization: Bearer $ARTILLECT_API_KEY")

# --- 4. Submit text-to-image ---
SUBMIT=$("${CURL[@]}" -X POST "$BASE/api/v1/images/generations/" "${AUTH[@]}" "${HDR[@]}" \
  -d '{"prompt":"Photorealistic red apple on white background","aspect_ratio":"1:1","resolution":"1K"}')
POLL_URL="$BASE$(jq -r .poll_url <<<"$SUBMIT")"

# --- 5. Poll + скачать PNG ---
while true; do
  POLL=$("${CURL[@]}" "$POLL_URL" "${AUTH[@]}")
  STATUS=$(jq -r .status <<<"$POLL")
  if [[ "$STATUS" == "completed" ]]; then
    IMG_URL=$(jq -r '.images[0].url' <<<"$POLL")
    "${CURL[@]}" -A "Mozilla/5.0" -o result.png "$IMG_URL"
    echo "OK: result.png ← $IMG_URL"
    break
  fi
  if [[ "$STATUS" == "failed" || "$STATUS" == "cancelled" ]]; then
    jq . <<<"$POLL" >&2
    exit 1
  fi
  sleep 3
done
```

**One-liner** (ключ уже в env, только генерация → poll → файл):

```bash
BASE="${ARTILLECT_BASE_URL:-https://app.artillect.pro}"; BASE="${BASE%/}"; \
POLL="$BASE$(curl -sS -L -X POST "$BASE/api/v1/images/generations/" \
  -H "Authorization: Bearer $ARTILLECT_API_KEY" -H "Content-Type: application/json" \
  -d '{"prompt":"Photorealistic red apple","aspect_ratio":"1:1","resolution":"1K"}' | jq -r .poll_url)"; \
while true; do R=$(curl -sS -L "$POLL" -H "Authorization: Bearer $ARTILLECT_API_KEY"); \
  S=$(jq -r .status <<<"$R"); [[ "$S" == "completed" ]] && curl -sS -L -A "Mozilla/5.0" -o result.png "$(jq -r '.images[0].url' <<<"$R")" && echo OK && break; \
  [[ "$S" == "failed" || "$S" == "cancelled" ]] && echo "$R" >&2 && exit 1; sleep 3; done
```

Сохраните скрипт в `artillect-generate.sh`, `chmod +x`, запустите. One-liner удобен для быстрой проверки, когда `ARTILLECT_API_KEY` уже экспортирован.  
В **zsh** (macOS по умолчанию) путь в jq обязательно в кавычках: `'.images[0].url'` (иначе `no matches found`).

---

## Ошибки

| HTTP | code                                                                         | Описание                                                                                                                                               |
| ---- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 401  | `API_KEY_REQUIRED`                                                           | Нет заголовка Authorization                                                                                                                            |
| 401  | `INVALID_API_KEY`                                                            | Ключ неверный, не `art_...` или отозван                                                                                                                |
| 401  | `SESSION_REQUIRED`                                                           | Нужен session JWT (браузер): `POST/GET /keys`, `DELETE /keys/{id}`, `POST /device/approve`                                                             |
| 402  | `INSUFFICIENT_BALANCE`                                                       | Недостаточно токенов на личном балансе                                                                                                                 |
| 402  | `PROJECT_TOKEN_LIMIT_EXCEEDED`                                               | Квота редактора в проекте (не личный баланс)                                                                                                           |
| 400  | `MODEL_UNKNOWN`                                                              | Неизвестный `model`                                                                                                                                    |
| 400  | `PROMPT_REQUIRED`                                                            | Пустой `prompt`                                                                                                                                        |
| 400  | `PROMPT_TOO_LONG`                                                            | `prompt` длиннее лимита модели (см. таблицу)                                                                                                           |
| 400  | `MESSAGES_TOO_LONG`                                                          | Слишком много сообщений или символов в chat                                                                                                            |
| 400  | `MESSAGES_INVALID`                                                           | Невалидная структура messages                                                                                                                          |
| 400  | `MESSAGES_TOO_MANY_IMAGES`                                                   | Слишком много картинок в chat                                                                                                                          |
| 400  | `CHAT_VISION_UNSUPPORTED`                                                    | Модель не поддерживает картинки                                                                                                                        |
| 400  | `INPUT_URL_FETCH_FAILED`                                                     | Artillect не смог скачать внешний URL (403, 404, timeout, hotlink)                                                                                     |
| 400  | `INPUT_URL_INVALID`                                                          | URL не картинка или неверный формат                                                                                                                    |
| 400  | `INPUT_URL_TOO_LARGE`                                                        | Файл > 10MB (изображение)                                                                                                                              |
| 400  | `INPUT_VIDEO_FETCH_FAILED` / `INPUT_VIDEO_INVALID` / `INPUT_VIDEO_TOO_LARGE` | Референс-видео (max 200MB)                                                                                                                             |
| 400  | `INPUT_AUDIO_FETCH_FAILED` / `INPUT_AUDIO_INVALID` / `INPUT_AUDIO_TOO_LARGE` | Референс-аудио (max 50MB)                                                                                                                              |
| 400  | `INPUT_MEDIA_UPLOAD_FAILED`                                                  | Не удалось сохранить input в media store                                                                                                               |
| 400  | `VIDEO_SLOT_UNSUPPORTED`                                                     | Слот не поддерживается выбранной видео-моделью                                                                                                         |
| 400  | `VIDEO_FRAMES_REFS_EXCLUSIVE`                                                | Кадры (start/end) и референсы вместе недопустимы                                                                                                       |
| 400  | `VIDEO_END_REQUIRES_START`                                                   | `end_image_url` без `start_image_url`                                                                                                                  |
| 400  | `VIDEO_TOO_MANY_REFS`                                                        | Превышен лимит референсов                                                                                                                              |
| 400  | `VIDEO_TASK_UNSUPPORTED`                                                     | Задача не поддерживается моделью/слотами                                                                                                               |
| 400  | `VIDEO_TIER_TASK_UNSUPPORTED`                                                | Tier (напр. kling 4k) не поддерживает задачу                                                                                                           |
| 400  | `SOURCE_VIDEO_REQUIRED`                                                      | Нужен source_video_url для edit/v2v                                                                                                                    |
| 400  | `PROJECT_ID_REQUIRED_FOR_ELEMENTS`                                           | @element в промпте без project_id                                                                                                                      |
| 400  | `ELEMENT_NOT_FOUND`                                                          | @element не найден в библиотеке проекта                                                                                                                |
| 400  | `VIDEO_RESOLUTION_UNSUPPORTED`                                               | Разрешение недоступно для mode/tier                                                                                                                    |
| 400  | `PROJECT_ID_INVALID`                                                         | `project_id` не положительное целое                                                                                                                    |
| 400  | `PROJECT_NOT_FOUND`                                                          | Проект не найден / не активен                                                                                                                          |
| 400  | `BILLING_SOURCE_REQUIRED`                                                    | Нужен `billing_source` для чужого проекта (editor)                                                                                                     |
| 403  | `PROJECT_ACCESS_DENIED`                                                      | Нет доступа к проекту                                                                                                                                  |
| 403  | `PROJECT_ROLE_FORBIDDEN`                                                     | Роль не editor — генерация в проекте запрещена                                                                                                         |
| 404  | `GENERATION_NOT_FOUND`                                                       | Неверный id, чужая генерация или job не найден                                                                                                         |
| 409  | `IDEMPOTENCY_CONFLICT`                                                       | Тот же `Idempotency-Key` ещё обрабатывается (параллельный запрос)                                                                                      |
| 410  | `DEVICE_CODE_EXPIRED`                                                        | Device code истёк или не найден                                                                                                                        |
| 429  | `RATE_LIMITED`                                                               | Rate limit; см. `Retry-After` и заголовки `RateLimit-*`. Повторите позже                                                                               |
| 429  | `SPEND_CAP_EXCEEDED`                                                         | Дневной лимит spend по API-ключу (UTC); см. `spent`/`cap` в теле. Env `PUBLIC_API_KEY_DAILY_TOKEN_CAP`                                                 |
| 409  | `GENERATION_NOT_CANCELLABLE`                                                 | Генерация уже завершена/отменена                                                                                                                       |
| 400  | `FILE_REQUIRED` / `FILE_TYPE_UNSUPPORTED` / `FILE_TOO_LARGE`                 | Ошибки загрузки файла (`POST /files`)                                                                                                                  |
| 400  | `MESSAGES_REQUIRED`                                                          | Пустой или невалидный `messages`                                                                                                                       |
| 400  | `INVALID_JSON`                                                               | Тело не JSON                                                                                                                                           |
| 502  | `SUBMIT_FAILED`                                                              | KIE submit images вернул ошибку (задача не принята)                                                                                                    |
| 502  | `BILLING_REFUND_FAILED`                                                      | Submit упал после списания; автоматический refund не завершился — токены могут остаться списанными, см. `hint`                                         |
| 502  | `CHAT_FAILED`                                                                | Ошибка LLM-провайдера                                                                                                                                  |
| 504  | `PROVIDER_TIMEOUT`                                                           | KIE не ответил в срок. Смотрите **`phase`**: `submit` — id не создан, retry POST; `chat` — ответа нет, retry POST. Поле **`hint`** — что делать дальше |
| 503  | `PROVIDER_NOT_CONFIGURED`                                                    | Нет `KIE_API_KEY` на сервере                                                                                                                           |

---

## Эндпоинты

| Method | Path                                               | Auth                                                |
| ------ | -------------------------------------------------- | --------------------------------------------------- |
| POST   | `/api/v1/device/code/`                             | —                                                   |
| POST   | `/api/v1/device/token/`                            | —                                                   |
| POST   | `/api/v1/device/approve/`                          | Session JWT (браузер)                               |
| POST   | `/api/v1/keys`                                     | Session JWT (браузер, альтернатива device flow)     |
| GET    | `/api/v1/keys`                                     | Session JWT — список активных ключей                |
| PATCH  | `/api/v1/keys/{id}`                                | Session JWT — name / scopes                         |
| DELETE | `/api/v1/keys/{id}`                                | Session JWT — отзыв ключа                           |
| GET    | `/connect?code=XXXX-XXXX`                          | —                                                   |
| GET    | `/api`                                             | Session (браузер) — страница управления ключами     |
| GET    | `/api/v1/health/`                                  | — (проверка backend + KIE key, **не** вызывает KIE) |
| GET    | `/api/v1/models/`                                  | — (список моделей и лимитов)                        |
| GET    | `/api/v1/me/`                                      | Bearer `art_...` — identity + balance               |
| GET    | `/api/v1/balance/`                                 | Bearer `art_...` — баланс токенов                   |
| POST   | `/api/v1/estimate/`                                | Bearer `art_...` — оценка цены без списания         |
| POST   | `/api/v1/files/`                                   | Bearer `art_...` — загрузка файла (multipart)       |
| POST   | `/api/v1/files/from-url/`                          | Bearer — зеркало публичного HTTPS → `{ key, url }`  |
| POST   | `/api/v1/files/presign/`                           | Bearer — presign PUT image/audio (не video)         |
| GET    | `/api/v1/projects/`                                | Bearer — список проектов                            |
| POST   | `/api/v1/projects/`                                | Bearer — создать проект                             |
| GET    | `/api/v1/projects/{id}/`                           | Bearer — детали + роль                              |
| PATCH  | `/api/v1/projects/{id}/`                           | Bearer — обновить (owner)                           |
| DELETE | `/api/v1/projects/{id}/`                           | Bearer — удалить (owner)                            |
| GET    | `/api/v1/projects/roles/`                          | Bearer — каталог ролей                              |
| GET    | `/api/v1/projects/{id}/members/`                   | Bearer — участники                                  |
| POST   | `/api/v1/projects/{id}/members/`                   | Bearer — пригласить                                 |
| PATCH  | `/api/v1/projects/{id}/members/{userId}/`          | Bearer — роль / лимит / remove                      |
| DELETE | `/api/v1/projects/{id}/members/{userId}/`          | Bearer — удалить участника                          |
| DELETE | `/api/v1/projects/{id}/members/me/`                | Bearer — выйти                                      |
| GET    | `/api/v1/projects/{id}/generations/`               | Bearer — лента (+ фильтры)                          |
| PATCH  | `/api/v1/projects/{id}/generations/{genId}/`       | Bearer — move / favorited                           |
| DELETE | `/api/v1/projects/{id}/generations/{genId}/`       | Bearer — удалить генерацию                          |
| POST   | `/api/v1/projects/{id}/generations/{genId}/copy/`  | Bearer — копировать                                 |
| GET    | `/api/v1/projects/{id}/favorites/`                 | Bearer — избранное проекта                          |
| GET    | `/api/v1/projects/{id}/analytics/`                 | Bearer — сводка                                     |
| GET    | `/api/v1/projects/{id}/billing/`                   | Bearer — лимит вызывающего                          |
| GET    | `/api/v1/folders/`                                 | Bearer — папки сайдбара                             |
| POST   | `/api/v1/folders/`                                 | Bearer — создать папку                              |
| GET    | `/api/v1/projects/{id}/elements/`                  | Bearer — element library                            |
| POST   | `/api/v1/projects/{id}/elements/`                  | Bearer — создать элемент                            |
| PATCH  | `/api/v1/projects/{id}/elements/{elementId}/`      | Bearer — обновить элемент                           |
| DELETE | `/api/v1/projects/{id}/elements/{elementId}/`      | Bearer — soft-delete                                |
| POST   | `/api/v1/projects/{id}/elements/reorder/`          | Bearer — порядок                                    |
| GET    | `/api/v1/projects/{id}/tags/`                      | Bearer — custom теги проекта                        |
| POST   | `/api/v1/projects/{id}/tags/`                      | Bearer — создать тег                                |
| PATCH  | `/api/v1/projects/{id}/tags/{tagId}/`              | Bearer — rename/recolor                             |
| DELETE | `/api/v1/projects/{id}/tags/{tagId}/`              | Bearer — удалить тег                                |
| POST   | `/api/v1/projects/{id}/generations/{genId}/tags/`  | Bearer — assign/remove custom tags                  |
| GET    | `/api/v1/projects/{id}/generations/{genId}/likes/` | Bearer — кто лайкнул                                |
| POST   | `/api/v1/projects/{id}/generations/{genId}/likes/` | Bearer — поставить/снять свой лайк                  |
| POST   | `/api/v1/images/generations/`                      | Bearer `art_...`                                    |
| GET    | `/api/v1/images/generations/`                      | Bearer `art_...` — история (пагинация)              |
| GET    | `/api/v1/images/generations/{id}/`                 | Bearer `art_...`                                    |
| POST   | `/api/v1/images/generations/{id}/cancel/`          | Bearer `art_...`                                    |
| POST   | `/api/v1/videos/generations/`                      | Bearer `art_...`                                    |
| GET    | `/api/v1/videos/generations/`                      | Bearer `art_...` — история (пагинация)              |
| GET    | `/api/v1/videos/generations/{id}/`                 | Bearer `art_...`                                    |
| POST   | `/api/v1/videos/generations/{id}/cancel/`          | Bearer `art_...`                                    |
| POST   | `/api/v1/upscale/generations/`                     | Bearer `art_...` — Topaz upscale                    |
| GET    | `/api/v1/upscale/generations/`                     | Bearer `art_...` — история (пагинация)              |
| GET    | `/api/v1/upscale/generations/{id}/`                | Bearer `art_...`                                    |
| POST   | `/api/v1/upscale/generations/{id}/cancel/`         | Bearer `art_...`                                    |
| POST   | `/api/v1/switchx/generations/`                     | Bearer `art_...` — Beeble SwitchX                   |
| GET    | `/api/v1/switchx/generations/`                     | Bearer `art_...` — история (пагинация)              |
| GET    | `/api/v1/switchx/generations/{id}/`                | Bearer `art_...`                                    |
| POST   | `/api/v1/switchx/generations/{id}/cancel/`         | Bearer `art_...`                                    |
| POST   | `/api/v1/audio/generations/`                       | Bearer `art_...` — Suno V5.5 (музыка)               |
| GET    | `/api/v1/audio/generations/`                       | Bearer `art_...` — история (пагинация)              |
| GET    | `/api/v1/audio/generations/{id}/`                  | Bearer `art_...`                                    |
| POST   | `/api/v1/audio/generations/{id}/cancel/`           | Bearer `art_...`                                    |
| POST   | `/api/v1/mesh/generations/`                        | Bearer `art_...` — Meshy v6 (3D mesh)               |
| GET    | `/api/v1/mesh/generations/`                        | Bearer `art_...` — история (пагинация)              |
| GET    | `/api/v1/mesh/generations/{id}/`                   | Bearer `art_...`                                    |
| POST   | `/api/v1/mesh/generations/{id}/cancel/`            | Bearer `art_...`                                    |
| POST   | `/api/v1/webhooks/`                                | Bearer `art_...` — register/update webhook          |
| GET    | `/api/v1/webhooks/`                                | Bearer `art_...` — current subscription             |
| DELETE | `/api/v1/webhooks/`                                | Bearer `art_...` — delete subscription              |
| POST   | `/api/v1/webhooks/rotate-secret/`                  | Bearer `art_...` — rotate signing secret            |
| GET    | `/api/v1/webhooks/deliveries/`                     | Bearer `art_...` — delivery outbox list             |
| GET    | `/api/v1/webhooks/deliveries/{event_id}/`          | Bearer `art_...` — delivery detail + payload        |
| POST   | `/api/v1/chat/completions/`                        | Bearer `art_...`                                    |

---

## Переменные окружения

```bash
ARTILLECT_BASE_URL=https://app.artillect.pro
ARTILLECT_API_KEY=art_...
```

---

_Последнее обновление: 2026-08-01 — аудио (153): Suno V5.5 submit/poll/list/cancel, `estimate type=audio`, MCP `generate_audio`; 3D (154): Meshy v6 t2m/i2m submit/poll/list/cancel, `estimate type=mesh`, MCP `generate_mesh`._
