Poll a Seedance 2.5 job in Go: submit, wait 30 s, check status

A Go program that submits a 30-second Seedance 2.5 job to Sume, polls the polling_url every 30 seconds for up to 20 minutes, and prints the result URL.

5 min readSume
All posts

In Go, submit the job with one POST to /v1/videos, read polling_url from the response, and loop with a 30-second sleep until status is completed or failed. The program below does that in about thirty lines using only the standard library, with a 40-poll cap that equals 20 minutes.

A 30-second 480p Seedance 2.5 draft costs $8.07 on Sume (read 2026-10-05), so it is a cheap way to test the loop before you point it at a 1080p job.

The program

It reads the key from SUME_API_KEY and stops with a clear message if the variable is empty. Error handling is trimmed to stay short; add checks for non-2xx responses before you use it for real.

package main
import ("bytes"; "encoding/json"; "fmt"; "net/http"; "os"; "time")

func call(method, url string, body []byte) map[string]any {
	req, _ := http.NewRequest(method, url, bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("SUME_API_KEY"))
	req.Header.Set("Content-Type", "application/json")
	res, err := http.DefaultClient.Do(req)
	if err != nil { panic(err) }
	defer res.Body.Close()
	var out map[string]any
	json.NewDecoder(res.Body).Decode(&out)
	return out
}
func main() {
	if os.Getenv("SUME_API_KEY") == "" { panic("SUME_API_KEY is not set") }
	body := []byte(`{"model":"seedance-2.5","prompt":"Slow one-take walk through a bakery","duration":30,"resolution":"480p","aspect_ratio":"16:9"}`)
	job := call("POST", "https://api.sume.com/v1/videos", body)
	url, _ := job["polling_url"].(string)
	for i := 0; i < 40; i++ {
		time.Sleep(30 * time.Second)
		s := call("GET", url, nil)
		fmt.Println(s["status"])
		if s["status"] == "completed" || s["status"] == "failed" { fmt.Println(s["unsigned_urls"], s["error"]); return }
	}
}

How it maps to the docs

Each step matches the four-step flow in Sume's video generation docs.

Async flow for a Sume video job, from docs read 2026-10-05
StepGo codeDocs
SubmitPOST /v1/videos202 with id, polling_url, status pending
PollGET polling_url every 30 sStatus moves pending, in_progress, completed
Stopcompleted or failedfailed carries an error field
DownloadGET unsigned_urls[0] with the same Authorization headerContent endpoint also works at /v1/videos/{jobId}/content

What to harden before production

  • Check res.StatusCode before decoding. A 402 or 429 on submit is not a job.
  • Add an Idempotency-Key header to the POST so a retry returns the original job. Derive it from the intended video, not from the clock.
  • Treat the 40-poll cap as your own limit. If it passes, record the job id and look the job up again later with GET /v1/jobs/{id}/status; the clip may still finish.
  • Stream the final download to disk instead of reading it into memory. A 30-second 1080p file is larger than a 5-second draft.

Why the loop caps at 40 polls

Forty polls at 30 seconds is 20 minutes of waiting, which is a design choice and not a Sume limit. The Sume docs say a video usually takes from 30 seconds to several minutes depending on the model and parameters, and a pending job can sit for a while when the queue is busy. A cap keeps a stuck job from hanging a worker; it does not cancel the job. When the loop returns without a result, store the job id and check again later instead of resubmitting, because a second submit with a new idempotency key creates a second charge.

Go makes it easy to poll many jobs at once with goroutines, but keep the sleep per job. Fifty goroutines each polling every 30 seconds is about 100 requests a minute. If that is too many for your service, a webhook is the better design.

Choose the resolution on purpose

The sample asks for 480p because the loop is the point. Price scales sharply with resolution on this model: the same 30 seconds is $17.34 at 720p and $42.65 at 1080p. Aspect ratio does not move the price, so 9:16 and 16:9 versions cost the same at the same length and resolution.

If you would rather not poll, add callback_url to the body and let Sume POST to your HTTPS endpoint when the job ends. The docs describe x-sume-webhook-signature and x-sume-webhook-timestamp headers over the raw JSON body; refuse to process a callback if your signing secret is empty or the signature does not match.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume