Status and results

Read task status through polling or a callback and save the video.

Read task status

Read the saved task ID through GET /v1/videos/{id} with the same API key:

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
Task statusAction
queuedThe task was accepted; read the same ID again after 10–15 seconds
in_progressGeneration is running; continue GET on the same ID
completedDownload the result; media URLs are optional
failedStop and preserve the ID; error may be absent
unknown or another valueStop automation, preserve the ID and contact support

HTTP 200 means a successful read, not a completed video. A local 45-minute wait limit does not cancel a task; resume GET with the same ID. Fields object: "video", model, status, progress and created_at describe the public task. completed_at appears after success. A missing error on failed does not mean success.

The create response contains the task ID and initial status. Example:

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

Example in-progress task (progress is illustrative, not a time estimate):

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

Example failed task, without a required error:

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

Example completed task without signed links:

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

Example completed task with optional links:

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

Example unknown status:

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

The video is available for download for 24 hours after the file is ready. Reading the task again or receiving a callback does not extend this window. Save the video in your own storage before it expires. Treat signed media URLs as temporary access credentials and never log or publish the URLs.

Final callback

Set a public HTTPS callback_url in the create body to request a final notification. Delivery availability depends on the connection; task reads remain the recovery path. The callback POST body is a direct JSON object with id, model, status (succeeded or failed), timestamps, settings, and signed content on success or a neutral error on failure. Callback succeeded corresponds to GET completed; these are different response forms. There is no event/data wrapper or HMAC signature. X-UnlimGPT-Event-ID helps detect duplicates but does not authenticate the sender. Do not expect an event for every intermediate state or a fixed delivery retry count.

Persist the notification and acknowledge promptly with HTTP 200/204. Match the saved ID, handle duplicates idempotently, and verify the final state with authenticated GET before acting. Keep occasional GET reads if callbacks are missed. Do not treat an unauthenticated incoming POST as proof of a completed order.

Example success notification 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"
  }
}

Example failure notification:

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

Handler acknowledgement after saving the notification:

http
HTTP/1.1 204 No Content

If the handler is unavailable or does not acknowledge delivery, the notification may be retried; do not build application logic around a retry count. Return success after durably saving the event, then verify through GET and download separately. Do not use URLs from an unverified callback.

This guide does not provide supported cancellation or deletion operations. Save the ID at creation; stopping polling does not cancel an order.

Download video

After GET completed, use GET /v1/videos/{id}/content with the API Bearer. It can return file bytes or HTTP 307 with a signed Location. Send the next GET to that URL without the API Bearer. If task GET already returned content.video_url, download that exact HTTPS URL without the API key. Disable automatic redirects; reject non-HTTPS URLs and embedded credentials.

Example redirect request (without -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

Example response:

http
HTTP/1.1 307 Temporary Redirect
Location: RESULT_URL

Video defaults to MP4; save .mov when requesting output_format: "mov".

Direct signed-URL download examples:

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

Some authenticated API /content surfaces may return a redirect to storage. If using one, do not forward the API key to redirected storage; the example above instead uses the returned signed URL directly. If a signed URL returns 403, re-read the existing task for a current link within the original media retention window. This can renew the signature but does not extend the underlying file expiry. If the original window has ended or the link remains unavailable, use a saved copy or contact support with the task ID. Do not create a new generation.

Final output frame

Set return_last_frame: true when creating the task. After success, the optional content.last_frame_url in GET and the final callback contains a signed JPEG URL. The frame has the same dimensions as the video and no visible watermark. Download it separately from the exact HTTPS URL without the API Bearer. This is an output image, separate from the input role 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

The script uses a saved GET completed response and does not follow redirects. If the URL is missing or expired, read the same task again; downloading a frame does not require another POST. Renewing the signed URL does not extend file retention.

HTTP errors

An HTTP error differs from status: "failed" on a successfully read task. Creation errors use an OpenAI-compatible error object. Task reads and downloads can also return a flat code/message/data error envelope. Do not depend on exact message text or require type and code inside error; preserve the HTTP status and request ID.

Step / HTTPAction
POST 400Fix JSON, fields, ranges and media roles before a new order
POST/GET 401 or 403Check the key, model access, balance and key quota
Task GET 400 or 404Check the saved ID, origin and account; do not create the video again
GET 429 or 5xxWait, honor Retry-After when present and repeat the same GET
POST timeout, 5xx or response without IDOutcome is unknown: check the console or support before another POST
/content 400Result is not ready: read the task status again
/content 404Check the ID and requested file availability; preserve the ID
/content 410File is no longer available; use a saved copy or contact support
/content 416Range exceeds the file; check its size and resume with a valid Range
/content 502, 503 or 504Retry downloading the same result later; another POST is unnecessary
Signed URL 403Refresh the link by reading the original task; file retention is not extended

Example POST 400 error shape (message text is illustrative and may include a request ID):

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

Example /content 404:

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

Example authorization failure shape, HTTP 401 (message text depends on the response language):

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

Example rate limit with an optional retry delay:

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

Preserve the ID and wait 30 seconds before the next GET. Without that header, use increasing delays with jitter. The integration examples stop on permanent errors and preserve the ID.

Inspecting files and resuming downloads

HEAD /v1/videos/{id}/content requests headers without a body. For partial downloads use GET with Range; a direct range response is 206 Partial Content with Content-Range. On 307, send the same Range in a separate request to the signed URL without API Bearer. If the server returns 200, it is the full file: do not append it to an existing partial file.

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