Timeline plan needs no Idempotency-Key; render does: a wrapper
POST /v1/timeline-1.0/plan is free and takes no Idempotency-Key. Render is paid and requires one. A wrapper plans first, then renders with a derived key.

The Timeline plan call and the render call differ on one header. POST /v1/timeline-1.0/plan creates nothing, so Idempotency-Key is not required and you can repeat it as often as you like. POST /v1/timeline-1.0/render creates a paid job, so the key is required. A small wrapper plans first, then renders with a key derived from the document, which makes a retried render return the original job.
Two calls, two rules
| Property | Plan | Render |
|---|---|---|
| Path | POST /v1/timeline-1.0/plan | POST /v1/timeline-1.0/render |
| Creates a job | No | Yes (type timeline_render) |
| Reserves credits | No | Yes, ceil(audio.duration_seconds / 60) minutes |
| Idempotency-Key | Not required | Required |
| Default mode | Not applicable | async; sync waits up to 30 s for a 200 |
| Reads | Result in the response | GET /v1/jobs/:id/status, then /result |
What the plan will not tell you
A plan runs schema checks, Sume-host URL checks and the compiler. It cannot predict warnings about a short source that must be padded or looped. A render reports those as soft warnings[], not failures. So treat a clean plan as a price and shape check, not a promise about every frame.
The wrapper
The key is a hash of the document, so repeating the wrapper with an unchanged document returns the same job. Change the document and the key changes. The rate is $0.10 per rounded-up minute, so the plan estimate and the render reserve should agree for the same input.
import hashlib, json, os
import httpx
def render_if_cheap(doc: dict, cap_micros: int):
key = os.environ.get("SUME_API_KEY", "")
if not key:
raise SystemExit("set SUME_API_KEY")
h = {"Authorization": f"Bearer {key}"}
base = "https://api.sume.com/v1/timeline-1.0"
plan = httpx.post(f"{base}/plan", headers=h, json=doc, timeout=30)
plan.raise_for_status()
est = plan.json()["estimated_cost_usd_micros"]
if est > cap_micros:
return None
canon = json.dumps(doc, sort_keys=True, separators=(",", ":"))
idem = "tl-" + hashlib.sha256(canon.encode()).hexdigest()[:32]
r = httpx.post(f"{base}/render", json=doc, timeout=60,
headers={**h, "Idempotency-Key": idem})
r.raise_for_status()
return r.json()Handling the result
A successful submit returns a job. When it is result_ready, GET /v1/jobs/:id/result returns kind: timeline_render with video_url, duration_seconds, segment_count and billable_minutes. There is no GET /v1/timeline-1.0/:id, so poll the job envelope.
Why the asymmetry is useful
Because a plan has no side effects, you can run it freely: on every edit in an editor, on every row of a spreadsheet, on every candidate cut. You get a duration, a segment count, billable minutes and an estimate, and no job exists to clean up. Reserve the paid call and its key for the document you actually mean to render.
If you use the hosted MCP route, the equivalent write needs idempotency_key in the tool arguments, and the plan-like read does not.
Render defaults to async and returns 202 with a job. Pass mode: "sync" to wait up to 30 seconds for a finished 200. For anything longer, poll GET /v1/jobs/:id/status, then fetch the result. A client timeout does not cancel the render, so keep the job id and the key. A retry with the same key returns the same job.
Sources
Related posts
More in Developers
- Transcribe a 90-second clip with video inspect: cost and reservation
Video inspect with transcribe true adds Sume STT at $0.01 per audio minute; send duration_seconds 90 so the hold fits, or it reserves one minute.
- Transcript from a video on Sume: inspect transcribe or detach first?
Sume can transcribe a video through video inspect (1,800 s limit, compute plus $0.01 per minute) or from a detached 16 kHz mono wav. How to choose.
- TTS word timestamps: timestamps.words and sentence segmentation
Sume TTS accepts timestamps.words and segmentation.mode sentence so a generated voiceover can drive caption timing. Request fields, rules and a working call.
- Turn a roleplay debrief into an avatar feedback clip in Python
Take the written debrief from a roleplay or survey session and render it as a 16:9 Sume avatar clip with a retry-safe key, a 12 to 168 word check and polling.
Written by Sume