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.
- Only 12 left badge over a product clip: compose overlay, $0.02
A low-stock badge on a product clip is $0.02 per variant with Sume compose overlay. Set position, width_ratio and margin_ratio; re-render as stock drops.
- Meta One plan prices vs 20 ad clips on Sume: $6.40
Meta One's four plans run $14.99 to $499 a month. Twenty clips through Sume's trim, caption and timeline jobs cost $6.40. What the two numbers measure.
- AI video with native audio vs a scripted voiceover: exact words
Seedance 2.5, MiniMax H3 and Kling make sound with the picture. When a line must be word-exact, generate the voiceover with Sume TTS and join it on a timeline.
Written by Sume