# Асинхронные задачи

Как отправлять задачи изображений и видео, опрашивать их или получать результат вебхуком, скачивать результат, что означает каждый статус и поле и как устроены возвраты.

> https://nezhagate.com/ru/docs/guide/async-jobs

## Какие эндпоинты асинхронные

Генерация изображений и видео выполняется асинхронными задачами: вызов сразу возвращает 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}` | Статус и результат видеозадачи. |

## Как это работает

1. Отправка: вызовите эндпоинт генерации — сразу придёт HTTP 202 с `id` задачи (вида `img_...`; у видеозадач тот же префикс).

2. Ожидание: опрашивайте эндпоинт задачи по этому id или добавьте при отправке `callback_url` — тогда результат придёт к вам сам.

3. Готово: `status` становится `succeeded`, ссылка на результат — в `data[0].url`; если статус стал `failed`, причина указана в `error`, а зарезервированные под задачу кредиты уже вернулись на баланс.

4. Скачивание: ссылки на результаты хранятся 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"}
```

[Документация по вебхукам →](https://nezhagate.com/ru/docs/webhooks)

## Оплата

- При отправке цена задачи резервируется на балансе; если средств не хватает, вернётся 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`. | Обычно повторная отправка проходит успешно. |

[Полный список кодов ошибок →](https://nezhagate.com/ru/docs/guide/errors)

## Полный пример

Отправка, опрос и скачивание в одном скрипте, с общим лимитом ожидания и обработкой ошибок. Замените 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"])
```
