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.
- 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.
- Backgrounded MCP tool lost progress: resume with Sume jobs_wait
A Claude Code fix covers MCP progress dropped when a tool moves to the background. Do not rely on progress for Sume jobs: re-issue jobs_wait in slices.
Written by Sume