Асинхронные задачи
Как отправлять задачи изображений и видео, опрашивать их или получать результат вебхуком, скачивать результат, что означает каждый статус и поле и как устроены возвраты.
Какие эндпоинты асинхронные
Генерация изображений и видео выполняется асинхронными задачами: вызов сразу возвращает id задачи, а сама работа идёт в фоне. Чат синхронный и задач не использует.
| Метод | Путь | Что делает |
|---|---|---|
| POST | /v1/images/generations | Изображение из текста. Сразу возвращает id задачи (HTTP 202). |
| POST | /v1/images/edits | Изображение из изображения. То же самое делает image в предыдущем вызове. |
| GET | /v1/images/jobs/{id} | Статус и результат задачи изображения. |
| POST | /v1/videos/generations | Создание видео. Сразу возвращает id задачи (HTTP 202). |
| GET | /v1/videos/jobs/{id} | Статус и результат видеозадачи. |
Как это работает
- Отправка: вызовите эндпоинт генерации — сразу придёт HTTP 202 с
idзадачи (видаimg_...; у видеозадач тот же префикс). - Ожидание: опрашивайте эндпоинт задачи по этому id или добавьте при отправке
callback_url— тогда результат придёт к вам сам. - Готово:
statusстановитсяsucceeded, ссылка на результат — вdata[0].url; если статус сталfailed, причина указана вerror, а зарезервированные под задачу кредиты уже вернулись на баланс. - Скачивание: ссылки на результаты хранятся 60 дней; всё, что нужно сохранить, скопируйте в своё хранилище.
curl https://nezhagate.com/v1/videos/generations \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "seedance-2.0", "prompt": "a paper boat drifting down a rainy street", "duration": 5, "size": "16:9"}'
# HTTP 202
{"id": "img_9c1e...7a", "object": "video.generation.job", "status": "queued", "created": 1791199400, "model": "seedance-2.0"}curl https://nezhagate.com/v1/videos/jobs/img_9c1e...7a -H "Authorization: Bearer YOUR_API_KEY"
Статусы задачи
| Статус | Что значит |
|---|---|
queued | В очереди. В ответе есть queue_position (сколько задач впереди) и eta_seconds (ожидаемое время ожидания). |
processing | Генерируется. |
succeeded | Готово. Результат в data[0].url, детали оплаты в usage. |
failed | Ошибка. Причина в error; зарезервированные кредиты возвращаются полностью. |
succeeded и failed — финальные статусы: после них задача больше не меняется.
Поля ответа
| Поле | Что значит |
|---|---|
id | ID задачи: по нему опрашивают задачу, он же приходит в вебхуках. |
object | image.generation.job для изображений, video.generation.job для видео. |
status | queued / processing / succeeded / failed. |
created | Время отправки задачи в секундах Unix. |
model | ID модели, указанный при отправке. |
queue_position | Только в статусе queued: сколько задач впереди вашей. |
eta_seconds | Только в статусе queued: примерно через сколько секунд задача начнётся. |
data[].url | Только в статусе succeeded: ссылка на результат (изображение или mp4), хранится 60 дней. |
usage | Только в статусе succeeded: что оплачено. Для изображений — images (сколько изображений выдано) и resolution, для видео — clips. |
error | Только в статусе failed: message, type, code и param; ориентируйтесь на code. |
{
"id": "img_9c1e...7a",
"object": "video.generation.job",
"status": "succeeded",
"created": 1791199400,
"model": "seedance-2.0",
"data": [{"url": "https://img.nezhagate.com/i/5d41402abc....mp4"}],
"usage": {"clips": 1, "model": "seedance-2.0"}
}{
"id": "img_9c1e...7a",
"object": "video.generation.job",
"status": "failed",
"created": 1791199400,
"model": "seedance-2.0",
"error": {"message": "...", "type": "invalid_request_error", "code": "content_policy", "param": null}
}Советы по опросу
- Опрашивайте изображения каждые 2–3 секунды, видео — каждые 5–10 секунд. Опрос бесплатный.
- Прекращайте опрос, как только увидите
succeededилиfailed. - Задайте в программе общий лимит ожидания: изображения обычно готовы за время от нескольких десятков секунд до нескольких минут, видео — от одной-двух минут до часа с лишним (длинные ролики и очередь увеличивают время); типичное время указано в документации каждой модели. Когда лимит истёк, не отправляйте задачу заново: она никуда не делась, продолжите опрос позже.
- Задача принадлежит аккаунту, который её отправил: прочитать её может любой ключ этого аккаунта, а любой другой аккаунт получит 404
job_not_found. Записи о задачах хранятся 3 дня; после этого по id задачи тоже вернётся 404, а ссылка на результат продолжит работать до конца своих 60 дней. - Если отправка оборвалась по таймауту на вашей стороне и id задачи вы не получили, перед повторной отправкой проверьте в журнале запросов в консоли, создана ли задача, — так вы не заплатите дважды.
Вебхуки вместо опроса
Добавьте при отправке callback_url — как только задача завершится, успешно или с ошибкой, шлюз отправит на этот URL ровно то же, что возвращает эндпоинт опроса; запрос подписан, а при неудачной доставке повторяется. Общий адрес для всего аккаунта можно также задать в консоли, в разделе «Настройки → Вебхуки».
{"model": "seedance-2.0", "prompt": "...", "duration": 5, "callback_url": "https://example.com/webhooks/nezhagate"}Оплата
- При отправке цена задачи резервируется на балансе; если средств не хватает, вернётся 402, и задача не будет создана.
- При успехе списывается то, что выдано: за изображения — по числу выданных картинок, за видео — по заказанной длительности или ролику (столько же, сколько было зарезервировано); всё, что зарезервировано сверх этого, возвращается автоматически. При неудаче резерв возвращается полностью.
- Опрос и доставка вебхуков бесплатны.
Сколько хранятся результаты
data[0].url ведёт в наше хранилище; через 60 дней файл удаляется автоматически. Чтобы пользоваться результатом дольше, скачайте его в своё хранилище или CDN.
Почему задача завершается ошибкой
За неудачную задачу деньги всегда возвращаются полностью. Вот коды, которые чаще всего встречаются у неудачных задач:
| code | Когда возникает | Что делать |
|---|---|---|
content_policy | Видео: промпт, референс или сгенерированный видеоряд либо звук не прошли проверку контента (если отклонён звук, param равен audio). | Перепишите промпт или замените референс и только потом отправляйте снова: те же входные данные будут отклонены повторно. |
moderation_blocked | Изображение: система безопасности поставщика модели отклонила промпт или референсное изображение (код может быть и content_policy_violation). | Перепишите промпт или замените референс; повторная отправка без изменений обычно снова отклоняется. |
render_failed | Видео: на этот раз ролик не получился; обычно это разовый сбой. | Повторная отправка того же запроса обычно проходит успешно. |
invalid_material | Видео: поставщик обнаружил проблему во входных данных. Это общая ошибка, и часто она разовая. | Сначала отправьте задачу ещё раз без изменений; если ошибка повторяется, проверьте референсы и длину промпта. |
render_timeout | Видео: очередь или рендер заняли слишком много времени, и поставщик отказался от задачи. | Отправьте задачу заново. |
wait_timeout | Видео: ролик не пришёл за максимальное время ожидания, которое допускает линия. | Отправьте задачу заново. |
upstream_unstable | Видео: во время рендера оборвалось соединение. | Отправьте задачу заново. |
result_fetch_failed | Видео создано, но на нашей стороне его так и не удалось скачать или сохранить, несмотря на повторные попытки. | Отправьте задачу заново. |
invalid_request | Видео: линия не принимает такое сочетание параметров (например, длительности, пропорции и разрешения). | Измените параметры согласно message и отправьте снова. |
upstream_error | Любая другая ошибка: перепробованы все линии, задача ждала 15 минут и так и не запустилась, поставщик отказал без объяснения причины и т. п.; что именно произошло, указано в message. | Обычно повторная отправка проходит успешно. |
Полный пример
Отправка, опрос и скачивание в одном скрипте, с общим лимитом ожидания и обработкой ошибок. Замените YOUR_API_KEY и запускайте.
import time
import requests
BASE = "https://nezhagate.com/v1"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}
def run_job(kind, payload, every=5, give_up=5400):
"""kind is "images" or "videos". Returns the finished job (succeeded or failed)."""
job = requests.post(f"{BASE}/{kind}/generations", headers=HEADERS, json=payload, timeout=120)
job.raise_for_status() # 4xx here: fix the request; nothing was billed
job_id = job.json()["id"]
deadline = time.time() + give_up
while time.time() < deadline:
time.sleep(every)
r = requests.get(f"{BASE}/{kind}/jobs/{job_id}", headers=HEADERS, timeout=30).json()
if r["status"] in ("succeeded", "failed"):
return r
raise TimeoutError(f"{job_id} still running; poll it again later, it is not lost")
job = run_job("videos", {"model": "seedance-2.0", "prompt": "a paper boat drifting down a rainy street",
"duration": 5, "size": "16:9"})
if job["status"] == "succeeded":
url = job["data"][0]["url"]
with open("clip.mp4", "wb") as f: # keep your own copy: links expire after 60 days
f.write(requests.get(url, timeout=300).content)
else:
print(job["error"]["code"], job["error"]["message"])