A 3-minute Timeline render: poll job status, don't sleep a fixed time
Timeline renders are async by default. Submit with an Idempotency-Key, poll /v1/jobs/:id/status until terminal, then read /result. Cost: 3 minutes is $0.30.

Submit the render, then poll GET /v1/jobs/:id/status until its terminal flag is true, and read GET /v1/jobs/:id/result when result_ready is true. Sume documents no render time for Timeline, so do not hard-code a sleep; the docs describe a polling flow, and the default mode is async. A 3-minute render reserves ceil(180 / 60) = 3 output minutes, which is $0.30 at $0.10 per minute.
YouTube's three-minute Shorts page says Shorts can be up to 3 minutes, which is why a 180-second render is a common target; the cost arithmetic is the same for any length.
The flow
The render needs an Idempotency-Key header, and all sources must be media.sume.com artifacts, imported first through POST /v1/media-imports. The submit returns a job with type: timeline_render and model: sume/timeline-1.0. When the job is result_ready, the result has kind: timeline_render, video_url, duration_seconds, segment_count, billable_minutes and optional warnings[].
A longer program with many slots is chunked: render.strategy defaults to auto, which chunks past 12 segments. A render that sets single above 12 slots is refused with render_strategy_unsafe. The job is worker ffmpeg only, with no provider inference.
| Step | Call | What you get |
|---|---|---|
| Plan (optional) | POST /v1/timeline-1.0/plan | duration_seconds, segment_count, billable_minutes, estimated_cost_usd_micros |
| Submit | POST /v1/timeline-1.0/render with Idempotency-Key | A job with a request_id |
| Poll | GET /v1/jobs/:id/status | terminal and result_ready flags |
| Read | GET /v1/jobs/:id/result | video_url, billable_minutes, warnings[] |
A polling loop
The script below polls, obeys next_poll_after_seconds when the response has it and otherwise backs off, and stops when terminal is true. It reads the result only when result_ready is true and prints the status otherwise. Pass the job id as an argument.
import asyncio, os, sys, urllib.request, json
BASE = "https://api.sume.com/v1/jobs/"
HEAD = {"Authorization": "Bearer " + os.environ["SUME_API_KEY"]}
def get(path):
req = urllib.request.Request(BASE + path, headers=HEAD)
return json.load(urllib.request.urlopen(req))
async def main():
job = sys.argv[1]
delay = 2
for _ in range(40):
status = get(job + "/status")
if status.get("terminal"):
ok = status.get("result_ready")
print(get(job + "/result") if ok else status)
return
delay = status.get("next_poll_after_seconds") or min(delay * 1.5, 15)
await asyncio.sleep(delay)
print("gave up; keep the job id", job)
asyncio.run(main())Safe retries
If your submit call times out, send the same request again with the same Idempotency-Key. That is what the key is for, and it protects you from paying for a second render. Keep the key and the job id together in your records so a restart of your own script can resume polling rather than submitting again.
For a webhook instead of polling, the communication fields (mode, webhook_url, wait_timeout_seconds) are shared by the Sume job endpoints; see the Sume jobs docs for details.
Cost before you start
Use the plan call first for a long render. It is free of an idempotency key and creates no job, and for 180 seconds of audio it reports 3 billable minutes and 300,000 micros of estimated cost, which is $0.30.
What can make a render fail
Most refusals happen at submit, before any job exists: an off-host URL is refused with unsupported_media_source, a dead one with source_not_found, a first slot that does not start at 0 with timeline_must_start_at_zero, and ffmpeg keys such as codec with a 400. Those are rejected at admit, so no job is created.
If a job reaches a terminal state without a result, the status response says so through the same terminal flag with result_ready false. Read /events for the sequence of what happened, and fix the program before you resubmit under a new idempotency key.
Soft warnings, such as a padded or looped short source, arrive in warnings[] on a successful result and are not failures; log them, but do not retry because of them.
A long render in practice
For a 180-second program with, say, 30 slots, expect the job to run as chunks, because auto chunks past 12 segments. You do not manage the chunks; you still poll one job. Keep the job id and the key, and set a generous attempt limit in your loop with a growing delay, as in the script above, so a slow render does not turn into a flood of status calls.
Sources
Related posts
More in Developers
- How many new Sume jobs can I submit now? Read generation_limits
Open slots are concurrency_limit minus active and queued jobs, capped by queue_capacity_remaining. A short Python function computes it from a submit response.
- Map a Sume API HTTP status to the next step: one Python function
One Python function turns a Sume status, error code and headers into the next action: wait, retry with the same key, fix the request, or stop. Tested.
- Order id or payload hash? Choosing a Sume Idempotency-Key
An order-id key gives 409 idempotency_conflict when the prompt changes; adding a payload hash gives a new paid job. Choose on purpose, with Python.
- Image 1.0 to POST /v1/images: image_urls, num_images, mask mapped
Moving from /v1/image-1.0/generate to /v1/images on Sume: image_urls becomes input_references, num_images becomes n, and the default mode becomes sync.
Written by Sume