Sora API gone: port to Sume with Python urllib, no extra packages

Replace the OpenAI videos calls with a stdlib-only Python script: submit to Sume /v1/videos, poll, then fetch the 302 artifact URL without sending your key.

5 min readSume
All posts

If your Sora integration ran in a locked-down image where you cannot add packages, you can port it to Sume with the Python standard library alone: urllib builds the JSON POST to /v1/videos, a loop polls GET /v1/videos/{id}, and a redirect-refusing opener reads the Location header of /content so the artifact download does not carry your API key.

OpenAI's deprecations page, read 2026-10-08, lists sora-2, sora-2-pro, their dated snapshots and the Videos API as removed on September 24, 2026, and its replacement column is empty. So the swap is yours to pick; this post only covers the transport.

What changes on the wire

The OpenAI guide shows POST /videos as multipart form data with a string seconds value and a size such as 1280x720. Sume's POST /v1/videos takes JSON, an integer duration, and separate resolution and aspect_ratio fields. It answers 202 with id, polling_url, status and model.

Sume returns 400 unsupported_parameter if you send size or seed, so do not carry those fields over.

Request mapping, vendor docs read 2026-10-08
OpenAI Videos APISume /v1/videos
POST /videos, multipartPOST /v1/videos, JSON body
seconds: "8" (string)duration: 8 (integer)
size: "1280x720"resolution: "720p", aspect_ratio: "16:9"
status queued, in_progress, completed, failedpending, in_progress, completed, failed, cancelled
GET /videos/{id}/contentGET /v1/videos/{id}/content, 302 to the artifact

The script

Run it with SUME_API_KEY set. The Idempotency-Key header makes a re-run return the first job instead of billing a second one. The poll interval is 30 seconds, which is what the Sume docs recommend.

import json, os, time, urllib.error, urllib.request
BASE = "https://api.sume.com"
KEY = os.environ["SUME_API_KEY"]
AUTH = {"Authorization": "Bearer " + KEY}
def call(method, path, body=None, extra=None):
    headers = {**AUTH, "Content-Type": "application/json", **(extra or {})}
    data = json.dumps(body).encode() if body is not None else None
    req = urllib.request.Request(BASE + path, data=data, headers=headers, method=method)
    return json.load(urllib.request.urlopen(req))
class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, *args, **kwargs):
        return None
def main():
    body = {"model": "sume/auto", "prompt": "A paper boat on a rain puddle", "duration": 8}
    job = call("POST", "/v1/videos", body, {"Idempotency-Key": "boat-001"})
    while job["status"] in ("pending", "in_progress"):
        time.sleep(30)
        job = call("GET", "/v1/videos/" + job["id"])
    if job["status"] != "completed":
        raise SystemExit("job ended as " + job["status"])
    url = BASE + "/v1/videos/" + job["id"] + "/content?index=0"
    try:
        urllib.request.build_opener(NoRedirect).open(urllib.request.Request(url, headers=AUTH))
    except urllib.error.HTTPError as err:
        if err.code != 302:
            raise
        urllib.request.urlretrieve(err.headers["Location"], "out.mp4")

main()

Why the opener refuses the redirect

The default opener follows the 302 and would re-send your Authorization header to the storage host, which does not need it. Returning None from redirect_request makes urllib raise HTTPError for the 302, and the handler reads Location and fetches the file with a plain urlretrieve call.

On the Sume side, /content answers 409 job_not_completed while the job is still running and 409 job_failed when it is dead, so check the poll status first as the script does.

Before you ship it

The script has no retry on 429 or 5xx and no timeout on the poll loop. Add both before it runs unattended, and store the job id so a crash can resume at the poll. The post on storing results from Sume jobs covers why you should copy the file out instead of keeping the redirect URL, and the shorter mapping post lists the field changes.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume