Статусы и результаты

Получите статус задачи через polling или callback и сохраните видео.

Чтение статуса

Читайте сохранённый ID через GET /v1/videos/{id} с тем же API-ключом:

bash
umask 077
touch result.json && chmod 600 result.json || exit 1
curl --fail --silent --show-error --max-time 60 "$BASE_URL/v1/videos/$TASK_ID" -H "Authorization: Bearer $API_KEY" -o result.json
jq '{id, model, status, error}' result.json
Статус задачиДействие
queuedЗадача принята; через 10–15 секунд снова прочитайте тот же ID
in_progressГенерация выполняется; продолжайте GET по тому же ID
completedСкачайте результат; content.video_url необязателен
failedОстановитесь и сохраните ID; поле error может отсутствовать
unknown или другое значениеОстановите автоматическую обработку, сохраните ID и обратитесь в поддержку

HTTP 200 означает успешное чтение, а не готовое видео. Локальный предел ожидания 45 минут не отменяет задачу; продолжайте GET по тому же ID. Поля object: "video", model, status, progress и created_at относятся к публичной задаче. completed_at появляется после успеха. Отсутствие error при failed не означает успех.

Ответ создания содержит ID и начальный статус. Пример:

json
{
  "id": "task_example",
  "object": "video",
  "model": "seedance-2.5",
  "status": "queued",
  "progress": 0,
  "created_at": 1790000000
}

Пример задачи в работе (progress иллюстративный, не прогноз времени):

json
{
  "id": "task_example",
  "object": "video",
  "model": "seedance-2.5",
  "status": "in_progress",
  "progress": 50,
  "created_at": 1790000000
}

Пример завершения с ошибкой, без обязательного error:

json
{
  "id": "task_example",
  "object": "video",
  "model": "seedance-2.5",
  "status": "failed",
  "progress": 100,
  "created_at": 1790000000
}

Пример успешной задачи без подписанных ссылок:

json
{
  "id": "task_example",
  "object": "video",
  "model": "seedance-2.5",
  "status": "completed",
  "progress": 100,
  "created_at": 1790000000,
  "completed_at": 1790000120
}

Пример успешной задачи с дополнительными ссылками:

json
{
  "id": "task_example",
  "object": "video",
  "model": "seedance-2.5",
  "status": "completed",
  "progress": 100,
  "created_at": 1790000000,
  "completed_at": 1790000120,
  "content": {
    "video_url": "RESULT_URL",
    "last_frame_url": "RESULT_URL"
  }
}

Пример неизвестного статуса:

json
{
  "id": "task_example",
  "object": "video",
  "model": "seedance-2.5",
  "status": "unknown",
  "progress": 0,
  "created_at": 1790000000
}

Видео доступно для скачивания в течение 24 часов после подготовки файла. Повторное чтение задачи или доставка callback не продлевают этот срок. Сохраните видео в своём хранилище до его истечения. Подписанные media URLs дают временный доступ к файлу: не записывайте ссылки в журнал и не публикуйте их.

Итоговый callback

Укажите публичный HTTPS callback_url в теле создания, чтобы запросить итоговое уведомление. Доставка зависит от подключения; чтение задачи остаётся способом восстановления. Тело POST callback — прямой JSON с id, model, status (succeeded или failed), временем, параметрами и подписанным content при успехе либо нейтральной error при ошибке. Callback succeeded соответствует GET completed; это разные формы ответа. Нет обёртки event/data и HMAC-подписи. Заголовок X-UnlimGPT-Event-ID помогает распознавать дубликаты, но не удостоверяет отправителя. Не ожидайте уведомления для каждого промежуточного статуса или фиксированного числа попыток доставки.

Сохраните уведомление и быстро ответьте HTTP 200/204. Сопоставляйте сохранённый ID, обрабатывайте дубликаты идемпотентно и проверяйте итоговый статус авторизованным GET до действий. При пропущенном callback периодически читайте задачу. Входящий POST без аутентификации сам по себе не доказывает успешность заказа.

Пример POST-уведомления об успехе:

json
{
  "id": "task_example",
  "model": "seedance-2.5",
  "status": "succeeded",
  "created_at": 1790000000,
  "updated_at": 1790000120,
  "resolution": "720p",
  "generate_audio": false,
  "duration": 5,
  "content": {
    "video_url": "RESULT_URL",
    "last_frame_url": "RESULT_URL"
  }
}

Пример уведомления об ошибке:

json
{
  "id": "task_example",
  "model": "seedance-2.5",
  "status": "failed",
  "created_at": 1790000000,
  "updated_at": 1790000120,
  "resolution": "720p",
  "generate_audio": false,
  "error": {
    "code": "model_unavailable",
    "message": "model unavailable"
  }
}

Ответ обработчика после сохранения уведомления:

http
HTTP/1.1 204 No Content

Если обработчик недоступен или не подтвердил доставку, уведомление может прийти повторно; количество попыток не используйте в логике приложения. Возвращайте успех после надёжного сохранения события, а проверку GET и скачивание выполняйте отдельно. Не используйте ссылки из непроверенного callback.

В этом руководстве нет поддерживаемых операций отмены или удаления задачи. Сохраните ID при создании; прекращение polling не отменяет заказ.

Скачивание видео

После GET completed используйте GET /v1/videos/{id}/content с API Bearer. Ответ может содержать файл или HTTP 307 с подписанной ссылкой в Location. Следующий GET по этой ссылке выполняйте без API Bearer. Если GET задачи уже вернул content.video_url, можно сразу скачать точный HTTPS URL без API-ключа. Не переходите автоматически по redirect; отклоняйте не-HTTPS URLs и встроенные credentials.

Пример получения redirect (без -L):

bash
umask 077
touch download-headers.txt && chmod 600 download-headers.txt || exit 1
curl --silent --show-error --max-time 60 \
  "$BASE_URL/v1/videos/$TASK_ID/content" \
  -H "Authorization: Bearer $API_KEY" \
  -D download-headers.txt -o seedance.download

Пример ответа:

http
HTTP/1.1 307 Temporary Redirect
Location: RESULT_URL

Видео по умолчанию MP4; для output_format: "mov" сохраняйте .mov.

Примеры прямого скачивания подписанного URL:

bash
VIDEO_URL=$(jq -er 'select(.status=="completed") | .content.video_url | select(type=="string" and startswith("https://"))' result.json)
case "$VIDEO_URL" in https://*@*)
  echo 'Embedded credentials rejected' >&2
  exit 1
  ;;
esac
HTTP_STATUS=$(curl --fail --silent --show-error --max-time 300 \
  --write-out '%{http_code}' "$VIDEO_URL" -o seedance.download)
case "$HTTP_STATUS" in
  2??) mv seedance.download seedance.mp4 ;;
  *)
    rm -f seedance.download
    exit 1
    ;;
esac

Некоторые авторизованные API-маршруты /content могут вернуть redirect в хранилище. При их использовании не пересылайте API-ключ на перенаправленный адрес; пример выше обращается напрямую к возвращённой подписанной ссылке. Если подписанная ссылка вернула 403, прочитайте существующую задачу для получения текущей ссылки в пределах исходного срока хранения медиа. Это может обновить подпись, но не продлевает срок жизни файла. Если исходный срок истёк или ссылка всё ещё недоступна, используйте сохранённую копию или обратитесь в поддержку с ID. Не создавайте новую генерацию.

Последний кадр результата

При создании укажите return_last_frame: true. После успеха необязательное поле content.last_frame_url в GET и итоговом callback содержит подписанную ссылку на JPEG. Кадр имеет те же размеры, что и видео, и не содержит видимой метки watermark. Скачивайте его отдельно по точному HTTPS URL без API Bearer. Это выходное изображение, а не входная роль last_frame.

bash
umask 077
FRAME_URL=$(jq -er 'select(.status=="completed") | .content.last_frame_url | select(type=="string" and startswith("https://"))' result.json) || exit 1
case "$FRAME_URL" in https://*@*)
  echo 'Embedded credentials rejected' >&2
  exit 1
  ;;
esac
DOWNLOAD_RESULT=$(curl -q --fail --silent --show-error --max-time 300 \
  --write-out '%{http_code} %{content_type}' "$FRAME_URL" -o seedance-last-frame.download) || exit 1
case "$DOWNLOAD_RESULT" in
  '200 image/jpeg' | '200 image/jpeg;'*) mv seedance-last-frame.download seedance-last-frame.jpg ;;
  *)
    rm -f seedance-last-frame.download
    exit 1
    ;;
esac

Скрипт использует сохранённый ответ GET completed и не следует redirect. Если ссылки нет или она истекла, снова прочитайте ту же задачу; новый POST для скачивания кадра не нужен. Обновление подписанной ссылки не продлевает срок хранения файла.

HTTP-ошибки

Ошибка HTTP отличается от status: "failed" в успешно прочитанной задаче. Ошибки создания используют OpenAI-совместимый объект error. Чтение задачи и скачивание также могут вернуть плоский ответ code/message/data. Не привязывайте обработку к точному тексту и не требуйте type и code внутри error; сохраняйте HTTP-статус и request ID.

Шаг / HTTPЧто делать
POST 400Исправьте JSON, поля, диапазоны и роли медиа перед новым заказом
POST/GET 401 или 403Проверьте ключ, доступ к модели, баланс и лимит ключа
GET задачи 400 или 404Проверьте сохранённый ID, origin и аккаунт; не создавайте видео повторно
GET 429 или 5xxПодождите, учтите Retry-After, если он есть, и повторите тот же GET
POST timeout, 5xx или ответ без IDИсход неизвестен: проверьте кабинет или поддержку до другого POST
/content 400Результат ещё не готов: снова прочитайте статус задачи
/content 404Проверьте ID и доступность запрошенного файла; сохраните ID
/content 410Файл больше недоступен; используйте сохранённую копию или поддержку
/content 416Диапазон за пределами файла; проверьте размер и возобновляйте корректным Range
/content 502, 503 или 504Повторите скачивание того же результата позже; новый POST не нужен
Подписанный URL 403Обновите ссылку через GET исходной задачи; срок хранения файла не продлевается

Пример формы ошибки запроса POST 400 (текст иллюстративный и может содержать ID запроса):

json
{
  "error": {
    "message": "Invalid request field",
    "type": "new_api_error",
    "code": ""
  }
}

Пример /content 404:

json
{
  "error": {
    "message": "Task not found",
    "type": "invalid_request_error"
  }
}

Пример формы отказа авторизации, HTTP 401 (текст зависит от языка ответа):

json
{
  "error": {
    "message": "Invalid token",
    "type": "new_api_error",
    "code": ""
  }
}

Пример ограничения частоты с необязательным временем ожидания:

http
HTTP/1.1 429 Too Many Requests
Retry-After: 30

Сохраните ID и подождите 30 секунд перед следующим GET. Если заголовка нет, используйте задержку с постепенным увеличением и случайным разбросом. Примеры интеграции останавливаются при окончательной ошибке и сохраняют ID.

Проверка файла и возобновление

HEAD /v1/videos/{id}/content позволяет запросить заголовки без тела. Для частичного скачивания используйте GET с Range; при прямой выдаче диапазона ответ — 206 Partial Content и Content-Range. При 307 отправьте тот же Range отдельным запросом на подписанный URL без API Bearer. Если сервер ответил 200, это полный файл: не дописывайте его к уже сохранённой части.

bash
umask 077
touch range-headers.txt && chmod 600 range-headers.txt || exit 1
curl --silent --show-error --max-time 60 --head \
  "$BASE_URL/v1/videos/$TASK_ID/content" \
  -H "Authorization: Bearer $API_KEY"
curl --silent --show-error --max-time 300 \
  "$BASE_URL/v1/videos/$TASK_ID/content" \
  -H "Authorization: Bearer $API_KEY" -H 'Range: bytes=0-1023' \
  -D range-headers.txt -o seedance.part