Status and results
Read task status through polling or a callback and save the video.
Read task status through polling or a callback and save the video.
Read the saved task ID through GET /v1/videos/{id} with the same API key:
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 status | Action |
|---|---|
queued | The task was accepted; read the same ID again after 10–15 seconds |
in_progress | Generation is running; continue GET on the same ID |
completed | Download the result; media URLs are optional |
failed | Stop and preserve the ID; error may be absent |
unknown or another value | Stop 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:
{
"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):
{
"id": "task_example",
"object": "video",
"model": "seedance-2.5",
"status": "in_progress",
"progress": 50,
"created_at": 1790000000
}
Example failed task, without a required error:
{
"id": "task_example",
"object": "video",
"model": "seedance-2.5",
"status": "failed",
"progress": 100,
"created_at": 1790000000
}
Example completed task without signed links:
{
"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:
{
"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:
{
"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.
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:
{
"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:
{
"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/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.
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):
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/1.1 307 Temporary Redirect
Location: RESULT_URL
Video defaults to MP4; save .mov when requesting output_format: "mov".
Direct signed-URL download examples:
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.
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.
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.
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 / HTTP | Action |
|---|---|
POST 400 | Fix JSON, fields, ranges and media roles before a new order |
POST/GET 401 or 403 | Check the key, model access, balance and key quota |
Task GET 400 or 404 | Check the saved ID, origin and account; do not create the video again |
GET 429 or 5xx | Wait, honor Retry-After when present and repeat the same GET |
POST timeout, 5xx or response without ID | Outcome is unknown: check the console or support before another POST |
/content 400 | Result is not ready: read the task status again |
/content 404 | Check the ID and requested file availability; preserve the ID |
/content 410 | File is no longer available; use a saved copy or contact support |
/content 416 | Range exceeds the file; check its size and resume with a valid Range |
/content 502, 503 or 504 | Retry downloading the same result later; another POST is unnecessary |
Signed URL 403 | Refresh 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):
{
"error": {
"message": "Invalid request field",
"type": "new_api_error",
"code": ""
}
}
Example /content 404:
{
"error": {
"message": "Task not found",
"type": "invalid_request_error"
}
}
Example authorization failure shape, HTTP 401 (message text depends on the response language):
{
"error": {
"message": "Invalid token",
"type": "new_api_error",
"code": ""
}
}
Example rate limit with an optional retry delay:
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.
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.
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