curl and jq: poll a Seedance 2.5 clip from a bash script

A bash script with curl and jq that submits a 30-second Seedance 2.5 job to Sume, polls every 30 seconds and saves the MP4, with costs at three resolutions.

5 min readSume
All posts

A bash script can render a Seedance 2.5 clip with two tools: curl to talk to the API and jq to read the JSON. Submit with POST /v1/videos, take polling_url from the response, loop with sleep 30 until status is completed, then download the MP4 from unsigned_urls[0].

The script below asks for 30 seconds at 480p, which is $8.07 on Sume (read 2026-10-05). Raise the resolution only after the script works.

The script

It exits if the key is not set, stops on failed or cancelled, and gives up after 40 polls.

#!/usr/bin/env bash
set -euo pipefail
: "${SUME_API_KEY:?SUME_API_KEY is not set}"
AUTH="Authorization: Bearer $SUME_API_KEY"

job=$(curl -sf -X POST https://api.sume.com/v1/videos \
  -H "$AUTH" -H "Content-Type: application/json" \
  -H "Idempotency-Key: bash-demo-001" \
  -d '{"model":"seedance-2.5","prompt":"Slow one-take walk through a bakery","duration":30,"resolution":"480p","aspect_ratio":"16:9"}')
url=$(echo "$job" | jq -r .polling_url)

for i in $(seq 1 40); do
  sleep 30
  s=$(curl -sf -H "$AUTH" "$url")
  status=$(echo "$s" | jq -r .status)
  echo "poll $i: $status"
  case "$status" in
    completed) curl -sf -H "$AUTH" "$(echo "$s" | jq -r '.unsigned_urls[0]')" -o clip.mp4
               echo "saved clip.mp4"; exit 0 ;;
    failed|cancelled) echo "$s" | jq .error; exit 1 ;;
  esac
done
echo "gave up; job may still finish: $url"; exit 2

What each piece does

set -euo pipefail makes the script stop on the first error. The :"${SUME_API_KEY:?...}" line aborts with a message if the key is unset. curl -sf fails on HTTP errors instead of printing an error page, so a 402 on submit stops the script before the loop.

The Idempotency-Key header makes a rerun return the original job instead of creating a second one. That matters for a script you might run twice by mistake: the docs say a replay returns the original job. Change the key when you want a new take.

Price before you raise the resolution

The same 30-second 16:9 request at each resolution, from Sume's price book.

Seedance 2.5, 30 s, 16:9, Sume price for the clip, list x 1.25 rounded up to the cent, read 2026-10-05
ResolutionPrice
480p$8.07
720p$17.34
1080p$42.65

Run it and read the output

Save the file as render.sh, run chmod +x render.sh, export SUME_API_KEY, and run ./render.sh. You will see one line per poll, such as poll 1: pending and later poll 6: in_progress, then saved clip.mp4. The script sleeps before the first poll on purpose: a job that was just accepted is almost never finished after a second.

If you only want to submit and come back later, stop after the url= line and print it. The polling URL stays valid, and you can poll it from another shell with the same Authorization header. That split is handy when you start several renders and want to collect them at the end of a meeting.

Change the request without breaking the loop

Only the -d body needs to change for other shapes. For a vertical clip use "aspect_ratio":"9:16"; for a first frame add a frame_images entry with frame_type set to first_frame; for style references add input_references. The poll loop does not care, because every Sume video model returns the same job shape.

Keep the model id in a shell variable if you switch between tiers. seedance-2-mini and seedance-2-fast take 4-15 seconds, so a 30-second duration would be rejected there; only seedance-2.5 accepts up to 30 seconds in the Seedance family.

Edge cases

If the loop ends at 40 polls, the job may still complete. The script prints the polling URL so you can check it later, or use GET /v1/jobs/{id}/status, which the docs say shows the same job. A job that fails returns an error field; read it before you retry, since a bad reference URL or a blocked prompt will fail the same way again.

Do not paste a real key into the script. Export it in the shell instead. The video file is served from Sume, and the sample sends the Authorization header on the download to be safe; the docs' content endpoint example does the same.

For many clips, wrap the script in a function with a per-clip key and run a few at a time.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume