Skip to content

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.

Model IDArk modelResolutionDurationNotes
seedance-2.5doubao-seedance-2-5-260628480p / 720p / 1080p4–30 sDraft preview, reference/edit/extend modes, mov output
seedance-2.0doubao-seedance-2-0-260128480p / 720p / 1080p / 4k4–15 sImage, video, and audio reference material
seedance-2.0-fastdoubao-seedance-2-0-fast-260128480p / 720p4–15 sFaster, cheaper 2.0 variant
seedance-2.0-minidoubao-seedance-2-0-mini-260615480p / 720p4–15 sLower-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.

Envelope fieldWhat the Seedance adaptor does
promptAppended as a trailing text item unless metadata.content already contains one, or content is a 2.5 draft_task. Required in every request
images / imageOne image_url content item per image, with no role. Ignored when metadata.content is present
ratioSent as the Ark ratio
resolutionSent as the Ark resolution
durationSent as the Ark duration; a top-level seconds that parses to a positive integer overrides it
generate_audioSent as generate_audio; an explicit false is forwarded
metadataDecoded 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.

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:

FieldTypeDescription
ratiostringAspect ratio: 16:9, 4:3, 1:1, 3:4, 9:16, 21:9, or adaptive
resolutionstring480p, 720p, 1080p, or 4k, model permitting
durationintegerDuration in seconds. 0 means “not set” and is not sent, so use metadata if you need to forward an explicit zero
generate_audiobooleanGenerate a soundtrack; false is forwarded explicitly
secondsstringLegacy form of the duration; when it parses to a positive integer it overrides both duration and metadata.duration. A non-numeric value is ignored
FieldTypeDescription
contentarrayInput material; replaces images. See metadata.content
ratio, resolution, duration, generate_audiosame as top levelTake precedence over the top-level value. A positive seconds still wins over the final duration
seedintegerRandom seed; 0 is forwarded as an explicit value
framesintegerFrame count. When set it replaces the duration in the cost estimate; Ark uses it instead of the duration
camera_fixedbooleanKeep the camera fixed
watermarkbooleanProvider-side watermark on the output. ByteDance documents false as the default
return_last_framebooleanAlso return the final frame as an image
service_tierstringProvider service tier, for example default. Passed through unchanged
execution_expires_afterintegerTask expiry in seconds. ByteDance documents 3600–259200 with a default of 172800; Ark enforces it
priorityintegerProvider-side queue priority, 0–9, higher runs first. Ark only orders tasks within your account
safety_identifierstringYour end-user identifier, up to 64 characters. Forwarded to Ark, which enforces the length
draftbooleanSeedance 2.5 draft preview — see Draft preview
output_formatstringOutput container, forwarded as is. Seedance 2.5 accepts mp4 (default) or mov
omni_reference_task_typestringSeedance 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.

content is an ordered array of input items. Each item has a type, the matching payload field, and an optional role:

typePayloadrole values
texttext—
image_urlimage_url.urlfirst_frame, last_frame, reference_image
video_urlvideo_url.urlreference_video
audio_urlaudio_url.urlreference_audio
draft_taskdraft_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 content is present it is used as is; top-level images are ignored rather than merged. An empty content array counts as absent, so top-level images still apply.
  • If content has no text item, the top-level prompt is appended as a trailing text item. The one exception is a 2.5 request whose content is a draft_task: no prompt is appended, but prompt must still be present in the body.
  • Refer to material by ordinal in the prompt (“image 1”, “video 1”), counted per type in content order.
  • 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.

ItemSeedance 2.52.0 series
Reference images1–301–9
Reference videosUp to 10 clips, ≤30 s totalUp to 3 clips, ≤15 s total
Reference audioUp to 10 clips, ≤30 s totalUp to 3 clips, ≤15 s total, and must accompany an image or video
Single imageUnder 30 MB, 300–6000 px per sideSame
Single videomp4 / mov, under 200 MBSame
Single audiowav / mp3, under 15 MBSame

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.

Terminal window
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
}'

A single image in images becomes an untagged image item, which Seedance treats as the first frame:

Terminal window
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:

Terminal window
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
}
}'

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:

Terminal window
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.

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:

Terminal window
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.

A draft is a low-cost preview. Generating the final video from it is a second, separately billed task.

  1. Submit a second-generation request with "metadata": { "draft": true, ... } and wait for SUCCESS.
  2. Read the upstream task ID from the task record: GET /v1/video/generations/{task_id} → data.data.id.
  3. Submit a new task whose content is a single draft_task.

Step 1:

Terminal window
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:

Terminal window
curl -s https://api.routeapi.ai/v1/video/generations/task_G2RdWGt4t1nck5fYqTjwREKJo8Ze4RAV \
-H "Authorization: Bearer $ROUTEAPI_KEY" | jq -r .data.data.id

Step 3:

Terminal window
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.

Terminal window
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:

FieldMeaning
data.data.idArk task ID, different from the outer data.task_id. Used for the 2.5 draft final generation
data.data.content.video_urlVideo URL Ark returned; the same value as the outer data.result_url
data.data.content.last_frame_urlLast-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 / seedActual generation parameters
data.data.usage.completion_tokens / total_tokensToken count Ark metered; total_tokens is the quantity used for settlement
data.data.errorArk 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 statusTask record statusProgress
pending, queuedQUEUED10%
processing, runningIN_PROGRESS50%
succeededSUCCESS100%
failed, expired, cancelledFAILURE100%
anything elseIN_PROGRESS30%

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.

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:

Terminal window
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.

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 / 1024
pixels = 480p: 864×496, 720p: 1280×720, 1080p: 1920×1080, 4k: 3840×2160

The 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).

RequestReserved at submitOn success
duration givenestimated tokens × priceArk’s usage.total_tokens × price, difference charged or returned
duration omittedestimate assumes 5 ssame settlement
metadata.frames setestimate uses the frame countsame settlement
draft_task with no durationestimate assumes 30 s / 15 ssame settlement
Fixed per-task price configuredthe fixed priceunchanged; no token settlement
SUCCESS without usage—reservation stands
FAILURE, expired, cancelled—reservation refunded
400nothing reservednothing 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:

  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.
  2. Ark reports usage.total_tokens: 105000, so the settled charge is $0.21 and $0.0078 is returned.
  3. Had the same request included a reference_video item, the built-in fast-model table would apply a 22/37 ≈ 0.5946 video-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.

StatusCodeMeaning
400invalid_requestMissing or blank prompt, malformed JSON, or a metadata field with the wrong type
400invalid_secondsTop-level duration / seconds, or a numeric metadata.duration, is negative or above 3600
400model_not_supportedThe request maps to a Seedance 1.x model, which is no longer served
400task_not_existThe task_id is unknown or belongs to another account
403insufficient_user_quota, pre_consume_token_quota_failedThe 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
500build_request_failed, do_request_failed, invalid_responseConversion failed, the upstream was unreachable, or Ark returned no task ID
500get_channel_failedSelecting a channel failed on the relay side (for example, the channel record disappeared mid-request)
503get_channel_failedThe channel hit a hard credential or billing fault upstream; details are hidden
upstream statusfail_to_fetch_taskArk rejected the submission; the HTTP status and message are Ark’s

Common 400 messages, exactly as returned:

messageCause
prompt is requiredprompt is missing, empty, or whitespace only
seconds must be between 1 and 3600Top-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 channelThe origin or upstream model name matches a retired Seedance 1.x model
task_not_existThe 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.

  • 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_id is RouteAPI’s ID, not Ark’s. Ark’s ID is data.data.id on 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_url and tools are dropped.
  • Billing is per token against the console price table, not Ark’s own billing.

Requires bash, curl, and jq.

#!/usr/bin/env bash
set -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 15
done
echo "timed out waiting for the task"
exit 1

The loop waits 15 minutes; raise the count for long or high-resolution jobs.