Tcl http package: submit a Gemini Omni video job and follow the 302
A 22-line Tcl script using the http, json and tls packages submits a 3 s 360p Gemini Omni job on Sume ($0.1125), polls it and follows the content redirect.

Tcl still sits inside EDA flows, network gear scripts and test harnesses where nobody will add a Python dependency. It ships with an HTTP client, and that is enough for Sume's video API, which is plain JSON over HTTPS. The script below submits the cheapest kind of job, Gemini Omni Flash at 3 seconds and 360p, which Sume prices at $0.0375 a second, so $0.1125 for the clip.
It needs three packages. http is in the core distribution, tls provides HTTPS, and json comes from tcllib, which some minimal installs leave out. Install tcllib first if package require json fails.
The script
Set SUME_API_KEY and run tclsh video.tcl. I ran it against a local stand-in server that mimics the 202, poll and 302 sequence, with the poll delay shortened, and it wrote the file and printed the cost. It was not run against the live API.
package require http
package require json
package require tls
http::register https 443 ::tls::socket
set base [expr {[info exists ::env(SUME_API_BASE)] ? $::env(SUME_API_BASE) : "https://api.sume.com"}]
set auth [list Authorization "Bearer $::env(SUME_API_KEY)"]
set body {{"model":"gemini-omni-flash-1.1","prompt":"A lighthouse in a storm","duration":3,"resolution":"360p","aspect_ratio":"16:9"}}
set t [http::geturl $base/v1/videos -query $body -headers [concat $auth {Content-Type application/json Idempotency-Key tcl-omni-0001}]]
if {[http::ncode $t] != 202} { puts stderr [http::data $t]; exit 1 }
set poll [dict get [json::json2dict [http::data $t]] polling_url]
while 1 {
after 30000
set job [json::json2dict [http::data [http::geturl $poll -headers $auth]]]
set status [dict get $job status]
if {$status in {failed cancelled}} { puts stderr $status; exit 1 }
if {$status eq "completed"} break
}
set r [http::geturl [lindex [dict get $job unsigned_urls] 0] -headers $auth]
foreach {k v} [http::meta $r] { if {[string tolower $k] eq "location"} { set loc $v } }
set r [http::geturl $loc]
set f [open out.mp4 wb]; puts -nonewline $f [http::data $r]; close $f
puts "cost [dict get $job usage cost]"Why the redirect is handled by hand
Sume's finished job lists unsigned_urls, and the first entry is a /content?index=0 URL on the API that answers 302 with the location of the file. The API request needs your key. The storage address it redirects to does not, and should not receive it, so the script makes the second request with no headers.
Tcl reports header names as the server sent them, so the loop compares location in lower case instead of indexing the dictionary with a fixed capital L. That small loop is the difference between working against one server and working against all of them.
What the job reports
The 202 response carries polling_url, and the script uses that value as given rather than building a path. The poll body has status, one of pending, in_progress, completed, failed or cancelled. When it is completed, usage.cost is the amount Sume billed for the job, which the script prints. Polling every 30 seconds is the cadence Sume's guide suggests.
| Field | Value | Reason |
|---|---|---|
| model | gemini-omni-flash-1.1 | Cheapest Omni setting |
| duration | 3 | Shortest Omni length |
| resolution | 360p | Priced at $0.0375 a second |
| Idempotency-Key | tcl-omni-0001 | A resend returns the same job |
Before you extend it
The script exits on failed or cancelled and has no retry for 402 or 429, which you should add before running it unattended. For the full catalog of ids, lengths and resolutions, call GET /v1/videos/models and read it before hard-coding a model, as in /blog/video-catalog-input-matrix-python-frame-and-reference-fields.
Sources
Related posts
More in Developers
- Test a Sume webhook receiver: stale timestamp and rotated secret
Two tests every receiver needs: a delivery older than 300 seconds must be refused, and a header with two sume-v1 entries must verify when either secret matches.
- Test a video poll loop with a fake clock, no real waits (Python)
Inject sleep and clock into your Sume job poller so a test of a 20-minute deadline runs in milliseconds. Includes a stdlib-only script that passes.
- Text-to-image-only models on Sume reject input_references
Five Sume image ids take no reference images: Soul, Imagen 4 Fast and Ultra, Recraft V4 and Qwen Image Max. The error you get and a Python guard before sending.
- Text to speech API with emotion: audition four values, keep the winner
Sume TTS 1.0 takes a free-text emotion up to 64 characters in generation_config. Run a four-take audition for 4 cents and store the winner.
Written by Sume