video-frames returned a null url: the job succeeded, one frame failed
If one instant fails to extract, video-frames sets that frame's url to null and the job still completes. How to detect it and retry only that time.

A Sume video frames job can finish as ready and still contain a frame with url: null. The docs say that when extraction fails for one instant, that frame's url is null and the job does not fail. So a status check of ready is not enough: loop over frames[] and look at each url before you build a contact sheet, a thumbnail set or an upload.
What the result looks like
The read route is GET /v1/video-frames/:id. When resource_status is ready, frames is a list of { t, url, width, height } entries where each URL is a durable artf_ image on media.sume.com. source_duration_seconds is the duration the worker probed. Submits always return 202, because this route pins async mode, so you poll.
Why a frame can be missing
The docs do not list a cause, so treat the null as a signal to retry and not as a diagnosis. Two things are documented. A timestamp outside [0, duration) fails the whole job with frame_time_out_of_range, so a null is a different case from a bad time. And on sources longer than 90 seconds the job can add a low_confidence_long_video warning, so read warnings[] when you see gaps on long clips.
Detect and retry
The snippet below reads a finished extract with the standard library, lists the missing times, and prints them so you can request just those with a new at[] (keep each request to 24 values or fewer, and send a fresh Idempotency-Key). It reads the key from the environment.
import json, os, urllib.request
def read_frames(extract_id):
req = urllib.request.Request(
f"https://api.sume.com/v1/video-frames/{extract_id}",
headers={"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"},
)
with urllib.request.urlopen(req) as resp:
return json.load(resp)
def missing_times(body):
frames = body.get("frames") or body.get("video_frames", {}).get("frames", [])
return [f["t"] for f in frames if not f.get("url")]
if __name__ == "__main__":
body = read_frames(os.environ["EXTRACT_ID"])
print("missing:", missing_times(body))Where this matters
Contact sheets are the obvious case. A grid built from 24 stills with one hole looks like a rendering error, and a thumbnail picker that sorts by t can end up offering an empty slot. For a character-drift review with fps, a null at the one frame where the drift happens defeats the check. Build the tile list from non-null entries, log the missing times, and request them again. The extract is billed by its Modal compute, with a reserved ceiling that the bill never exceeds, so a retry of a few frames is a small job.
Frames keep the source size unless you send max_edge (16 to 2160), and the source clip must be 300 seconds or shorter.
Sources
Related posts
More in Media tools
- video-inspect frames: at[] and fps together is a 400, pick one
video-inspect and video-frames take a list of times or a sample rate, never both. The codes, the 24-still cap, the fps 2 ceiling, and the empty object.
- Inspect a 3-minute Short with a transcript: set duration_seconds 180
video-inspect reserves one minute of speech-to-text unless you send duration_seconds (max 600). Why a 180 hint matters, the $0.01 per minute rate, and the 400s.
- Source too long? The 1800 s and 300 s caps for each Sume media tool
Trim, detach and inspect take sources up to 1800 s; filter, frames and compose stop at 300 s. The error each returns and the order to cut a long file.
- Which URL does each Sume media tool accept: public or media.sume.com?
Video captions and face swap take a public HTTPS URL; trim, filter, frames, inspect, detach, compose and timeline need your workspace's media.sume.com file.
Written by Sume