Sora API is gone: replace the calls with POST /v1/videos on Sume

OpenAI removed the Videos API and sora-2 on 2026-09-24. Point video code at Sume POST /v1/videos: new model ids, duration, resolution, polling, download.

5 min readSume
All posts

OpenAI's deprecations page says the Videos API and the Sora 2 model aliases and snapshots were removed from the API on September 24, 2026, and it lists no replacement. If your app still has a video job path that called them, the fix on Sume is POST /v1/videos with a catalog model id such as seedance-2.5 or wan-3.0, then a poll and a download.

This is a rewrite of the request body, not a base-URL swap. The submit, poll, and download shape stays the same, and the model, size, and result handling change.

What changes in your code

Model ids are bare catalog ids, with no provider prefix. Ask GET /v1/videos/models for the live list instead of hard-coding one. Length is duration in whole seconds, and the range depends on the model: seedance-2.5 takes 4-30 and wan-3.0 takes 2-30.

Output shape is resolution plus aspect_ratio, for example 720p and 9:16. A size string like 1280x720 is rejected with 400 unsupported_parameter, because every v1 model reports supported_sizes: null. Audio comes with the model, and generate_audio is optional.

Sora-era video job steps and the Sume equivalent (read 2026-10-07)
StepSume callNote
CreatePOST /v1/videosReturns id, polling_url, status pending
CheckGET /v1/videos/{id}pending, in_progress, completed, failed, cancelled
DownloadGET /v1/videos/{id}/content?index=0Send the API key header
Retry safelyIdempotency-Key on the createA replay returns the original job
Callbackcallback_url on the createHTTPS only, signed, terminal events

A working replacement

The script submits, polls every 15 seconds, and saves the clip. It reads unsigned_urls[0], which points at the Sume content endpoint, so the download call sends the API key too.

import os, time, requests
BASE = "https://api.sume.com/v1/videos"
H = {"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"}
job = requests.post(BASE, timeout=30,
    headers={**H, "Idempotency-Key": "sora-port-demo-1"},
    json={"model": "seedance-2.5", "prompt": "A paper boat on a rainy street",
          "duration": 10, "resolution": "720p", "aspect_ratio": "16:9"})
job.raise_for_status()
poll_url = job.json()["polling_url"]
while True:
    s = requests.get(poll_url, headers=H, timeout=30).json()
    if s["status"] in ("completed", "failed", "cancelled"):
        break
    time.sleep(15)
if s["status"] != "completed":
    raise SystemExit(f"job ended {s['status']}: {s.get('error')}")
video = requests.get(s["unsigned_urls"][0], headers=H, timeout=120)
open("clip.mp4", "wb").write(video.content)

Things that do not carry over

Prompts were tuned for one model, and a different model will read them differently. Run a short test per model before you move traffic. Length limits also differ, so check supported_durations before you submit.

Keep the job id in your own store. The same job can be read at GET /v1/jobs/{id}/status and GET /v1/jobs/{id}/result, which is useful when you want queue state or a webhook receipt.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume