Sociaro

spicy/seedance-2-5

Seedance 2.5 — the large-input, long-output member of the family.

Up to 30 reference images, 10 reference clips and 10 audio tracks, audio-only input, and output up to 30 seconds.

Not a cheaper successor to 2.0 — the most expensive model in the family.

Slugspicy/seedance-2-5
Kindvideo
Vendorbytedance
EndpointPOST /v1/spicy/generations

Charged per job from what the generator reports it rendered — your Logs show the exact figure for each one; see balance and billing.

Calling it

curl https://api.sociaro.com/v1/spicy/generations \
  -H "Authorization: Bearer $SOCIARO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "model": "spicy/seedance-2-5",
        "prompt": "a paper boat on a rain-soaked street",
        "resolution": "720p",
        "duration": 5
      }'

# then poll the job id it returns:
curl https://api.sociaro.com/v1/spicy/generations/JOB_ID \
  -H "Authorization: Bearer $SOCIARO_API_KEY"

Parameters

Seedance 2.5 — the large-input, long-output tier

FieldTypeRequiredDefaultValuesWhat it does
contentarrayyes—parts, each with its own type and an optional roleThis is the whole input. `{"type":"text","text":…}`, `{"type":"image_url","image_url":{"url":…},"role":"first_frame"|"last_frame"|"reference_image"}`, `{"type":"video_url",…,"role":"reference_video"}`, `{"type":"audio_url",…,"role":"reference_audio"}`. Every other model on this endpoint takes flat fields — these three do not. One more part type exists on this model alone: `{"type":"draft_task","draft_task":{"id":…}}`, which renders the final of an earlier draft — see `draft` below.
resolutionstringno720p480p · 720p · 1080pA tier this model does not have is refused with a 400 naming the ones it does. The generator would ACCEPT it and silently render 720p, so the check is ours and it costs you nothing.
durationintegerno54–30, or -1Seconds. `-1` lets the model pick the length that suits the inputs.
ratiostringnoadaptive21:9 · 16:9 · 4:3 · 1:1 · 3:4 · 9:16 · adaptive
output_formatstringnomp4mp4 · mov`mov` carries 10-bit colour and PCM audio for editing work, and 2.5 is the only model here that offers it.
generate_audiobooleanno—true · false
bitrate_modestringnothe model's ownstandard · highNot in the vendor's public reference, but the generator honours it: `high` encodes at a higher bitrate than `standard` (in one 480p measurement about 4.6–6.9 Mbps against 1.2–1.9 Mbps), and the price is the same, because these models are metered per token. The default differs by model, so leave it out to take the model's own. On 2.5, leaving it out already gives a high bitrate (6.96 Mbps in the same measurement).
return_last_framebooleannofalsetrue · falseAsks for the closing still beside the clip. It arrives as `last_frame_url`, on the same expiring /v1/media terms, at NO extra cost — useful as the `first_frame` of the next clip.
watermarkbooleannofalse heretrue · falseThe vendor defaults this on; we default it off.
seedintegernorandom—Every Seedance model we serve accepts it. The same seed gives similar, not identical, results: the vendor says complete consistency is not guaranteed.
omni_reference_task_typestringnoautoauto · reference · edit · extendWhat the references in `content` are FOR — generate from them, edit the source, or extend it; `auto` lets the model infer it from the prompt. Needs REFERENCE media in `content`: the generator refuses it on text-to-video and on first-frame jobs. Under `extend`, `duration` is the length of the ADDITION, not of the finished clip. THIS MODEL ONLY — the 2.0 rows accept the field and silently ignore it.
draftbooleannofalsetrue · falseRenders a cheap 480p DRAFT instead of the finished clip, so you can judge the shot before paying for it. Send `resolution` as `480p` or leave it out — any other tier is a 400, because the generator renders 480p regardless and you would be charged for the tier you asked for. Keep the `job_id`: for the next SEVEN DAYS you can render the 1080p final from it by sending `{"type":"draft_task","draft_task":{"id":""}}` as a `content` part, with `resolution` set to `1080p` — the generator REQUIRES the field on a final and refuses a request without it, in its own words (measured 2026-10-01) — and no `draft` flag. The final reuses the draft's prompt, references, duration, ratio, seed and audio settings automatically — you do not resend them. The two calls are billed separately, each as an ordinary video at its own resolution; there is no draft surcharge and no draft discount. A final can only be rendered while we still hold the draft's billing record, which is a shorter window than the generator's seven days — past it the request is refused and you start from a new draft. THIS MODEL ONLY: the 2.0 rows do not support draft mode (the generator refuses `draft` there by name, measured 2026-10-05), and we refuse it too. ON `/v2/jobs` draft mode works too, under the official `bytedance/seedance-2.5` id — `POST /v2/jobs` does not serve the `spicy/` one and answers `model_not_found` for it. Through that id the steps above change in two ways. You name OUR id, not the generator's: send `draft: true` in `input` for the draft, keep the `job_...` that `POST /v2/jobs` returns, and once that job has `succeeded` render the final with a second job whose `input.content` is `[{"type":"draft_task","draft_task":{"id":""}}]`. And `/v2` sets the resolution itself, `480p` on the draft and `1080p` on the final, so send none (if you do send one it must be that step's own value, or it is a 400). The final is billed at the rate the DRAFT's inputs earn: a final whose draft had an input video is charged the with-video rate, though the final's own body carries no video. A draft that has not finished, or a job that is not a draft, is refused; a job that belongs to someone else reads exactly like an id that does not exist.

Anything not listed here is refused rather than ignored, so a typo fails loudly instead of quietly producing something else.

Worked example

# extend an existing clip — duration is the length of the ADDITION
curl https://api.sociaro.com/v1/spicy/generations \
  -H "Authorization: Bearer $SOCIARO_API_KEY" -H "Content-Type: application/json" \
  -d '{ "model": "spicy/seedance-2-5",
        "content": [
          { "type": "text", "text": "the camera drifts on past the lantern" },
          { "type": "video_url", "video_url": {"url": "https://your-cdn.example.com/clip.mp4"},
            "role": "reference_video" }
        ],
        "omni_reference_task_type": "extend",
        "resolution": "1080p", "duration": 6 }'

Not a cheaper successor to 2.0 — the most expensive model in the family. What it buys is scale of input and length of output:

2.0 2.5
reference images up to 9 up to 30
reference videos up to 3, each 2–15 s up to 10, each 2–30 s
reference audio up to 3 up to 10
audio-only input not allowed allowed
output length 4–15 s up to 30 s
resolution up to 4K up to 1080p — no 4K

Metered per token, with the same minimum cost on video-input jobs as the rest of the family.

1080p arrived on 2026-08-14 and 4K is still unpriced by the vendor, so 4K is a 400. The tier list on this page is read from what BytePlus prices, so it changes when they publish one.

omni_reference_task_type exists on this model alone. The 2.0 rows accept it and ignore it, which buys you a clip that never used the setting and no error saying so.

How a job runs

Generation takes longer than a request should wait, so it goes in three moves: submit, then poll, then collect.

# 1. submit — the model id plus the fields above
curl -sS https://api.sociaro.com/v1/spicy/generations \
  -H "Authorization: Bearer $SOCIARO_API_KEY" -H "Content-Type: application/json" \
  -d '{ "model": "…", "prompt": "…" }'
# -> { "job_id": "<id>", "status": "submitted" }

# 2. poll every 3-5 s for images, 5-10 s for video
curl -sS https://api.sociaro.com/v1/spicy/generations/<id> \
  -H "Authorization: Bearer $SOCIARO_API_KEY"
# -> { "job_id": "…", "status": "processing" }
# -> { "job_id": "…", "status": "completed", "model": "…",
#      "media_url": "https://api.sociaro.com/v1/media/<media_id>" }
# -> { "job_id": "…", "status": "failed", "model": "…", "error": { "code": …, "message": … } }

# 3. collect within the hour — the opaque id IS the credential, no header needed
curl -sSL "https://api.sociaro.com/v1/media/<media_id>" -o out.mp4

Use the job_id verbatim as the path segment when polling: the two names are the same value. Which submit and poll URL this model uses is at the top of this page, under Calling it.

Reading the poll

status is one of processing · completed · failed. Only completed carries media_url, only failed carries error.

Gate on status, never on the poll's HTTP code. A poll-time failure is HTTP 200 with status: "failed" and an error object of { "code", "message" }, plus provider_code when the generator supplied one — on every failure branch, including one where the result could not be delivered. A submit-time rejection is different: it comes back as its own HTTP status with the reason in the body.

code is ours and stable; message is the generator's. Match on code: it comes from a small fixed set, and provider_failed still means what it always meant. A rejection of what you sent — a size out of range, an unusable input — additionally sets code: "invalid_request", because that is a different thing for your code to do. message now carries the generator's own sentence, and provider_code its own code (OutputVideoSensitiveContentDetected.PolicyViolation, IPInfringementSuspect), so a moderated generation, a copyright refusal and a broken renderer are finally distinguishable. Treat provider_code as informational: the vendors change these strings without telling us, so branch on ours.

message is variable text, capped at 400 characters. It used to be a single fixed 43-character sentence, so if you compare it by equality or keep it in a narrower column, that needs changing — match on code, and on provider_code when you need the finer distinction.

Two things that text is not. It does not name the generator behind the model: the names of the services and hosts that actually run your job are substituted out before you see it, so do not parse it for one. What it does not hide is a name you already have — the halves of the model id you called (alibaba, bytedance, wan, qwen, seedance, seedream, happyhorse) stay as written, because blanking those would garble the message exactly where it is useful: Model alibaba/wan-2-7-image not found has to survive intact. And the text can quote your own request back: a moderation message often contains the fragment it objected to. That is why we do not write it into our own logs, and why forwarding or storing a failed poll's message is your decision to make rather than something to do by default.

Collecting the result

media_url points at our host, not the generator's. It is a capability: the opaque id is the credential, so no Authorization header is needed and anyone holding the link can fetch it. It expires within the hour — download rather than store the link.

A job that asked for a second asset gets one beside the first on the same terms: Seedance's return_last_frame arrives as last_frame_url, a decomposition's layers as layers[]. Treat them as optional: they appear when you asked, the generator returned one, and the link passes the same delivery check the main asset passed. If it does not, we omit the field rather than hand you a link /v1/media would refuse — and the main asset is unaffected.

/v2 jobs do not return the Seedance still yet: return_last_frame is accepted there, but the closing frame is not delivered, so a /v2 job's result carries the clip alone. Use /v1 when you need the still.

Several images are not optional extras. An Alibaba image model asked for more than one picture (n above 1) is charged for the number of pictures the generator reports making, and every link it returns is delivered, never fewer than that number: media_urls lists all of them in the generator's order (present on every image result, even a single one) and media_url is the first. Each has its own link and its own expiry. If any one of them cannot be delivered the job is reported failed and nothing is charged — unlike a still or a layer, an image you paid for is never left out.

Rules the gateway adds

Two fields are ours and never reach the generator:

Field Notes
model which model to run
user your own end-user id. Recorded against this job's spend so you can attribute cost per end user, and stripped before the request leaves us, so the generator never sees it. Validated BEFORE the job is created, so a rejection is free: a string of at most 128 characters, no control characters, valid UTF-8. Omitting it, or sending null, is fine and simply records no end user

An unknown field is always a 400. Most generators reject one themselves; some accept it, ignore it, render something other than what you asked for and bill you — so we refuse it for you. Either way you get 400 unknown field(s): [...] naming the offender, never a surprise render.

Ranges and enums are the generator's own and we do not re-validate most of them, so an out-of-range value comes back as its rejection, in its wording and with its numbers. The one thing we DO check ourselves is resolution, because an unsupported tier is silently ignored rather than refused: you would ask for the higher tier, be handed a lower one, and be charged. The tiers on this page are read from what the vendor prices, so they change when the vendor publishes one.

Authentication

Authorization: Bearer <your-api-key> on submit and on poll. The key must be valid AND granted this model — 403 otherwise.

One deliberate exception on poll: a key that is over budget may still poll and collect a job it already submitted, so work you have already paid for is never stranded by a budget that ran out mid-generation. While the key is over budget the gateway cannot read its model list at all, so the per-model grant is not re-checked on those polls — a grant revoked after submit still collects that job. Ownership is always enforced: a job can only be polled by the key that created it, and submit is always refused in that state.