Jupyter: move Sora cells to Sume, where a cell rerun is a retry
Re-running a notebook cell resubmits the request. Hold one Idempotency-Key per take in a variable so Sume returns the first video job, and show the file inline.

In a notebook, you re-run cells all the time, and each re-run of a cell that posts to /v1/videos is a retry from the API's point of view. Put a TAKE value in its own variable and send it as the Idempotency-Key, so running the cell again returns the first Sume job instead of billing another one; bump TAKE only when you want a new render.
OpenAI's deprecations page, read 2026-10-08, lists the Videos API as removed on September 24, 2026, which is why old notebook cells that call it now fail.
The cell rules
Split the work so the expensive step is repeatable and the cheap steps are free to repeat.
| Cell | Does | Safe to re-run |
|---|---|---|
| 1 | set TAKE, PROMPT, read SUME_API_KEY | yes |
| 2 | POST /v1/videos with Idempotency-Key = TAKE | yes, same body returns the first job |
| 3 | poll GET /v1/videos/{id} every 30 seconds | yes |
| 4 | fetch /content and write out.mp4 | yes, overwrites the file |
| 5 | display the video | yes |
One cell that does it all
If you prefer a single cell, this works in any recent Jupyter or VS Code notebook with requests installed. Changing PROMPT without changing TAKE makes Sume answer 409 idempotency_conflict, which is the signal that you meant a new take.
import os, time, requests
from IPython.display import Video, display
TAKE = "harbor-take-1"
PROMPT = "A kite over a harbor at golden hour"
BASE = "https://api.sume.com"
HEAD = {"Authorization": "Bearer " + os.environ["SUME_API_KEY"]}
res = requests.post(BASE + "/v1/videos", timeout=60,
headers={**HEAD, "Idempotency-Key": TAKE},
json={"model": "sume/auto", "prompt": PROMPT, "duration": 8})
res.raise_for_status()
job = res.json()
while job["status"] in ("pending", "in_progress"):
time.sleep(30)
job = requests.get(BASE + "/v1/videos/" + job["id"], headers=HEAD, timeout=60).json()
print(job["id"], job["status"])
if job["status"] == "completed":
url = BASE + "/v1/videos/" + job["id"] + "/content?index=0"
with open("out.mp4", "wb") as f:
f.write(requests.get(url, headers=HEAD, timeout=300).content)
display(Video("out.mp4", embed=True))Interrupting a cell
Stopping the kernel while the loop is sleeping does not cancel the job; it keeps running and reserving credits. Print the job id as above, and on the next run the same TAKE brings it back. If you want a progress bar during the wait, the tqdm post shows one without a fake percentage.
Do not commit the notebook with the API key in an output cell. Read it from the environment, as the first lines do.
Sources
Related posts
More in Developers
- Kotlin: submit and poll a Sume video job with java.net.http
A Kotlin port of a Sora videos call: POST /v1/videos with an Idempotency-Key, poll every 30 seconds until done, with the JDK client and kotlinx.serialization.
- Kubernetes CronJob that submits a nightly 30-second Wan 3.0 clip
A CronJob manifest using curlimages/curl and a Secret: one dated Idempotency-Key per night, concurrency forbidden, and a month of reserves at each resolution.
- List your Sume Formats in Python and keep the video ones
Page through GET /v1/formats with next_cursor, keep io.output_kind video, and print each vanity_invoke_url. A runnable snippet with field caveats.
- MAI-Voice-2.1 SSML style="happiness" is not in the voice style list
Microsoft's MAI-Voice-2.1 SSML example uses style="happiness", but the Learn page's own style lists say happy or joyful. Check styles before you ship.
Written by Sume