Seedance
Seedance is ByteDance’s video generation family, served by Volcengine Ark channels. RouteAPI converts your request into Ark’s /api/v3/contents/generations/tasks call, so every task goes through the same submit, poll, and download flow as the other video models — see Create a task and Get a task. This page covers what the Seedance adaptor reads, the per-model limits, how to read a Seedance task record, and how the charge is calculated.
Video generation is asynchronous: submit, keep the task_id, poll every 10–15 seconds, then download when the status is SUCCESS. A 4-second 480p clip usually takes one to five minutes; higher resolution and longer duration take longer.
Models
Section titled “Models”| Model ID | Ark model | Resolution | Duration | Notes |
|---|---|---|---|---|
seedance-2.5 | doubao-seedance-2-5-260628 | 480p / 720p / 1080p | 4–30 s | Draft preview, reference/edit/extend modes, mov output |
seedance-2.0 | doubao-seedance-2-0-260128 | 480p / 720p / 1080p / 4k | 4–15 s | Image, video, and audio reference material |
seedance-2.0-fast | doubao-seedance-2-0-fast-260128 | 480p / 720p | 4–15 s | Faster, cheaper 2.0 variant |
seedance-2.0-mini | doubao-seedance-2-0-mini-260615 | 480p / 720p | 4–15 s | Lower-cost 2.0 variant |
Send the model ID from GET /v1/models. A channel that serves these models maps that ID onto the corresponding Ark model name in the second column, which is why the adaptor accepts both naming schemes. Which models your account can use and their prices are set in the RouteAPI console.
The resolution and duration columns are ByteDance’s per-model ranges. RouteAPI does not validate them: the only duration check on our side is the shared rule that a top-level duration / seconds must fall within 0–3600 (see Limits and errors). Everything else is enforced by Ark — a 30-second request on a 2.0 model, an unsupported resolution, or too many reference clips is submitted, then rejected by Ark and surfaced as fail_to_fetch_task with Ark’s status and message. Nor does RouteAPI fill in a missing resolution or duration; Ark applies its own default. The cost estimate uses 1080p and 5 seconds in those cases, so the reserved amount is an estimate, not the Ark default.
Seedance 1.x (doubao-seedance-1-0-*, doubao-seedance-1-5-*) is no longer served. A request that maps to a 1.x name is rejected with 400 and code model_not_supported after model mapping but before anything is reserved or sent upstream.
How the common envelope maps
Section titled “How the common envelope maps”| Envelope field | What the Seedance adaptor does |
|---|---|
prompt | Appended as a trailing text item unless metadata.content already contains one, or content is a 2.5 draft_task. Required in every request |
images / image | One image_url content item per image, with no role. Ignored when metadata.content is present |
ratio | Sent as the Ark ratio |
resolution | Sent as the Ark resolution |
duration | Sent as the Ark duration; a top-level seconds that parses to a positive integer overrides it |
generate_audio | Sent as generate_audio; an explicit false is forwarded |
metadata | Decoded into a typed structure; values win over the top-level equivalents |
size, input_reference, and mode belong to the shared envelope but the Seedance adaptor does not read them: use resolution / ratio, and images or content, instead.
metadata types are validated before submission: a wrong type (content as an object, image_url as a bare string, duration: "abc") returns 400 and nothing is charged. The message starts with unmarshal metadata failed. Numeric and boolean values inside metadata also accept their string forms ("duration": "6", "watermark": "false"); the top-level scalar fields must use their JSON types.
Top-level parameters
Section titled “Top-level parameters”These are read from the top level of the body, so the same shape you send to other providers works without wrapping them in metadata:
| Field | Type | Description |
|---|---|---|
ratio | string | Aspect ratio: 16:9, 4:3, 1:1, 3:4, 9:16, 21:9, or adaptive |
resolution | string | 480p, 720p, 1080p, or 4k, model permitting |
duration | integer | Duration in seconds. 0 means “not set” and is not sent, so use metadata if you need to forward an explicit zero |
generate_audio | boolean | Generate a soundtrack; false is forwarded explicitly |
seconds | string | Legacy form of the duration; when it parses to a positive integer it overrides both duration and metadata.duration. A non-numeric value is ignored |
metadata parameters
Section titled “metadata parameters”| Field | Type | Description |
|---|---|---|
content | array | Input material; replaces images. See metadata.content |
ratio, resolution, duration, generate_audio | same as top level | Take precedence over the top-level value. A positive seconds still wins over the final duration |
seed | integer | Random seed; 0 is forwarded as an explicit value |
frames | integer | Frame count. When set it replaces the duration in the cost estimate; Ark uses it instead of the duration |
camera_fixed | boolean | Keep the camera fixed |
watermark | boolean | Provider-side watermark on the output. ByteDance documents false as the default |
return_last_frame | boolean | Also return the final frame as an image |
service_tier | string | Provider service tier, for example default. Passed through unchanged |
execution_expires_after | integer | Task expiry in seconds. ByteDance documents 3600–259200 with a default of 172800; Ark enforces it |
priority | integer | Provider-side queue priority, 0–9, higher runs first. Ark only orders tasks within your account |
safety_identifier | string | Your end-user identifier, up to 64 characters. Forwarded to Ark, which enforces the length |
draft | boolean | Seedance 2.5 draft preview — see Draft preview |
output_format | string | Output container, forwarded as is. Seedance 2.5 accepts mp4 (default) or mov |
omni_reference_task_type | string | Seedance 2.5 reference mode, forwarded as is: auto, reference, edit, or extend |
callback_url is not forwarded and no callback is sent — poll for the result. tools is dropped as well; upstream web search is not available through this API. Unrecognized keys, including these, do not error; they are ignored. metadata.model is not read: the body’s model is always the channel’s upstream model name.
metadata.content
Section titled “metadata.content”content is an ordered array of input items. Each item has a type, the matching payload field, and an optional role:
type | Payload | role values |
|---|---|---|
text | text | — |
image_url | image_url.url | first_frame, last_frame, reference_image |
video_url | video_url.url | reference_video |
audio_url | audio_url.url | reference_audio |
draft_task | draft_task.id | — |
RouteAPI validates the item shape (a wrong payload type is a 400) but not the role name; an unknown role is forwarded and Ark decides whether to reject it. URLs may be public http(s) links, data: URIs for images and audio, or asset://<asset_id> references — see Assets for uploading material and for real-person images, which must come from a verified asset group.
Rules that come from the conversion:
- If
contentis present it is used as is; top-levelimagesare ignored rather than merged. An emptycontentarray counts as absent, so top-levelimagesstill apply. - If
contenthas notextitem, the top-levelpromptis appended as a trailing text item. The one exception is a 2.5 request whosecontentis adraft_task: no prompt is appended, butpromptmust still be present in the body. - Refer to material by ordinal in the prompt (“image 1”, “video 1”), counted per type in
contentorder. - First-frame, first/last-frame, and multi-reference material are three different modes. Do not mix them unless you have checked the model accepts the combination.
The reference limits below are Ark’s, not RouteAPI checks: an over-limit request is submitted and rejected upstream.
| Item | Seedance 2.5 | 2.0 series |
|---|---|---|
| Reference images | 1–30 | 1–9 |
| Reference videos | Up to 10 clips, ≤30 s total | Up to 3 clips, ≤15 s total |
| Reference audio | Up to 10 clips, ≤30 s total | Up to 3 clips, ≤15 s total, and must accompany an image or video |
| Single image | Under 30 MB, 300–6000 px per side | Same |
| Single video | mp4 / mov, under 200 MB | Same |
| Single audio | wav / mp3, under 15 MB | Same |
Seedance 2.x rejects reference images and videos that contain a real person’s face. To use a real person’s likeness, complete real-person verification first, then reference the verified material with asset:// — Assets covers the verification session, the verified group, and the request rules.
Text to video
Section titled “Text to video”curl https://api.routeapi.ai/v1/video/generations \ -H "Authorization: Bearer $ROUTEAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "seedance-2.0-fast", "prompt": "A paper boat drifting down a rain-soaked street, cinematic lighting", "ratio": "16:9", "resolution": "720p", "duration": 5, "generate_audio": false }'First frame and first + last frame
Section titled “First frame and first + last frame”A single image in images becomes an untagged image item, which Seedance treats as the first frame:
curl https://api.routeapi.ai/v1/video/generations \ -H "Authorization: Bearer $ROUTEAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "seedance-2.0", "prompt": "The camera slowly pushes in as the person looks up and smiles", "images": ["https://cdn.example.com/first.jpg"], "ratio": "adaptive", "resolution": "720p", "duration": 5 }'For explicit first/last frame control, use content and put the prompt inside it:
curl https://api.routeapi.ai/v1/video/generations \ -H "Authorization: Bearer $ROUTEAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "seedance-2.0", "prompt": "The bud slowly opens into a full bloom", "metadata": { "content": [ { "type": "text", "text": "The bud slowly opens into a full bloom" }, { "type": "image_url", "image_url": { "url": "https://cdn.example.com/bud.jpg" }, "role": "first_frame" }, { "type": "image_url", "image_url": { "url": "https://cdn.example.com/flower.jpg" }, "role": "last_frame" } ], "ratio": "adaptive", "resolution": "720p", "duration": 5 } }'Multimodal references
Section titled “Multimodal references”Reference images, videos, and audio are mixed in one content array using the reference_* roles. Number the material per type, in content order, and refer to it that way in the prompt:
curl https://api.routeapi.ai/v1/video/generations \ -H "Authorization: Bearer $ROUTEAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "seedance-2.0", "prompt": "The person in image 1 dances following the moves in video 1, with audio 1 as background music", "metadata": { "content": [ { "type": "text", "text": "The person in image 1 dances following the moves in video 1, with audio 1 as background music" }, { "type": "image_url", "image_url": { "url": "https://cdn.example.com/person.jpg" }, "role": "reference_image" }, { "type": "video_url", "video_url": { "url": "https://cdn.example.com/dance.mp4" }, "role": "reference_video" }, { "type": "audio_url", "audio_url": { "url": "https://cdn.example.com/bgm.mp3" }, "role": "reference_audio" } ], "ratio": "16:9", "resolution": "720p", "duration": 10 } }'A request that includes a video input is billed at the model’s video-input rate — see Billing. Reference counts, durations, and formats are enforced by Ark; see the limits table under metadata.content.
Vendor-specific modes (Seedance 2.5)
Section titled “Vendor-specific modes (Seedance 2.5)”Seedance 2.5 adds a mode selector and a container choice. Both are forwarded unchanged for every Seedance model, and Ark decides whether it accepts them:
curl https://api.routeapi.ai/v1/video/generations \ -H "Authorization: Bearer $ROUTEAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "seedance-2.5", "prompt": "Continue the scene as the camera pulls back over the harbour", "metadata": { "omni_reference_task_type": "extend", "output_format": "mov", "content": [ { "type": "text", "text": "Continue the scene as the camera pulls back over the harbour" }, { "type": "video_url", "video_url": { "url": "https://cdn.example.com/scene.mp4" }, "role": "reference_video" } ], "ratio": "16:9", "duration": 8 } }'omni_reference_task_type takes auto (default), reference, edit, or extend. Which input combination each mode expects is ByteDance’s contract; RouteAPI only forwards the value.
Draft preview
Section titled “Draft preview”A draft is a low-cost preview. Generating the final video from it is a second, separately billed task.
- Submit a second-generation request with
"metadata": { "draft": true, ... }and wait forSUCCESS. - Read the upstream task ID from the task record:
GET /v1/video/generations/{task_id}→data.data.id. - Submit a new task whose
contentis a singledraft_task.
Step 1:
curl https://api.routeapi.ai/v1/video/generations \ -H "Authorization: Bearer $ROUTEAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "seedance-2.5", "prompt": "An astronaut walks slowly on the moon", "metadata": { "draft": true, "duration": 5, "ratio": "16:9" } }'Step 2, after the draft task reports SUCCESS. data.data.id is the Ark task ID and is not the outer data.task_id:
curl -s https://api.routeapi.ai/v1/video/generations/task_G2RdWGt4t1nck5fYqTjwREKJo8Ze4RAV \ -H "Authorization: Bearer $ROUTEAPI_KEY" | jq -r .data.data.idStep 3:
curl https://api.routeapi.ai/v1/video/generations \ -H "Authorization: Bearer $ROUTEAPI_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "seedance-2.5", "prompt": "x", "metadata": { "content": [ { "type": "draft_task", "draft_task": { "id": "cgt-xxxxxxxx" } } ], "resolution": "1080p", "duration": 5 } }'prompt is still required; it is not sent as text for this form, so any non-empty placeholder works. A draft-task continuation with no explicit duration is reserved as if it would run the model’s auto-duration ceiling (30 seconds on 2.5, 15 seconds on 2.0) and settles to the actual usage.
Draft IDs belong to Ark. ByteDance documents a 7-day lifetime; RouteAPI does not check it, so an expired draft ID fails upstream.
Query the task
Section titled “Query the task”curl https://api.routeapi.ai/v1/video/generations/task_G2RdWGt4t1nck5fYqTjwREKJo8Ze4RAV \ -H "Authorization: Bearer $ROUTEAPI_KEY"{ "code": "success", "message": "", "data": { "id": 4512, "created_at": 1792048450, "updated_at": 1792048569, "task_id": "task_G2RdWGt4t1nck5fYqTjwREKJo8Ze4RAV", "platform": "54", "user_id": 1024, "group": "default", "channel_id": 18, "quota": 108900, "action": "generate", "status": "SUCCESS", "progress": "100%", "fail_reason": "", "result_url": "https://ark-content.example/xxxx.mp4", "submit_time": 1792048458, "start_time": 1792048461, "finish_time": 1792048569, "properties": { "input": "{\"model\":\"doubao-seedance-2-0-fast-260128\",\"content\":[{\"type\":\"text\",\"text\":\"An orange cat stretching on a windowsill at sunrise\"}],\"generate_audio\":false,\"resolution\":\"720p\",\"ratio\":\"16:9\",\"duration\":5}", "origin_model_name": "seedance-2.0-fast", "upstream_model_name": "doubao-seedance-2-0-fast-260128" }, "data": { "id": "cgt-20261023100444-9kz46", "model": "doubao-seedance-2-0-fast-260128", "status": "succeeded", "content": { "video_url": "https://ark-content.example/xxxx.mp4" }, "seed": 8202, "resolution": "720p", "duration": 5, "ratio": "16:9", "framespersecond": 24, "usage": { "completion_tokens": 108900, "total_tokens": 108900 }, "created_at": 1792048458, "updated_at": 1792048569 } }}The outer fields are RouteAPI’s, and Get a task describes them all. For Seedance, read data.status, data.progress, data.result_url, data.fail_reason, and data.quota, which holds the amount charged so far — after a successful task it is the settled amount. data.properties.input is a snapshot of the exact JSON RouteAPI sent to Ark (media URLs longer than 512 bytes are replaced with [omitted, N bytes]), the quickest way to confirm that a request really went out at the resolution you intended. data.platform is the channel type: 54 on a Doubao video channel, 45 on a Volcengine channel.
data.data is Ark’s own task body, stored as returned, and may be incomplete before the task finishes. If the channel’s upstream answers through a gateway envelope ({"code": 200, "data": {...}}), the fields below sit one level deeper, under data.data.data — judge completion by data.status, not by the presence of a field. The fields worth reading:
| Field | Meaning |
|---|---|
data.data.id | Ark task ID, different from the outer data.task_id. Used for the 2.5 draft final generation |
data.data.content.video_url | Video URL Ark returned; the same value as the outer data.result_url |
data.data.content.last_frame_url | Last-frame image. Present only if return_last_frame was enabled. RouteAPI stores this raw payload but only parses content.video_url from it |
data.data.duration / resolution / ratio / framespersecond / seed | Actual generation parameters |
data.data.usage.completion_tokens / total_tokens | Token count Ark metered; total_tokens is the quantity used for settlement |
data.data.error | Ark error code and message on failure |
Ark’s statuses map onto the task record like this; a status RouteAPI does not recognize is treated as still running:
Ark status | Task record status | Progress |
|---|---|---|
pending, queued | QUEUED | 10% |
processing, running | IN_PROGRESS | 50% |
succeeded | SUCCESS | 100% |
failed, expired, cancelled | FAILURE | 100% |
| anything else | IN_PROGRESS | 30% |
If Ark reports succeeded before it has a video_url, RouteAPI keeps the task IN_PROGRESS and polls again rather than settling a task with no result. On FAILURE, fail_reason is Ark’s error message, falling back to task failed; an expired or cancelled task without an error object reports task expired or task cancelled. If RouteAPI’s own task timeout fires first (24 hours by default), the status is also FAILURE with a platform message, and the reservation is refunded either way.
Download the video
Section titled “Download the video”data.result_url is Ark’s own URL, and Ark expires it on its own schedule (ByteDance documents roughly 24 hours) — download it promptly. RouteAPI’s content endpoint re-fetches the stored URL when you call it, so it saves you from tracking expiring links but cannot revive one that has already expired:
curl -L -o out.mp4 \ https://api.routeapi.ai/v1/videos/task_G2RdWGt4t1nck5fYqTjwREKJo8Ze4RAV/content \ -H "Authorization: Bearer $ROUTEAPI_KEY"The content endpoint only serves finished tasks owned by the caller: an unfinished task is a 400 naming its current status, an unknown or foreign task_id is a 404, and an Ark fetch failure is a 502. Its errors use the {"error": {"message": "...", "type": "..."}} shape without a code field. If return_last_frame was enabled, data.data.content.last_frame_url expires on Ark’s schedule too — download it at the same time.
Billing
Section titled “Billing”Seedance models are billed per token, not per request or per second. The console price is quoted per million output tokens for a resolution tier; a request that carries video input is charged at the model’s video-input rate.
At submit, RouteAPI estimates the output tokens from the requested resolution and frame count:
frames = 24 × duration + 1 (frames replaces duration when metadata.frames is set)tokens = pixels × frames / 1024pixels = 480p: 864×496, 720p: 1280×720, 1080p: 1920×1080, 4k: 3840×2160The duration defaults to 5 seconds when omitted, and to the model’s auto-duration ceiling (30 seconds on 2.5, 15 seconds on 2.0) when content is a draft_task without an explicit duration. An omitted or unrecognized resolution is estimated at 1080p. ByteDance’s automatic duration (-1) is rejected at the top level by the 0–3600 check; sent as the string "-1" inside metadata it is forwarded, and the estimate uses the same auto-duration ceiling. The estimate is multiplied by the model ratio, the group ratio, and the video-input multiplier, and that amount is reserved. For a model billed by ratio rather than a fixed price, the estimate is compared with the normal pre-charge and the larger amount is reserved. Token settlement on completion only runs when the model has a model ratio configured; for a model without one, the reserved amount stands (and if the model has neither a ratio nor a fixed price, requests are refused unless unset-ratio models are allowed for your account).
| Request | Reserved at submit | On success |
|---|---|---|
duration given | estimated tokens × price | Ark’s usage.total_tokens × price, difference charged or returned |
duration omitted | estimate assumes 5 s | same settlement |
metadata.frames set | estimate uses the frame count | same settlement |
draft_task with no duration | estimate assumes 30 s / 15 s | same settlement |
| Fixed per-task price configured | the fixed price | unchanged; no token settlement |
SUCCESS without usage | — | reservation stands |
FAILURE, expired, cancelled | — | reservation refunded |
400 | nothing reserved | nothing charged |
The per-token price depends on the output resolution tier, whether the request contains video input, and — on models whose price table defines it — whether audio is generated. When a model’s price table is in use and the resulting multiplier is not 1.0, the submit response’s X-New-Api-Other-Ratios header carries it as a video_input entry. The built-in fallback table gives the 2.0 model separate base (480p / 720p), 1080p, and 4k tiers plus a video-input rate for each; the fast variant has a single base tier and a 22/37 video-input ratio. seedance-2.5 and seedance-2.0-mini have no built-in price table, so their tiers come from the console configuration. A channel can also override the ratios with a VideoPrice table keyed by the model ID you request. Prices and tiers shown in the console are authoritative.
Worked example (hypothetical prices; use the console for real ones). Suppose seedance-2.0-fast is priced at $2 per 1M output tokens at 720p and the group ratio is 1:
- Submit at
720p,duration: 5. Estimate: 1280 × 720 = 921,600 pixels, 24 × 5 + 1 = 121 frames, 921,600 × 121 / 1024 = 108,900 tokens, reserved at $0.2178. - Ark reports
usage.total_tokens: 105000, so the settled charge is $0.21 and $0.0078 is returned. - Had the same request included a
reference_videoitem, the built-in fast-model table would apply a22/37 ≈ 0.5946video-input multiplier to both the estimate and the settlement: reservation ≈ $0.1295, settled ≈ $0.1249.
A request whose balance or Key quota cannot cover the reservation is rejected before anything is sent upstream: 403 insufficient_user_quota for the wallet, 403 pre_consume_token_quota_failed for the Key quota.
Limits and errors
Section titled “Limits and errors”| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_request | Missing or blank prompt, malformed JSON, or a metadata field with the wrong type |
| 400 | invalid_seconds | Top-level duration / seconds, or a numeric metadata.duration, is negative or above 3600 |
| 400 | model_not_supported | The request maps to a Seedance 1.x model, which is no longer served |
| 400 | task_not_exist | The task_id is unknown or belongs to another account |
| 403 | insufficient_user_quota, pre_consume_token_quota_failed | The wallet balance or the Key quota cannot cover the reservation |
| 429 | — | Upstream capacity exhausted; the message becomes Upstream load is saturated, please try again later |
| 500 | build_request_failed, do_request_failed, invalid_response | Conversion failed, the upstream was unreachable, or Ark returned no task ID |
| 500 | get_channel_failed | Selecting a channel failed on the relay side (for example, the channel record disappeared mid-request) |
| 503 | get_channel_failed | The channel hit a hard credential or billing fault upstream; details are hidden |
| upstream status | fail_to_fetch_task | Ark rejected the submission; the HTTP status and message are Ark’s |
Common 400 messages, exactly as returned:
message | Cause |
|---|---|
prompt is required | prompt is missing, empty, or whitespace only |
seconds must be between 1 and 3600 | Top-level duration / seconds, or a numeric metadata.duration, is negative or too large |
unmarshal metadata failed: ... | A metadata field has the wrong type; the rest of the message names it |
model <id> is not supported by this channel | The origin or upstream model name matches a retired Seedance 1.x model |
task_not_exist | The task ID is unknown, or belongs to another account |
Failures after submission (content moderation, provider timeout, cancellation) do not appear as HTTP errors. They surface as FAILURE with a fail_reason on the task record, and the reserved amount is refunded. expired and cancelled are reported the same way.
Differences from the Volcengine Ark API
Section titled “Differences from the Volcengine Ark API”- The endpoints are different. RouteAPI does not expose
/api/v3/contents/generations/tasks, so pointing the Volcengine SDK at our base URL does not work. - Ark-specific parameters go under
metadata; the common fields (prompt,images,ratio,resolution,duration,generate_audio) can also be sent at the top level and are mapped for you. - The returned
task_idis RouteAPI’s ID, not Ark’s. Ark’s ID isdata.data.idon the task record. - The model you send is a catalog ID; the channel maps it to the Ark model name, so you never address Ark endpoint IDs (
ep-...) directly. - Task listing, task cancellation, callback notification, and web search are not supported;
callback_urlandtoolsare dropped. - Billing is per token against the console price table, not Ark’s own billing.
Complete script
Section titled “Complete script”Requires bash, curl, and jq.
#!/usr/bin/env bashset -euo pipefail: "${ROUTEAPI_KEY:?set ROUTEAPI_KEY first}"BASE="${ROUTEAPI_BASE:-https://api.routeapi.ai}"AUTH="Authorization: Bearer $ROUTEAPI_KEY"
TASK_ID=$(curl -sS -X POST "$BASE/v1/video/generations" \ -H "$AUTH" -H "Content-Type: application/json" \ -d '{"model":"seedance-2.0-fast","prompt":"An orange cat stretching on a windowsill at sunrise","duration":5,"resolution":"720p","ratio":"16:9","generate_audio":false}' \ | jq -er .task_id)echo "task: $TASK_ID"
for _ in $(seq 1 60); do RESP=$(curl -sS "$BASE/v1/video/generations/$TASK_ID" -H "$AUTH") STATUS=$(printf '%s' "$RESP" | jq -r .data.status) echo "status: $STATUS $(printf '%s' "$RESP" | jq -r .data.progress)"
case "$STATUS" in SUCCESS) curl -sSL -o "$TASK_ID.mp4" "$BASE/v1/videos/$TASK_ID/content" -H "$AUTH" echo "saved: $TASK_ID.mp4" exit 0 ;; FAILURE) echo "failed: $(printf '%s' "$RESP" | jq -r .data.fail_reason)" exit 1 ;; esac sleep 15done
echo "timed out waiting for the task"exit 1The loop waits 15 minutes; raise the count for long or high-resolution jobs.