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
- Kling motion control: send image_url or avatar_handle, not both
Sume's Kling 3.0 Motion Control takes exactly one visual source: image_url or an avatar_id/avatar_handle. A short Python check before the $0.1575/s submit.
- Kling motion control on Sume: keep_original_sound and orientation
Two optional Kling motion control fields on Sume: keep_original_sound defaults to true and character_orientation defaults to video, or image.
- 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.
Written by Sume