A one-file Python CLI that replaces a Sora script, on Sume
One argparse script, standard library only: prompt, model, seconds, resolution and aspect flags in, mp4 out, with an idempotency key derived from the arguments.

If the thing you lost on 2026-09-24 was a small command-line script, the replacement is one Python file: argparse for flags, one POST to https://api.sume.com/v1/videos, a poll loop, and a download. The version below uses only the standard library and derives its Idempotency-Key from the arguments.
OpenAI's deprecations page lists the Sora video models and the Videos API as removed on that date and names no replacement. A CLI you wrote for it stops working with the rest, and a CLI is the easiest thing to rebuild.
Flags that map to Sume fields
Sume's /v1/videos takes model, prompt, duration as an integer number of seconds, resolution and aspect_ratio. The repo docs say it rejects size and seed with 400 unsupported_parameter, so the CLI has no flags for either. Fewer flags means fewer ways to send a request that cannot succeed.
| Flag | Request field | Default | Limit on the default model |
|---|---|---|---|
| prompt | prompt | required | none stated |
| --model | model | gemini-omni-flash-1.1 | catalog id |
| --seconds | duration | 5 | 3 to 10 s |
| --resolution | resolution | 720p | 360p, 720p, 1080p, 4K |
| --aspect | aspect_ratio | 16:9 | 16:9 or 9:16 |
The script
Save it as sumevid.py, export SUME_API_KEY, and run python3 sumevid.py "Paper boat on a rain puddle" --seconds 4. The default key is a SHA-256 of the parsed arguments, so running the exact same command twice returns the same job instead of a second paid one. Python's built-in hash() would be wrong here, because string hashing is randomized per process.
import argparse, hashlib, json, os, sys, time, urllib.request, urllib.error
ap = argparse.ArgumentParser(prog="sumevid")
ap.add_argument("prompt")
ap.add_argument("--model", default="gemini-omni-flash-1.1")
ap.add_argument("--seconds", type=int, default=5)
ap.add_argument("--resolution", default="720p")
ap.add_argument("--aspect", default="16:9")
ap.add_argument("--key", help="Idempotency-Key, default derived from args")
ap.add_argument("--out", default="clip.mp4")
a = ap.parse_args()
def call(url, body=None):
h = {"Authorization": "Bearer " + os.environ["SUME_API_KEY"], "Content-Type": "application/json"}
if body is not None: h["Idempotency-Key"] = a.key or "cli-" + hashlib.sha256(repr(vars(a)).encode()).hexdigest()[:16]
req = urllib.request.Request(url, json.dumps(body).encode() if body else None, h)
try:
with urllib.request.urlopen(req, timeout=60) as r: return json.load(r)
except urllib.error.HTTPError as e: sys.exit(f"{e.code}: {e.read().decode()}")
job = call("https://api.sume.com/v1/videos", {"model": a.model, "prompt": a.prompt,
"duration": a.seconds, "resolution": a.resolution, "aspect_ratio": a.aspect})
while job["status"] in ("pending", "in_progress"):
time.sleep(10); job = call(job["polling_url"])
if job["status"] != "completed": sys.exit(f'{job["status"]}: {job.get("error")}')
urllib.request.urlretrieve(job["unsigned_urls"][0], a.out)
print("saved", a.out, job.get("usage"))
Why a derived key is the right default
A person at a terminal retries by pressing the up arrow. If the first run died after the request left, the second run should find the original job. Hashing the arguments gives exactly that. If they change the prompt, the key changes, and a new job is the right result. Pass --key explicitly when you want to rerun the identical command and pay for a fresh render.
At the repo-documented Omni 720p rate of $0.125 a second, the 5-second default costs 0.625, billed as $0.63.
- Add a --dry-run that prints the body and exits, if you share the script.
- Keep the poll at 10 seconds for interactive use; background jobs can wait longer.
- Exit nonzero on failure so shell scripts can chain it.
What it leaves out
There is no reference image, no first frame and no webhook. Each of those is another field on the same request, and the stored posts on first frames and reference tags cover them. Add flags only for the fields you actually use.
Sources
Related posts
More in Developers
- One handler for both: Sume run webhook payload equals the GET receipt
A Sume run webhook's payload is byte-identical to GET /v1/format-runs/{run_id}. Write one function for polling and webhook, and branch on outcome.
- One Sume probe, eight platform limits: a Python preflight
Check one video inspect probe against Truth Social, Odysee, ArtStation, Linktree, Spotlight, Reels, TikTok API and Shorts limits in under 30 lines of Python.
- Agents SDK blocked_tool_names: hide Sume paid tools from a model
create_static_tool_filter takes allowed and blocked tool names. Use it so an agent on Sume's hosted MCP can read, but never sees a paid generate tool.
- OpenAI gave 184 days to leave the Videos API: a CI catalog check
Notice March 24, removal September 24, 2026, no replacement listed. An 18-line Python check fails CI when a video model id or duration leaves Sume's catalog.
Written by Sume