Animate a still by API: first-frame jobs on Sume
fal lists FLUX 3 as animating one still into video. On Sume you send the still as a first frame to a video model; this Python script submits and polls.

To animate one still image through the Sume API, send it as a first_frame in frame_images on POST /v1/videos with a model that lists first_frame in supported_frame_images, such as seedance-2. The fal model list describes FLUX 3 from Black Forest Labs as animating a single still image into video; I did not read the FLUX 3 API, so this post covers only the Sume side.
How the two descriptions line up
If both frame_images and input_references are sent, frame_images wins and the request is treated as image-to-video. Reference images guide style and do not fix a frame.
| Source | What it says |
|---|---|
| fal | Newest-video list includes FLUX 3 from Black Forest Labs, described as animating a single still image into video |
| Sume | frame_images with frame_type first_frame (and optionally last_frame) selects image-to-video |
Submit and poll in Python
The script below wraps everything in main and calls it with asyncio.run. It submits with an Idempotency-Key, polls the polling URL every 15 seconds, and prints the first download URL. The image URL must be a public HTTPS URL. Video jobs usually take between 30 seconds and several minutes, so allow time.
Each model advertises its own durations and resolutions in GET /v1/videos/models, so read them before you pin values.
import asyncio, os, uuid
import requests
BASE = 'https://api.sume.com/v1/videos'
H = {'Authorization': f"Bearer {os.environ['SUME_API_KEY']}", 'Content-Type': 'application/json'}
async def main():
body = {
'model': 'seedance-2',
'prompt': 'Gentle camera drift; keep the product locked in frame',
'frame_images': [{
'type': 'image_url',
'image_url': {'url': 'https://example.com/first-frame.png'},
'frame_type': 'first_frame',
}],
'resolution': '720p',
}
r = requests.post(BASE, headers={**H, 'Idempotency-Key': str(uuid.uuid4())}, json=body)
r.raise_for_status()
job = r.json()
while True:
await asyncio.sleep(15)
s = requests.get(job['polling_url'], headers=H).json()
if s['status'] == 'completed':
print(s['unsigned_urls'][0])
return
if s['status'] in ('failed', 'cancelled'):
raise SystemExit(s.get('error', s['status']))
asyncio.run(main())Practical notes
- Use a first frame that already has the composition you want; the model animates from it.
- For a start and end pair, add a second frame_images entry with frame_type last_frame, on models that list it.
- Download from unsigned_urls with your key, or use the content endpoint, and store the Sume URL.
Sources
Related posts
More in Developers
- Ask for the aspect ratio first: an MCP input_required round trip
MCP multi round-trip requests let a tool answer input_required to ask for an aspect ratio or spend approval before a render, with state in requestState.
- ASP.NET minimal API: raw body and FixedTimeEquals for Sume webhooks
Take HttpRequest, not a bound model, in an ASP.NET minimal API. Read the raw body, verify sume-v1 with FixedTimeEquals and refuse an empty signing secret.
- Attach a terminal to a run: ant sessions connect vs Sume jobs
The ant CLI can attach to a Managed Agents session. For Sume generation jobs, use sume jobs watch and MCP jobs_wait instead, and never resubmit a paid job.
- Audit logs without file names: what to log for Sume jobs
Claude's Compliance API Activity Feed stopped returning file names. For Sume jobs, log request ids and job ids, never media URLs or transcripts.
Written by Sume