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

REST API: **генерация изображений, видео, аудио и 3D** (async poll), **image/video upscale** (async poll), SwitchX (async poll) и **LLM chat** (sync).
Async: images/videos/upscale/switchx/audio/mesh submit → poll (или push через webhooks, где поддерживается). Chat: один запрос → ответ.

Typed media editing (`trim` / ordered `concat`) существует в контракте API, но сейчас является WIP: в production `POST /api/v1/media/operations/` и связанные poll/cancel endpoints возвращают `503 MEDIA_OPERATIONS_WIP`, пока не включён `MEDIA_OPERATIONS_PUBLIC_ENABLED`.

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

---

## Base URL

| Окружение      | URL                         | Примечание    |
| -------------- | --------------------------- | ------------- |
| **Production** | `https://app.artillect.pro` | Public API v1 |

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

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

**OpenAPI 3.1:** [`/docs/artillect-openapi.yaml`](./artillect-openapi.yaml) — `https://app.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). **CLI (терминал):** [`/docs/cli`](/docs/cli) · markdown [`artillect-cli.md`](./artillect-cli.md). **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.3.3   | Generation cancel/refund: independent four-second pre-submit window for Studio, REST API, MCP and CLI; after provider submission cancellation is unavailable. Deferred jobs survive navigation/process restart via reconcile.                                                                                                            |
| 1.3.2   | OpenAPI: assets (`ast_…` + content/tile), universal `GET /generations/{id}/`, chat threads, agent `POST /feedback/`, media operations (WIP). Drift gate covers all v1 `route.ts`.                                                                                                                                                        |
| 1.3.0   | Media operations: asynchronous typed `trim` / ordered `concat` for owned video `asset_id` values; `POST /api/v1/media/operations/` → poll / cancel. No arbitrary FFmpeg arguments. MCP: `edit_media`, `get_media_operation`, `cancel_media_operation`.                                                                                   |
| 1.1.10  | Poll: `completed` only with durable MinIO `images`/`videos`; premature empty → `running` + finalizing hint (fixes agent pipelines). Webhook `generation.completed` deferred until stable URLs. MCP 0.4.8                                                                                                                                 |
| 1.2.0   | **LLM на OpenRouter**: новые слаги (`gpt-5.6-luna` default, `gpt-5.6-terra`, `claude-sonnet-5`, `claude-opus-5`, `deepseek-v4-flash`); старые (`gpt-5-5`, `claude-sonnet-4-6`, `claude-opus-4-8`) — deprecated-алиасы; **лимит 32 сообщения снят**; тред-режим `POST /chat/conversations/{id}/messages/`; `stream:true` для всех моделей |
| 1.1.9   | Poll/MCP: `media_urls` / `images` **only** Artillect media (no `tempfile.aiquickdraw.com` fallback); empty + finalizing hint until mirror ready; document submit spacing ~3s (429)                                                                                                                                                       |
| 1.1.8   | Files: `POST /files/from-url/`, quota-aware `POST /files/presign/` + `/files/presign/finalize/` (image/audio); input media **max 1 redirect hop**; MCP Agent DX (`get_started`, `upload_file({url})`, `create_upload_url` + `finalize_upload`, element≠turbo)                                                                            |
| 1.1.7   | 3D: `POST /api/v1/mesh/generations/` (Meshy v6/v7 t2m/i2m/multi-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`. Каталог `GET /api/v1/models/` — машинно-читаемый источник тех же лимитов.
Для **images** лимит — поле `prompt` (символы Unicode, `String.length`). Для **chat** — сумма всех `messages[].content` (только строки).

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

| `model`                 | max `prompt` (символов) | Источник                                                                    | Другое на вход                                                |
| ----------------------- | ----------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------- |
| `gpt-image-2` (default) | **7 000**               | KIE/FAL route cap                                                           | i2i: до **16** ref (`input_urls` / `images`)                  |
| `gpt-image-15`          | **2 000**               | app/provider probe (2k accepted, 5k rejected)                               | i2i: до **16** refs; `quality` medium\|high                   |
| `nano-banana`           | **8 000**               | app guardrail                                                               | i2i: до **10** refs                                           |
| `nano-banana-2`         | **8 000**               | app guardrail                                                               | i2i: до **14** refs                                           |
| `nano-banana-2-lite`    | **8 000**               | app guardrail                                                               | i2i: до **10** refs; 1K only                                  |
| `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`          | **3 000**               | app/provider probe (3k accepted, 4k rejected)                               | i2i: до **10** refs                                           |
| `seedream-5-lite`       | **2 000**               | app/provider probe (2k accepted, 3k rejected)                               | i2i: до **14** refs                                           |
| `seedream-5-pro`        | **5 000**               | KIE Seedream 5 Pro                                                          | i2i: до **14** refs; `quality` **1K\|2K** (→ basic\|high)     |
| `seedream-5-pro-layers` | **5 000**               | KIE Seedream layer-decomposition                                            | ровно **1** input; `size` auto\|1K\|1.5K\|2K; prompt optional |
| `qwen3`                 | **800**                 | [KIE Qwen3](https://docs.kie.ai/market/qwen3/text-to-image.md)              | i2i: до **3** refs; resolution 1K\|2K                         |
| `qwen3-pro`             | **800**                 | [KIE Qwen3 Pro](https://docs.kie.ai/market/qwen3-pro/text-to-image.md)      | i2i: до **3** refs; 1K\|2K                                    |
| `flux2-pro`             | **8 000**               | app guardrail                                                               | i2i: до **8** refs; resolution 1K\|2K                         |
| `flux2-max`             | **8 000**               | app guardrail                                                               | i2i: до **8** refs; resolution 1K\|2K                         |
| `flux2-flex`            | **8 000**               | app guardrail                                                               | i2i: до **8** refs; resolution 1K\|2K                         |
| `grok-imagine`          | **5 000**               | app/provider probe (5k accepted, 6k rejected)                               | i2i: до **1** ref                                             |
| `wan-27-image`          | **500**                 | FAL route cap                                                               | i2i: до **9** refs; default resolution 2K                     |
| `wan-27-image-pro`      | **500**                 | FAL route cap                                                               | i2i: до **9** refs; default resolution 2K                     |
| `cosmos-3-super`        | **8 000**               | app/provider probe                                                          | t2i only (refs → `400`)                                       |
| `mai-image-25`          | **8 000**               | app/provider probe                                                          | i2i: до **8** refs                                            |
| `mai-image-25-pro`      | **8 000**               | app/provider probe                                                          | i2i: до **1** ref                                             |
| `reve-21`               | **8 000**               | app guardrail                                                               | i2i: 1→edit / 2+→remix, max **8**                             |
| `krea-v2-large`         | **8 000**               | app guardrail                                                               | style refs на t2i, max **10**                                 |
| `krea-v2-medium`        | **8 000**               | app guardrail                                                               | style refs на t2i, max **10**                                 |
| `krea-v2-medium-turbo`  | **8 000**               | app guardrail                                                               | style refs на t2i, max **10**                                 |
| `qwen-multiple-angles`  | **8 000**               | app guardrail                                                               | **обязателен** 1 ref; `prompt` опционален                     |

Превышение → `400 PROMPT_TOO_LONG`. Одинаковый лимит для text-to-image и image-to-image (поле `prompt`). Значения с пометкой `app guardrail` — безопасный предел Artillect, пока провайдер не публикует число.

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

| `model`                  | Контекст OpenRouter | Выход OpenRouter |     Вход Artillect | Vision |
| ------------------------ | ------------------: | ---------------: | -----------------: | :----: |
| `gpt-5.6-luna` (default) |   1 050 000 токенов |  128 000 токенов | **200 000** симв.¹ |   ✓    |
| `deepseek-v4-flash`      |   1 310 720 токенов |  943 718 токенов | **200 000** симв.¹ |   ✗    |
| `gpt-5.6-terra`          |   1 050 000 токенов |  128 000 токенов | **200 000** симв.¹ |   ✓    |
| `claude-sonnet-5`        |   1 000 000 токенов |  128 000 токенов | **200 000** симв.¹ |   ✓    |
| `claude-opus-5`          |   1 000 000 токенов |  128 000 токенов | **200 000** симв.¹ |   ✓    |
| `gpt-5.6-sol`            |   1 050 000 токенов |  128 000 токенов | **200 000** симв.¹ |   ✓    |
| `gemini-3.6-flash`       |   1 048 576 токенов |   65 536 токенов | **200 000** симв.¹ |   ✓    |
| `grok-4.5`               |     500 000 токенов |  450 000 токенов | **200 000** симв.¹ |   ✓    |
| `kimi-k3`                |   1 048 576 токенов |  943 718 токенов | **200 000** симв.¹ |   ✓    |
| `muse-spark-1.2`         |   1 048 576 токенов |  943 718 токенов | **200 000** симв.¹ |   ✓    |

**Deprecated-алиасы** (продолжают работать): `gpt-5-5` → `gpt-5.6-luna`, `claude-sonnet-4-6` → `claude-sonnet-5`, `claude-opus-4-8` → `claude-opus-5`.

¹ **200 000** — guardrail Artillect API (защита от abuse), **не** char-cap OpenRouter. В discovery также есть `provider_context_length_tokens` — окно конкретного top-provider-маршрута. Значения контекста и вывода взяты из официального [OpenRouter Models API](https://openrouter.ai/docs/api/api-reference/models/get-models) на 2026-08-28; фактический доступный input зависит от токенизации и места, оставленного под ответ.

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

**Выход chat (не вход):** Artillect принимает `max_tokens` 1–8192 (по умолчанию Studio 4096); OpenRouter-предел каждой модели указан в таблице и в `GET /api/v1/models/` как `provider_max_output_tokens`.

Превышение 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 (image/video 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/v7, 3D mesh)
18. GET  /api/v1/mesh/generations/{id}/ → poll mesh (GLB)
19. POST /api/v1/chat/completions/   → LLM ответ, stateless (sync; опц. SSE)
19b. POST /api/v1/chat/conversations/{id}/messages/ → тред-режим (историю держит сервер)
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/media/operations/`                        | WIP: typed `trim` / ordered `concat` для owned video Assets (сейчас 503 в production)           |
| `GET /api/v1/media/operations/{id}/`                    | WIP: poll media operation                                                                       |
| `POST /api/v1/media/operations/{id}/cancel/`            | WIP: cancel media operation                                                                     |
| `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/`                           | Quota-aware presigned PUT для image/audio (не video) → `{ asset_id, upload_url, finalize_url }` |
| `POST /api/v1/files/presign/finalize/`                  | Завершить direct PUT, активировать Asset Library и списать резерв квоты                         |
| `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/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`, `mcp` или `cli`. Опция `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, audio, mesh): каждый item — `{ id, status, model, created_at, cost_tokens, tokens_charged, project_id, preview_text, source, preview_url }`, плюс `next_cursor`.

**Фильтры листингов** (не листайте курсором вглубь истории — сужайте выборку на сервере): `date=YYYY-MM-DD`, `date_from` / `date_to`, `before_created_at` (ISO), `q` (подстрока промпта, минимум 2 символа), `kind`, `source`. Фид проекта дополнительно принимает `mine_only=1`, `actor_user_id`, `tag_ids=1,2` (совпадение по ВСЕМ тегам) и `service`. Невалидные границы дат дают пустую выборку, а не игнорируются. Query `?source=all` (default) \| `public_api` \| `mcp` \| `cli` \| `studio` фильтрует по `client_source` в БД. Новые Public image jobs: id `{uuid}-api-image` (legacy poll: `{uuid}-api`). Project feed: `GET /api/v1/projects/{id}/generations/`. Poll по id — `{ id, status, model, cost_tokens, … }` (video/switchx также `task`).

**Универсальный poll:** `GET /api/v1/generations/{id}/` — сам определяет модальность по записи генерации и отвечает телом соответствующего роута плюс поле `modality`. Нужен, когда модальность из id не выводится: суффикс несёт её только у Public API сабмитов, а студийные job'ы получают голый `-api` независимо от вида. Per-modality роуты остаются как были.

**История и poll:** живая job-запись живёт 24 часа. Дальше poll по id отдаёт результат **из истории генераций** — терминальный статус, media URL и `hint`, что запись архивная (повторный поллинг не нужен). Доступ — создатель генерации **или** любой участник её проекта, так что чужую студийную генерацию из своего проекта тоже можно опросить по id из `GET /api/v1/projects/{id}/generations/`. Если медиа удалено ретеншеном (`media_purged_at`), придёт пустой список и явная ошибка.

**Отмена генерации:** REST/MCP/CLI и Studio могут отменить задачу с возвратом токенов только в первые 4 секунды, до отправки провайдеру. После provider submit отмена недоступна: провайдерская генерация уже оплачена и может продолжиться; сервер отвечает `409 GENERATION_NOT_CANCELLABLE`.

**Оценка цены:** `POST /api/v1/estimate/` с тем же телом, что и submit, плюс `type: "image" | "video" | "chat" | "switchx" | "upscale" | "audio" | "mesh"` → `{ cost_tokens }`. Ничего не списывает. Для Seedance с видео-рефом передайте `reference_videos` и желательно `reference_video_seconds` (ceil по каждому клипу). Без секунд оценка = 2× длительность пикера, не cap 30 с. На сабмите длина с ffprobe; если проба умерла — клиентские секунды, иначе тот же 2× запас. `duration: -1` на video-edit остаётся механизмом KIE (выход = длина клипа).

**Несколько картинок и 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`, затем `POST /api/v1/files/presign/finalize/` с `{ "asset_id": "ast_…" }`). Все пути создают Asset Library-запись и учитываются в квоте. Лимиты: изображение 150MB, видео 200MB, аудио 50MB. HEIC/HEIF, AVIF, WebP, TIFF, GIF и SVG принимаются и нормализуются перед провайдером. Video **не** поддерживает direct 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.6-luna`. Без встроенных system prompts — только ваши `messages`.  
(При нехватке ёмкости у провайдера → `503 PROVIDER_BUSY`: retry или модель подешевле.)  
По умолчанию списание — с **личного баланса**, результаты — в проект **«Мои генерации {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** минимальный интервал **0.5 с** между submit одним ключом (Redis; при недоступности Redis throttle деградирует до per-process и **не** блокирует submit). При превышении — `429` с кодом `RATE_LIMITED`, заголовками `Retry-After` и `RateLimit-*`; тело ошибки называет само правило. Идемпотентный replay (`Idempotency-Key`) не обходит per-key throttle.

> Интервал был 3 с и срабатывал на нормальном батче агента — из-за этого весь публичный rate limiter ушёл под kill switch `PUBLIC_API_RATE_LIMIT_ENABLED`. 0.5 с ловит runaway-цикл и двойной submit, не мешая человеческому темпу.

**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.

### Хранилище и квота

Загруженные файлы и результаты генераций становятся **Asset'ами** и занимают место в хранилище
владельца: **100 ГиБ бесплатно** на пользователя, плюс подключённые add-ons. Ассет, созданный в
проекте, списывается на **владельца проекта** — независимо от того, кто его загрузил.

Когда место кончилось, запись отклоняется с `413 STORAGE_FULL`: файл не сохраняется, генерация не
запускается и токены не списываются. Ничего не удаляется автоматически — освободить место можно в
`/storage`. Текущее потребление и остаток — `GET /api/storage/usage/`.

---

## 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` опущен  | **7 000** симв.                                   | да  | да (`input_urls` / `images`)      |
| `gpt-image-15`          | GPT Image 1.5                      | **2 000** симв.                                   | да  | да, max **16** refs               |
| `nano-banana`           | Nano Banana (legacy)               | **8 000** симв.                                   | да  | да, max **10** refs               |
| `nano-banana-2`         | Nano Banana 2                      | **8 000** симв.                                   | да  | да, max **14** refs               |
| `nano-banana-2-lite`    | Nano Banana 2 Lite                 | **8 000** симв.                                   | да  | да, max **10** refs; 1K only      |
| `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)           | **3 000** симв.                                   | да  | да, max **10** refs               |
| `seedream-5-lite`       | Seedream 5 Lite (ByteDance)        | **2 000** симв.                                   | да  | да, max **14** refs               |
| `seedream-5-pro`        | Seedream 5 Pro (ByteDance)         | **5 000** симв.                                   | да  | да, max **14** refs               |
| `seedream-5-pro-layers` | Seedream 5 Pro Layers              | **5 000** симв.                                   | —   | да, ровно **1** input             |
| `qwen3`                 | Qwen3 (Alibaba, KIE)               | **800** симв.                                     | да  | да, max **3** refs                |
| `qwen3-pro`             | Qwen3 Pro (Alibaba, KIE)           | **800** симв.                                     | да  | да, max **3** refs                |
| `flux2-pro`             | Flux 2 Pro                         | **8 000** симв.                                   | да  | да, max **8** refs                |
| `flux2-max`             | Flux 2 Max                         | **8 000** симв.                                   | да  | да, max **8** refs                |
| `flux2-flex`            | Flux 2 Flex                        | **8 000** симв.                                   | да  | да, max **8** refs                |
| `grok-imagine`          | Grok Imagine                       | **5 000** симв.                                   | да  | да, max **1** ref                 |
| `wan-27-image`          | Wan 2.7 Image                      | **500** симв.                                     | да  | да, max **9** refs                |
| `wan-27-image-pro`      | Wan 2.7 Image Pro                  | **500** симв.                                     | да  | да, max **9** refs                |
| `cosmos-3-super`        | Nvidia Cosmos 3S (FAL)             | **8 000** симв.                                   | да  | нет (refs → `400`)                |
| `mai-image-25`          | MAI Image 2.5 (Microsoft, FAL)     | **8 000** симв.                                   | да  | да, max **8** refs                |
| `mai-image-25-pro`      | MAI Image 2.5 Pro (Microsoft, FAL) | **8 000** симв.                                   | да  | да, max **1** ref                 |
| `reve-21`               | Reve 2.1 (FAL)                     | **8 000** симв.                                   | да  | 1→edit / 2+→remix, max **8**      |
| `krea-v2-large`         | Krea 2 Large (FAL)                 | **8 000** симв.                                   | да  | style refs на t2i, max **10**     |
| `krea-v2-medium`        | Krea 2 Medium (FAL)                | **8 000** симв.                                   | да  | style refs на t2i, max **10**     |
| `krea-v2-medium-turbo`  | Krea 2 Medium Turbo (FAL)          | **8 000** симв.                                   | да  | style refs на t2i, max **10**     |
| `qwen-multiple-angles`  | Qwen Multiple Angles (FAL angles)  | **8 000** симв.                                   | нет | **обязателен** 1 ref image        |
| `muse-image`            | Meta Muse Image (FAL)              | **8 000** (app guardrail; FAL max не опубликован) | да  | edit: **1–10** refs               |
| `google-virtual-try-on` | Google Virtual Try On (FAL)        | **0** (prompt не используется)                    | нет | ровно **2** input: человек → вещь |

Роутинг как в 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 семьи и `muse-image` (`jpeg`\|`png`\|`webp`); у остальных моделей игнорируется.

`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"
}
```

**Meta Muse Image** (`muse-image`):

```json
{
  "model": "muse-image",
  "prompt": "A product hero shot with clean studio lighting",
  "aspect_ratio": "16:9",
  "n": 3,
  "output_format": "webp"
}
```

**Google Virtual Try On** (`google-virtual-try-on`):

```json
{
  "model": "google-virtual-try-on",
  "input_urls": ["https://example.com/person.jpg", "https://example.com/jacket.jpg"],
  "n": 2
}
```

Ровно два входных изображения обязательны: `input_urls[0]` — человек, `input_urls[1]` —
фото вещи. `n`/`num_images` принимает целое **1–4**. `prompt`, `aspect_ratio`,
`resolution`, `quality`, `output_format` и `sync_mode` для этой модели не
используются. Цена — **8 токенов за каждое output image**; при `n=2` — 16 токенов.

Для редактирования передайте `input_urls` или `images` (от 1 до 10 URL). Muse принимает
`aspect_ratio` из `21:9`, `16:9`, `4:3`, `3:2`, `1:1`, `2:3`, `3:4`, `9:16`, `9:21`; без
него размеры выбираются автоматически. `n`/`num_images` — целое от 1 до 10, цена — **1
токен за каждый output image**. `output_format` — `jpeg`\|`png`\|`webp`; `sync_mode=true`
просит data URI вместо сохранённого URL результата.

**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 символов **зависит от модели** (см. ниже); для `google-virtual-try-on` не используется                                                                        | —             |
| `model`                 | нет   | см. таблицу моделей выше (`gpt-image-2`, `gpt-image-15`, `nano-banana*`, `seedream-*`, `flux2-*`, `grok-imagine`, `wan-27-image*`, `muse-image`, `google-virtual-try-on`) | `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 зависит от модели (см. таблицу)                                                                                                                             | —             |
| `n` / `num_images`      | нет   | integer **1–10** для `muse-image`, **1–4** для `google-virtual-try-on` (у остальных — по каталогу)                                                                        | из каталога   |
| `sync_mode`             | нет   | boolean; data URI вместо persisted URL для моделей, которые это поддерживают                                                                                              | `false`       |

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

| `model`                        | max `prompt` |
| ------------------------------ | ------------ |
| `gpt-image-2`                  | **7 000**    |
| `gpt-image-15`                 | **2 000**    |
| `nano-banana`                  | **8 000**    |
| `nano-banana-2`                | **8 000**    |
| `nano-banana-2-lite`           | **8 000**    |
| `nano-banana-pro`              | **10 000**   |
| `seedream-4`                   | **5 000**    |
| `seedream-4-5` / `seedream-45` | **3 000**    |
| `seedream-5-lite`              | **2 000**    |
| `seedream-5-pro`               | **5 000**    |
| `flux2-pro` / `flux2-flex`     | **8 000**    |
| `mai-image-25` / `-pro`        | **8 000**    |
| `reve-21`                      | **8 000**    |
| `grok-imagine`                 | **5 000**    |
| `wan-27-image` / `-pro`        | **500**      |

Превышение → `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 (SSRF: private/metadata IP блокируются; **не более одного** redirect hop с повторной проверкой). Провайдер получает `https://{origin}/api/backend/media/download/?key=...`. Уже загруженные пути `/api/backend/media/download/?key=...` принимаются как есть. Max **150MB** на файл. `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`.

Во время `queued`/`running` поле `progress` равно `null`: Artillect не подставляет фиктивный процент, если провайдер не дал достоверный промежуточный прогресс. На `completed` оно равно `100`, на явном `failed`/`cancelled` — `0`.

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

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

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

```json
{
  "status": "running",
  "progress": null,
  "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** (Artillect media — единственный контракт для агентов):

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

Если провайдер уже закончил, а зеркало в MinIO ещё нет — **`status` остаётся `running`** (не `completed`), `images: []`, `hint` про mirroring, `recommended_poll_interval_sec: 3`, `progress` ~95. Пайплайны/агенты не должны останавливаться на пустых URL. Provider CDN (`tempfile.aiquickdraw.com`) **не отдаём** в poll. Через ~15 мин без durable URL → `failed`.

Webhook `generation.completed` тоже **не шлётся**, пока нет stable media (следующий poll/finalize докинет outbox).

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

| Фаза                       | Формат                     | Примечание                                         |
| -------------------------- | -------------------------- | -------------------------------------------------- |
| Input после зеркалирования | Stable Artillect media URL | Передаётся обратно в submit/poll как transport URL |
| Output                     | Stable Artillect media URL | Только после mirror; иначе poll ещё раз            |

Не разбирайте URL и не извлекайте из него внутренние object keys. Для долгоживущей идентификации
пользовательского файла используйте **Asset ID** `ast_…`: `GET /api/v1/assets/{asset_id}/` возвращает
свежий разрешённый URL и метаданные, а `GET /api/v1/assets/?q=…&kind=image|video|audio` позволяет
найти ранее сохранённый файл. Asset можно повторно использовать в другом запуске или чате, пока у
ключа есть доступ к personal/project scope. Копирование между personal и project создаёт независимый
Asset и отдельно учитывается в квоте.

**Скачивание результата:** `GET` по `images[0].url` **без** Authorization для Artillect media. Не используйте CDN провайдера.

---

## 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` → first+last)                            |
| `source_video_url` | string   | Исходное видео для **edit** / **v2v** (Kling O3)                                    |
| `reference_images` | string[] | Референс-изображения (стиль/объект). На **`flux-3`** — alias для `keyframes`        |
| `keyframes`        | string[] | **Только `flux-3`:** упорядоченная раскадровка (storyboard), ≤10 URL. Не style-refs |
| `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`, `seedance-2.5`, `happy-horse`, `happy-horse-1-1`, `minimax-h3`, `wan-27`, `veo-31`, `grok-imagine-video`, `flux-3`.

На **`flux-3`** `@имя` подтягивает frontal из библиотеки проекта и кладёт его в `keyframes` (раскадровка, не native character-id как у Kling). Нужен `project_id`.

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

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

URL — HTTPS (сервер скачает и зеркалирует в Artillect media) или готовый `/api/backend/media/download/?key=...`. Лимиты: изображение **150MB**, видео **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 |
| `minimax-h3`              |  ✓  | start / start+end | img≤9, vid≤3, aud≤3, Σ≤15                                | ✗          | resolution **768P**\|**2K**; duration 4–15                                                                                                      | 768P, 2K    |
| `minimax-h3-max`          |  ✓  | start / start+end | img/video/audio, **12 combined**                         | ✗          | resolution **480P**\|**768P**; duration **5–15**; aspect ratio/**adaptive**; seed; prompt expansion; safety checker; sync mode; frames XOR refs | 480P, 768P  |
| `flux-3`                  |  ✓  | start / start+end | **keyframes** ≤10 (storyboard; alias `reference_images`) | ✗          | `generate_audio` (default on); duration **5–20**; aspect **auto\|21:9\|2:1\|16:9\|4:3\|1:1\|3:4\|9:16**; timed `[sec,url]` pins not in API yet  | 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.5` | ✓ | start / start+end | img≤30, vid≤10, aud≤10, Σ≤50 | ✗ | `generate_audio`; `output_format` **mp4** (default) \| **mov**; MOV — выше битрейт для монтажа и больше файл, MP4 — компактнее и совместимее; duration 4–30\|`-1` (auto); video-ref clips > 4s and ≤ 30s; video-edit 422 → adaptive / `-1` | 480p, 720p, **1080p** |
| `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 |
| `wan-30` | ✓ | start / start+end | img≤10, vid≤5, aud≤5, **one file XOR one public link** | ✗ | `audio` (default true), `seed` 0–2147483647, `nsfw_checker` (default false); prompt ≤20000; duration 2–30 or -1 | 480P, 720P, 1080P |
| `wan-30-prime` | ✓ | start / start+end | img≤10, vid≤5, aud≤5, **one file XOR one public link** | ✗ | Same contract as Wan 3; high-speed KIE market model | 480P, 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 |
| `runway-gen-45` | ✓ | start | ✗ | ✗ | `seed` | 720p; 2–10 sec |
| `runway-aleph-2` | ✓ | ✗ | ✗ | **edit** (`source_video_url`) | `seed` | exactly one source video + prompt; no image refs |

Для **`seedance-2.5`** формат результата выбирается полем `output_format`: `mp4` (default,
компактнее и совместимее) или `mov` (выше битрейт для монтажа, файл больше). Это поле
поддерживается только этой моделью и одинаково принимается submit/estimate, MCP и CLI.

```json
{
  "model": "seedance-2.5",
  "prompt": "cinematic product shot",
  "output_format": "mov"
}
```

### Лимиты `prompt` для видео

`max_prompt_chars` возвращается для каждой модели в `GET /api/v1/models/` и используется
одинаково для t2v/i2v/ref2v, если модель поддерживает эти режимы. Для `seedance-2.5`
лимит провайдера — 10 000 токенов; API/UI используют приблизительный эквивалент
40 000 символов (по оценке 4 символа на токен).

| Модели                                                                                        |        max `prompt` (символов) |
| --------------------------------------------------------------------------------------------- | -----------------------------: |
| `seedance-2`, `seedance-2-mini`                                                               |                     **20 000** |
| `seedance-2.5`                                                                                |                     **40 000** |
| `kling-3`, `kling-3-turbo`, `kling-o3`                                                        |                      **2 500** |
| `kling-26`                                                                                    |                      **1 000** |
| `happy-horse`, `happy-horse-1-1`                                                              |                      **2 500** |
| `minimax-h3`, `minimax-h3-max`                                                                |                      **7 000** |
| `wan-27`                                                                                      |                      **5 000** |
| `wan-30`, `wan-30-prime`, `veo-31`, `flux-3`, `gemini-omni*`                                  |                      **8 000** |
| `grok-imagine-video`, `grok-imagine-15`                                                       |                      **4 096** |
| `ltx-23`                                                                                      |                      **5 000** |
| `luma-ray-32`                                                                                 |                      **6 000** |
| `runway-gen-45`                                                                               |                      **1 000** |
| `wan-22`, `kling-3-motion`, `kling-26-motion`, `volcengine`, `sync-v3`, `heygen`, `omnihuman` | **0** (prompt не используется) |

Для моделей, не перечисленных выше, действует bounded fallback **8 000** до появления
числа в каталоге провайдера. Абсолютный потолок Artillect — 20 000 символов.

### MiniMax H3 Max — full FAL contract

`minimax-h3-max` uses `minimax/h3-max/text-to-video` for text generation,
`minimax/h3-max/image-to-video` for first/last-frame generation, and
`minimax/h3-max/reference-to-video` for multimodal references. The public API accepts
these fields:

| Field                           | Values / limits                                         | Default                      |
| ------------------------------- | ------------------------------------------------------- | ---------------------------- |
| `prompt`                        | required string                                         | —                            |
| `start_image_url` / `image_url` | optional first-frame URL                                | —                            |
| `end_image_url`                 | optional last-frame URL                                 | —                            |
| `reference_images`              | image reference URLs                                    | —                            |
| `reference_videos`              | video reference URLs                                    | —                            |
| `reference_audios`              | audio reference URLs                                    | —                            |
| `duration`                      | integer 5–15 seconds                                    | `5`                          |
| `resolution`                    | `480P` or `768P`                                        | `768P`                       |
| `aspect_ratio`                  | `adaptive`, `21:9`, `16:9`, `4:3`, `1:1`, `3:4`, `9:16` | `16:9` / `adaptive` in ref2v |
| `seed`                          | integer                                                 | —                            |
| `prompt_expansion_mode`         | `disabled`, `balanced`, `quality`                       | `balanced`                   |
| `enable_safety_checker`         | boolean                                                 | `true`                       |
| `sync_mode`                     | boolean; returns base64 instead of a CDN URL            | `false`                      |

Task selection is automatic: no media → `t2v`; a first frame → `i2v`; both first and last
frames → `flf2v`; any references → `ref2v`. `end_image_url` is only valid together with the
first frame. Frames and references are mutually exclusive. In `ref2v`, at least one image or
video is required; audio cannot be the only reference. Each video/audio reference is 2–15
seconds, with at most 15 seconds total per media type and 12 reference files combined.
Documents, web inputs, source-video input, and multishot controls are unsupported. FAL returns
`video` and may include `expanded_prompt` and `timings`.

Pricing is time-sensitive: through **2026-09-07**, 480P costs $0.025/s (2.5 Artillect
tokens/s) and 768P costs $0.04/s (4 tokens/s). After the promotion, rates are
$0.05/s (5 tokens/s) and $0.08/s (8 tokens/s). The estimate is
`ceil(rate × ceil(duration))`, with reference-to-video billed at the regular $0.08/s plus 2
tokens per billable 1K image tokens after FAL's first 4096 image tokens; fractional tokens round up.

### Wan 3 / Wan 3 Prime — full KIE contract

Both slugs expose the same KIE input contract; only the provider model id differs:
`wan/3-0-video` and `wan/3-0-video-prime` (Prime is the high-speed tier). The exact
provider payload fields are `prompt`, `first_frame_url`, `last_frame_url`,
`reference_image_urls`, `reference_video_urls`, `reference_audio_urls`,
`reference_file_urls`, `reference_link_urls`, `resolution`, `aspect_ratio`, `duration`,
`audio`, `seed`, and `nsfw_checker`.

Rules from KIE:

- `prompt`: max **20,000** characters; required for t2v, recommended with media, and
  optional for i2v/flf2v/ref2v. In ref2v, address inputs as `Image1`, `Video1`, and
  `Audio1` in array order.
- `first_frame_url` and `last_frame_url`: one image each; JPEG/JPG/PNG (no
  transparency), BMP, or WEBP; each side 240–8000 px; aspect ratio ≤8:1; ≤20 MB.
  They are strict first/last frames and cannot be combined with any `reference_*_urls`.
- `reference_image_urls`: max **10**, same image constraints as frames.
- `reference_video_urls`: max **5**; MP4/MOV; each 1–15 s, total ≤15 s; sides
  240–4096 px; aspect ratio ≤8:1; ≤100 MB each; input video duration + output
  duration ≤30 s.
- `reference_audio_urls`: max **5**; WAV/MP3; each 1–15 s, total ≤15 s; ≤15 MB
  each. Audio-only input is not recommended; pair it with image/video media.
- `reference_file_urls`: max **1**; document formats `docx`, `doc`, `xlsx`, `xls`,
  `pptx`, `ppt`, `pdf`, `txt`, `key`, `pages`, `numbers`, `md`; ≤100 MB and
  document/PDF-style inputs ≤50 pages.
- `reference_link_urls`: max **1** public webpage, with no login required. A file
  and a webpage are mutually exclusive, and both are mutually exclusive with frames.
- `resolution`: `480P` | `720P` | `1080P`, default `1080P`.
- `aspect_ratio`: `adaptive` | `16:9` | `4:3` | `1:1` | `3:4` | `9:16`, default
  `adaptive`.
- `duration`: integer 2–30 seconds, default **5**; `-1` delegates duration to the
  model. With video references, the combined input + output cap still applies.
- `audio`: output audio-track switch, default **true**. `seed`: integer
  0–2147483647. `nsfw_checker`: KIE default **false**; KIE documents false as
  disabling its content filtering, so use it deliberately.

Example (document and webpage cannot be sent together):

```json
{
  "model": "wan-30-prime",
  "prompt": "Create a concise product video from this source",
  "reference_file_urls": ["https://example.com/product.pdf"],
  "resolution": "720P",
  "aspect_ratio": "adaptive",
  "duration": 8,
  "audio": true,
  "seed": 12345,
  "nsfw_checker": false
}
```

Pricing is converted from KIE credits using the Artillect rule **2 KIE credits = 1
Artillect token**, with the final fractional token rounded up. Wan 3 is 4 / 8 / 16
tokens per second at 480P / 720P / 1080P; Prime is 6.1 / 12.6 / 25.2 tokens per
second. The estimate is `ceil(rate × ceil(duration))`; `duration=-1` uses 5 seconds
for a deterministic pre-submit estimate.

**`flux-3` — что есть что:**

| Режим                    | Слоты                                                | Комментарий                                                                                                                                                          |
| ------------------------ | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| t2v                      | только `prompt`                                      | текст → видео                                                                                                                                                        |
| i2v                      | `start_image_url`                                    | один стартовый кадр                                                                                                                                                  |
| flf2v                    | `start_image_url` + `end_image_url`                  | first + last                                                                                                                                                         |
| keyframes (task `ref2v`) | `keyframes` (предпочтительно) или `reference_images` | упорядоченный storyboard ≤10 URL; **не** style/character refs как у Seedance/Kling. FAL сам раскладывает кадры по `duration`. Timed-пины `[sec, url]` в API пока нет |

```json
{
  "model": "flux-3",
  "prompt": "Cinematic pass through the storyboard",
  "keyframes": ["https://.../frame1.jpg", "https://.../frame2.jpg", "https://.../frame3.jpg"],
  "duration": 12,
  "aspect_ratio": "16:9",
  "resolution": "1080p",
  "generate_audio": true
}
```

### 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                                 |
| `gemini-omni-flash-11` | t2v/i2v/flf2v/ref2v/edit | ref2v: `reference_images` / `reference_videos`; edit: `source_video_url`      | required (edit) | 7 weighted units; 360p/720p/1080p/4k                 |
| `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" }` or `{ "name", "kind", "asset_id" }` for an Asset already copied into the project. Use `media_json: { "images": ["frontal", "ref2"] }` for an image and `{ "video_url": "clip" }` for a video. 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
}
```

**Использование в video:** передайте `project_id` того же проекта и `@hero` в `prompt` **или** `element_names: ["hero"]` в `POST /api/v1/videos/generations/`. Native elements работают в Kling 3/O3; image frontal как pseudo-element — в поддержанных Seedance/Wan/Veo/Happy Horse/Grok/Flux моделях. Video-element на image-only/pseudo image adapters отклоняется.

**Использование в image:** image-element можно передать тем же способом в `POST /api/v1/images/generations/` — `project_id` + `@hero` в `prompt` или `element_names: ["hero"]`. Frontal добавляется в reference inputs, а prompt для provider переписывается в `image 1`, `image 2`, …; video-element для image generation запрещён.

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

```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://app.artillect.pro/api/backend/media/download/?key=..." }],
  "poster_url": "https://app.artillect.pro/api/backend/media/download/?key=...",
  "progress": 100
}
```

Только Artillect media URLs; если `videos` пустые при `completed` — поллите ещё (mirror).

`poster_url` — первый кадр готового видео (появляется только вместе с непустым `videos`). Нужен там, где само видео показать нельзя: превью в списке, `<video poster>`, и агенты, которым видео не заинлайнить.

---

## Upscale (Topaz + Crystal + FLUX Video Upscale)

Topaz, Crystal и FLUX Video Upscale через FAL. Topaz — **только Public API** (не в UI `/enhance`). Crystal и FLUX — тот же backend, что Studio `/enhance`. 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`   |
| `flux-video-upscale`          | `video_url` | `blackforestlabs/flux-video-upscale` |

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

- `model` — slug Artillect API (`topaz-image`, `topaz-video`, `crystal-image`, `crystal-video`, `flux-video-upscale`).
- `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` для точной оценки.

**FLUX Video Upscale** (`flux-video-upscale`) — видео до **20 секунд и 50 MB**. Обязательное поле `video_url`; исходное соотношение сторон сохраняется. Параметры FAL: `upscale_factor` **1.5–3** (default 2), `creativity` **0/1** (0 = precise, 1 = creative, default 1), необязательный `prompt` для creative detail enhancement и `safety_tolerance` **0–4** (default 2). Выходной tier определяется размерами входа × коэффициентом: 1080p — **14/20 токенов/с**, 2K — **25/35**, 4K — **55/79** (precise/creative). Для оценки передайте `duration`, `width`, `height`; эти три поля нужны только биллингу и не отправляются провайдеру.

```json
{
  "model": "flux-video-upscale",
  "video_url": "https://cdn.example.com/input.mp4",
  "upscale_factor": 2,
  "creativity": 0,
  "safety_tolerance": 2,
  "duration": 10,
  "width": 1280,
  "height": 720
}
```

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

Оценка до 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 / v7)

Асинхронная генерация 3D-моделей поддерживает Meshy 6 (`meshy-v6-t2m`, `meshy-v6-i2m`) и Meshy 7 (`meshy-v7-t2m`, `meshy-v7-i2m`, `meshy-v7-multi-i2m`). Провайдер — FAL, сервис `generate-mesh`. Параметры и цены v6/v7 различаются; актуальные ограничения приведены ниже. Запекание 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 (max 1 redirect hop / без приватных 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
}
```

До отправки провайдеру mesh-задачу можно отменить через общий или modality-specific cancel endpoint в первые 4 секунды с возвратом токенов; после submit отмена отсутствует. История — `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`.

### Meshy 7

Доступны `meshy-v7-t2m`, `meshy-v7-i2m` и `meshy-v7-multi-i2m`. Для v7 text-to-3D лимит `prompt` — 600 символов. Image-to-3D принимает один `image_url`; multi-image принимает `image_urls` от 1 до 4 изображений одного объекта с разных углов. Входы JPG/JPEG/PNG, а AVIF/HEIF автоматически конвертируются.

Общие параметры v7: `model_type` (`standard|lowpoly|smart-topology`), `topology` (`quad|triangle`), `target_polycount` (100–300000; smart-topology максимум 15000), `symmetry_mode` (`off|auto|on`), `should_remesh`, `enable_pbr`, `pose_mode` (`a-pose|t-pose|""`), `texture_prompt`, `texture_image_url`, `enable_rigging`, `rigging_height_meters` (default 1.7), `enable_animation`, `animation_action_id` (0–696), `enable_safety_checker`.

У `meshy-v7-t2m` дополнительно есть `mode` (`preview|full`), `seed` и `enable_prompt_expansion`; `ultra_mode` доступен только для стандартной полнотекстурированной генерации. У `meshy-v7-i2m` есть `should_texture` и `ultra_mode` с тем же ограничением. У multi-image есть `should_texture`, но текущая схема endpoint не принимает `ultra_mode`.

Цена v7: 80 токенов без текстур, 120 с текстурами, 140 с текстурами и ultra; `enable_rigging` добавляет 20, `enable_animation` — 12. Multi-image: 120 с текстурами, +20/+12 за rigging/animation. Оценка учитывает все переданные параметры через `POST /api/v1/estimate/` с `type: "mesh"`.

> 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`             | Описание                              | Биллинг       | Стоимость (токены)      |
| ------------------- | ------------------------------------- | ------------- | ----------------------- |
| `gpt-5.6-luna`      | **Default** — дешёвая, видит картинки | фиксированный | **2** за ответ          |
| `deepseek-v4-flash` | Самая дешёвая, только текст           | фиксированный | **2** за ответ          |
| `gpt-5.6-terra`     | Качественнее дефолта                  | по факту      | ~1–4, зависит от объёма |
| `claude-sonnet-5`   | Claude Sonnet 5                       | по факту      | ~2–6                    |
| `claude-opus-5`     | Claude Opus 5 — топ                   | по факту      | ~5–15                   |
| `gpt-5.6-sol`       | GPT Sol — сильный reasoning           | по факту      | зависит от объёма       |
| `gemini-3.6-flash`  | Быстрый мультимодальный Gemini        | по факту      | зависит от объёма       |
| `grok-4.5`          | Grok 4.5                              | по факту      | зависит от объёма       |
| `kimi-k3`           | Kimi K3                               | по факту      | зависит от объёма       |
| `muse-spark-1.2`    | Muse Spark 1.2                        | по факту      | зависит от объёма       |

**Два режима оплаты.** У дешёвых моделей себестоимость ответа при любой длине треда
меньше цента, поэтому цена фиксированная и известна заранее. У дорогих она гуляет в
разы в зависимости от длины диалога и числа картинок — фиксированную пришлось бы
ставить по худшему случаю, поэтому берётся **фактическая**: перед вызовом холд 2 токена,
после ответа доначисляется остаток. Итог всегда приходит в `cost_tokens` ответа.

> Для моделей «по факту» `estimate` возвращает **диапазон**, а не точное число:
> итог известен только после ответа. Курс — 1 токен = 1 US-цент.

Лимит входа для всех моделей — **200 000** символов суммарно в `content`¹.

¹ Сумма `messages[].content` (только текст). **Лимита на количество сообщений больше нет** —
ограничивается объём, а не длина диалога.

### Два режима чата

|                       | `POST /chat/completions/`                    | `POST /chat/conversations/{id}/messages/` |
| --------------------- | -------------------------------------------- | ----------------------------------------- |
| История               | **вы присылаете** весь `messages` каждый раз | **держит сервер**                         |
| Картинки между ходами | вы переприсылаете сами                       | живут в треде, переподаются автоматически |
| Формат                | OpenAI-совместимый                           | `{ "message": "...", "images": ["..."] }` |
| `conversation_id`     | только архивирует ход                        | сам тред                                  |

Тред-режим — то, что нужно для многоходового разговора про картинку: изображение
зеркалится к нам один раз и переподаётся модели на следующих ходах, поэтому
«обсудить картинку» работает и на втором, и на десятом сообщении.

```bash
# 1) создать тред
curl -X POST https://app.artillect.pro/api/v1/chat/conversations/ \
  -H "Authorization: Bearer $ARTILLECT_API_KEY" -H "Content-Type: application/json" \
  -d '{"title":"Разбор фото"}'

# 2) первый ход с картинкой
curl -X POST https://app.artillect.pro/api/v1/chat/conversations/123/messages/ \
  -H "Authorization: Bearer $ARTILLECT_API_KEY" -H "Content-Type: application/json" \
  -d '{"message":"Что на картинке?","images":["https://example.com/photo.jpg"]}'

# 3) второй ход — картинку присылать НЕ нужно
curl -X POST https://app.artillect.pro/api/v1/chat/conversations/123/messages/ \
  -H "Authorization: Bearer $ARTILLECT_API_KEY" -H "Content-Type: application/json" \
  -d '{"message":"А какого цвета фон?"}'
```

Ответ: `{ id, conversation_id, model, content, usage, cost_tokens, project_id }`.
Ошибки: `409 CHAT_IN_FLIGHT` (предыдущий ход ещё отвечает), `429 RATE_LIMITED`,
`404 CONVERSATION_NOT_FOUND`.

**Vision (картинки):** все модели кроме `deepseek-v4-flash`. Форматы тела (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** суммарно, **150MB**/файл. Внешние 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` — **для всех моделей** (на OpenRouter стрим единый). Ответ `Content-Type: text/event-stream` с OpenAI-ish чанками (`object: chat.completion.chunk`, `choices[0].delta.content`), затем `data: [DONE]`. В финальном чанке — `finish_reason: stop`, `usage`, `cost_tokens`. Заголовок `Idempotency-Key` со stream несовместим (`400 STREAM_IDEMPOTENCY_UNSUPPORTED`). MCP `chat_completion` всегда sync.

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

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

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

```json
{
  "model": "gpt-5.6-luna",
  "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`                                                        | Файл > 150MB (изображение)                                                                                                                             |
| 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`                                                       | Записи с таким `external_job_id` нет                                                                                                                   |
| 403  | `GENERATION_ACCESS_DENIED`                                                   | Генерация существует, но вы не её создатель и не участник её проекта                                                                                   |
| 404  | `JOB_ID_FORMAT_REJECTED`                                                     | Формат id не подходит этому эндпоинту (студийные id — голый `-api`). Зовите `GET /api/v1/generations/{id}/`                                            |
| 404  | `GENERATION_JOB_EXPIRED`                                                     | Live job-запись истекла (24ч), а генерация так и не дошла до финального статуса. Поллить дальше бесполезно                                             |
| 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`                                                 |
| 400  | `FILE_REQUIRED` / `FILE_TYPE_UNSUPPORTED` / `FILE_TOO_LARGE`                 | Ошибки загрузки файла (`POST /files`)                                                                                                                  |
| 413  | `STORAGE_FULL`                                                               | Хранилище владельца заполнено (100 ГиБ бесплатно + add-ons). Файл не записан и токены не списаны — освободите место в `/storage` или подключите add-on |
| 503  | `ASSET_STORAGE_UNAVAILABLE` / `ASSET_WRITE_FAILED`                           | Хранилище временно недоступно или запись не удалась. Резерв квоты снимается автоматически; повторите позже                                             |
| 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 — quota-aware presign PUT image/audio                        |
| POST   | `/api/v1/files/presign/finalize/`                  | Bearer — activate direct-upload Asset + commit quota                |
| 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/videos/generations/`                      | Bearer `art_...`                                                    |
| GET    | `/api/v1/videos/generations/`                      | Bearer `art_...` — история (пагинация)                              |
| GET    | `/api/v1/videos/generations/{id}/`                 | Bearer `art_...`                                                    |
| POST   | `/api/v1/upscale/generations/`                     | Bearer `art_...` — image/video upscale                              |
| GET    | `/api/v1/upscale/generations/`                     | Bearer `art_...` — история (пагинация)                              |
| GET    | `/api/v1/upscale/generations/{id}/`                | 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/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/{modality}/generations/{id}/cancel/`      | Bearer `art_...` — cancel + refund only before provider submit (4s) |
| POST   | `/api/v1/generations/{id}/cancel/`                 | Bearer `art_...` — modality-agnostic MCP/CLI cancel (4s)            |
| POST   | `/api/v1/mesh/generations/`                        | Bearer `art_...` — Meshy v6/v7 (3D mesh)                            |
| GET    | `/api/v1/mesh/generations/`                        | Bearer `art_...` — история (пагинация)                              |
| GET    | `/api/v1/mesh/generations/{id}/`                   | 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-09-01 — общий независимый 4-секундный pre-submit cancel + refund для Studio, REST API, MCP и CLI; после отправки провайдеру отмена недоступна. Предыдущая редакция: 2026-08-01 — аудио (153), 3D (154)._
