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.

5 min readSume
All posts

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

Plan versus render, as of 2026-10-08
PropertyPlanRender
PathPOST /v1/timeline-1.0/planPOST /v1/timeline-1.0/render
Creates a jobNoYes (type timeline_render)
Reserves creditsNoYes, ceil(audio.duration_seconds / 60) minutes
Idempotency-KeyNot requiredRequired
Default modeNot applicableasync; sync waits up to 30 s for a 200
ReadsResult in the responseGET /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

All Developers posts

Written by Sume