В v1 настройки генерации жили внутри идентификатора модели. Нужно видео 1080p на пять секунд — берёшь seedance-1-pro-1080P-5s. Нужны те же пять секунд, но в 720p — это уже другой id, и его надо найти в каталоге. Разрешение, длительность, качество перемножались, каталог рос, а интеграция превращалась в таблицу соответствия «параметры пользователя → строка id».
Мы переписали медиа-API. v2 — не новые адреса для старых методов, а другой контракт: параметры переехали в тело запроса, цену можно узнать до генерации, а ретрай после сетевого таймаута больше не создаёт вторую задачу. v1 продолжает работать на том же ключе, но он заморожен: новые возможности выходят только в v2.
Один запрос вместо перебора тиров
// v1: настройки зашиты в id, менять нечего
POST https://gptunnel.ru/v1/media/create
{ "model": "seedance-1-pro-1080P-5s", "prompt": "cat", "ar": "16:9" }// v2: базовая модель, настройки — в params
POST https://gptunnel.ru/api/v2/media/tasks
{
"model": "seedance-1-pro",
"prompt": "cat",
"params": { "aspect_ratio": "16:9", "resolution": "1080P", "duration": 5 }
}Разница не косметическая. В v1 из запроса читается только соотношение сторон (ar), всё остальное подставляется из дефолтов модели — поэтому часть настроек через v1 просто недоступна, независимо от того, что модель умеет. Тировые id вроде seedance-1-pro-1080P-5s в каталоге v2 не появятся: там базовая модель и список её параметров.
Базовый URL меняется с https://gptunnel.ru/v1/media на https://gptunnel.ru/api/v2/media. Ключ и заголовок Authorization те же, новый выпускать не нужно.
Схему параметров отдаёт сам каталог
Угадывать, что принимает конкретная модель, не нужно — это описано машиночитаемо:
curl https://gptunnel.ru/api/v2/media/models \
-H 'Authorization: YOUR_API_KEY'{
"id": "kling-v3",
"type": "VIDEO",
"price": 210,
"prompt": "required",
"params": [
{ "key": "resolution", "type": "enum", "default": "1080p",
"options": [{ "value": "720p" }, { "value": "1080p" }] },
{ "key": "duration", "type": "number", "default": 5,
"min": 5, "max": 10, "step": 5, "integer": true, "unit": "s" }
],
"inputs": [
{ "key": "image", "kind": "image", "role": "first_frame",
"required": false, "count": { "min": 0, "max": 1 },
"formats": ["jpg", "png", "webp"] }
]
}По этому ответу форма настроек в своём интерфейсе строится сама: params говорит, что можно задать (тип, допустимые значения, дефолт, границы), inputs — какие файлы модель принимает, в какую роль, сколько штук и в каких форматах. Поле prompt со значением required | optional | none снимает старую проблему v1, где промпт был обязателен даже для моделей, которым он не нужен.
Отдельно стоит запомнить: в params публикуется только то, что клиент вправе задать сам. У модели могут быть параметры с фиксированным значением — их видно в ответе задачи и расчёта цены, но изменить нельзя.
Цена — до генерации, а не по факту списания
Цена зависит не только от модели, но и от параметров: секунда видео, разрешение, качество. В v1 в каталоге была одна цена на id, и посчитать конкретную комбинацию было негде.
POST /api/v2/media/price принимает ту же пару { model, params }, что и создание задачи, и возвращает итог, ничего не запуская и не списывая:
{ "code": 0, "model": "kling-v3", "price": 210, "currency": "RUB",
"params": { "resolution": "1080p", "duration": 5, "aspect_ratio": "16:9" } }Цена считается той же функцией, что и фактическое списание, — «посчитали одно, списали другое» тут невозможно. Два практических следствия. Первое: params в ответе — это применённые значения: присланное плюс дефолты и фиксированные параметры модели; если присланного параметра в ответе нет, он не настраивается. Второе: этим же запросом удобно проверять тело перед боевым вызовом — валидация та же, но без денег. Оговорка одна: модели, которые тарифицируются ещё и по промпту или входным файлам (токены, мегапиксели), возвращают цену без этой части.
Ретрай перестал стоить денег
Классический сценарий: клиент получил сетевой таймаут, повторил запрос, задача создалась дважды, деньги списались дважды. Теперь у создания есть idempotency_key:
POST /api/v2/media/tasks
{
"model": "seedance-1-pro",
"prompt": "cat",
"params": { "aspect_ratio": "16:9", "resolution": "1080P", "duration": 5 },
"idempotency_key": "order-1042",
"webhook_url": "https://your.app/hooks/media"
}Повтор с тем же ключом вернёт ту же задачу. Если тот же ключ прислали с другим телом, ответ будет 409 (code: 23), а не тихий дубль: сервер сравнивает хеш запроса, а не только ключ.
Ключ должен быть привязан к бизнес-операции — id заказа, id сообщения, id строки в очереди, — а не генерироваться заново на каждую попытку. Случайный ключ на каждый ретрай не защищает ни от чего.
Один объект задачи вместо трёх форм ответа
POST /api/v2/media/tasks создаёт задачу для любой медиа-модели, и ответ везде одинаковый: id, status (queued | running | done | failed), params, price, result. Тот же объект возвращает GET /api/v2/media/tasks/:id и тот же уходит в webhook. В v1 форма ответа зависела от семейства модели: у обычных генераций результат приходил строкой в url, у музыки — массивом со своими именами полей, а часть моделей вообще жила на отдельных ветках.
Главная ловушка при переносе — читать result[0]. В v2 result всегда массив, и в нём может быть больше одного файла: модели с несколькими выходами возвращают все, а музыкальные (Suno, Mureka, MiniMax) отдают дорожки, у каждой свои url, title, duration, cover_url, lyric.
if (task.status === 'done') {
for (const item of task.result) save(item.url)
}Для быстрых моделей есть короткий синхронный режим: wait: true в теле создания держит соединение до готовности и отдаёт финальный объект сразу. Ожидание ограничено примерно 30 секундами; не успели — вернётся задача в текущем статусе, и результат забирается обычным способом. Писать поллинг ради картинки, которая рисуется шесть секунд, больше не нужно. Для видео на wait полагаться бессмысленно — там webhook или опрос. В v1 синхронный режим был отдельным эндпоинтом /v1/media/generate и работал ровно с одной моделью.
Ошибки: код для ветвления, title для точности
При успехе HTTP-статус 200 и code: 0, при ошибке — осмысленный статус и машиночитаемый code в теле. Каталог кодов расширился: 20 — модель не найдена, 22 — задача не найдена, 23 — конфликт идемпотентности, 25 — входной файл отклонён, 26 — промпт отклонён, 27 — комбинация параметров не поддерживается, 28 — временный сбой, повтор осмыслен. Коды, общие с v1 (3, 5, 99, 1001), сохранили значения — переписывать существующие обработчики под новую нумерацию не нужно.
Раньше причина отказа почти всегда приходила как code: 1: в каталоге было пять причин из четырёх с лишним десятков, остальные схлопывались во внутреннюю ошибку. Теперь у отказа есть код, а рядом — error.title, точный машиночитаемый идентификатор причины: ERR_IMAGE_TOO_LARGE и ERR_UNSUPPORTED_FILE_FORMAT оба дают code: 25, но различаются в title. Новые причины добавляются в title без изменения таблицы кодов, так что ветвиться в проде надёжнее по нему. Внутренние ошибки несут trace_id — с ним поддержка находит запрос сразу.
Если код ветвится по error.code при status: failed, обработчики стоит перечитать: часть отказов, раньше приходивших как 1, теперь приходит с 25–28, 1001 или 1002.
Что поменять в коде
| Было в v1 | Стало в v2 |
|---|---|
POST /v1/media/create | POST /api/v2/media/tasks |
GET /v1/media/result?task_id=… | GET /api/v2/media/tasks/:id |
GET /v1/media/models | GET /api/v2/media/models |
POST /v1/media/generate (одна модель) | wait: true в теле создания |
| — | POST /api/v2/media/price |
ar: "16:9" | params.aspect_ratio (имя — из схемы модели) |
images: ["https://…"] | inputs: { "<role>": ["https://…"] } |
callback_url | webhook_url |
url в ответе | result[].url |
code/message в корне задачи | error.code, error.title, error.message |
modelration в каталоге | moderation |
Отдельная поправка на строгость. Тело запроса в v2 валидируется строго: неизвестное поле верхнего уровня отклоняется с code: 3, а его имя перечислено в details. Это ровно тот случай, когда idempotencyKey вместо idempotency_key молча снимал защиту от двойного списания, а забытое поле ar из v1 подменяло соотношение сторон дефолтом — и всё это отвечало 200 OK. Внутри params правило мягче: неизвестный модели ключ не роняет запрос, а возвращается в ignored_params с причиной. Ошибкой остаётся недопустимое значение принимаемого параметра — duration: 999 при пределе 10 даст 400, а не тихую подстановку.
Что важно помнить в проде
- Ссылки на результат живут 48 часов. Нужен файл дольше — забирай к себе сразу после
done. - Webhook доставляется «хотя бы один раз». При неудаче мы повторим примерно через 5, 20, 60 и 180 секунд, так что обработчик должен быть идемпотентным по
idзадачи. Подпись лежит в заголовкеX-Gptunnel-Signatureв форматеt=<unix>,v1=<hex>: этоHMAC-SHA256от строки"<t>.<сырое тело>"с API-ключом в качестве секрета. Адрес должен быть публичным HTTPS. - Файлы можно слать инлайном.
inputsпринимает не только публичные ссылки, но иdata:-URL — до 20 МБ на файл и 50 МБ на всё тело. Битый файл отклоняется до создания задачи и до списания, сcode: 25. 28— единственный код, который стоит повторять автоматически. Остальные 4xx повторять бессмысленно: ответ не изменится.- Снятые модели не ломают интеграцию. У модели с объявленным снятием в каталоге есть
deprecated_atиdeprecation_redirect_to. После даты задача автоматически уходит на аналог, списание идёт по запрошенной модели, о подмене говорят заголовкиDeprecation,Sunset,x-model-requested,x-model-served. Аналога нет — создание вернёт410сcode: 21.
Мигрировать можно по частям: v1 и v2 работают параллельно на одном ключе, так что разумно перевести один сценарий, посмотреть на метрики и продолжить.
Полная спецификация — в документации: О CreativeLab API, Список моделей, Создание задачи, Расчёт цены, Webhooks и Справочник ошибок.
