LinkedIn video ad thumbnail: pick a still with video frames
LinkedIn's initializeUpload takes an optional thumbnail. Pull up to 24 candidate stills from your MP4 with POST /v1/video-frames and pick one.
To choose a thumbnail for a LinkedIn video ad, pull candidate stills from the finished MP4 with POST /v1/video-frames (up to 24 frames per call, at times you name) and pick the best one. LinkedIn's Videos API page says uploadThumbnail defaults to false, that a system-generated thumbnail may be added if you do not upload one, and that for an ads video a requested thumbnail must process successfully for the video to be servable (read 2026-10-06). So a thumbnail you chose beats one you did not, but it is also one more thing that can block serving.
Sources: LinkedIn Videos API, Microsoft Learn (read 2026-10-06) and Video frames: stills at times you name. That page does not state thumbnail size or format rules, so check them on LinkedIn's own pages before you upload.
How to pick the moments
Ask for six or eight times spread over the clip, not only the start. The first frame of a video is often a fade or a logo card. Frames at 1, 2.5, 4, 6, 8 and 10 seconds give you a face, a product and a text card to compare. Each value must be at least 0 and less than the clip's duration, or the worker fails with frame_time_out_of_range and tells you the probed duration. The source must be on media.sume.com and at most 300 seconds.
The call
A submit always returns 202 because the family is async only; do not send mode: "sync". format is jpeg by default or png for lossless inspection, and max_edge clamps the long edge between 16 and 2160 pixels. Leave it off to keep the source frame size, which is what you want for a thumbnail that will be scaled later. Poll GET /v1/video-frames/{id} until resource_status is ready, then read frames[], each with t, url, width and height.
import json, os, time, urllib.request
H = {"Authorization": "Bearer " + os.environ["SUME_API_KEY"],
"Content-Type": "application/json"}
def call(method, url, body=None, extra=None):
data = json.dumps(body).encode() if body else None
req = urllib.request.Request(url, data=data, method=method,
headers={**H, **(extra or {})})
with urllib.request.urlopen(req) as r:
return json.load(r)
sub = call("POST", "https://api.sume.com/v1/video-frames", {
"video_url": "https://media.sume.com/artifacts/artf_demo/ad-30s.mp4",
"at": [1, 2.5, 4, 6, 8, 10],
"format": "jpeg",
}, {"Idempotency-Key": "li-thumb-001"})
rid = sub["request_id"]
while True:
res = call("GET", "https://api.sume.com/v1/video-frames/" + rid)
if res.get("resource_status") in ("ready", "failed"):
break
time.sleep(3)
for f in res.get("video_frames", res).get("frames", []):
print(f["t"], f["url"])
What to look for in a thumbnail
Choose the frame where the subject is sharp, the face is not mid-blink and the text, if any, is whole. Avoid frames with a burned caption cut off mid-word. A frame whose url is null means that one instant failed; the job still succeeds for the others, so read the rest and request the missing time again.
| Item | Source | Detail |
|---|---|---|
| uploadThumbnail | Default false; system thumbnail may be added | |
| Ads video with thumbnail | Thumbnail must process to be servable | |
| at[] | Sume | 1 to 24 times, each in the clip |
| Source length | Sume | Up to 300 seconds |
| format / max_edge | Sume | jpeg or png; 16 to 2160 px |
What to do with the picked frame
Download it and open it at full size. In LinkedIn's flow you set uploadThumbnail to true in initializeUpload, and the response then carries a thumbnailUploadUrl. The page's example uploads the image to that URL with a media-type-family: STILLIMAGE header and an application/octet-stream content type, and expects a 201 Created back. If none of the stills works, the clip itself probably needs a stronger opening; trim a different start and run the extraction again. Because frames are durable media.sume.com images, you can keep the chosen one with the ad's record.
What the frames cost
Video frames does not have a flat price. Sume bills the job by its Modal compute, container seconds times the Modal list rate times 1.25, plus the platform fee, and the bill is never more than the hold reserved at submit. Pulling six frames from a short ad is a small job, so asking for a generous spread of candidates costs little compared with uploading a poor thumbnail to an ad that must serve.
Sources
Related posts
More in Use cases
- Live-commerce clip webhook: verify the Sume signature in Python
Verify a Sume webhook in Python: HMAC SHA256 over timestamp.raw_body, any sume-v1 entry, a 300-second window, and reject an empty secret.
- Live-commerce run spend cap: $120 typical, $400 default, $500 maximum
A Sume live-commerce run is capped near $120, against a $400 default and $500 maximum for other Formats. What the cap does and how to set it on an API call.
- Live sale replay: three highlight trims, keyframe then exact
Cut three highlights from a live-sale replay under 30 minutes with Sume video-trim: keyframe for a fast first look, exact for the ad, $0.02 per cut.
- Log segment count and billable minutes per holiday ad
Read segment_count, billable_minutes, and warnings from each Sume timeline render result and log one cost line per holiday ad. A Python script, no network.
Written by Sume