Best practices
One worked example for Mobidoo backend integrators — what a create → receipt → clip-retry loop can look like. Not a contract.
Team workspace API key: Team Formats need a team key.
API surface: Live Commerce API.
How input and typed receipts work in depth:
Calling a Format — what is carried,
Structured output.
1. Example create
Instruction + convenient input + the partner output_schema (so the receipt
is typed — full cut and per-scene clips). Tabs are cURL / TypeScript /
JavaScript / Python for the same body.
Example create — AHC 30 s live cut
POST /v1/formats/{handle}/{slug}/runs
What the create body has to get right
Four things in that body are load-bearing. Here is the same body the tab above sends, as JSON — the run it came from returned a 34.72 s cut, 7 scenes, $7.02 — with the long strings abridged for length:
output_schema is the whole object — { name, strict, schema } with the
JSON Schema inline. The registered name on its own is not enough. Pair it with
primary_output_key, and confirm it took: the create response echoes
output_schema.source, which reads request_override when your schema is the
one in force.
generation_spend_cap_usd has to be non-zero and comfortably above your
estimate — a zero or missing cap is not "no limit". $20 was the cap on the run
above; it billed $7.02.
input is flat snake_case — product_url, product_name, on_card_name,
host_image_url, vo_language, price, highlights, script.segments. No
nesting under a wrapper key, no camelCase.
Partner ALL-CAPS segment tags work. INTRO / MID / FIN need no
conversion before you send them; the receipt reports them back normalized to
Intro / Mid / Fin in act and tags. Match on the receipt's casing, not
on what you sent.
Anything that has no recognized input key belongs in instruction. That is
where the run above put "use the segments verbatim", "do not lengthen the
script", "no BGM, no captions, 9:16".
Script length is the duration control
There is no duration parameter. The finished cut is as long as the script takes
to read, so the only lever on length is how much text goes into
script.segments. On Korean copy the ratio across this pack was roughly 8.2
characters per second of finished video — 283 characters came back as a
34.72 s cut. Use it to size a slot before you write the script; it is a planning
number, not an SLA.
You get a run back:
Wait for terminal — poll status_url / result_url or take
the run webhook (format.run.terminal). One webhook per
turn, not one per scene.
When it lands, check the receipt actually carries the script you sent before you
show it to an operator. middle: [] and closing: [] satisfy the schema, so a
run that read only part of the script can still come back completed with no
error on it. Comparing the returned script text against your segments is a
cheap guard.
2. Example receipt
The snippet above is a fill-in template. Everything from here down is copied
verbatim out of a real production run — arun_42ed6a48b3d44092 on Format
v5, a Korean sunscreen cut, 11 scenes, 73.36 s, $14.96 — so every
media.sume.com URL below is a live file you can open. Abridged to 3 of the 11
scene rows (sc_1–sc_3, sc_5, sc_6, sc_8–sc_10 dropped for length).
Schema details: Structured output.
For a “retry this clip” control, persist four things off this turn:
| Store | From | Used for |
|---|---|---|
run.id | create response | becomes previous_run_id on the retry |
thread_id | create response | groups every turn of one production — read-only, never sent back in a body |
scene.id | each receipt row | names the clip to retry — frozen for the production, so never key off array index |
scene.status | each receipt row | stand-in / failed is what enables the retry button |
Store scene.id as an opaque string. This run returned sc_0 … sc_10;
older productions on earlier Format versions returned other shapes. Persist
whatever the receipt gives you and never parse or reconstruct it.
Every scene in this run came back succeeded, so the retry below is an
operator asking for a different take, not a repair. The other trigger is a row
that came back retryable — a dashboard lights up “retry this clip” on either
of these:
Illustrative statuses only — this production returned 11/11 succeeded.
3. Example retry
Dashboard loop: operator selects a clip → your backend already has
previous_run_id (create run id) and scene_id (from the receipt row). Post
the same Format endpoint with that scene_id in input and the same
output_schema as create so the typed receipt stays the same shape.
Do not put thread_id in the body. It is a response field; sending it is
rejected with unknown_parameter. previous_run_id is the only continuation
key.
What is left is instruction — and that is the field that decides what you get
back.
Prefer concrete per-scene feedback
When the operator clicks “retry this clip” because they do not like the take,
ask them why and put their answer in instruction: which scene, what is
wrong with it, how it should change. A retry that carries a real note is the
recommended shape; the fixed template below is the fallback, not the default.
Four parts, in the instruction, in whatever language the production runs in:
- the scene —
scene_idand role, repeated in prose so the note is unambiguous - the observed defect — what the operator actually saw in this take
- the concrete fix — what to do instead, as instructions, not adjectives
- the guardrails — keep the VO spine, script text, host, wardrobe, set; vertical 9:16, no BGM, no captions
Both instruction examples below are lifted from a second production on this
recipe — a different thread from the receipts above, which is why their
previous_run_id is not the one in §2. They are shown as bodies, not receipts.
The first is a broll scene that came back as a packshot floating in an empty
room. Note the banner rule in it: on-frame card and banner typography has to
finish inside the top 40 % of the frame, because the live UI covers the
bottom.
Example retry — scene note in the instruction
POST /v1/formats/{handle}/{slug}/runs
A talk scene reads the same way — same four parts, a different defect. This
one was a greeting where the host's gaze dropped and her eyes closed on some
frames:
Why bother: a talk scene retried with the bare template has come back
byte-identical to the take the operator rejected, while the same scene
retried with a note like the one above re-rendered and fixed the gaze. Treat
that as integrator guidance, not an API guarantee — a bare retry is not
promised to be identical and an instructed one is not promised to change. What
you control is how much the instruction tells the run. §5 measures one of these
end to end — eight named fixes on a price card, every one applied, six sibling
clips untouched.
The bare template is the mechanical fallback
For a stand-in or failed row there is nothing to describe — the operator
never saw a take to object to. A fixed template is the right call there, and it
is also the honest default for a plain “roll it again”:
Example retry — selected scene on the same thread
POST /v1/formats/{handle}/{slug}/runs
New run, same thread:
Retrying several clips at once is the same call with scene_ids and the plural
instruction — nothing else changes:
Clip retry re-renders how a scene looks. Changing what it says — wording, price read, line length — is a new production with new scene ids; see What clip retry can and cannot change.
Two caveats before you wire the button
Serialize retries on a thread. Do not post two retries off the same
previous_run_id while the first is still running — one production thread,
one turn in flight. Queue the second retry until the first reaches a terminal
status; retrying two scenes in the same turn is what scene_ids is for.
previous_run_id picks the thread, not a snapshot. Naming an older run
does not branch from that older receipt — the new receipt is assembled from
the thread's latest scene state, so a scene some earlier turn already changed
stays changed. To diff a retry, compare it against the previous turn's
receipt, not against the create.
4. Example retry receipt
Again a full receipt — the whole opening / middle / closing list, not a
patch and not a delta. sc_7 carries a new media URL; every other scene keeps
the exact URL it already had, and full_video is re-assembled. Same three rows
as above so you can diff them by eye:
What the two receipts prove, measured on this pair:
Create arun_42ed6a48b3d44092 | Retry arun_808e4eb3e6c04cf7 | |
|---|---|---|
thread_id | thr_96012fce-… | same |
| scene rows returned | 11 | 11 — full list, not a patch |
| sibling scene URLs | — | 10 of 11 byte-identical |
sc_7 URL | artf_nlQMSVkb… | new — artf_Hy18xtqP… |
sc_7 duration | 17.76 s | 17.76 s — VO spine did not move |
full_video | 73.360 s, 12,049,030 B | re-assembled, 73.360 s, 12,080,322 B |
| billable | $14.959638 | $0.742530 — about a twentieth |
| wall | 11m08s | 2m53s |
The old sc_7 URL still resolves 200 after the retry. Prior-turn artifacts
are immutable, so a create receipt you already stored stays valid — you are
free to keep both takes and let the operator pick.
A regenerated clip is a new generation, not a re-encode. Anything generative inside the frame can drift between takes. In the pair above the structured overlay facts held exactly (price, discount label, SPF grade, set composition), but decorative on-pack copy rendered into the product art changed wording. Do not promise your operators a pixel-identical frame with one detail fixed; promise them another take.
5. A shorter pair, measured
§2–§4 walk a 73 s, 11-scene production. The same loop on the 34.72 s cut from §1 is worth a second look, because it puts a number on what a targeted retry costs next to re-running the whole thing:
Create arun_206837aa88d34c8d | Instructed retry arun_0c315b373e24433a | |
|---|---|---|
| thread | thr_45c64858-… | same |
previous_run_id | null | arun_206837aa88d34c8d |
| script sent | 283 KO chars, INTRO / MID / FIN | — |
| result | 34.72 s, 7 scenes, all succeeded | 34.72 s, 7 scenes, one moved |
| billed | $7.0221 | $0.7361 |
| wall | 10m 08s | 2m 24s |
The retry body is the §3 shape and nothing more: previous_run_id,
input: { "scene_id": "sc_5" }, the same output_schema as create, and an
instruction that names the scene, the defect and the fix. sc_5 was the
closing price card — an under_banner scene carrying the product name and price
and little else. Abridged:
고칠 씬: sc_5 (under_banner / Fin, 클로징 가격 카드, 9.44초).
지금 무엇이 잘못됐는지: 카드에 제품명과 가격만 있고, 이 상품의 핵심 소구점인 자외선 차단 지수(SPF50+ PA++++)가 빠져 있습니다. … "30% 할인"이 카드에서 가장 크게 잡혀 있고, 정작 소비자가 결제하는 금액인 23,030원이 그보다 작게 보입니다. … 제품 이미지 위쪽에 빈 여백이 크게 남아 카드 상단이 비어 보입니다.
어떻게 바꿀지: 제품명 바로 아래에 "SPF50+ PA++++" 한 줄을 배지 형태로 추가해 주세요. … 판매가 23,030원을 카드에서 가장 큰 요소로 만들어 주세요. "30% 할인"은 그보다 작게, 보조 배지로 내려 주세요. 정가 32,900원은 취소선을 유지해 주세요. … 배경 톤, 카드 색감, 제품 사진 구도(박스 1개 + 튜브 2개)는 지금 그대로 두세요.
Eight specific asks, each one an edit rather than an adjective, and behind them
a list of what must not change. All eight landed, and nothing outside sc_5
moved:
| Row | Changed |
|---|---|
sc_0–sc_4, sc_6 | no — same artifact URL on all six |
sc_5 | yes — artf_psot5zRM…/images.png → artf_GWQTB6n2…/timeline.mp4 |
full_video | yes — re-concat, still 34,720 ms, new bytes |
Diff per-scene URLs, not full_video. full_video is re-assembled on every
turn, so its URL changes whether or not any clip re-rendered — it proves
nothing on its own. The per-scene URL is the signal, and for talk scenes hash
the bytes too, since a talk retry has come back byte-identical on a fresh URL.
scene.video is not always a video. In the create receipt sc_5.video was a
PNG still — "type": "image", "content_type": "image/png", with width,
height and duration_ms all null and the length carried only by the row's
duration_seconds: 9.44. After the retry the same scene id held an MP4:
"type": "video", 720×1280, duration_ms: 9440. Read type and content_type
off each row rather than assuming a video element can play it, take the length
from duration_seconds, and expect the type to change across turns on one scene
id.
A targeted retry cost about a tenth of a re-create here — $0.74 against $7.02, 2m 24s against 10m 08s. When an operator wants one clip changed, retry that clip; re-running the production is the expensive way to get the same fix.
That’s the loop. Details live on Live Commerce API and Structured output.

